Commit Graph
1690 Commits
Author SHA1 Message Date
Juan DenisandClaude Opus 5 f49a3e6cfc feat(recycle-bin): soft delete for records, with restore and purge
Deleting is the only action in a control panel that repeating cannot undo, so
records a person destroys by hand now leave a tombstone and the Recycle Bin
hands them back.

SoftDeleteMixin adds deleted_at/deleted_by_id plus soft_delete/restore and
query_active()/query_deleted(). recycle_bin_service is a REGISTRY: a model
registers with a label function and an optional on_restore hook, so the API
lists and restores across types without knowing what a domain is. Adding the
next soft-deletable model is one register() call.

Applied to Domain (a real, destructive case -- deleting one used to lose the
record outright) and SavedView. DELETE /domains/<id> now tombstones and still
tears the vhost down; restoring a row does NOT re-do side effects unless the
type supplies on_restore, which is the honest default.

API: GET /recycle-bin (all kinds, newest first), POST
/recycle-bin/<kind>/<id>/restore, DELETE /recycle-bin/<kind>/<id> (purge,
admin-only -- the one irreversible action), POST /recycle-bin/purge-expired
with a 30-day default retention.

THE PART THAT NEEDED CARE: both tables had a plain UNIQUE covering what is now
a tombstone, so deleting a domain would have permanently burned its name and
deleting a view would have blocked re-creating one by the same name. Migration
083 replaces each with a PARTIAL unique index predicated on deleted_at IS NULL
-- supported by SQLite 3.8+ and Postgres. Tests pin exactly that: a deleted
name can be reused, and the live row stays unique.

Also gives SavedView a `slug` (backfilled), the handle the upcoming
?view=<slug> links resolve against. A tombstoned view does not reserve its
slug, so deleting a view frees the link name too.

Migration is idempotent on both columns and indexes: `deleted_at` is
index=True on the mixin, so a database built from the models already has that
index, and a bare create_index would abort the upgrade on exactly the installs
we most want to keep working.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 07:14:55 -04:00
Juan DenisandClaude Opus 5 fab29ce9b9 feat(views): grounded presets for Monitors, Jobs and Telemetry
Monitors +4 (worst 30d uptime, paused-and-still-silenced, slow web checks, in
maintenance), Jobs +5 (stuck in flight, backlog, failures by kind, backup
runs, app deploys), Telemetry +4 (critical only, deploy failures, backup
failures, audit trail).

Each state uses the page's OWN `filters` shape, which on these three is the
SERVER-side query object -- {status, type}, {status, kind} and the
EMPTY_FILTERS set respectively -- not the per-column {match, rules} object.
They filter the query rather than the rendered array, so they are strictly
stronger than column rules here. Comments added at each array to stop the two
meanings being conflated later.

These three keep their existing FilterButton/FilterDrawer and their top-bar
ViewMenu; moving their picker to a page heading is a separate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 06:57:11 -04:00
Juan DenisandClaude Opus 5 09cecff92f feat(servers): /servers on the shared list chrome, with 4 new presets
Replaces the toolbar's Views/Sort/Group/Columns buttons with the same chrome
as /domains, /services and /cron: the saved view is the page heading, column
rules get a chip bar and the Filters/Fields drawer, and sort/group/hide move
into each column's own menu.

Presets: "Not reporting" (has an agent but stopped answering, longest silence
first -- `pending` excluded, that means no agent was ever installed),
"Under load" (saturated on EITHER axis, because a box swapping at 95% memory
with calm CPU is the one that falls over), "Disk pressure", and "Ungrouped"
-- fleet commands target a group_id, so an ungrouped server is silently
skipped by every bulk action.

Only `gt` is used on cpu/memory/disk, and there is a comment saying why: those
sortValues are null for a non-live row, so a `lt` rule would sweep in every
offline and pending server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 06:57:11 -04:00
Juan DenisandClaude Opus 5 b113bd0af9 feat(cron): /cron on the shared list chrome, with 6 presets
Was the odd one out: it had saved views but behind the old toolbar ViewMenu
button, so it looked nothing like /domains. Now the same heading picker,
chip bar, filter drawer and tools menu as every other list page, driven by
useTableChrome off its existing sorts/hiddenKeys state.

Adds "Failing now", "Stale schedules" and "Health roll-call" to the three
shipped presets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 06:48:48 -04:00
Juan DenisandClaude Opus 5 b0f46510fd feat(list): ResourceListPage on the grid chrome -- Services + Workspaces
Swaps the toolbar's Views/Sort/Group/Columns buttons for the shared chrome:
the saved view becomes the page heading, filters get a chip bar and a
Filters/Fields drawer, and Sort/Group/Columns move into each column's own
"⋮" next to the column they act on. The cards/list switch, selection bulk
bar and page-size footer are unchanged. One wrapper, so both pages move
together.

Presets grow from 3 thin ones to 7 (Services) and 5 (Workspaces), each a
question someone opens the page to ask -- "Broken or unknown", "Stale live
services", "Not git-deployed", "Empty workspaces", "Archived but still
staffed".

Services' `status` column gains an explicit `value`. It had only
`sortValue`, which returns a severity RANK (0-5) rather than the status
string, and fieldValue prefers value->sortValue->row[key] -- so a "status is
running" rule compared against the number and matched nothing. Where a
column's sort key differs from its real value, the column has to say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 06:48:47 -04:00
Juan DenisandClaude Opus 5 43c2babcf8 feat(ds): useTableChrome -- one list chrome for every DataTable page
The view picker, chip bar, filter drawer and tools menu were written against
DataGrid's single `cfg` object; the older list pages keep sorts[]+hiddenKeys
+groupBy instead. Rather than fork the chrome per page -- the duplication a
shared layout exists to stop -- this hook adapts that state once and returns
ready-to-spread props.

Column types are inferred HERE and the same typed list goes to the chip bar,
the drawer and the table. Inferring separately inside DataTable left the
chrome reasoning about untyped columns while the table reasoned about typed
ones, and the drawer's rule editor looked up OPS[undefined] and crashed on
any rule the header menu had just created.

The saved-view key is `columnFilters`, NOT `filters`: Jobs and Monitors
already capture a `filters` key holding the FilterDrawer's {status, kind}
pair. Sharing the name would have silently cross-wired their presets.

GridFilterDrawer/GridToolsMenu gain showRowDetail/showDensity so hosts whose
table auto-sizes its rows don't render controls that change nothing, and the
drawer now skips a rule whose column is no longer filterable instead of
crashing on a stale saved view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 06:48:34 -04:00
Juan DenisandClaude Opus 5 8d2ce87875 fix(grid): a missing value must not satisfy a numeric filter
Number(null) === 0, so an unguarded `lt` matched every row whose metric was
absent: "CPU under 20%" returned every offline server in the fleet. lt/gt/eq
now reject null/empty/NaN outright, in both directions.

Type inference also returns null (not 'text') for a column with no readable
values. Several columns render from fields their accessor never exposes --
Servers' `host` and `agent` keys are not row properties, the real fields are
hostname/agent_version -- and those were offering a text filter that could
only ever match nothing. No readable value now means no Filter section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 06:48:33 -04:00
Juan Denis cf557c0f3a Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-12 06:42:08 -04:00
Juan DenisandClaude Opus 5 f442fc148a fix(backups): app picker read the wrong key off the /apps response
GET /apps responds { apps: [...] } (apps.py:442) but the page read
`.applications`, so setApps() got [] on every load and the Application
dropdown in the backup dialogs was permanently empty -- while still fetching
and discarding the full ~93KB payload.

Verified in the browser against the mock API: the picker went from
["Select Application"] to all five apps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 05:52:38 -04:00
Juan DenisandClaude Opus 5 3d9bf09bea fix(api): GET /servers never sent the metrics the servers list renders
The CPU/Memory/Disk gauges read `server.metrics`, the same key the
per-server status endpoint uses (servers.py:757). The list endpoint never
populated it -- to_dict() defaults to include_metrics=False, and even when
asked it emits `latest_metrics` -- so on a live panel every gauge cell fell
back to the "no data" dash.

It survived because the screenshot fixtures DO supply `metrics`
(sections.mjs:294): the UI looked correct in every capture and was empty in
production. The fixture had encoded the intended contract; the backend never
implemented it.

Batched deliberately: Server.metrics is lazy='dynamic', so the obvious
per-row lookup costs one extra SELECT per server. Newest row per server =
highest id (autoincrement on insert), which also avoids the tie a
max(timestamp) join hits for same-second rows -- two queries total,
regardless of server count.

Tests pin all three properties, and each fails without the fix: the key and
field names the table reads, absence (not {}) for a server that never
reported, and that the query count stays flat as servers are added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 05:52:37 -04:00
Juan DenisandClaude Opus 5 26fbe7a4ca refactor(domains): rebuild the page on DataGrid
Saved views as the page heading (personal views persist through
/api/v1/views), filter chips, bulk bar, grouping, density, pagination and
CSV/JSON export; search, the filter icon and the overflow menu move into the
shared top-bar slots. Columns declare type + width, so the header menu
offers real operators instead of inferred ones.

Only fields the API actually returns are columns -- no DNSSEC or record-count
column, because neither /domains nor /dns/portfolio carries them.

Keeps every page invariant: RegistrarPortfolio, the app+provider merged row,
DomainDnsPanel in the drawer, PluginSlot, and the ?open= / ?focus=create
deep links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 05:52:23 -04:00
Juan DenisandClaude Opus 5 d75e1a1647 feat(datatable): per-column menu on every table, with inferred filter types
Puts the DataGrid column menu into DataTable itself, so all 62 table
instances across 48 files gain sort / group-by / filter / move / hide with
ZERO call-site changes. The audit made this the safe move: renderRow is used
in exactly 1 of 62 instances, `header` is always a string, and no consumer
passes unknown column keys -- so optional keys are additive everywhere.

The part that makes it work without editing 50 files is type inference: a
column that declares no `type` gets one sampled from the data -- boolean,
number, ISO string, or a repeating low-cardinality string (enum: a pick-list
with live per-value counts). Anything else stays text, which is never wrong,
just less specific. Pages that care override by declaring `type`.

Filters and column order are uncontrolled by default; pass filters/
onFiltersChange or columnOrder/onColumnOrderChange to mirror them in a chip
bar or drawer. Filtered-to-nothing keeps the header on screen, because the
control that undoes it lives in the header menu.

Also restyles .sk-dtable to match the grid (top-lit header, reserved menu
lane), and turns the top bar's "search is right-most" rule into explicit
slots -- [actions] [search] [filter/overflow] -- so the search box sits in
the same pixels on every page and does not move when a page gains a button.

Verified live on 12 pages: menus on all 12; enum filters actually move the
row count (servers 4->1, cron 8->1, services 6->1, backups 5->1,
domains 5->1, monitors 5->2), including the one renderRow consumer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 05:52:23 -04:00
Juan DenisandClaude Opus 5 c66d5037ef feat(ds): DataGrid -- the list surface for pages where the table is the page
Ported from the Domains design mock. A CSS grid, not a <table>: header and
rows share one --sk-grid-tpl track list, which is what makes show/hide and
move-left/right a pure data change with no <colgroup> to keep in sync.

  fields.js         field model + filter-rule engine (text/enum/num/bool/date),
                    sort, group, CSV/JSON export
  DataGrid          grid, sticky head, groups, selection, density, sub-detail
  ColumnMenu        per-column popover: sort / group / filter / move / hide.
                    Flat, state-model-agnostic props so <DataTable> can drive
                    it from sorts[]+hiddenKeys and <DataGrid> from one cfg.
  useGridConfig     the whole chrome as ONE serialisable object, so a saved
                    view is a snapshot of it and "dirty" is a deep-equal away
  useGridRows       filter -> sort -> page -> group, memoised
  GridViewPicker    saved views rendered as the page heading
  GridFilterDrawer  Filters + Fields panes
  GridChips / GridBulkBar / GridFooter / GridToolsMenu / GridFilterButton

Does not replace ds/DataTable -- column descriptors are a superset, so a
list can move up by adding `type` and `width`.

Adds the --surface-0 token (the recessed step below --surface) that group
headers, drawer tab strips and drawer footers sit on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 05:52:05 -04:00
Juan DenisandClaude Opus 5 dbb0488d75 fix(views): memoize allViews/activeView to stop an unbounded update loop
allViews rebuilt `{...v, builtin: true}` copies on every render, so
activeView had a fresh identity every time. That identity is public --
callers put it in effect dependency arrays -- and useTopbarActions sets
state in the parent layout from such an effect, so the pair looped until
React bailed with "Maximum update depth exceeded".

Keyed on the builtins' names so an inline builtinViews={[...]} literal is
safe as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 05:51:52 -04:00
Juan DenisandClaude Opus 5 4f2116a533 fix(ai): hide the assistant bubble for modals only, not every popover
The fallback selector matched a bare [role='dialog'], and Radix gives
Popover.Content that role too -- so opening Sort, Columns, Views, Filter or
the topbar overflow blinked the bubble out and straight back in. Paired with
the off-screen popover bug this read as "the button did something to the AI
and nothing else".

Scoped to the actual modal surfaces (.ui-dialog-content / .ui-sheet-content);
the body[data-scroll-locked] signal is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 05:51:51 -04:00
Juan DenisandClaude Opus 5 dc9825ce23 fix(ui): forwardRef Button so Radix popovers anchor to their trigger
Button was a plain function component. Used as
`<PopoverTrigger asChild><Button/></PopoverTrigger>` React dropped the ref
("Function components cannot be given refs ... Primitive.button.SlotClone"),
so @radix-ui/react-popper never received a reference element, useFloating
never positioned, and the panel stayed parked at its pre-position
`translate(0, -200%)` -- open, focusable, and entirely off-screen.

Measured before the fix on /domains: the Sort trigger sat at (1232, 106)
while its panel rendered at (0, -623). Every table menu in the app looked
like a dead button. Affects all 45 `asChild` trigger call sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 05:51:41 -04:00
Juan Denis 5ecf1bb8c9 fix(dev): dev.ps1 honors SERVERKIT_KILL_PORTS=0; print localhost URL under WSL
The Windows-side port sweep now respects the same opt-out dev.sh has, and
the startup banner leads with http://localhost:<port> (the WSL interface IP
stays as a printed fallback) so the URL you open is never the dead one.
2026-08-11 20:15:12 -04:00
Juan Denis d6bd0e6a52 fix(db): WAL + busy_timeout for SQLite — stop 'database is locked'
The queue-bus consumers, job system and metrics collector share one SQLite
file with request handling, and the default journal mode lets any writer
lock the whole file — the sporadic 'database is locked' errors in the
notification/job consumers. Connections now open with journal_mode=WAL
(readers and a writer coexist), busy_timeout=30s and synchronous=NORMAL.
Skipped under TESTING, where conftest owns connection pragmas.
2026-08-11 20:04:19 -04:00
github-actions[bot] a52fb10f69 chore: bump version to 1.7.87 [skip ci] 2026-08-11 23:54:27 +00:00
Juan Denis c4930fa6dd Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-11 19:54:15 -04:00
Juan DenisandTyler Boykin 9fb0a6cab5 fix(security): close git argument injection in repo_url/app_path/branch (GHSA-8vx6-432p-h62q)
An authenticated user of any role could inject arguments into git
subprocesses running as the backend user (root under systemd):

- repo_url was passed unvalidated to git ls-remote/clone, enabling the
  ext:: transport (host command execution on git < 2.52 defaults) and
  file:// local-repository disclosure (all git versions).
- app_path on POST /deploy/clone was passed without a '--' terminator,
  enabling --upload-pack=<cmd> option injection independent of git
  version and protocol.*.allow policy.
- branch flowed into 'git fetch origin <branch>' as an unterminated
  refspec.

Fix (new app/utils/git_security.py applied at every affected sink):

- repo_url scheme allowlist (https/http/git/ssh + scp-like SSH);
  rejects ext::/file://, local paths, leading '-', whitespace.
- '-c protocol.ext.allow=never -c protocol.file.allow=never' pinned on
  every remote-touching git call, plus a restrictive GIT_ALLOW_PROTOCOL
  in the subprocess environment.
- '--' positional terminator before URL/path/refspec arguments —
  co-load-bearing with protocol pinning, which alone does not stop
  option injection such as --upload-pack=<cmd>.
- branch/ref validation (leading '-', ref-format metacharacters) for
  clone --branch, fetch refspecs and stored deploy configs.
- POST /deploy/clone now confines app_path to the managed apps root,
  mirroring how /apps/from-repository derives it server-side.

Adds 31 regression tests; bumps version to 1.7.86.

Reported-by: CaptBoykin
Co-authored-by: Tyler Boykin <25166954+CaptBoykin@users.noreply.github.com>
2026-08-11 19:43:46 -04:00
github-actions[bot] aeae8245fa chore: bump version to 1.7.86 [skip ci] 2026-08-11 23:29:31 +00:00
Juan Denis bd96f40d69 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-11 19:29:06 -04:00
Juan Denis 527deaae16 feat(empty-states): next-best-action CTAs on filtered/installed empties
Domains and DeliveryLog filtered empties get Clear filters; the extension
catalog filtered empty gets Clear filters and the installed empty gets a
Browse catalog button.
2026-08-09 14:04:55 -04:00
Juan Denis 7aa257e1fd feat(servers): bulk edit — assign selected servers to a group
The fleet table gains native selection (DataTable selectable) and a floating
bulk bar with a Set group… picker (including Ungrouped) that applies via
updateServer per row.
2026-08-09 14:04:54 -04:00
Juan Denis a958b05c97 feat(service-detail): unified activity timeline
The Events tab becomes the CRM record timeline: deployments (expandable,
as before) interleaved newest-first with environment-variable changes and —
for admins — audit-log entries targeting the app, via the new target_id
feed filter. Compact non-expandable rows for config/audit entries.
2026-08-09 14:04:54 -04:00
Juan Denis ede0f0fdbd feat(activity): audit feed filters by target_id
AuditService.get_logs gains a target_id filter (paired with target_type) and
/admin/activity/feed passes both through — the per-resource activity
timeline can now ask 'what happened to THIS app'.
2026-08-09 13:53:59 -04:00
Juan Denis f8bfb94630 feat(recents): visit tracking + favorite stars on detail pages
Service/Server/Monitor/Workspace/Project detail pages record visits for the
palette's Recently used section and host a FavoriteStar in their title areas
that pins the entity into the palette's Favorites group.
2026-08-09 13:46:30 -04:00
Juan Denis acfce7dbeb feat(ux): undo toasts for reversible deletes + cron create deep-link
Removing an IP list entry or deleting a cron job now toasts with an Undo
action that re-POSTs the captured row (comment/description preserved). The
useConfirm gate stays; cron's ?focus=create:cron deep-link rides along.
2026-08-09 13:46:30 -04:00
Juan Denis e980104d5a feat(quick-create): create deep-links on the list pages
Each QuickCreate destination opens its create surface via useFocusParam:
?focus=create:server|monitor|domain|workspace|project (cron lands with the
jobs commit).
2026-08-09 13:46:30 -04:00
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