Commit graph

5 commits

Author SHA1 Message Date
iamtoruk
62cb4f8630 test: isolate the real home on Windows and make the CLI suite pass there
os.homedir() reads USERPROFILE on Windows and never HOME, so the
isolation setup and every test that moved home were still resolving the
developer's real home directory. Fixtures came back empty, real sessions
leaked into counts, and the code under test wrote plans, receipts and a
sync ledger into the real ~/.config/codeburn during a run.

USERPROFILE now joins the redirected variables and a shared setHome()
helper moves HOME and USERPROFILE together; every spawn env carries both.
The remaining Windows-only failures were POSIX-shaped fixtures: chmod
tricks that only toggle a read-only attribute on NTFS, pid 1 as an
always-alive lock holder, splitting paths on '/', bare tar, mode-bit
expectations, and per-file timeouts too short for the cost of a tsx spawn.
Each gets a platform-aware equivalent so nothing is skipped on Windows and
the POSIX branches stay byte-for-byte what they were.

Full run on Windows: 267 files passed, 2 skipped, 3592 tests passed.
2026-09-02 13:43:57 -07:00
ozymandiashh
525b3c1d71 fix(dash): keep the disambiguator visible, make the bootstrap safe by construction
Three follow-ups from an adversarial pass over this branch.

The title cap was sized against the wrong number. 80 code points was chosen
"for both the max-w-40 legend and the tooltip", but max-w-40 is 160px and the
legend renders at text-[10px], which shows roughly 32 characters. Everything
past that is clipped -- and that is exactly where the short session id, the
provider and every collision-tier suffix lived. Two sessions in one repository
whose AI titles share a 32-character prefix rendered as the same legend entry,
which is worse than main and is the scenario #997 is about. The label now leads
with the disambiguator so it is always inside the visible width, and both the
legend and the tooltip carry title= so the full label is reachable on hover.

injectDashboardBootstrap was not safe by construction. Extracting the helper
fixed the $-substitution problem but left the security-critical '<' escaping at
the call site 94 lines away, and the new test called the helper with raw
JSON.stringify output -- so deleting that escape left every test green while
the served page became injectable through any project, device or model name.
Nothing in tests/ asserted that escaping at all. The escaping moves inside the
helper, with a test that pushes </script> through a payload value.

preferredSessionTitle picked alphabetically, not most recently. types.ts
documents title as the last ai-title entry, so when one session id yields two
summaries the legend could show the superseded one. It now picks the greatest
lastTimestamp, keeping the alphabetical order only to break exact ties so the
result stays deterministic. Entries are also ordered by key before the
collision tiers run, so the same corpus cannot emit a different label set
depending on input order.
2026-08-18 05:49:16 +03:00
ozymandiashh
b6a9622e07 fix(dash): disambiguate session legend labels, and stop $-patterns in the bootstrap
Follow-ups to the title change on this branch, all from an adversarial pass over
it.

Two series could render byte-identical labels. sessionKey is
provider/projectPath/sessionId, so the same session id under two project paths
is two series -- a session resumed in a different cwd, or two Claude config
directories holding the same project slug. The old label led with the project
so they stayed apart; leading with the title dropped the only thing separating
them. Labels are now built once at the end from per-key inputs: identical base
labels get the short project label appended, and the residual case (two
worktrees whose last two path segments match) falls back to the full path plus
full session id.

The label also depended on cache state. The hoist's comment claimed every input
was constant per sessionKey, which is false: title and project name are not in
the key, so two SessionSummary objects can share a key with different titles and
whichever arrived first won. Reproduced through the real parser -- two config
dirs, same project slug and session id, one with a title. Title candidates are
now collected per key and a valid one is preferred over none, deterministically.

The length cap sliced UTF-16 code units, so a title with an astral character on
the boundary left a lone high surrogate in the payload. It counts code points
now.

Separately, and pre-existing rather than introduced here: the web dashboard
injects its bootstrap with html.replace(string, string), so $-substitution
patterns in the replacement are interpreted. A payload value containing $` or $'
expands to the raw document around the match -- which contains </script> -- so
the '<' escaping upstream does not stop it. Device, project and model names
already reached that sink; session titles only widen the surface. The injection
is factored out and uses a replacer function.
2026-08-18 04:54:22 +03:00
ozymandiashh
5d366fb03b
feat(dashboard): add granular session and model timelines (#673)
* feat(dashboard): add granular usage timelines

* fix(dashboard): readable timeline legend and month-scale buckets

Session legend labels now use the real projectPath's last two segments
(app/web) instead of the sanitized project dir, which rendered as an
unreadable -Users-name-... dump and truncated in the legend.

Hourly buckets now cap at 8 days; longer ranges bucket daily. A 30-day
window rendered 720 overlapping hourly spikes, unreadable as a chart.

The browser no longer gives the backend session_other/model_other
aggregate a top-N slot, which rendered two identical Other legend
entries whenever the backend fold out-ranked a real series.

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-07-16 03:04:54 -07:00
ozymandiashh
7cd1f90631
fix(web): reject invalid dashboard periods without exiting (#554)
* fix(web): reject invalid dashboard periods without exiting

* test(web): assert invalid periods return 400 without exiting; drop redundant /api/devices re-parse

- Add tests/web-dashboard.test.ts: boots the dashboard on an ephemeral port and asserts
  /api/usage and /api/devices answer 400 (not process.exit) for a bad period, and that
  the server keeps serving afterward. runWebDashboard now returns the http.Server so it
  can be exercised in-process; callers that ignore the return value are unaffected.
- /api/devices: resolve periodInfo once instead of validating then re-parsing it inside
  localGetUsage (pullDevices invokes localGetUsage with the same already-validated query).

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-06-28 19:35:46 +02:00