feat: add self-hosted admin dashboard#199
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
✅ Files skipped from review due to trivial changes (4)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughThis 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. ChangesCaplets Admin Dashboard
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
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.
|
|
| 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
%%{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
Reviews (6): Last reviewed commit: "docs: add public dashboard guidance" | Re-trigger Greptile
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (10)
packages/core/src/serve/http.ts (1)
784-802: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueSSE endpoint returns a single event then closes.
dashboardEventssends oneruntime_healthevent and ends the response. A browserEventSourcewill 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 aretry: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 toapplication/octet-streamand 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 winExtract shared dashboard test fixtures into a common helper module.
testApp,httpOptions,testContext,tempDir,dashboardGet/dashboardPost/appPost,approvalCode, andauthenticatedDashboardare duplicated almost verbatim across this file anddashboard-api.test.ts,dashboard-catalog.test.ts,dashboard-runtime.test.ts, anddashboard-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.tsexporting 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 winNo timeout on child-process shutdown wait.
If the spawned
servechild doesn't exit promptly afterSIGTERM(e.g., hung shutdown), thisawaithas 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 winRename colliding helper functions to avoid method/function name collision.
The public class methods
denyPendingLoginFlowandapprovePendingLoginFlow(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 withthis.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
baseUrlis deprecated in TypeScript 6.0 —pathsalone 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,baseUrland theignoreDeprecationssuppression 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 winSentinel-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 valueStylelint flags font-family keyword casing (likely a config/rule tuning issue, not a real defect).
Stylelint's
value-keyword-caserule wants"Inter"/"BlinkMacSystemFont"lowercased, but these are conventional platform font-stack names (matching the widely-used-apple-system, BlinkMacSystemFontconvention). 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 winCustom Tab-trap likely duplicates base-ui's built-in focus trap.
@base-ui/reactDialog wrapsPopupinFloatingFocusManagerand traps focus automatically whenevermodalistrue/'trap-focus'(the default). ReimplementingtrapTabKeyon top of that risks two focus-trap implementations fighting each other (e.g. both callingpreventDefault/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 winRemove the custom Tab trap
Dialog.Popupalready traps Tab/Shift+Tab in modal mode, sotrapTabKeyduplicates 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
⛔ Files ignored due to path filters (7)
apps/dashboard/public/dashboard/favicon.pngis excluded by!**/*.pngapps/dashboard/public/dashboard/icon-header-dark.pngis excluded by!**/*.pngapps/dashboard/public/dashboard/icon.pngis excluded by!**/*.pngapps/dashboard/public/favicon.pngis excluded by!**/*.pngapps/dashboard/public/icon-header-dark.pngis excluded by!**/*.pngapps/dashboard/public/icon.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (67)
.changeset/admin-dashboard.md.gitignoreCONCEPTS.mdCONTEXT.mdapps/dashboard/astro.config.mjsapps/dashboard/components.jsonapps/dashboard/package.jsonapps/dashboard/src/components/DashboardApp.tsxapps/dashboard/src/components/DashboardHead.astroapps/dashboard/src/components/ui/alert.tsxapps/dashboard/src/components/ui/badge.tsxapps/dashboard/src/components/ui/button.tsxapps/dashboard/src/components/ui/card.tsxapps/dashboard/src/components/ui/dialog.tsxapps/dashboard/src/components/ui/input.tsxapps/dashboard/src/components/ui/select.tsxapps/dashboard/src/components/ui/separator.tsxapps/dashboard/src/components/ui/sheet.tsxapps/dashboard/src/components/ui/sidebar.tsxapps/dashboard/src/components/ui/skeleton.tsxapps/dashboard/src/components/ui/sonner.tsxapps/dashboard/src/components/ui/table.tsxapps/dashboard/src/components/ui/tooltip.tsxapps/dashboard/src/hooks/use-mobile.tsapps/dashboard/src/lib/api.test.tsapps/dashboard/src/lib/api.tsapps/dashboard/src/lib/paths.tsapps/dashboard/src/lib/utils.tsapps/dashboard/src/pages/dashboard/access.astroapps/dashboard/src/pages/dashboard/activity.astroapps/dashboard/src/pages/dashboard/caplets.astroapps/dashboard/src/pages/dashboard/catalog.astroapps/dashboard/src/pages/dashboard/index.astroapps/dashboard/src/pages/dashboard/runtime.astroapps/dashboard/src/pages/dashboard/settings.astroapps/dashboard/src/pages/dashboard/vault.astroapps/dashboard/src/pages/index.astroapps/dashboard/src/styles/globals.cssapps/dashboard/tsconfig.jsondocs/adr/0003-remote-client-role-boundaries.mddocs/plans/2026-07-03-001-feat-caplets-admin-dashboard-plan.mdpackages/core/package.jsonpackages/core/scripts/copy-dashboard-dist.mjspackages/core/src/cli.tspackages/core/src/cli/install.tspackages/core/src/dashboard/activity-log.tspackages/core/src/dashboard/auth.tspackages/core/src/dashboard/catalog.tspackages/core/src/dashboard/routes.tspackages/core/src/dashboard/session-store.tspackages/core/src/dashboard/types.tspackages/core/src/remote-control/dispatch.tspackages/core/src/remote/server-credential-store.tspackages/core/src/remote/server-credentials.tspackages/core/src/serve/http.tspackages/core/test/dashboard-activity.test.tspackages/core/test/dashboard-api.test.tspackages/core/test/dashboard-catalog.test.tspackages/core/test/dashboard-runtime.test.tspackages/core/test/dashboard-session.test.tspackages/core/test/dashboard-static.test.tspackages/core/test/dashboard-ui.test.tspackages/core/test/dashboard-vault.test.tspackages/core/test/remote-login-cli.test.tspackages/core/test/remote-pairing.test.tspackages/core/test/serve-http.test.tsscripts/check-package-runtime.mjs
Preview DeployedLanding: https://pr-199.preview.caplets.dev Built from commit 1b8c84e |
There was a problem hiding this comment.
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 winUse the injected dashboard store and credential-store directory fallback.
io.dashboardSessionStoreis currently only read for.dir; the instance itself is discarded. Also, an injectedremoteCredentialStorewithoutoptions.remoteCredentialStateDirsends 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (10)
apps/dashboard/package.jsonpackages/core/src/dashboard/activity-log.tspackages/core/src/dashboard/session-store.tspackages/core/src/remote/server-credential-store.tspackages/core/src/serve/http.tspackages/core/test/dashboard-activity.test.tspackages/core/test/dashboard-api.test.tspackages/core/test/dashboard-session.test.tspackages/core/test/remote-pairing.test.tsscripts/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
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
Validation
pnpm --filter @caplets/core buildpnpm --filter caplets buildnode scripts/check-package-runtime.mjspnpm --filter @caplets/core typecheckpnpm --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.tspnpm --filter @caplets/dashboard typecheckpnpm exec vitest run apps/dashboard/src/lib/api.test.tspnpm --filter @caplets/dashboard buildSummary by CodeRabbit