Skip to content

feat: add self-hosted admin dashboard#199

Merged
ian-pascoe merged 6 commits into
mainfrom
feat/caplets-admin-dashboard
Jul 8, 2026
Merged

feat: add self-hosted admin dashboard#199
ian-pascoe merged 6 commits into
mainfrom
feat/caplets-admin-dashboard

Conversation

@ian-pascoe

@ian-pascoe ian-pascoe commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Caplets can now expose a self-hosted operator dashboard for administering the current host from the browser. Before this branch, approvals, revocation, Vault administration, catalog review, and runtime inspection all depended on CLI-first workflows; now those flows are available behind durable operator sessions in a packaged dashboard that also supports a no-auth dev path for local iteration.

Reviewer notes

  • packaged installs now bundle the dashboard assets instead of depending on monorepo-relative paths
  • dashboard URLs, cookies, and API calls respect non-root serve paths and HTTPS public origins
  • operator approvals keep the pending-login freshness checks and include the configured remote state path in the generated approval command
  • catalog risk acknowledgement is no longer wired to destructive force-overwrite behavior
  • expired or revoked sessions return the browser to authorization instead of leaving stale admin state rendered

Validation

  • pnpm --filter @caplets/core build
  • pnpm --filter caplets build
  • node scripts/check-package-runtime.mjs
  • pnpm --filter @caplets/core typecheck
  • pnpm --filter @caplets/core exec vitest run test/dashboard-session.test.ts test/dashboard-activity.test.ts test/dashboard-catalog.test.ts test/dashboard-static.test.ts test/dashboard-ui.test.ts test/dashboard-vault.test.ts test/dashboard-api.test.ts test/dashboard-runtime.test.ts test/remote-login-cli.test.ts test/serve-http.test.ts
  • pnpm --filter @caplets/dashboard typecheck
  • pnpm exec vitest run apps/dashboard/src/lib/api.test.ts
  • pnpm --filter @caplets/dashboard build

Compound Engineering

Summary by CodeRabbit

  • New Features
    • Introduced a self-hosted Caplets Admin Dashboard with static-ready pages (overview, access, caplets, catalog, runtime, vault, settings, activity) and dashboard-backed APIs.
    • Added Access vs Operator client roles, role-aware pending login approvals, operator-controlled admin routing, and an operator activity feed.
    • Extended the remote/host admin approval flow with role selection support.
  • Bug Fixes
    • Improved dashboard base-path handling, theming, and secure static asset/session/CSRF enforcement.
  • Documentation
    • Added/updated dashboard and authorization documentation, including role boundary guidance.
  • Tests
    • Added/expanded end-to-end dashboard, catalog, vault, runtime, session, and remote-approval test coverage.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e7b6ea63-9677-4f3a-8023-9c6bcf69c0fe

📥 Commits

Reviewing files that changed from the base of the PR and between c7854a5 and 817616f.

📒 Files selected for processing (11)
  • apps/docs/astro.config.mjs
  • apps/docs/src/content/docs/agent-integrations.mdx
  • apps/docs/src/content/docs/configuration.mdx
  • apps/docs/src/content/docs/dashboard.mdx
  • apps/docs/src/content/docs/index.mdx
  • apps/docs/src/content/docs/install.mdx
  • apps/docs/src/content/docs/remote-attach.mdx
  • packages/core/src/dashboard/session-store.ts
  • packages/core/src/remote/server-credential-store.ts
  • packages/core/src/serve/http.ts
  • packages/core/test/dashboard-session.test.ts
✅ Files skipped from review due to trivial changes (4)
  • apps/docs/src/content/docs/index.mdx
  • apps/docs/src/content/docs/dashboard.mdx
  • apps/docs/src/content/docs/agent-integrations.mdx
  • apps/docs/src/content/docs/remote-attach.mdx
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/core/src/serve/http.ts
  • packages/core/test/dashboard-session.test.ts
  • packages/core/src/dashboard/session-store.ts
  • packages/core/src/remote/server-credential-store.ts

📝 Walkthrough

Walkthrough

This PR adds a self-hosted Caplets Admin Dashboard with role-scoped remote authorization, dashboard session and activity persistence, catalog and HTTP route support, a new Astro/React frontend, packaging updates, and expanded integration coverage.

Changes

Caplets Admin Dashboard

Layer / File(s) Summary
Docs and role model
CONCEPTS.md, CONTEXT.md, docs/adr/0003-remote-client-role-boundaries.md, docs/plans/2026-07-03-001-feat-caplets-admin-dashboard-plan.md, .changeset/admin-dashboard.md
Adds dashboard and remote-client glossary entries, the role-boundary ADR, the implementation plan, and the release changeset.
Remote roles and CLI flow
packages/core/src/remote/server-credentials.ts, packages/core/src/remote/server-credential-store.ts, packages/core/src/cli.ts, packages/core/src/remote-control/dispatch.ts, packages/core/src/cli/install.ts
Adds role-bearing remote credentials and pending-login state, CLI approval/role output, and allow-risk-increase propagation through update flows.
Session and activity storage
packages/core/src/dashboard/types.ts, packages/core/src/dashboard/auth.ts, packages/core/src/dashboard/session-store.ts, packages/core/src/dashboard/activity-log.ts
Adds dashboard session types/cookie helpers, a file-backed session store, and a bounded/redacted JSONL activity log.
Catalog APIs and dashboard HTTP routing
packages/core/src/dashboard/catalog.ts, packages/core/src/dashboard/routes.ts, packages/core/src/serve/http.ts
Adds catalog query/install/update helpers, dashboard shell/static serving, operator-gated dashboard routes, and updated discovery/service-path wiring.
Frontend UI, pages, and packaging
apps/dashboard/src/components/..., apps/dashboard/src/hooks/use-mobile.ts, apps/dashboard/src/lib/*, apps/dashboard/src/pages/..., apps/dashboard/src/styles/globals.css, apps/dashboard/{astro.config.mjs,components.json,package.json,tsconfig.json}, packages/core/scripts/copy-dashboard-dist.mjs, packages/core/package.json
Adds the dashboard Astro app, shared UI primitives, utility hooks, page routes, styles, and build/copy packaging integration.
Integration and smoke tests
packages/core/test/dashboard-*.test.ts, packages/core/test/remote-login-cli.test.ts, packages/core/test/remote-pairing.test.ts, packages/core/test/serve-http.test.ts, scripts/check-package-runtime.mjs
Adds dashboard session/activity/catalog/vault/runtime/static tests, role-aware CLI and pairing coverage, HTTP serve coverage, and a runtime smoke check.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

A rabbit tapped the keys all night,
And lit the dashboard up with light 🐇
Roles and cookies, logs and plans,
Static pages, admin hands.
Hop, hop—catalogs now bloom,
Dashboard shells no longer loom.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a self-hosted admin dashboard.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/caplets-admin-dashboard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Jul 7, 2026

Copy link
Copy Markdown

@socket-security

socket-security Bot commented Jul 7, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm powershell-utils is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/powershell-utils@0.1.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/powershell-utils@0.1.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a self-hosted operator dashboard to Caplets, exposing vault administration, catalog install/update, access management, runtime inspection, and activity logs through a session-authenticated browser UI backed by new API routes in the core HTTP server. Several issues from a prior review pass have been addressed: the ghost-client role guard in completePendingLogin now fires before the client record is written; validateDashboardSession returns 503 (not 401) on SERVER_UNAVAILABLE; dashboard summary hrefs use ${paths.dashboard} rather than the hardcoded /dashboard segment; and dev-mode CSRF validation is enforced with a known sentinel token.

  • New dashboard API layer (packages/core/src/serve/http.ts, dashboard/): session creation/validation with HttpOnly cookies and CSRF tokens, catalog proxy through dispatchRemoteCliRequest, vault set/delete/reveal/grant endpoints, access client revoke/role-change, and an append-only JSONL activity log.
  • Static asset serving (packages/core/src/dashboard/routes.ts): path-traversal–safe safeJoin, correct immutable cache headers for hashed _astro/ assets, and a graceful fallback shell for unbuilt dashboards.
  • Client-side dashboard (apps/dashboard/): Astro static site with a React component, base-path inference from a meta tag updated by an inline script before React loads, and a typed fetch wrapper that always sends the session CSRF token.

Confidence Score: 4/5

The core login-flow security improvements look correct and previous review issues have been addressed, but two remaining issues in session-store.ts and activity-log.ts affect reliability under concurrent load.

The ghost-client role guard, base-path hrefs, dev-mode CSRF enforcement, and lock-contention-to-503 mapping are all correctly addressed. What remains is the synchronous Atomics.wait spin-loop in DashboardSessionStore.acquireLock, which stalls the event loop for up to LOCK_TIMEOUT_MS on every contended session validation — and the activity-log trim path (readEntries → writeEntries) still has no lock, so concurrent dashboard page loads can silently drop audit entries during a trim cycle.

packages/core/src/dashboard/session-store.ts (synchronous spin-wait lock) and packages/core/src/dashboard/activity-log.ts (lock-free trim path).

Important Files Changed

Filename Overview
packages/core/src/serve/http.ts Main HTTP server; adds all dashboard API endpoints (login, session, catalog, vault, access, runtime, activity). Most previous-review issues are addressed (ghost-client guard, base-path hrefs, dev-mode CSRF, SERVER_UNAVAILABLE → 503), but spin-wait lock contention and activity-log concurrent writes remain.
packages/core/src/dashboard/session-store.ts New session store using a directory-based mutex. Correctly validates operator client role, idle timeout, and CSRF. The lock uses Atomics.wait (synchronous spin), which blocks the Node.js event loop for up to LOCK_TIMEOUT_MS per contention event.
packages/core/src/dashboard/activity-log.ts New audit log using JSONL append. The atomic appendFileSync is safe for individual appends, but the subsequent read→retainBoundedEntries→writeEntries trim path has no lock; concurrent trims can overwrite each other, silently dropping entries.
packages/core/src/dashboard/routes.ts Static asset serving with safeJoin path-traversal guard, correct cache-control headers for hashed _astro assets, and graceful fallback shell for unbuilt dashboards.
packages/core/src/dashboard/catalog.ts Catalog search/install/update proxied through dispatchRemoteCliRequest. allowRiskIncrease properly decoupled from force for updates. Official and community catalog sources handled correctly.
packages/core/src/remote/server-credential-store.ts completePendingLogin now checks requiredRole before creating the client record, fixing the ghost-client issue from a previous review. State lock and completion replay are correctly handled.
apps/dashboard/src/lib/paths.ts Base-path inference reads the meta tag updated by the DashboardHead inline script, falling back to lastIndexOf("dashboard") in the pathname. API URLs are correctly prefixed.
apps/dashboard/src/lib/api.ts Thin fetch wrapper; uses same-origin credentials, always sends CSRF token header, and surfaces structured API errors. isDashboardUnauthorized correctly checks 401 only (503 from lock contention is now a separate code path).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant B as Browser
    participant S as HTTP Server
    participant CS as CredentialStore
    participant SS as SessionStore

    B->>S: POST /dashboard/api/login/start
    S->>CS: createPendingLogin requestedRole operator
    CS-->>S: flowId, operatorCode, approvalCommand
    S-->>B: flowId, pendingCompletionSecret, approvalCommand

    note over B,S: Operator runs approval command in CLI

    loop poll until approved
        B->>S: POST /dashboard/api/login/poll
        S->>CS: pollPendingLogin
        CS-->>S: status pending or approved
        S-->>B: status
    end

    B->>S: POST /dashboard/api/login/complete
    S->>CS: completePendingLogin requiredRole operator
    note over CS: Role guard fires BEFORE client write
    CS-->>S: IssuedRemoteClientCredentials
    S->>SS: create operatorClientId
    SS-->>S: cookieValue and session with csrfToken
    S-->>B: Set-Cookie and session JSON

    B->>S: GET /dashboard/api/session with cookie
    S->>SS: validate cookie and credentialStore
    SS-->>S: DashboardSessionView
    S-->>B: authenticated session

    B->>S: POST /dashboard/api/vault/set with x-caplets-csrf header
    S->>SS: validate requireCsrf true
    SS-->>S: session OK
    S-->>B: vault status
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant B as Browser
    participant S as HTTP Server
    participant CS as CredentialStore
    participant SS as SessionStore

    B->>S: POST /dashboard/api/login/start
    S->>CS: createPendingLogin requestedRole operator
    CS-->>S: flowId, operatorCode, approvalCommand
    S-->>B: flowId, pendingCompletionSecret, approvalCommand

    note over B,S: Operator runs approval command in CLI

    loop poll until approved
        B->>S: POST /dashboard/api/login/poll
        S->>CS: pollPendingLogin
        CS-->>S: status pending or approved
        S-->>B: status
    end

    B->>S: POST /dashboard/api/login/complete
    S->>CS: completePendingLogin requiredRole operator
    note over CS: Role guard fires BEFORE client write
    CS-->>S: IssuedRemoteClientCredentials
    S->>SS: create operatorClientId
    SS-->>S: cookieValue and session with csrfToken
    S-->>B: Set-Cookie and session JSON

    B->>S: GET /dashboard/api/session with cookie
    S->>SS: validate cookie and credentialStore
    SS-->>S: DashboardSessionView
    S-->>B: authenticated session

    B->>S: POST /dashboard/api/vault/set with x-caplets-csrf header
    S->>SS: validate requireCsrf true
    SS-->>S: session OK
    S-->>B: vault status
Loading

Reviews (6): Last reviewed commit: "docs: add public dashboard guidance" | Re-trigger Greptile

Comment thread packages/core/src/dashboard/activity-log.ts
Comment thread packages/core/src/serve/http.ts
Comment thread packages/core/src/serve/http.ts
Comment thread packages/core/src/dashboard/session-store.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (10)
packages/core/src/serve/http.ts (1)

784-802: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

SSE endpoint returns a single event then closes.

dashboardEvents sends one runtime_health event and ends the response. A browser EventSource will treat this as a dropped connection and auto-reconnect (default ~3s), producing continuous reconnect cycles rather than a live stream. If this is a placeholder, consider adding a retry: hint or documenting the polling cadence; otherwise it should keep the stream open.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/serve/http.ts` around lines 784 - 802, The dashboardEvents
SSE handler currently returns a single runtime_health payload and immediately
closes, which causes EventSource to reconnect in a loop. Update the
app.get(paths.dashboardEvents) handler to keep the stream open by sending a
proper ongoing SSE response (or, if it must remain one-shot, add an explicit
retry hint and document the behavior) so validateDashboardSession and the
runtime_health event flow align with a live stream instead of a dropped
connection.
packages/core/src/dashboard/routes.ts (1)

75-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

contentType() omits common Astro font types.

Astro can emit .woff2/.woff (and occasionally .ttf) font assets, which will fall through to application/octet-stream and may not load correctly in some browsers. Consider adding font MIME types.

♻️ Optional: add font MIME types
   if (filePath.endsWith(".webp")) return "image/webp";
+  if (filePath.endsWith(".woff2")) return "font/woff2";
+  if (filePath.endsWith(".woff")) return "font/woff";
   if (filePath.endsWith(".json")) return "application/json; charset=utf-8";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/dashboard/routes.ts` around lines 75 - 86, contentType() is
missing MIME mappings for common Astro font assets, so .woff2/.woff/.ttf files
currently fall through to application/octet-stream. Update the contentType
function to recognize these font extensions alongside the existing
html/js/css/image cases and return the correct font MIME types, keeping the
change localized to contentType().
packages/core/test/dashboard-activity.test.ts (1)

145-272: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared dashboard test fixtures into a common helper module.

testApp, httpOptions, testContext, tempDir, dashboardGet/dashboardPost/appPost, approvalCode, and authenticatedDashboard are duplicated almost verbatim across this file and dashboard-api.test.ts, dashboard-catalog.test.ts, dashboard-runtime.test.ts, and dashboard-vault.test.ts. Any future change to the login/auth bootstrap flow requires touching five files in lockstep.

Consider extracting a shared packages/core/test/dashboard-test-helpers.ts exporting these functions, parameterized where contexts diverge (e.g., authDir, globalCapletsRoot).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/dashboard-activity.test.ts` around lines 145 - 272, The
dashboard test setup and request helpers are duplicated across multiple test
files, so move the shared login/bootstrap and HTTP utilities into a common
helper module. Extract authenticatedDashboard, dashboardGet, dashboardPost,
appPost, approvalCode, testApp, httpOptions, testContext, and tempDir into a
shared dashboard-test-helpers module, and parameterize any environment-specific
pieces like authDir or globalCapletsRoot so dashboard-api.test.ts,
dashboard-catalog.test.ts, dashboard-runtime.test.ts, dashboard-vault.test.ts,
and dashboard-activity.test.ts can reuse the same helpers.
scripts/check-package-runtime.mjs (1)

95-100: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No timeout on child-process shutdown wait.

If the spawned serve child doesn't exit promptly after SIGTERM (e.g., hung shutdown), this await has no fallback and could hang CI indefinitely.

♻️ Proposed fix: add a SIGKILL fallback with timeout
   } finally {
     if (child.exitCode === null) {
       child.kill("SIGTERM");
-      await new Promise((resolve) => child.once("exit", resolve));
+      await Promise.race([
+        new Promise((resolve) => child.once("exit", resolve)),
+        new Promise((resolve) => setTimeout(resolve, 5000)),
+      ]);
+      if (child.exitCode === null) child.kill("SIGKILL");
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-package-runtime.mjs` around lines 95 - 100, The shutdown path
in the finally block can hang forever because it waits unconditionally on the
child process after child.kill("SIGTERM"). Update the child-process cleanup in
check-package-runtime.mjs to wait for exit with a timeout, and if the serve
child still hasn’t exited after that window, escalate by sending SIGKILL and
then await termination. Keep the fix localized to the child exit handling so the
serve process is always cleaned up even when SIGTERM is ignored.
packages/core/src/remote/server-credential-store.ts (1)

354-381: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename colliding helper functions to avoid method/function name collision.

The public class methods denyPendingLoginFlow and approvePendingLoginFlow (dashboard flowId-based actions) share exact names with unrelated module-level helper functions of different signatures (denyPendingLoginFlow(flow, now) / approvePendingLoginFlow(flow, grantedRole, now)). Bare calls inside the class currently resolve correctly to the module functions, but this is a confusing footgun — a future edit accidentally qualifying the call with this. would silently invoke the wrong function with mismatched arguments.

♻️ Suggested rename
-function denyPendingLoginFlow(flow: StoredPendingLogin, now: Date): void {
+function transitionPendingLoginToDenied(flow: StoredPendingLogin, now: Date): void {
   if (flow.status !== "pending") {
     throw new CapletsError("AUTH_FAILED", `Pending login is already ${flow.status}.`);
   }
   if (Date.parse(flow.flowExpiresAt) <= now.getTime()) {
     flow.status = "expired";
     throw new CapletsError("AUTH_FAILED", "Pending login has expired.");
   }
   flow.status = "denied";
   flow.deniedAt = now.toISOString();
 }

-function approvePendingLoginFlow(
+function transitionPendingLoginToApproved(
   flow: StoredPendingLogin,
   grantedRole: RemoteClientRole | undefined,
   now: Date,
 ): void {
   ...
 }

And update the two call sites (lines 364 and 449 / 377) accordingly.

Also applies to: 442-453, 1046-1075

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/remote/server-credential-store.ts` around lines 354 - 381,
Rename the module-level helper functions that collide with the class methods
`denyPendingLoginFlow` and `approvePendingLoginFlow` in `ServerCredentialStore`
so the dashboard actions and internal flow mutators are clearly distinct. Update
the call sites inside the methods `denyPendingLogin` and `denyPendingLoginFlow`
(and the matching approve path) to use the new helper names, keeping the class
method names unchanged and avoiding any ambiguity with `this.` resolution.
apps/dashboard/tsconfig.json (1)

6-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

baseUrl is deprecated in TypeScript 6.0 — paths alone should suffice.

TypeScript 6.0's release notes state "--baseUrl is deprecated" and "The fix is to move the mapping into the paths setting, which has handled this on its own for a long time." Since paths: { "@/*": ["src/*"] } is already present, baseUrl and the ignoreDeprecations suppression it requires can likely be dropped.

♻️ Suggested fix
   "compilerOptions": {
     "jsx": "react-jsx",
     "types": ["astro/client"],
-    "baseUrl": ".",
     "paths": {
       "`@/`*": ["src/*"]
-    },
-    "ignoreDeprecations": "6.0"
+    }
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/tsconfig.json` around lines 6 - 10, The tsconfig configuration
still uses the deprecated baseUrl setting and the ignoreDeprecations workaround,
even though the existing paths mapping already handles the alias. Remove baseUrl
from the dashboard tsconfig and also drop ignoreDeprecations if it is only there
to suppress that deprecation; keep the `@/`* path mapping intact so module
resolution continues to work.
apps/dashboard/src/components/DashboardHead.astro (1)

13-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sentinel-based base-path detection is coupled to a duplicated magic literal.

The script infers "configured vs. default" base path by comparing the meta tag's content against the hardcoded literal "/dashboard" (Line 47), which duplicates the default set on Line 13. If that default value is ever changed on Line 13 without updating Line 47 (or vice versa), server-configured non-root base paths would silently be misdetected as "unconfigured," falling back to client-side inference.

This assumes some server-side step rewrites the meta content for non-root deployments before serving — that isn't visible in this file set, so worth confirming the actual mechanism is not broken by this coupling.

♻️ Suggested fix: derive the sentinel from a single source
+  const DEFAULT_BASE_PATH = "/dashboard";
   const dashboardBasePath = normalizeBasePath(
-    dashboardBasePathMeta instanceof HTMLMetaElement && dashboardBasePathMeta.content !== "/dashboard"
+    dashboardBasePathMeta instanceof HTMLMetaElement && dashboardBasePathMeta.content !== DEFAULT_BASE_PATH
       ? dashboardBasePathMeta.content
       : inferDashboardBasePath(),
   );

Also applies to: 46-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/src/components/DashboardHead.astro` at line 13, The base-path
sentinel check in DashboardHead.astro is coupled to the hardcoded default
"/dashboard", which can drift from the meta tag default. Update the base-path
detection logic in the script that reads the caplets-dashboard-base-path meta
tag to derive the “default vs configured” sentinel from a single shared source
used by both the meta declaration and the comparison, so the default cannot get
out of sync with the fallback logic.
apps/dashboard/src/styles/globals.css (1)

31-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stylelint flags font-family keyword casing (likely a config/rule tuning issue, not a real defect).

Stylelint's value-keyword-case rule wants "Inter"/"BlinkMacSystemFont" lowercased, but these are conventional platform font-stack names (matching the widely-used -apple-system, BlinkMacSystemFont convention). Consider excluding font-family values from this rule instead of lowercasing brand/system font names.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/src/styles/globals.css` around lines 31 - 33, The Stylelint
warning comes from the `value-keyword-case` rule incorrectly treating the
`--font-sans` font stack in `globals.css` as a casing defect. Update the
Stylelint configuration or rule tuning so font-family values are excluded from
this check, and keep the existing `Inter` and `BlinkMacSystemFont` names
unchanged in the `--font-sans` variable rather than lowercasing them.

Source: Linters/SAST tools

apps/dashboard/src/components/ui/dialog.tsx (1)

10-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Custom Tab-trap likely duplicates base-ui's built-in focus trap.

@base-ui/react Dialog wraps Popup in FloatingFocusManager and traps focus automatically whenever modal is true/'trap-focus' (the default). Reimplementing trapTabKey on top of that risks two focus-trap implementations fighting each other (e.g. both calling preventDefault/redirecting focus on the same Tab press) and adds maintenance surface for behavior the library already provides.

♻️ Suggested simplification
-const focusableSelector =
-  "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])";
-
-function trapTabKey(event: React.KeyboardEvent<HTMLElement>, container: HTMLElement | null) {
-  ...
-}
-
 function DialogContent({
   className,
   children,
   showCloseButton = true,
-  onKeyDown,
   ...props
 }: DialogPrimitive.Popup.Props & {
   showCloseButton?: boolean;
 }) {
-  const contentRef = React.useRef<HTMLDivElement | null>(null);
   return (
     <DialogPortal>
       <DialogOverlay />
       <DialogPrimitive.Popup
-        ref={contentRef}
         data-slot="dialog-content"
         aria-modal="true"
         className={cn(...)}
-        onKeyDown={(event) => {
-          trapTabKey(event, contentRef.current);
-          onKeyDown?.(event);
-        }}
         {...props}
       >

If there's a known gap in base-ui's trap (e.g. specific to 'trap-focus' mode) that motivated this custom logic, please confirm — otherwise this can be removed.

Also applies to: 80-83

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/src/components/ui/dialog.tsx` around lines 10 - 28, The custom
tab trapping logic in trapTabKey is duplicating the focus management already
provided by `@base-ui/react` Dialog/FloatingFocusManager when modal trapping is
enabled. Remove the manual Tab key handling from this dialog component and rely
on the library’s built-in focus trap; if there is a specific gap in the default
trap behavior, document it and keep only the minimal exception in the dialog
implementation.
apps/dashboard/src/components/ui/sheet.tsx (1)

8-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the custom Tab trap

Dialog.Popup already traps Tab/Shift+Tab in modal mode, so trapTabKey duplicates built-in focus management and can drift from the dialog’s own tabbable-element handling. Removing it would keep the sheet aligned with Base UI’s native behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/dashboard/src/components/ui/sheet.tsx` around lines 8 - 26, Remove the
custom Tab focus trap logic in the sheet component because Dialog.Popup already
handles Tab/Shift+Tab in modal mode. Delete the trapTabKey helper and any
focusableSelector-based manual cycling in sheet.tsx, and rely on the
Dialog.Popup built-in focus management so the Sheet stays consistent with Base
UI behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/dashboard/activity-log.ts`:
- Around line 92-101: Cursor pagination in activity-log is moving the wrong
direction: `getEntries` slices to entries after `input.after`, which returns
newer items even though results are reversed newest-first and `nextCursor` is
based on `page.at(-1)`. Update the `ActivityLog.getEntries` cursor handling so
`after` selects entries older than the cursor (the side before the matched id in
the stored oldest→newest order), then keep the current reverse/page/nextCursor
flow consistent with `readEntries`, `input.after`, and `nextCursor`.

In `@packages/core/src/remote/server-credential-store.ts`:
- Around line 629-643: The changeClientRole method is incorrectly updating
lastUsedAt when only the client role changes, which makes idle clients appear
recently active. Update changeClientRole in RemoteCredentialStore so it only
changes the role and persists state without touching lastUsedAt; if you need a
separate audit trail, add a distinct field or event instead. Keep clientStatus
and the dashboard’s Access section semantics aligned by reserving lastUsedAt for
actual credential usage only.

---

Nitpick comments:
In `@apps/dashboard/src/components/DashboardHead.astro`:
- Line 13: The base-path sentinel check in DashboardHead.astro is coupled to the
hardcoded default "/dashboard", which can drift from the meta tag default.
Update the base-path detection logic in the script that reads the
caplets-dashboard-base-path meta tag to derive the “default vs configured”
sentinel from a single shared source used by both the meta declaration and the
comparison, so the default cannot get out of sync with the fallback logic.

In `@apps/dashboard/src/components/ui/dialog.tsx`:
- Around line 10-28: The custom tab trapping logic in trapTabKey is duplicating
the focus management already provided by `@base-ui/react`
Dialog/FloatingFocusManager when modal trapping is enabled. Remove the manual
Tab key handling from this dialog component and rely on the library’s built-in
focus trap; if there is a specific gap in the default trap behavior, document it
and keep only the minimal exception in the dialog implementation.

In `@apps/dashboard/src/components/ui/sheet.tsx`:
- Around line 8-26: Remove the custom Tab focus trap logic in the sheet
component because Dialog.Popup already handles Tab/Shift+Tab in modal mode.
Delete the trapTabKey helper and any focusableSelector-based manual cycling in
sheet.tsx, and rely on the Dialog.Popup built-in focus management so the Sheet
stays consistent with Base UI behavior.

In `@apps/dashboard/src/styles/globals.css`:
- Around line 31-33: The Stylelint warning comes from the `value-keyword-case`
rule incorrectly treating the `--font-sans` font stack in `globals.css` as a
casing defect. Update the Stylelint configuration or rule tuning so font-family
values are excluded from this check, and keep the existing `Inter` and
`BlinkMacSystemFont` names unchanged in the `--font-sans` variable rather than
lowercasing them.

In `@apps/dashboard/tsconfig.json`:
- Around line 6-10: The tsconfig configuration still uses the deprecated baseUrl
setting and the ignoreDeprecations workaround, even though the existing paths
mapping already handles the alias. Remove baseUrl from the dashboard tsconfig
and also drop ignoreDeprecations if it is only there to suppress that
deprecation; keep the `@/`* path mapping intact so module resolution continues to
work.

In `@packages/core/src/dashboard/routes.ts`:
- Around line 75-86: contentType() is missing MIME mappings for common Astro
font assets, so .woff2/.woff/.ttf files currently fall through to
application/octet-stream. Update the contentType function to recognize these
font extensions alongside the existing html/js/css/image cases and return the
correct font MIME types, keeping the change localized to contentType().

In `@packages/core/src/remote/server-credential-store.ts`:
- Around line 354-381: Rename the module-level helper functions that collide
with the class methods `denyPendingLoginFlow` and `approvePendingLoginFlow` in
`ServerCredentialStore` so the dashboard actions and internal flow mutators are
clearly distinct. Update the call sites inside the methods `denyPendingLogin`
and `denyPendingLoginFlow` (and the matching approve path) to use the new helper
names, keeping the class method names unchanged and avoiding any ambiguity with
`this.` resolution.

In `@packages/core/src/serve/http.ts`:
- Around line 784-802: The dashboardEvents SSE handler currently returns a
single runtime_health payload and immediately closes, which causes EventSource
to reconnect in a loop. Update the app.get(paths.dashboardEvents) handler to
keep the stream open by sending a proper ongoing SSE response (or, if it must
remain one-shot, add an explicit retry hint and document the behavior) so
validateDashboardSession and the runtime_health event flow align with a live
stream instead of a dropped connection.

In `@packages/core/test/dashboard-activity.test.ts`:
- Around line 145-272: The dashboard test setup and request helpers are
duplicated across multiple test files, so move the shared login/bootstrap and
HTTP utilities into a common helper module. Extract authenticatedDashboard,
dashboardGet, dashboardPost, appPost, approvalCode, testApp, httpOptions,
testContext, and tempDir into a shared dashboard-test-helpers module, and
parameterize any environment-specific pieces like authDir or globalCapletsRoot
so dashboard-api.test.ts, dashboard-catalog.test.ts, dashboard-runtime.test.ts,
dashboard-vault.test.ts, and dashboard-activity.test.ts can reuse the same
helpers.

In `@scripts/check-package-runtime.mjs`:
- Around line 95-100: The shutdown path in the finally block can hang forever
because it waits unconditionally on the child process after
child.kill("SIGTERM"). Update the child-process cleanup in
check-package-runtime.mjs to wait for exit with a timeout, and if the serve
child still hasn’t exited after that window, escalate by sending SIGKILL and
then await termination. Keep the fix localized to the child exit handling so the
serve process is always cleaned up even when SIGTERM is ignored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b2297fe-1cc8-42f1-bc98-10f20d5e8a08

📥 Commits

Reviewing files that changed from the base of the PR and between bc58da8 and 8f5f040.

⛔ Files ignored due to path filters (7)
  • apps/dashboard/public/dashboard/favicon.png is excluded by !**/*.png
  • apps/dashboard/public/dashboard/icon-header-dark.png is excluded by !**/*.png
  • apps/dashboard/public/dashboard/icon.png is excluded by !**/*.png
  • apps/dashboard/public/favicon.png is excluded by !**/*.png
  • apps/dashboard/public/icon-header-dark.png is excluded by !**/*.png
  • apps/dashboard/public/icon.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (67)
  • .changeset/admin-dashboard.md
  • .gitignore
  • CONCEPTS.md
  • CONTEXT.md
  • apps/dashboard/astro.config.mjs
  • apps/dashboard/components.json
  • apps/dashboard/package.json
  • apps/dashboard/src/components/DashboardApp.tsx
  • apps/dashboard/src/components/DashboardHead.astro
  • apps/dashboard/src/components/ui/alert.tsx
  • apps/dashboard/src/components/ui/badge.tsx
  • apps/dashboard/src/components/ui/button.tsx
  • apps/dashboard/src/components/ui/card.tsx
  • apps/dashboard/src/components/ui/dialog.tsx
  • apps/dashboard/src/components/ui/input.tsx
  • apps/dashboard/src/components/ui/select.tsx
  • apps/dashboard/src/components/ui/separator.tsx
  • apps/dashboard/src/components/ui/sheet.tsx
  • apps/dashboard/src/components/ui/sidebar.tsx
  • apps/dashboard/src/components/ui/skeleton.tsx
  • apps/dashboard/src/components/ui/sonner.tsx
  • apps/dashboard/src/components/ui/table.tsx
  • apps/dashboard/src/components/ui/tooltip.tsx
  • apps/dashboard/src/hooks/use-mobile.ts
  • apps/dashboard/src/lib/api.test.ts
  • apps/dashboard/src/lib/api.ts
  • apps/dashboard/src/lib/paths.ts
  • apps/dashboard/src/lib/utils.ts
  • apps/dashboard/src/pages/dashboard/access.astro
  • apps/dashboard/src/pages/dashboard/activity.astro
  • apps/dashboard/src/pages/dashboard/caplets.astro
  • apps/dashboard/src/pages/dashboard/catalog.astro
  • apps/dashboard/src/pages/dashboard/index.astro
  • apps/dashboard/src/pages/dashboard/runtime.astro
  • apps/dashboard/src/pages/dashboard/settings.astro
  • apps/dashboard/src/pages/dashboard/vault.astro
  • apps/dashboard/src/pages/index.astro
  • apps/dashboard/src/styles/globals.css
  • apps/dashboard/tsconfig.json
  • docs/adr/0003-remote-client-role-boundaries.md
  • docs/plans/2026-07-03-001-feat-caplets-admin-dashboard-plan.md
  • packages/core/package.json
  • packages/core/scripts/copy-dashboard-dist.mjs
  • packages/core/src/cli.ts
  • packages/core/src/cli/install.ts
  • packages/core/src/dashboard/activity-log.ts
  • packages/core/src/dashboard/auth.ts
  • packages/core/src/dashboard/catalog.ts
  • packages/core/src/dashboard/routes.ts
  • packages/core/src/dashboard/session-store.ts
  • packages/core/src/dashboard/types.ts
  • packages/core/src/remote-control/dispatch.ts
  • packages/core/src/remote/server-credential-store.ts
  • packages/core/src/remote/server-credentials.ts
  • packages/core/src/serve/http.ts
  • packages/core/test/dashboard-activity.test.ts
  • packages/core/test/dashboard-api.test.ts
  • packages/core/test/dashboard-catalog.test.ts
  • packages/core/test/dashboard-runtime.test.ts
  • packages/core/test/dashboard-session.test.ts
  • packages/core/test/dashboard-static.test.ts
  • packages/core/test/dashboard-ui.test.ts
  • packages/core/test/dashboard-vault.test.ts
  • packages/core/test/remote-login-cli.test.ts
  • packages/core/test/remote-pairing.test.ts
  • packages/core/test/serve-http.test.ts
  • scripts/check-package-runtime.mjs

Comment thread packages/core/src/dashboard/activity-log.ts
Comment thread packages/core/src/remote/server-credential-store.ts Outdated
Comment thread packages/core/src/dashboard/activity-log.ts Outdated
Comment thread packages/core/src/serve/http.ts
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/serve/http.ts (1)

177-181: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use the injected dashboard store and credential-store directory fallback.

io.dashboardSessionStore is currently only read for .dir; the instance itself is discarded. Also, an injected remoteCredentialStore without options.remoteCredentialStateDir sends dashboard state to ".", splitting it from the configured credential state.

Suggested fix
   const dashboardStateDir =
-    options.remoteCredentialStateDir ?? io.dashboardSessionStore?.dir ?? ".";
-  const dashboardSessionStore = new DashboardSessionStore({
-    dir: dashboardStateDir,
-  });
+    options.remoteCredentialStateDir ??
+    io.dashboardSessionStore?.dir ??
+    remoteCredentialStore?.dir ??
+    ".";
+  const dashboardSessionStore =
+    io.dashboardSessionStore ?? new DashboardSessionStore({ dir: dashboardStateDir });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/serve/http.ts` around lines 177 - 181, The dashboard
session setup in http.ts is ignoring the injected DashboardSessionStore instance
and can fall back to "." even when a remoteCredentialStore is configured. Update
the DashboardSessionStore initialization logic to prefer the injected
io.dashboardSessionStore instance when present, and use its dir as the fallback
for dashboard state whenever options.remoteCredentialStateDir is not set. Make
this change in the http server bootstrap where dashboardStateDir and
dashboardSessionStore are created so credential and dashboard state stay
aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/dashboard/session-store.ts`:
- Around line 178-190: The lock acquisition in `DashboardSessionStore` currently
fails immediately after a single `mkdirSync()` race, which can surface transient
503s under normal bursts. Keep the existing stale-lock recovery in the
lock-creation flow, but add a short bounded retry/wait loop around the
`mkdirSync()` attempts before throwing `CapletsError("SERVER_UNAVAILABLE",
...)`, so `lockPath()` acquisition can succeed if the lock clears moments later.

---

Outside diff comments:
In `@packages/core/src/serve/http.ts`:
- Around line 177-181: The dashboard session setup in http.ts is ignoring the
injected DashboardSessionStore instance and can fall back to "." even when a
remoteCredentialStore is configured. Update the DashboardSessionStore
initialization logic to prefer the injected io.dashboardSessionStore instance
when present, and use its dir as the fallback for dashboard state whenever
options.remoteCredentialStateDir is not set. Make this change in the http server
bootstrap where dashboardStateDir and dashboardSessionStore are created so
credential and dashboard state stay aligned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 69d2129a-3dd2-4e44-a877-d2de9224a542

📥 Commits

Reviewing files that changed from the base of the PR and between 8f5f040 and c7854a5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • apps/dashboard/package.json
  • packages/core/src/dashboard/activity-log.ts
  • packages/core/src/dashboard/session-store.ts
  • packages/core/src/remote/server-credential-store.ts
  • packages/core/src/serve/http.ts
  • packages/core/test/dashboard-activity.test.ts
  • packages/core/test/dashboard-api.test.ts
  • packages/core/test/dashboard-session.test.ts
  • packages/core/test/remote-pairing.test.ts
  • scripts/check-package-runtime.mjs
💤 Files with no reviewable changes (1)
  • apps/dashboard/package.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/core/test/remote-pairing.test.ts
  • packages/core/test/dashboard-activity.test.ts
  • packages/core/src/dashboard/activity-log.ts
  • packages/core/test/dashboard-session.test.ts
  • scripts/check-package-runtime.mjs
  • packages/core/test/dashboard-api.test.ts

Comment thread packages/core/src/dashboard/session-store.ts Outdated
Comment thread packages/core/src/serve/http.ts
@ian-pascoe ian-pascoe merged commit 11c3710 into main Jul 8, 2026
7 checks passed
@ian-pascoe ian-pascoe deleted the feat/caplets-admin-dashboard branch July 8, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant