Repository navigation
feat: replicate the omp-stats dashboard - #53
Merged
Merged
Conversation
Reads ~/.omp/stats.db, ~/.omp/agent/agent.db, and snapcompact savings read-only, matching the upstream omp-stats metrics: stop reasons, cache rate, cache savings, throughput, error signatures, subscription window insights, gain, and per-page view slicing.
Adds /api/omp?range=&view= with a five second memo per window and view, and /api/omp/session?file= for one transcript. The export snapshot carries one window. repo-integrity now resolves Vite asset imports, so a vendored .svg target counts as tracked.
One hash route per view, matching the upstream routes and layout page for page: a page kit (header, numbered cards, column tables, stat tiles with sparklines), the eleven omp pages, a session timeline behind &s=, and Tersio's own ledger on #/usage. Settings and Share move to the rail footer and the theme button to the topbar. Brand marks are vendored, so no page fetches a logo.
README gains the route table and the rail and topbar notes, ARCHITECTURE gains the omp-stats section and its invariants, the changelog carries the entry, and ERRORS.md records the five mistakes the build caught.
The stat strip is now one solid card with a rule between cells and inline percentage hints, replacing the translucent tiles. The segmented control is shadcn tabs, matching the Activity card.
Neither source exists on this host: snapcompact-savings.jsonl is never written, and user_messages plus frustration_verdicts are absent from stats.db. Both views, their query groups, their fixture helpers, and their tests go with them.
PagedTable replaces the row cap on the request log, failures, models, projects, tools, sessions, provider totals, windows, and accounts: one page at a time with the row count, a rows-per-page picker, and page buttons. The one look for every page now pages instead of clipping.
A null made recharts reset the stack, so every stacked area broke into disjoint shapes; the Models share chart showed a hard vertical cut. A stacked series now reads zero in a bucket with no data, and only a lone series keeps its nulls. The tooltip title came from the tick text, which read Invalid Date, and its values printed raw. The title now comes from the hovered row's own category and the values use the page formatter, so a share reads 98%. The Costs view never asked for modelSeries, so its first chart painted an empty frame.
A scripted edit rewrote the file from a stale buffer and appended a second copy of every release section. This drops the copy, so the file holds one section per release.
The drawer opens on the reference's sections: Timing, Tokens, API-equivalent cost, Agent, Identity, then the three payloads. It fetches the journal entry behind the row on demand from /api/omp/entry, so the Output message, the Session entry, and the Stats row all carry real bytes instead of an empty block. The Agent section is the twist: the agent type, the model, the thinking level, the mode, and whether the model resolved as a fallback, all read from the change entries that precede the request in its own journal. The drawer also shows the cache hit rate, each token bucket's share of the row, the API-equivalent cost per output token, and its Trace button deep-links the session.
…mance panel The Models table opens a detail row that spans every column, the shape the reference uses: Efficiency, Latency, the four token buckets, and the first and last seen ages on the left, then a per-active-day chart on the right. The chart needs throughput and first-token latency per model, provider, and bucket, which the aggregate did not read, so readModelPerformance now reads them and a model with no timing sample reads as an empty state instead of an empty frame. Two units share one plot through a new right axis on the shared chart.
A card header carries 12px over 8px with a tighter description line, a flush card drops the shell's own bottom padding, and a pager footer drops to 40px with its trigger at the page-button height. Measured on six pages: header 55px, pager footer 39px, and no dead band under a flush table.
… name The brand mark leads the model cell, the provider moves to its own dim line instead of dragging beside the name, and the marks gain a table size so a 18px glyph stops inflating the row from 35px to 49px. The requests cell carries a share bar against the busiest model, the error rate reads as a chip whose tint follows the rate with the figure kept in ink for contrast, and the per-second and first-token figures drop the unit the header already states.
…al data layer One change set across the dashboard, because five shared files (data.ts, App.tsx, dialogs.tsx, lib/carbon.ts, cli/dashboard.ts) each carry hunks from several of these modules, so any split would have produced commits that do not build. Data layer, host-neutral: - extensions/shared/omp-stats.ts is now agent-stats.ts, the Omp* types are Agent*, useOmpData is useAgentData, the pages move from components/omp to components/agents, the API is /api/agent, and the seams are TERSIO_AGENT_STATS_DB and TERSIO_AGENT_SESSIONS_DB. - The reader still opens omp's own databases; the naming no longer claims otherwise. Brand marks: - One catalog in extensions/shared/brand-marks.ts names every mark's URL: Simple Icons, the owner-hosted marks, the owner avatars, and the two marks no host carries. The vendored SVG/PNG/WebP files, the fetch script, and SOURCES.md are gone, and the dashboard export inlines the same catalog once. - A tile fills with the brand's own color and takes the opposite ink by WCAG luminance, so DeepSeek blue, Meta blue, Kimi blue, Xiaomi orange and Mistral orange read white while Kilo yellow and StepFun teal read black. Groq draws as a masked glyph because its own favicon is one flat orange. - Each tile now carries the same visible size of logo: 44px in a detail view, 36px in a drawer, 30px with a 23px glyph in a table row, and Kimi, Z.ai and OpenRouter scale down to match while Magpie scales up. Pages: - Carbon page: the estimate per model, the equivalence figures, and the By model table with the provider under the model name. - Costs: Where it went is the billing-component breakdown the reference shows, priced per component from the same records, and the chart card takes the 2fr side of the split the Providers page uses. - Providers: a row expands in place to its token mix, both burn charts share one palette, and every picker renders its label instead of the raw value. - A page table chains its wheel to the page again, and a page newer than the running server says so instead of blaming a missing database. Share: - The share action is back in the topbar beside the theme button, with a card built from the agent statistics database for the window in force. Removed: - The Usage page and everything that existed only to show Tersio's own ledger: /data.json, /reset, /backups, the ledger /export, the hero, savings bento, activity chart, ledger tables, share card, and reset pane. The ledger stays a product: tersio usage, tersio reset, and the backups still work.
…e remaining discipline
…ped changelog lines
15 of 18 tasks
…ed section already covers
…INSTALL.md's doctor output
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The dashboard replicates the upstream
@oh-my-pi/omp-statsdashboard page for page, and adds the parts Tersio needs on top of it. A later stretch of this branch rebuilds usage pricing on top of that work, described under Usage pricing.Each view is one hash-routed page with the reference's own header, stat tiles, charts, and tables, read from omp's databases without ever writing to them. The routes match the reference exactly, so
#/providers?range=7dand#/traces?range=7d&s=<sessionFile>work in either dashboard.On top of the replication: the Traces session page is a flamegraph with the reference's axis choice, span search, legend, and brush; the share card builds a GitHub-style contribution graph and copies itself as PNG; Settings gains a Data pane that snapshots every file Tersio keeps and restores one behind a confirm; the rail footer holds the credits and the social marks.
Changes Made
extensions/shared/agent-stats.ts: one host-neutral aggregate over~/.omp/stats.db,~/.omp/agent/agent.db, and~/.omp/snapcompact-savings.jsonl, matching the reference metric for metric: stop reasons, cache rate, throughput, error signatures, subscription window insights, snapcompact gain, request percentiles, per-provider hourly burn, and session transcripts. Aviewargument selects the query groups a page needs, so a page payload stays in the tens of kilobytes. It also folds every subagent transcript into its root session, and derives the span model a flamegraph needs from the transcript itself.cli/backups.tsandcli/dashboard.ts: a snapshot is a directory with a manifest, so one snapshot holds the usage ledger, the reset watermark, the settings defaults, both price caches, the agent stats and quota databases, and the command history./api/datacreates, lists, restores, and deletes one, and both destructive moves ask first: a restore names the files it copies back, a delete names the files going with it. The four WAL failure modes (-readonlywithout side files,immutable=1over a live log, a plain open creating side files) are handled in order instead of failing or silently reading stale rows.extensions/shared/usage-ledger.tsandextensions/shared/usage-store.ts: the ledger records the agent kind beside the host. A transcript withparentSessionon its ownsessionrow is a subagent, and that kind lands in the newacolumn, so a subagent's work reads apart from its root's. A store written before this branch gains the column with a bareALTER, so no rows move and none are lost.tersio usageprints a BY AGENT block. An OpenCode row carries no subagent marker, so it records as the main agent rather than as a NULL.dashboard/app: a page kit (page header, card, column-definition tables with sorting and paging, stat tiles with inline sparklines, meters) and the pages built on it, plus the session flamegraph behind&s=and the Settings panes. Settings and Share move to the rail footer; the theme button sits at the top right of the topbar.dashboard/app/src/components/common.tsx: onetoolIconmap names the glyph a tool name carries, so a transcript row reads as its verb rather than borrowing a vendor mark.dashboard/app/src/lib/format.ts:hostOfSessionnow returnsomp,pi, oropencode, the same idsAgentLogoswitches on, so the request drawer carries the host's own brand art instead of a terminal glyph.dashboard/app/src/lib/carbon.ts: the What that equals card runs ten comparisons, each with the published rate it used, seven visible and the rest scrolling.dashboard/app/src/components/charts/stats.tsx: each breakpoint tier names its own cells, so a stat band draws a rule between every pair at a phone width, a tablet width, and a desktop width.cli/install.ts: omp merges the plugin manifest with the extension directory it discovers by default, so Tersio extensions lived in two trees at once. Every OMP mode extension now writes into the plugin tree and the agent-tree copies an older build wrote are dropped, withrtk.tskept whereconfig.ymlregisters it.cli/manifest.tsandcli/doctor-fix.ts: the two doctor repair paths each walkedTREE_FILESreading the bundled source and writing it out, sosyncTreeFilesholds that loop for both. The rule-body skip rides along as a predicate, so an upstream fetch newer than the bundled copy is never overwritten by a repair.withResolvers,missingTreeFiles,EXTENSIONS_KEY_RE);AgentWindowPoint, which was declared twice thirty lines apart inagent-stats.ts, is now declared once;doctor.tstakes the installed plugin path fromompPackageDir()instead of rebuilding it; seven comment blocks were trimmed with every distinct fact kept; and nine tests were removed where they asserted a constant, a pure function equalling itself, a static literal map, or a case another file already covers.docs/ERRORS.mdentries for the mistakes the build caught.Usage pricing
tersio usagerepriced every model from the LiteLLM feed and discarded the per-message cost the host itself reports. On this machine the report read $373.49 where the true figure is $71.23, an overstatement of $302.27.kilo/stepfun/step-5-preview, 1.46B tokens, was billed at the $2/M Sonnet default for $368.94 against the $69.08 the host reported for those tokens.space-bunny,muse-spark-1.3-contributor,glm-5.3-flash,qwen38-flash-next,stealth/ox-alpha,MiniMax-M3,DeepSeek-V4-Flash,ling-3.0-flash-sante,Qwen3.8-27Band more) each paid $2/M for tokens that cost nothing, because the feed carries no row for any of them.The information was already being read.
costOf()parsedusage.cost,accum.costMeasuredsummed it, andusage.dbstored it.summarizeUsagethen threw it away and repriced instead.Each price hit now carries a tier, and
PRICE_TIERS_FIRMnames the firm ones:measured(the host's own cost for those tokens),override(the operator's file),exact, andaliasall read as prices;tailanddefaultread as estimates.usdCostreturnspricedfrom the tier alone, never from the fact of a match, so an estimate is summed apart from measured cost.That a recorded zero is a real zero was verified rather than assumed:
cost_unpricedis 0 across all 29,749 rows in~/.omp/stats.db, and no row is both unpriced-flagged and zero-cost.A model is host-priced only when its measured coverage matches its message count. Where the counts diverge,
usdCostfalls back to a rate card rather than reading the unpriced messages as zero, which is the one error worse than a guess.One model key answers for every host. A model reads as the same key whichever agent ran it, so one operator entry prices it everywhere. Verified: omp, pi and opencode spell the same model three ways, all three fold to one key, the per-host split survives the fold, and one key yields one resolution rather than three.
~/.tersio/prices.local.jsonholds the operator's own rates, keyed either as the transcript spells the model or with a*prefix that covers every gateway prefix. It merges over the feed cache, and the model memo keys on that file's mtime, so an edit applies on the next lookup. When models still fall to a guess,tersio usagenames the five heaviest rather than counting them.Cache savings is removed. It read
(noCacheCost - cachedCost) / noCacheCost, sourced fromcost_no_cache_input, which omp leaves at zero in 26,176 of 29,749 rows and populates only fordeepseek. The denominator was near-always zero, so the dashboard rendered a confident0.0%built from no data — worse than an absent panel, because a reader sees a number. It went as a tile, a stat hint and a table column, andnoCacheCostandcachedCostwent with them. Cache rate stays.Screenshots/Recordings
#/overview,#/models,#/providers,#/costs,#/requests,#/errors,#/traces,#/tools,#/frustration,#/projects,#/gaintersio usagereporting $373.49 of estimated costRun
tersio dashboard --openand compare with the reference at127.0.0.1:3847on the same route and window.Testing
bun run verifyat this head: frozen lockfile, build green, 59 test files and 420 tests passed.tailanddefaultreport as estimates, an override covers a gateway the feed keys under a different upstream, and partially covered measured cost falls back to a rate card instead of reading zero.test/cli/update-command.test.tsis hermetic: a finishedtersio updaterefreshes the price cache under a fifteen-second window, which outran the child's spawn timeout on a loaded machine and failed the test for a reason unrelated to update. The fetch now points at a closed port and the cache at a throwaway file.1h,24h,7d,30d,90d, and all time, including the subscription window insights.~/.tersio/usage.dbholdsomp|main 6,361andomp|subagent 1,692with no NULL kinds, against omp's 25,694 main and 2,559 subagent.rule.mdsurvived adoctor --fix extensionswhose bundled copy is 5990 bytes,rule-ultra.mdsurvived with it, all nineteen tree files landed, and the stubbedindex.tswas restored from the bundle.isAnimationActive={true}and watching it fail, then reverting.oxlintexits 0 on both packages, and the new files add no warnings.Additional Notes
sessionsDirs()reads omp's and pi's directories andhostOfSessionnames omp, pi, and OpenCode. The aggregate is omp-only, becausestatsDbPathandagentDbPathdefault to~/.ompand omp's schema is the only one carryingagent_type, the cost columns,tool_calls, and the quota windows. So Overview, Models, Providers, Costs, Requests, Errors, Tools, and Carbon read omp's database only; pi and OpenCode work reaches the dashboard through the session list and its flamegraph.extensions/shared/omp-stats.tsisagent-stats.ts, theOmp*types areAgent*,/api/ompis/api/agent, and the seams areTERSIO_AGENT_STATS_DBandTERSIO_AGENT_SESSIONS_DB. The reader still opens omp's own databases; the naming no longer claims other hosts.tersio usage,tersio reset, andtersio dashboard --exportstill work, and the CLI still owns~/.tersio/usage.db.api/agentevery 30 seconds while the tab is visible and on focus; the server memoizes the aggregate for 5 seconds keyed on both database file stamps, so a new row reaches the page within 5 seconds of the write and the sqlite spawn cost is not repeated by the poll.usage.dbsyncs on demand, on no timer.plugins/package.jsonacrossinstall.ts,doctor-fix.ts, anduninstall.ts. Two of them then shell out to npm and they differ in whether the write is conditional, so one shared helper would take four options and still branch. The RTK download-and-verify path is likewise duplicated betweeninstall.tsanddoctor-fix.ts; extracting it touches the checksum gate, which is the one place where a mistake runs an unverified binary. Both are named here rather than abstracted for its own sake.stats.dbneeds no shrinking from this side. It is 42 MB withfreelist_count = 0, soVACUUMreclaims nothing; 58% of the file is 18 indexes, of which 10.4 MB areUNIQUEautoindexes that cannot be dropped without a schema change. Retention and unused-index removal are omp's decisions, and Tersio's read-only contract keeps them there.