Conversation
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>
…ow 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>
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>
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.
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.
…oter 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.
…er, 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.
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).
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.
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.
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).
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.
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.
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.
…ataTableFooter, hooks)
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.
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.
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.
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.
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.
…aTable 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.
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.
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.
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.
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.
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.
ViewMenu in the listhead; a view captures status, group, search, sorts and hidden columns. Also drops an unused eslint-disable in useTableViews.
.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.
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.
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.
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.
…ettings 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.
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.
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).
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.
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.
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.
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.
…oard 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.
…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.
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.
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.
Each QuickCreate destination opens its create surface via useFocusParam: ?focus=create:server|monitor|domain|workspace|project (cron lands with the jobs commit).
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.
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.
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'.
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.
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.
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.
…ch (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>
There was a problem hiding this comment.
Pull request overview
This PR standardizes list-page tables across the panel by moving pages onto shared DataTable-based primitives (toolbar/menus/footer/empty states), adds saved table views (backend + frontend API + hook + UI), and introduces local “recents/favorites” plus a global quick-create entry point.
Changes:
- Consolidate dozens of list/detail table UIs onto shared table primitives (
DataTable,ListToolbar,DataTableFooter, sort/columns/group/view menus) and shared empty-state handling. - Add per-user saved table views (
/api/v1/views) with persistence and UI to save/apply/update/delete views. - Harden git subprocess invocations against argument injection and speed up backend tests via a shared session-scoped Flask app fixture with opt-out markers.
Reviewed changes
Copilot reviewed 167 out of 168 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSION | Bump app version |
| frontend/src/utils/recents.js | Local recents/favorites storage |
| frontend/src/styles/pages/_telemetry.scss | Remove unused loading style |
| frontend/src/styles/pages/_settings.scss | Remove legacy empty style |
| frontend/src/styles/pages/_service-detail.scss | Add compact row styling |
| frontend/src/styles/pages/_servers.scss | Align servers page styles |
| frontend/src/styles/pages/_server-survey.scss | Note shared ListToolbar |
| frontend/src/styles/pages/_server-services.scss | Note shared ListToolbar |
| frontend/src/styles/pages/_server-packages.scss | Note shared ListToolbar |
| frontend/src/styles/pages/_security.scss | Add table-actions cluster style |
| frontend/src/styles/pages/_remote-access.scss | Remove legacy empty style |
| frontend/src/styles/pages/_notification-center.scss | Remove legacy empty style |
| frontend/src/styles/pages/_monitors.scss | Remove legacy listhead/empty |
| frontend/src/styles/pages/_jobs.scss | Remove custom pager styles |
| frontend/src/styles/pages/_ftp-server.scss | Add header actions layout |
| frontend/src/styles/pages/_email.scss | Align filters with ListToolbar |
| frontend/src/styles/pages/_domains.scss | Remove legacy listhead/empty |
| frontend/src/styles/pages/_docker.scss | Remove legacy sort control |
| frontend/src/styles/pages/_deployments.scss | Remove legacy toolbar/empty |
| frontend/src/styles/pages/_cron.scss | Remove legacy listhead styles |
| frontend/src/styles/pages/_backups.scss | Remove legacy listhead/empty |
| frontend/src/styles/components/_notification-center.scss | Style quick-create + fav star |
| frontend/src/styles/components/_empty-state.scss | Update empty-state cascade note |
| frontend/src/services/api/views.js | Saved views API client |
| frontend/src/services/api/index.js | Register views API methods |
| frontend/src/services/api/auth.js | Add activity feed target filters |
| frontend/src/pages/WorkspaceDetail.jsx | Record visits + favorite star |
| frontend/src/pages/Vaults.jsx | Convert dialogs to Modal |
| frontend/src/pages/Templates.jsx | Standardize drawer width |
| frontend/src/pages/ServiceDetail.jsx | Record visits + favorite star |
| frontend/src/pages/ServerDetail.jsx | Record visits + favorite star |
| frontend/src/pages/RemoteAccess.jsx | Use EmptyState for empty |
| frontend/src/pages/Projects.jsx | Convert dialog to Modal + focus link |
| frontend/src/pages/ProjectDetail.jsx | Record visits + favorite + Modal |
| frontend/src/pages/MonitorDetail.jsx | Add sort/columns menus + favorites |
| frontend/src/pages/Marketplace.jsx | Add empty-state escape actions |
| frontend/src/pages/Incidents.jsx | Replace listhead with ListToolbar |
| frontend/src/pages/FleetProxy.jsx | Enable sortable columns |
| frontend/src/pages/Documentation.jsx | Use EmptyState for empty |
| frontend/src/pages/Deployments.jsx | Use ListToolbar + EmptyState |
| frontend/src/pages/DeliveryLog.jsx | Convert table + pager to DataTable |
| frontend/src/hooks/useTableViews.js | Saved views state management |
| frontend/src/hooks/useTableSort.js | Multi-column sorting utilities |
| frontend/src/hooks/useRecordVisit.js | Hook to record recent visits |
| frontend/src/hooks/useColumnVisibility.js | Column visibility persistence |
| frontend/src/components/workspaces/WorkspaceSitesTab.jsx | DataTable conversion |
| frontend/src/components/workspaces/WorkspaceServicesTab.jsx | DataTable conversion |
| frontend/src/components/workspaces/WorkspaceServersTab.jsx | DataTable conversion |
| frontend/src/components/workspaces/WorkspaceMembersTab.jsx | DataTable conversion |
| frontend/src/components/Sidebar.jsx | Add QuickCreate control |
| frontend/src/components/settings/WebhooksTab.jsx | Convert dialogs to Modal |
| frontend/src/components/settings/MigrationHistoryTab.jsx | DataTable conversion + EmptyState |
| frontend/src/components/settings/InvitationsTab.jsx | DataTable conversion + EmptyState |
| frontend/src/components/settings/connections/ConnectionsHub.jsx | Replace window.confirm with useConfirm |
| frontend/src/components/settings/ActivityTab.jsx | Use EmptyState + DataTableFooter pager |
| frontend/src/components/service-detail/SettingsTab.jsx | Replace confirms with useConfirm |
| frontend/src/components/service-detail/PackagesTab.jsx | DataTable conversion |
| frontend/src/components/serverdetail/SurveyTab.jsx | ListToolbar + DataTable conversion |
| frontend/src/components/serverdetail/ServerSettingsTab.jsx | Convert Dialog to Modal |
| frontend/src/components/serverdetail/PackagesTab.jsx | Replace toolbar with ListToolbar |
| frontend/src/components/security/VulnerabilityTab.jsx | Use EmptyState component |
| frontend/src/components/security/IntegrityTab.jsx | DataTable conversion |
| frontend/src/components/security/AutoUpdatesTab.jsx | Use EmptyState component |
| frontend/src/components/security/AuditTab.jsx | Use EmptyState component |
| frontend/src/components/QuickCreate.jsx | Global quick-create menu |
| frontend/src/components/monitoring/FleetThresholdsPanel.jsx | Sort/columns + DataTable conversion |
| frontend/src/components/monitoring/FleetCapacityPanel.jsx | DataTable conversion |
| frontend/src/components/monitoring/DoctorPanel.jsx | Convert Dialog to Modal |
| frontend/src/components/MobileTopBar.jsx | Add QuickCreate control |
| frontend/src/components/FavoriteStar.jsx | Favorite toggle component |
| frontend/src/components/EnvironmentVariables.jsx | Env history DataTable conversion |
| frontend/src/components/ds/ViewMenu.jsx | Saved views menu UI |
| frontend/src/components/ds/SortMenu.jsx | Sort menu UI |
| frontend/src/components/ds/SortChipBar.jsx | Sort chips UI |
| frontend/src/components/ds/ListToolbar.jsx | Shared list toolbar primitive |
| frontend/src/components/ds/index.js | Export new DS components |
| frontend/src/components/ds/GroupMenu.jsx | Grouping menu UI |
| frontend/src/components/ds/DataTableFooter.jsx | Shared table footer |
| frontend/src/components/ds/ColumnsMenu.jsx | Column visibility menu UI |
| frontend/src/components/databases/EngineInstallDrawer.jsx | Standardize drawer width |
| frontend/src/components/databases/EngineCatalogDrawer.jsx | Standardize drawer width |
| frontend/src/components/dashboard/grid/WidgetLibrary.jsx | Standardize drawer width |
| frontend/src/components/CommandPalette.jsx | Favorites + recents integration |
| frontend/src/components/backups/BackupsOverview.jsx | Latest snapshots DataTable conversion |
| frontend/src/components/backups/BackupHistoryList.jsx | DataTable conversion |
| frontend/src/components/backups/BackupDetailDrawer.jsx | Standardize drawer width |
| backend/tests/test_wp_hook_inversions.py | Mark fresh_app for structure mutation |
| backend/tests/test_views.py | Tests for saved views API |
| backend/tests/test_trusted_client_ip.py | Mark fresh_app for structure mutation |
| backend/tests/test_support_bundle.py | Mark fresh_app for structure mutation |
| backend/tests/test_status_extraction.py | Mark fresh_app for structure mutation |
| backend/tests/test_speed_test.py | Mark fresh_app for structure mutation |
| backend/tests/test_site_import.py | Mark fresh_app for structure mutation |
| backend/tests/test_shared_resources.py | Mark fresh_app for structure mutation |
| backend/tests/test_serverkit_gui_agent_gate.py | Mark fresh_app for structure mutation |
| backend/tests/test_remote_access_extraction.py | Mark fresh_app for structure mutation |
| backend/tests/test_plugins_pipeline.py | Mark fresh_app for structure mutation |
| backend/tests/test_plugin_store_sdk.py | Mark fresh_app for structure mutation |
| backend/tests/test_managed_db_users.py | Mark fresh_app for structure mutation |
| backend/tests/test_htaccess_converter.py | Mark fresh_app for structure mutation |
| backend/tests/test_ftp_extraction.py | Mark fresh_app for structure mutation |
| backend/tests/test_fixture_scope_guard.py | Guard fixture scopes |
| backend/tests/test_extension_signing.py | Mark fresh_app for structure mutation |
| backend/tests/test_extension_platform.py | Mark fresh_app for structure mutation |
| backend/tests/test_extension_migration.py | Mark fresh_app for structure mutation |
| backend/tests/test_email_extraction.py | Mark fresh_app for structure mutation |
| backend/tests/test_drift_doctor.py | Mark fresh_app for structure mutation |
| backend/tests/test_db_admin_sso.py | Mark fresh_app for structure mutation |
| backend/tests/test_cloud_provision_extraction.py | Mark fresh_app for structure mutation |
| backend/tests/test_bandwidth.py | Mark fresh_app for structure mutation |
| backend/tests/test_audit_log.py | Mark fresh_app + add target_id test |
| backend/tests/test_api_scopes.py | Mark fresh_app for structure mutation |
| backend/tests/test_agent_features_honest.py | Mark fresh_app for structure mutation |
| backend/tests/conftest.py | Session-scoped app + fresh_app marker |
| backend/tests/BASELINE_COUNT | Update baseline test count |
| backend/migrations/versions/082_saved_views.py | Add saved_views table migration |
| backend/app/services/saved_view_service.py | Saved view CRUD service |
| backend/app/services/git_service.py | Route git calls through hardening helpers |
| backend/app/services/git_deploy_service.py | Harden git fetch/reset |
| backend/app/services/audit_service.py | Add target_id filter support |
| backend/app/models/saved_view.py | SavedView model |
| backend/app/models/init.py | Export SavedView |
| backend/app/api/views.py | Saved views API endpoints |
| backend/app/api/deploy.py | Constrain clone path to apps dir |
| backend/app/api/admin.py | Pass target filters through |
| backend/app/init.py | Register views blueprint |
| AGENTS.md | Document list-page/table conventions |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+36
to
+40
| const av = getValue(a); | ||
| const bv = getValue(b); | ||
| if (av == null && bv == null) continue; | ||
| if (av == null) return direction; | ||
| if (bv == null) return -direction; |
Comment on lines
177
to
+180
| if target_type: | ||
| query = query.filter(AuditLog.target_type == target_type) | ||
| if target_id: | ||
| query = query.filter(AuditLog.target_id == target_id) |
Comment on lines
+1
to
+15
| import { useState } from 'react'; | ||
| import { Star } from 'lucide-react'; | ||
| import { cn } from '@/lib/utils'; | ||
| import { isFavorite, toggleFavorite } from '@/utils/recents'; | ||
|
|
||
| // Pin/unpin an entity as a favorite (surfaced in the command palette's | ||
| // Favorites section). Sits in detail-page title areas. | ||
| // | ||
| // <FavoriteStar type="service" id={service.id} path={`/services/${service.id}`} label={service.name} /> | ||
| export function FavoriteStar({ type, id, path, label, className }) { | ||
| const [fav, setFav] = useState(() => isFavorite(type, id)); | ||
|
|
||
| const toggle = () => { | ||
| setFav(toggleFavorite({ type, id, path, label })); | ||
| }; |
Comment on lines
+12
to
+15
| useEffect(() => { | ||
| if (entry) recordVisit(entry); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [entry?.type, entry?.id, entry?.label]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The panel had one
DataTableprimitive and about sixty tables that had politely declined to use it. Each page grew its own toolbar row (.dom-listhead,.cron-listhead,.bk-listhead,.servers-listhead,.wp-list__toolbar— same anatomy, a dozen names), its own pager, its own empty state, and its own idea of whether a column could be sorted. This PR moves all of them onto shared primitives and then builds the things that only become affordable once they're shared: multi-column sorting, column visibility, row grouping, native selection with a floating bulk bar, keyboard navigation, and per-user saved views backed by a real table. The toolbar split is the one design decision worth arguing about — page-level actions, advanced filters and search publish into the shared topbar, while quick filters, counts and the table menus live in the in-pageListToolbar; that rule is now written down in AGENTS.md so the next page doesn't reinvent a thirteenthlisthead. Riding along are two performance fixes that were blocking work (the backend suite rebuilt the entire Flask app once per test, and the Templates page re-parsed 118 YAML files plus refetched every remote index on every render) and a fix for a git argument-injection flaw where an attacker-controlled repository URL or clone path could be parsed by git as its own options.Contributors
Highlights
Technical changes
Security — git argument injection (GHSA-8vx6-432p-h62q)
backend/app/utils/git_security.pycentralizes every defence for git subprocesses. List-formsubprocess.rundoes not help here: git parses its own arguments, soext::sh -c <cmd>,file:///…or--upload-pack=<cmd>arriving as a "URL" or "path" is executed by git itself.validate_repo_urlallowlistshttps/http/git/sshplus scp-likeuser@host:path, and rejects leading-, embedded::, whitespace/control characters and over-long values.validate_ref_namerejects leading-and git-check-ref-format metacharacters.validate_clone_pathrequires an absolute path that is not itself an option or transport specifier.git_argv()pins-c protocol.ext.allow=never -c protocol.file.allow=neveron every network-touching invocation andgit_env()sets a restrictiveGIT_ALLOW_PROTOCOL, so the transport tiers are closed regardless of the host git version (theprotocol.ext.allow=userdefault only changed in git 2.52).--positional terminator now precedes every URL/path/refspec argument. This is co-load-bearing with protocol pinning — pinning restricts transports only and does not stop--upload-pack=style option injection.GitService.clone_repository,pull_repository,get_remote_branches_from_urlandGitDeployService's fetch/reset path route through the helpers.POST /deploy/cloneadditionally confinesapp_pathtopaths.APPS_DIRvia anos.path.abspathprefix check, since that route takes the path from the request body rather than deriving it server-side.backend/tests/test_git_security.pycovers the validators and the argv/env shape.Saved views (backend)
SavedViewmodel,saved_view_service, andviews_bpblueprint at/api/v1/views(GET/POST/PUT/DELETE), with migration082_saved_views(idempotent, guards on table presence, cascade onusers.id).user_id, so a view id belonging to another user 404s rather than being editable. Unique constraint on(user_id, page, name); at most one default per(user, page), enforced by clearing the previous default inside the same transaction;MAX_VIEWS_PER_PAGE = 50.AuditService.get_logsandGET /admin/activityaccepttarget_idalongsidetarget_type, which is what lets a detail page pull only its own audit entries.Table primitives (frontend)
DataTablegains multi-column sorting (sortsarray where order is priority, uncontrolled by default or controlled viasorts/onSortsChange), column visibility (hiddenKeys), grouping (groupBy+groupable/groupValue/groupLabelon columns), controlled selection with an indeterminate header checkbox, afooterslot, andstorageKeyfor localStorage persistence.input, textarea, select, [contenteditable], [role="dialog"], so it can't hijack typing or fire behind a modal. The cursor row is kept on screen withscrollIntoView({ block: 'nearest' }).hooks/useTableSort.js(exportsapplyTableSortsandnextSortsso controlled hosts share the upsert-toggle logic instead of duplicating it),hooks/useColumnVisibility.js, andhooks/useTableViews.js. All localStorage access is wrapped — private mode and quota errors degrade to "the choice doesn't persist".ds/SortMenu,ds/SortChipBar,ds/ColumnsMenu,ds/GroupMenu,ds/ViewMenu,ds/DataTableFooter,ds/ListToolbar, all exported fromds/index.js.useTableViewsdirty-tracking normalizes state key-order-agnostically and compareshiddenKeysas a set, while sorts stay order-sensitive because order is priority;ViewMenusurfaces the dirty dot with Update/Reset.List-page consolidation
ResourceListPagewires sort/columns/group/views menus, page-size slicing, native selection and the floating.sk-bulkbar, and takessearchInTopbarfor pages that publish aSearchFieldinto the shared topbar.DataTable: security tabs (firewall, fail2ban, IP lists, quarantine, scanner, SSH keys, integrity), backups (snapshots/schedules/history), git (repos/webhooks/deploys), FTP, fleet monitoring, workspaces, DNS, packages, settings (users/invitations/API/migration history), server-detail tabs, queues, docker containers, and the servers fleet table.DataTableFooter.ui/dialoguses convert to the sharedModal.jsxwrapper and drawer widths snap to a 380/520/640/720 scale (ConnectProviderModalkeeps its custom chrome; domain detail keeps its 1100 exception).window.confirmand per-page confirm state give way touseConfirm()across the security tabs; per-page__emptymarkup gives way to the sharedEmptyState.DataTablenow passesdescriptionrather thanmessagetoEmptyState, matching the component's actual prop.html .empty-statecascade hack is retained but re-documented: the page-partial copies it was fighting are gone (_security.scss's deleted,_servers.scss's now scoped under.servers-page), so it stands as defence-in-depth rather than a live conflict.AGENTS.mddocuments the table primitives and codifies the list-page anatomy, including the topbar-vs-ListToolbarplacement rule, drawer/modal width scale, confirm policy and row-click policy.Navigation and record UX
utils/recents.jstracks visited entities and pinned favorites in localStorage (deduped bytype+id, capped at 10 recents), withuseRecordVisitfor detail pages and aFavoriteStartoggle in detail-page title areas. Favorite labels are refreshed on revisit so a renamed entity doesn't keep a stale star.CommandPalettegrows Favorites and Recently-visited groups, ranked ahead of raw entity hits on ties.QuickCreateglobal "+" menu; route-based flows navigate directly, modal/drawer flows use a?focus=create:<kind>deep link the destination page opens.useConfirmgate stays in front of both.service-detail/EventsTabmerges deployments (still expandable) with environment-variable changes and admin-only audit entries filtered bytarget_id.Performance
backend/tests/conftest.pybuilds the Flask app once per session (_flask_app) instead of once per test — fixture setup was 89.5% of the suite's runtime against 5.4% actually running tests. The fixture bootscreate_app('testing')twice on purpose:create_app()seeds rows as a boot side effect, so the schema has to exist before the app that seeds into it.fresh_appmarker or by depending on thewp_extensionfixture, which is detected from the fixture graph rather than requiring each module to remember the marker. Flagship extension rows are reseeded after the per-testdrop_all/create_all.test_fixture_scope_guard.pyasserts_flask_appstays session-scoped andappstays function-scoped — a revert would otherwise still pass, just slowly, which is how CI got slow the first time.backend/.test_durations(3,177 entries) lets pytest-split pack shards by time; without it, pytest-split falls back to splitting by test count, and these tests range from 0.01s to 10s+.TemplateService.list_all_templatesdedupes by id with local winning over remote (the bundle and the registry deliberately overlap, so concatenating listed every bundled template twice).save_config— the single choke point for repo mutations — invalidates the cache so a newly added repo appears immediately.(st_mtime_ns, st_size)viaos.scandir, so a sync or edit invalidates exactly the file that changed and writers need no explicit invalidation. Parse failures are cached negatively and deliberately do not claim the template id, so a valid file of the same name in the fallback directory can still supply it. Cached entries are returned as deep copies, and the cache is pruned to what the directory scan still finds.