Commit Graph
1659 Commits
Author SHA1 Message Date
Juan Denis 364aa592d5 feat(chrome): global quick-create button + favorite star
The CRM '+' lives in the sidebar footer and mobile top bar next to the
notification bell: one menu for New Service/Server/Monitor/Domain/Cron/
Workspace/Project. Route-based flows navigate directly; modal-based flows
use ?focus=create:<kind> deep links. FavoriteStar pins entities from detail
pages.
2026-08-09 04:31:36 -04:00
Juan Denis fd775fc629 feat(palette): Favorites + Recent entity visits sections
utils/recents.js tracks detail-page visits (10 max) and pinned favorites in
localStorage; useRecordVisit is the one-line detail-page hook. The palette
gains a Favorites group (weighted high, searchable) and folds visited
entities into Recently used.
2026-08-09 04:31:35 -04:00
Juan Denis 084638984c feat(services,workspaces,servers): group-by + native selection + row cursor
Services groups by project or status, Workspaces and Servers by status; the
hand-rolled __select columns are gone in favor of DataTable's native
selection; Servers captures groupBy in its saved views.
2026-08-09 03:53:43 -04:00
Juan Denis 3a0e047038 feat(resource-list): wire grouping, GroupMenu, native selection, keyboard nav
ResourceListPage owns persisted groupBy state (captured in saved views),
renders GroupMenu in the toolbar, accepts selectable/selectedIds/
onToggleSelect/onSelectAll for the native checkbox column, and turns the row
cursor on by default for list pages.
2026-08-09 03:53:43 -04:00
Juan Denis ddb557ec1e feat(views): dirty indicator — Update/Reset on modified views
useTableViews deep-compares the live table state against the active view
(hiddenKeys set-normalized, sorts order-sensitive); a dirty view shows an
amber dot on the trigger and Update / Reset-to-saved actions in the popover.
2026-08-09 03:53:43 -04:00
Juan Denis 99dbef56ad feat(datatable): row grouping, keyboard nav, native selection
Group-by: groupable columns + groupBy prop render collapsible group header
rows (chevron + label + count); GroupMenu popover picks the grouping. Keyboard
nav: j/k (or arrows) move a cursor row, Enter opens it, x toggles selection —
ignored while typing or in dialogs. Selection: selectable + controlled
selectedKeys/onToggleRow/onToggleAll render a real checkbox column with an
indeterminate select-all, ending the hand-rolled __select column pattern.
2026-08-09 03:53:43 -04:00
Juan Denis 2939542b44 docs(agents): codify the list-page anatomy
One anatomy diagram, the toolbar placement rule, the drawer width scale, and
the EmptyState/Modal/useConfirm/row-click conventions; primitives list gains
ListToolbar, SortChipBar, ViewMenu and the views hook.
2026-08-09 03:33:23 -04:00
Juan Denis cb085fafb7 refactor(list-page): one toolbar placement rule
ResourceListPage renders the shared ListToolbar and gains a searchInTopbar
flag; Workspaces moves its search into the shared topbar to match Services
(search state still flows through for views). Superseded .wp-list__toolbar /
__tabletools SCSS removed; the .empty-state specificity comment is updated
now that the page-partial copies are gone.
2026-08-09 03:33:23 -04:00
Juan Denis 27acadebaf refactor(drawers,modals): standard width scale + Modal.jsx wrapper
Drawer widths snap to the 380/520/640/720 scale; thirteen raw ui/dialog uses
(services, projects, vaults, serverdetail tabs, webhooks, doctor panel)
convert to the shared Modal wrapper. ConnectProviderModal keeps its custom
chrome (branded header + scroll region Modal can't express).
2026-08-09 03:33:22 -04:00
Juan Denis 037e8842f7 refactor(security): shared EmptyState + useConfirm everywhere
Security tabs' legacy empty-state markup becomes the component, their bespoke
confirmDialog flows (firewall, fail2ban, ip lists, ssh keys) move to
useConfirm, and the remaining window.confirm calls (telemetry cleanup,
service settings, connections hub) are replaced. The duplicate .empty-state
block in _security.scss is gone.
2026-08-09 03:33:06 -04:00
Juan Denis e6f9a88352 refactor(empty-states): shared EmptyState across jobs/delivery/docs/settings
DataTable emptyState overrides and legacy empty-state markup become the
shared component (wording and CTAs preserved). Also fixes DataTable passing
emptyMessage to a nonexistent EmptyState prop — it now lands on
'description', so custom empty messages actually render.
2026-08-09 03:33:06 -04:00
Juan Denis df34943b8a refactor(servers,deployments,email,serverdetail): ListToolbar everywhere
servers/deployments/server-services/packages/survey toolbars and the email
accounts/queue rows move to ListToolbar (selects get .sk-listhead__select);
email table emptyState overrides become emptyTitle/emptyMessage; the legacy
unscoped .empty-state in _servers.scss is scoped under .servers-page.
2026-08-09 03:32:51 -04:00
Juan Denis 897f9a8764 refactor(cron,backups,incidents): ListToolbar + shared EmptyState
cron/bk/incidents listheads move to ListToolbar (counts and menus preserved);
bk-empty and mon-empty become EmptyState; run-history drawer width snapped to
the 640 step of the standard scale.
2026-08-09 03:32:51 -04:00
Juan Denis 106a83778d refactor(domains,git): ListToolbar + shared EmptyState
The .dom-listhead rows (incl. Git's six borrowed section headers) move to the
shared ListToolbar; .dom-empty filtered-empty blocks become EmptyState;
retired SCSS deleted.
2026-08-09 03:32:51 -04:00
Juan Denis b0dd554216 feat(ds): ListToolbar — the one list-page toolbar row
.sk-listhead [title][filters][extras] [count][tools] replaces the five
hand-rolled listhead patterns; includes a standard __select style for
secondary pickers. Codifies the placement rule in its docstring.
2026-08-09 03:10:23 -04:00
Juan Denis f2c7924493 feat(email): saved views for accounts and mail queue
Sort-preset builtins (Active first, Largest quota, Largest first, By sender);
ViewMenu in each table's toolbar before SortMenu.
2026-08-09 02:47:11 -04:00
Juan Denis 5d3f6a6a33 feat(jobs,telemetry): saved views — Failed / Errors / Warnings / Newest builtins
Both tables switch to controlled sort/cols (same storageKeys); views capture
the FilterDrawer value plus sorts and columns, never server-side page state.
ViewMenu publishes into the topbar via useTopbarActions with a primitive
active-key dep to avoid re-publish loops.
2026-08-09 02:47:10 -04:00
Juan Denis 5f9ddb3384 feat(cronjobs): saved views — Paused / Active / Recently run builtins 2026-08-09 02:47:10 -04:00
Juan Denis 411200487c feat(domains): saved views — Expiring soon / No SSL builtins
ViewMenu in the listhead tools; a view captures the seg filter, search, sorts
and hidden columns.
2026-08-09 02:46:59 -04:00
Juan Denis eccc93ea29 feat(monitors): saved views — Down / Degraded / Slowest builtins
ViewMenu published into the shared topbar before the filter button; the table
switches to controlled sort/cols (same storageKeys, choices survive). A view
captures search, the FilterDrawer status/type value, sorts and columns.
2026-08-09 02:46:58 -04:00
Juan Denis eb9d5914e9 feat(servers): saved views — Online / Offline / By CPU builtins
ViewMenu in the listhead; a view captures status, group, search, sorts and
hidden columns. Also drops an unused eslint-disable in useTableViews.
2026-08-09 02:46:58 -04:00
Juan Denis 1c226b0ad6 feat(views): view picker UI + ResourceListPage integration
useTableViews merges builtin views with the user's saved views from the API
(auto-applying the default on load); ViewMenu is the picker popover — apply
on click, star for default, delete, save-current-as form, update-active. The
ResourceListPage toolbar grows the menu (state = filter/search/sorts/hidden
columns/page size), so Services and Workspaces get views by declaring their
builtin defs.
2026-08-09 02:38:48 -04:00
Juan Denis b6c257bfc7 feat(views): per-user saved table views API
SavedView model (user/page/name/state JSON/is_default, unique per
user+page+name), idempotent 082 migration, saved_view_service with
one-default-per-page enforcement, and a JWT-scoped /api/v1/views CRUD
blueprint. Covered by 9 endpoint tests.
2026-08-09 02:32:55 -04:00
Juan Denis fa6b9f3e26 refactor(queues,docker,misc): move remaining tables onto DataTable
Queue messages/operations, delivery log, process table, env-history and
docker containers migrated. The containers tab's select+button sort UI is
deleted in favor of native header sorting (running-first default kept) with
controlled menus; FilterDrawer pairings untouched.
2026-08-09 02:12:17 -04:00
Juan Denis 65703c165d refactor(serverdetail): move server-detail tab tables onto DataTable
Docker containers/images (size parses to bytes for numeric sort), cloudflared
tunnels, cron, systemd services and survey sites now use the shared primitive;
tabs with toolbars (cloudflared, cron, services) get controlled menus.
2026-08-09 02:12:16 -04:00
Juan Denis 5d87b42fa6 refactor(settings): move settings tables onto DataTable
Users, invitations, API keys, webhook deliveries, top endpoints and migration
history gain sortable columns (compact variants preserved) and standard
footers. ActivityTab keeps its server-paged fetch; its ad-hoc pager swaps to
DataTableFooter's paged mode.
2026-08-09 02:11:56 -04:00
Juan Denis 1a0ca61c41 refactor(workspaces,dns,packages): move remaining tab tables onto DataTable
Workspace sites/services/servers/members, the domain DNS record grid and the
service packages table now use the shared primitive with sortable columns and
footers; conditional DNS columns and fixed-layout classes preserved.
2026-08-09 02:11:56 -04:00
Juan Denis 0cf847b739 refactor(ftp,fleet): move FTP and fleet-monitoring tables onto DataTable
FTP users/connections and the nine fleet tables (versions, rollouts, command
queue, approvals, alerts, anomalies, thresholds, capacity) gain sortable
columns, persisted choices and standard footers; tables with an in-page
toolbar get controlled SortMenu/ColumnsMenu.
2026-08-09 02:11:55 -04:00
Juan Denis 3811c4004b refactor(git): move repo/webhook/deploy tables onto DataTable
Repos and webhooks gain controlled SortMenu/ColumnsMenu in the listhead and
filtered footers; the webhook drawer's recent-deployments table sorts
uncontrolled. The commits feed is not table-shaped and stays as-is.
2026-08-08 23:56:52 -04:00
Juan Denis 3d026208f4 refactor(backups): move snapshot/schedule/history tables onto DataTable
Snapshots table gains controlled sort/column menus in the listhead plus a
sort chip bar; schedules, latest-snapshots digest and run history sort
uncontrolled with persisted choices; standard footers throughout.
2026-08-08 23:56:51 -04:00
Juan Denis 249546720a refactor(security): move all security tab tables onto DataTable
Firewall, Fail2ban, IP allow/blocklists, integrity changes, quarantine,
scanner findings/history and SSH keys now use the shared primitive: sortable
columns (epoch/numeric sortValues), SortMenu+ColumnsMenu in card headers
(.sec-tableactions slot), standard footers, persisted choices.
2026-08-08 23:56:51 -04:00
Juan Denis 7b4cf90b0c feat(datatable): active-sort chip bar (Twenty-style)
SortChipBar renders one chip per sort level above the table — click to flip
direction, x to remove, Reset to clear — and collapses to nothing when no
sort is active. Wired into ResourceListPage (list view) and the Servers page.
2026-08-08 23:41:30 -04:00
Juan Denis 5aa4ddc6b2 docs(agents): document the table primitives (SortMenu, ColumnsMenu, DataTableFooter, hooks) 2026-08-08 23:37:43 -04:00
Juan Denis f64a537144 refactor(servers): move the hand-rolled table onto DataTable
The servers list now runs on the shared primitive: identical cell markup and
style hooks, sortable name/status/CPU/mem/disk/last-seen (nulls last),
SortMenu + ColumnsMenu in the listhead, a shared footer, and the page's
sticky header preserved. Meter cells generated by a small helper.
2026-08-08 23:37:43 -04:00
Juan Denis e79538eb37 feat(services,workspaces): opt columns into sorting, persist table state
Both pages declare sortable columns with meaningful sortValue accessors
(status severity order, timestamps, numerics, nulls last) and pass a
storageKey so ResourceListPage persists sort/columns/page-size.
2026-08-08 23:37:42 -04:00
Juan Denis 11460670e9 feat(jobs,telemetry): standard table footer replaces ad-hoc pagers
Jobs' custom Prev/Next 'Page N' pager and Telemetry's bare 'Load more' swap
to the shared DataTableFooter (same state setters, fetch logic untouched);
both tables gain sortable columns and persisted choices. Orphaned pager SCSS
removed.
2026-08-08 23:37:31 -04:00
Juan Denis d73f0c4b31 feat(email,fleet): sortable tables with menus and footers
Email components/accounts/queue tables gain sortable columns; accounts and
queue toolbars get SortMenu + ColumnsMenu (new .sk-email__tabletools slot).
FleetProxy keeps its renderRow layout but gains header sorting (column
visibility intentionally off — renderRow emits every cell).
2026-08-08 23:37:30 -04:00
Juan Denis 2fbacb2fd1 feat(cron,domains): sortable columns, table menus, footers
Cron jobs + run history and the domains list get sortable headers (schedule,
last/next run, duration, expiry sort by real values), SortMenu/ColumnsMenu in
the in-page listheads (new .dom-listhead__tools slot), and standard footers.
2026-08-08 23:37:20 -04:00
Juan Denis a394bc38c6 feat(monitors): enable sortable columns and table footers
Monitor list sorts by name/type/status/response/uptime/last check; check log
gains SortMenu + ColumnsMenu in its panel header (controlled mode), sortable
time/result/latency columns, and a shared footer. Choices persist via
storageKey.
2026-08-08 23:37:20 -04:00
Juan Denis d3490a4aaf feat(resource-list): wire sort/columns menus, footer, floating bulk bar
ResourceListPage now owns the table chrome end to end: SortMenu + ColumnsMenu
in the toolbar, DataTableFooter with client-side page-size slicing under the
table, and a floating bottom bulk-actions pill replacing the permanent bar.
Optional storageKey persists sort/columns/page-size per page. Sorting flips
on globally (columns still opt in via sortable: true).
2026-08-08 23:37:10 -04:00
Juan Denis 334226a86b style(tables): sticky headers, sort priority badge, table menus, footer, floating bulk bar
Extends .sk-dtable with is-sorted accent + multi-sort priority numbers and an
opt-in sticky-header wrap; adds .sk-tablemenu (sort/columns popovers),
.sk-dtable-footer, and the floating .sk-bulkbar pill. Also restores the
.wp-list chrome styles (toolbar/search/viewswitch/cards) that were lost when
the WordPress frontend moved into its extension, plus the new tabletools slot.
2026-08-08 23:37:02 -04:00
Juan Denis fc2c4f6697 feat(datatable): multi-column sorting, column visibility, standard footer
DataTable v2: sorts become an ordered array (datatables.net model) — plain
header click cycles asc/desc/none, shift+click stacks sort levels with a
priority badge. New useTableSort/useColumnVisibility hooks (optional
localStorage persistence), SortMenu/ColumnsMenu toolbar popovers, and a
shared DataTableFooter (Shown X of Y, page-size segment, load-more, pager).
All backward compatible; existing call sites keep working unchanged.
2026-08-08 23:36:52 -04:00
Juan Denis 73a79a20e0 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-08 23:28:35 -04:00
Juan Denis 6e03c0a523 perf(templates): cache parsed local templates on file mtime
With the remote index memoized, re-parsing every bundled YAML on each call
was the entire remaining cost of a Templates page load. Entries are now
memoized per file and keyed on (st_mtime_ns, st_size), so a sync or an edit
invalidates exactly the file that changed: measured on the real catalog,
list_local_templates() goes from 304ms to 0.89ms (118 templates), and the
full catalog listing from 1707ms to 1.26ms.

The writers need no invalidation call - mtime keying covers create, overwrite
and delete, the last because the listing only returns what the directory scan
finds. Cache entries are pruned to what is still on disk so a long-running
panel that churns templates cannot grow it without bound.

Failed parses are cached too, keyed on the same stat tuple, so one malformed
YAML no longer costs a parse on every render; it is retried the moment the
file changes. A file that fails to parse still does not claim its id, so a
valid file of the same name in the fallback directory can supply it - pinned
by a test, since that is easy to lose in a refactor.

Entries are deep-copied out: a shallow copy would share the nested
categories/repo/engine containers, so a caller appending to categories would
corrupt every later listing.

Also swaps os.listdir for os.scandir (DirEntry.stat() reuses data the scan
already returned) and sorts directory entries, which makes a same-id
collision (foo.yaml vs foo.yml) resolve identically on every platform instead
of depending on filesystem order.
2026-08-06 09:32:40 -04:00
Juan Denis 9d64b4ab4d fix(templates): dedupe the catalog by id and cache remote indexes
Both bugs were latent only while DEFAULT_REPOS pointed at a 404. Now that
serverkit.ai proxies a live registry, they bite on every Templates page load.

list_all_templates() concatenated local + remote with no id merge, so every
bundled template was listed twice the moment a real registry answered.
Measured against the live registry: 224 entries for 116 unique ids. It now
merges by id with local winning, matching get_template(), which already
resolves local directories before any repo; among repos, the first enabled
one to claim an id keeps it.

fetch_remote_templates() ran inline on every listing with a 30s timeout and
no memoization, so one unreachable repo stalled the whole catalog on every
render. It is now memoized with a TTL (5m success, 60s failure), a 10s
timeout, and a last-good fallback so a transient blip cannot blank a catalog
the panel has already shown. The last-good payload is carried forward across
repeated failures rather than being overwritten by the first one. The cache
is dropped in save_config(), the single choke point for repo add/remove/
enable (sync_templates routes through it too), and force=True bypasses it.

11 proving tests, including the repeated-failure case and a regression test
that a cached entry cannot be corrupted by a caller mutating it.
2026-08-06 08:52:31 -04:00
github-actions[bot] 308e3694da chore: bump version to 1.7.84 [skip ci] 2026-08-06 12:32:38 +00:00
Juan DenisandClaude Opus 5 32e3c8a839 test: record per-test durations so CI shards balance by time, not count
pytest-split falls back to splitting by test COUNT when no durations file
exists, and these tests range from 0.01s to 10s+. That produced visibly lopsided
shards on the first sharded CI run — 6m17s for the fastest against 10m46s for
the slowest, and the slowest shard is the whole job's wall clock:

    pytest (4/4)   6m 17s
    pytest (2/4)   8m 42s
    pytest (1/4)  10m 01s
    pytest (3/4)  10m 46s   <- Backend CI was 11m06s because of this one

With real durations, pytest-split packs the shards to equal TIME instead, which
should pull the slowest shard down toward the ~9m average.

Generated from a full local run: 3175 entries, 20 failed / 3108 passed /
47 skipped in 11m53s. All 20 failures are pre-existing and unrelated (a
serverkit-wordpress checkout absent locally, plus Linux-only fail2ban tests) —
verified by re-running those same files against the pre-plan-64 conftest.

Regenerating this file was only affordable because plan 64 Phase 1 landed
first; before it, a full run was slow enough that Phase 0 abandoned the attempt.
Regenerate it whenever suite timings shift materially:

    cd backend && python -m pytest tests --store-durations

Plan 64 Phase 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 03:25:21 -04:00
Juan DenisandClaude Opus 5 baff2452b0 test: guard the app fixture's scope so the suite can't quietly get slow again
Nothing catches a revert of `_flask_app` to scope='function'. The suite would
still pass — just ~3x slower — and CI would drift back to a 20-minute wait,
which is how it got that way in the first place. Same idea as the
collected-test-count ratchet: turn a silent regression into a red test.

Also asserts the inverse for `app`, which must stay function-scoped: it is what
resets the database between tests, and session-scoping it would leak each
test's rows into the next and fail while pointing at the wrong test.

Verified both directions — flipping _flask_app back to scope='function' fails
the guard with the explanation, and restoring it passes.

Ratchet 3173 -> 3175 for the two new tests.

Plan 64 Phase 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 03:01:24 -04:00
Juan DenisandClaude Opus 5 9d570834aa perf(tests): build the Flask app once per session, not once per test
The `app` fixture called create_app('testing') for every one of 3173 tests,
registering 90+ blueprints each time. Measured on a fixed 199-test sample
(pytest --splits 16 --group 5 --durations=0):

    setup     144.9s   89.5%
    call        8.8s    5.4%
    teardown    8.2s    5.0%

95% of the suite's time was preparing to test; 5% was testing. The app is now
built once per process and shared.

Same 794-test slice, before and after — identical results, 2.66x faster:

    before   4 failed, 773 passed, 17 skipped   593.30s
    after    4 failed, 773 passed, 17 skipped   223.11s

(the 4 are pre-existing locally: serverkit-wordpress isn't checked out here.)

Sharing one app surfaces three things a per-test app hid, all handled in the
fixture rather than patched test by test:

1. create_app() is not a pure constructor — it SEEDS the bundled flagship
   extension rows as a boot side effect (test_cloudflare_extraction asserts
   they are "seeded on boot"). The per-test drop_all wipes them, and a shared
   app never re-boots, so every test after the first saw no flagship and the
   extension routes answered 503. seed_flagship_extensions() is now re-run
   after each create_all. The session fixture also boots twice on purpose:
   once to create the schema, once to seed into a schema that exists.

2. Config mutation now leaks where it used to die with the app (e.g.
   test_demo_deploys sets DEMO_DEPLOYS_ENABLED=False and never restores it),
   so config is snapshotted and restored around every test.

3. Structure mutation cannot be undone at all — Flask has no
   unregister_blueprint, and refuses setup methods once an app has served a
   request. Those tests opt out via `pytestmark = pytest.mark.fresh_app` and
   get a private app. Three groups needed it: 13 modules calling
   register_blueprint/add_url_rule directly, 11 that install plugins (
   plugin_service hot-loads their blueprints onto the live app), and
   test_audit_log which adds routes with @app.route.

Tests using the `wp_extension` fixture are detected from the fixture graph
instead of being marked, since that fixture mounts blueprints on whatever app
it is handed — miss one marker and the failure lands in an unrelated test much
later in the run.

Plan 64 Phase 1. Phase 2 (savepoint rollback instead of create_all/drop_all)
is NOT included: it currently takes the slice from 4 failures to 100 failures
and 201 errors, and needs its own round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 02:59:14 -04:00
github-actions[bot] 20effa2a1b chore: bump version to 1.7.83 [skip ci] 2026-08-06 05:34:26 +00:00