Skip to content

feat(cli): offline mode β€” direct SQLite reads when the server is down, explicit refusals otherwise#214

Merged
hoangsonww merged 3 commits into
feat/ccam-start-indicatorfrom
feat/ccam-offline-mode
Jul 8, 2026
Merged

feat(cli): offline mode β€” direct SQLite reads when the server is down, explicit refusals otherwise#214
hoangsonww merged 3 commits into
feat/ccam-start-indicatorfrom
feat/ccam-offline-mode

Conversation

@hoangsonww

@hoangsonww hoangsonww commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Stacked on #213 (merge that first; this PR's base is feat/ccam-start-indicator). With the server stopped, the CLI now does as much as can be done correctly without it β€” and says exactly why for the rest.

Offline capability matrix

Falls back to direct data/dashboard.db reads (with ⚠ Offline mode banner) Refuses with the printed reason
sessions Β· session <id> (all but the cost line) Β· agents Β· events Β· kanban Β· stats Β· pricing (list) Β· alerts (list) Β· rules Β· export ("exported_offline": true) Β· doctor (offline variant) tail (live capture) Β· analytics/workflows/runs/cost (server-side aggregation + pricing math) Β· alerts ack Β· webhooks Β· pricing set/delete/reset Β· import Β· cleanup Β· clear-data Β· reinstall-hooks Β· info Β· health

Mechanics

  • api() throws ServerDownError instead of exiting; the dispatcher routes to the command's offline handler or a reasoned refusal (No offline fallback for this command: <reason> + the standard ccam start guidance from feat(cli): server up/down indicator + ccam start / ccam statusΒ #213).
  • Offline data providers produce API-shaped payloads and feed the same renderers as the online path (extracted renderStats/Sessions/Agents/Events/Kanban) β€” identical output.
  • SQLite connection: repo's better-sqlite3 (falls back to node:sqlite), never creates a file, SELECT-only by construction, and deliberately opened without SQLite's readonly flag β€” a strict readonly connection can't attach a live WAL's shared-memory index and silently reads stale pre-WAL state (caught by test); a normal connection under WAL reads consistently.

Tests & docs

  • CLI suite grows to 46 (full server suite 625/625): offline banner + parity per command, refusal reasons (cost/tail/analytics/pricing-set), clear-data --yes refused offline with data verified intact, offline-export markers.
  • Docs: docs/CLI.md Offline Mode section + matrix; README + VN + CN notes; ARCHITECTURE row; wiki CLI section paragraph with zh/vi/ko translations (cache wiki-v34, i18n ?v=25).

Type of Change

  • New feature (non-breaking change that adds functionality)

Terminal UI Overhaul (second commit)

9a2ab7e upgrades ccam's output into a full terminal UI β€” still dependency-free and 100% script-safe:

  • Color system β€” on for TTYs, off when piped; NO_COLOR / --no-color (anywhere on the command line) disable, FORCE_COLOR / CCAM_COLOR=1 force.
  • Box-drawn tables β€” bold headers, dim borders, right-aligned numeric columns, terminal-width fitting with ellipsis clipping, (no rows) placeholder.
  • Status icons + colors everywhere: ● active, ◐ working, β—‹ waiting, βœ” completed, βœ– error, β—¦ abandoned; per-type event colors across events / tail / session.
  • Inline bar charts β€” sessions-by-status (stats), top tools + agent types (analytics), per-model breakdown (cost).
  • Real trees β€” β”œβ”€/└─ with continuation rails for the agent hierarchy; kanban lanes with branch rows.
  • Session detail card β€” aligned key/value metadata, shared renderer for online + offline so both paths render byte-identically; session tables gain a relative Updated column.
  • ccam start spinner on a TTY (dot trail when piped); colorized help; new ccam version / --version / -v.

Data correctness is untouched: offline liveness correction, staleness caveats, and server-only refusals all preserved; info stays raw JSON for jq.

Tests: 52 CLI tests (4 new: version, --no-color placement, no-ANSI-when-piped, box borders + Updated column); full backend suite 631/631. Docs: docs/CLI.md output section rewritten (color-rules table), README + VN + CN, ARCHITECTURE.md, wiki feature card + CLI intro in EN/zh/vi/ko with service-worker cache bump (wiki-v35, i18n ?v=26).

How to Test

  1. Stop the server. ccam sessions / stats / kanban / doctor β†’ offline banner + real data; ccam cost / tail / clear-data --yes β†’ reasoned refusal.
  2. Start the server β€” identical commands produce identical tables without the banner.
  3. npm run test:server β€” 631/631.
  4. In a real terminal: ccam stats, ccam kanban, ccam session <id>, ccam analytics, ccam cost β€” colored tables/charts/trees; pipe any of them to cat and the output is plain text.

Checklist

  • I have read the contributing guidelines
  • I have signed the CLA (the πŸ–‹οΈ CLA Assistant bot will prompt me on my first PR)
  • My code follows the project's coding standards
  • I have added/updated tests that prove my fix or feature works
  • All new and existing tests pass (npm test)
  • Code is formatted (npm run format:check)
  • I have updated documentation where necessary

πŸ€– Generated with Claude Code

https://claude.ai/code/session_0146oeseRLX17wL3iTqc1trq

…, explicit refusals otherwise

With the dashboard server stopped, the CLI previously refused everything.
Now every command that can run correctly without the server does, and the
rest say exactly why they can't:

- Read-only fallback: sessions, session <id>, agents, events, kanban,
  stats, pricing (list), alerts (list), rules, export, and doctor fall
  back to reading data/dashboard.db directly under an explicit banner
  ("⚠ Offline mode β€” … data as of the last capture; live capture needs
  the server: ccam start"). The offline data providers produce the same
  shapes as the API responses and feed the exact renderers the online
  path uses (extracted renderStats/renderSessions/renderAgents/
  renderEvents/renderKanban), so output is identical. session <id> shows
  everything but the cost line, which is flagged as server-only; offline
  exports carry "exported_offline": true; offline doctor reports the
  server as down plus DB size/row counts and hook installation read from
  ~/.claude/settings.json.

- Server-only refusals with reasons: tail (live capture), analytics /
  workflows / runs / cost (server-side aggregation and pricing math),
  alerts ack, webhooks, pricing mutations, import, cleanup, clear-data,
  reinstall-hooks, info, health β€” each prints "No offline fallback for
  this command: <reason>" above the standard start guidance.

- Mechanics: api() now throws ServerDownError instead of exiting; the
  dispatcher routes it to the command's offline handler or the reasoned
  refusal. The SQLite connection comes from the repo's better-sqlite3
  (node:sqlite fallback), never creates a file, is SELECT-only by
  construction, and is deliberately opened WITHOUT SQLite's readonly
  flag: a strict readonly connection cannot attach a live WAL's
  shared-memory index and silently reads the stale pre-WAL state β€” a
  normal connection under WAL reads consistently (bug found by test).

Tests: CLI suite grows to 46 β€” an offline describe block covers the
fallback banner + parity for every offline command, the cost/tail/
analytics/pricing-set/clear-data refusals (data verified intact), and
offline export markers. Full server suite green.

Docs: docs/CLI.md gains an Offline Mode section with the capability
matrix; README + VN + CN notes; ARCHITECTURE bin/ccam.js row expanded;
wiki CLI section gains the lifecycle/offline paragraph with zh/vi/ko
translations (cache wiki-v33 -> v34, i18n ?v=24 -> 25).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeseRLX17wL3iTqc1trq
Copilot AI review requested due to automatic review settings July 8, 2026 11:15
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed question Further information is requested labels Jul 8, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces an "Offline Mode" for the ccam CLI, allowing read-only commands (such as stats, sessions, agents, events, kanban, rules, alerts, export, and doctor) to fall back to reading the SQLite database directly when the dashboard server is down. Commands requiring server-side logic or mutations are rejected with specific reasons. The feedback highlights two issues in bin/ccam.js: first, a date comparison bug in the offline stats query where comparing SQLite's created_at format with an ISO string fails lexicographically; second, a usability issue in the offline session command where partial session ID lookups are not supported, and subsequent queries do not use the resolved full session UUID.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread bin/ccam.js
Comment on lines +339 to +342
events_today: one(
"SELECT COUNT(*) AS n FROM events WHERE created_at >= ?",
midnight.toISOString()
),

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.

high

In SQLite, created_at is stored as a TEXT field in the format YYYY-MM-DD HH:MM:SS (without the T separator or timezone suffix). Comparing it directly against midnight.toISOString() (which produces a string like YYYY-MM-DDTHH:MM:SS.sssZ) will fail lexicographically because the space character ( ) is smaller than T. This results in events_today always returning 0.

Convert the ISO string to the database's format by replacing T with a space and slicing to 19 characters.

Suggested change
events_today: one(
"SELECT COUNT(*) AS n FROM events WHERE created_at >= ?",
midnight.toISOString()
),
events_today: one(
"SELECT COUNT(*) AS n FROM events WHERE created_at >= ?",
midnight.toISOString().replace("T", " ").slice(0, 19)
),

Comment thread bin/ccam.js
Comment on lines +1033 to +1065
const rows = db.all("SELECT * FROM sessions WHERE id = ?", id);
if (!rows.length) {
console.error(c.red(`βœ– Session not found: ${id}`));
process.exit(1);
}
const s = rows[0];
console.log(`${c.bold(s.name || s.id)} ${c.dim(`(${s.id})`)}`);
console.log(
`status ${colorStatus(s.status)} Β· model ${fmtModel(s.model)} Β· ` +
`duration ${fmtDuration(s.started_at, s.ended_at)} Β· cwd ${s.cwd || "-"}`
);
console.log(c.dim("cost: requires the server (pricing math runs server-side)"));
const agents = db.all("SELECT * FROM agents WHERE session_id = ? ORDER BY started_at ASC", id);
if (agents.length) {
console.log(`\n${c.bold("Agents")} (${agents.length})`);
const byParent = {};
for (const a of agents) (byParent[a.parent_agent_id || ""] ||= []).push(a);
const walk = (parentId, depth) => {
for (const a of byParent[parentId] || []) {
const pad = " ".repeat(depth + 1);
const tool = a.current_tool ? c.cyan(` [${a.current_tool}]`) : "";
console.log(
`${pad}${colorStatus(a.status)} ${a.type === "main" ? c.bold(a.name) : a.name}${tool} ${c.dim(fmtDuration(a.started_at, a.ended_at))}`
);
walk(a.id, depth + 1);
}
};
walk("", 0);
}
const events = db.all(
"SELECT * FROM events WHERE session_id = ? ORDER BY created_at DESC LIMIT 10",
id
);

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.

high

When retrieving session details offline, the user might provide a partial session ID (e.g., the 8-character prefix shown in ccam sessions). If they do, the exact match id = ? will fail. Furthermore, even if the query is updated to support prefix matching, using the user-provided id directly in the subsequent agents and events queries will fail to return any results because those tables store the full session UUID.

To fix this, support prefix matching for the session lookup, and then use the resolved session's full ID (s.id) for the agents and events queries.

    const rows = db.all("SELECT * FROM sessions WHERE id = ? OR id LIKE ?", id, id + "%");
    if (!rows.length) {
      console.error(c.red("βœ– Session not found: " + id));
      process.exit(1);
    }
    const s = rows[0];
    console.log(c.bold(s.name || s.id) + " " + c.dim("(" + s.id + ")"));
    console.log(
      "status " + colorStatus(s.status) + " Β· model " + fmtModel(s.model) + " Β· " +
        "duration " + fmtDuration(s.started_at, s.ended_at) + " Β· cwd " + (s.cwd || "-")
    );
    console.log(c.dim("cost: requires the server (pricing math runs server-side)"));
    const agents = db.all("SELECT * FROM agents WHERE session_id = ? ORDER BY started_at ASC", s.id);
    if (agents.length) {
      console.log("\n" + c.bold("Agents") + " (" + agents.length + ")");
      const byParent = {};
      for (const a of agents) (byParent[a.parent_agent_id || ""] ||= []).push(a);
      const walk = (parentId, depth) => {
        for (const a of byParent[parentId] || []) {
          const pad = "  ".repeat(depth + 1);
          const tool = a.current_tool ? c.cyan(" [" + a.current_tool + "]") : "";
          console.log(
            pad + colorStatus(a.status) + " " + (a.type === "main" ? c.bold(a.name) : a.name) + tool + " " + c.dim(fmtDuration(a.started_at, a.ended_at))
          );
          walk(a.id, depth + 1);
        }
      };
      walk("", 0);
    }
    const events = db.all(
      "SELECT * FROM events WHERE session_id = ? ORDER BY created_at DESC LIMIT 10",
      s.id
    );

While the server is down its dead-session liveness reap is not running,
so the database can hold active/waiting rows for sessions that exited
after the server stopped β€” and offline mode would have printed those
stale statuses as-is.

Offline handlers now run the SAME process-liveness probe the server's
watchdog uses (server/lib/session-liveness.js, ps/lsof or /proc) and
correct the DISPLAYED status of any active session whose cwd has no
running claude process β€” sessions, session <id>, agents, kanban, and the
stats distribution all correct consistently (agent rows follow their
session), each with a footnote: "β€» N session(s) displayed as completed by
the process-liveness probe". The database is never written; persisting
the correction stays the server's job. Where the probe cannot answer
(Windows, containers, DASHBOARD_LIVENESS_PROBE=0) a "Statuses are as
stored" caveat is printed instead β€” and only when active rows are
actually shown, so quiet DBs stay quiet.

Tests: 48 CLI tests β€” new cases assert the caveat path (probe disabled)
and the correction path (real probe, skipped automatically on platforms
where the probe is unavailable), including that the stored DB status
remains untouched after a corrected display. Docs: offline-correctness
paragraph in docs/CLI.md, clauses in README + VN + CN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeseRLX17wL3iTqc1trq
Greatly enhanced ccam output for real command-line UX while staying
dependency-free and 100% script-safe:

- Color system: SGR helpers with proper nesting (bold inside color), on
  for TTYs, off when piped, NO_COLOR / --no-color (anywhere on the
  command line) to disable, FORCE_COLOR / CCAM_COLOR=1 to force.
- Box-drawn tables: bold headers, dim borders, right-aligned numeric
  columns, terminal-width fitting (widest column clipped with an
  ellipsis so frames never wrap), "(no rows)" placeholder.
- Status icons + colors everywhere a status appears (● active,
  ◐ working, β—‹ waiting, βœ” completed, βœ– error, β—¦ abandoned); event
  types get stable per-type colors across events, tail, and session.
- Inline bar charts: sessions-by-status (stats), top tools and agent
  types (analytics), per-model cost breakdown (cost); non-zero values
  always render at least one block.
- Real trees: β”œβ”€/└─ with continuation rails for the agent hierarchy in
  session <id>; kanban lanes render colored headers with branch rows.
- Session detail is a metadata card (aligned key/value) shared by the
  online and offline paths so both render byte-identically; sessions
  tables gain a relative "Updated" column.
- ccam start animates a braille spinner on a TTY (dot trail piped);
  help is colorized with section sidebars; new version command
  (--version / -v) reads package.json.

Data correctness is unchanged and conservative: offline liveness
correction, staleness caveats, and server-only refusals all preserved;
`info` remains raw JSON for jq.

Tests: 52 CLI tests (4 new: version, --no-color placement, no-ANSI when
piped, box borders + Updated column); full server suite 631/631.
Docs: docs/CLI.md output section rewritten (color rules table, charts,
trees, version), README + VN + CN CLI sections, ARCHITECTURE row, wiki
feature card + CLI intro in EN/zh/vi/ko with sw cache bump (wiki-v35,
i18n ?v=26).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeseRLX17wL3iTqc1trq
@hoangsonww hoangsonww merged commit ad8c3bd into feat/ccam-start-indicator Jul 8, 2026
17 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed question Further information is requested

Projects

Development

Successfully merging this pull request may close these issues.

2 participants