Commit Graph

10 Commits

Author SHA1 Message Date
Jiayuan Zhang
ff0979008b fix(dashboard): hide deleted agents from usage leaderboard (MUL-3771) (#4637)
* fix(dashboard): hide deleted agents from usage leaderboard (MUL-3771)

The usage leaderboard fell back to rendering the raw agent UUID when an
agent was no longer in the workspace agent list (`agent?.name ?? row.agentId`).
Hard-deleted agents only survive as legacy usage rollup rows, so they showed
up as a bare UUID.

Filter the leaderboard rows down to agents still present in the workspace.
The agent list is fetched with `include_archived: true`, so archived agents
keep their names and stay; only hard-deleted agents drop out. Filtering is
skipped until the agent list has loaded so a slow fetch doesn't transiently
blank the board. Top-line KPI totals are unchanged — only the per-agent list
is affected.

Co-authored-by: multica-agent <github@multica.ai>

* fix(dashboard): stabilize empty agent list

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Lambda <lambda@multica.ai>
2026-06-27 01:09:54 +08:00
Naiyuan Qing
bd1fb10afa chore: react-doctor cleanup — button types, useContext→use(), toSorted, error fixes (#3350)
- Add explicit type="button" to 61 <button> elements missing the attribute
- Replace useContext() with React 19 use() across 16 context consumers
- Replace [...arr].sort() with arr.toSorted() in 12 web/desktop files
  (mobile excluded — Hermes lacks toSorted support)
- Fix rules-of-hooks violation: useSidebar try/catch → useSidebarSafe null check
- Fix nested component definition: useMemo wrapping HeaderRight → useCallback
- Fix missing ARIA: add aria-expanded + aria-controls to combobox in create-squad

React Doctor score: 23 → 30. No behavioral changes, no business logic modified.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:57:07 +08:00
YYClaw
614dfae884 MUL-2488 feat(timezone): Scheduling / Viewing two-layer timezone architecture (#2968)
* docs(timezone): add scheduling/viewing timezone architecture RFC

* feat(db): replace daily rollups with task_usage_hourly, add user.timezone

Migrations 100-104: add "user".timezone (Viewing tz), build the UTC
hourly task_usage_hourly rollup with its pipeline, drop the legacy
task_usage_daily / task_usage_dashboard_daily pipelines, and drop the
agent_runtime.timezone column. Report queries now slice day boundaries
at read time by the caller-supplied @tz instead of materialising in a
fixed tz. Regenerate sqlc.

* feat(server): add task_usage_hourly backfill command

Replace the two legacy backfill commands (daily / dashboard_daily) with
a single backfill_task_usage_hourly that loads historical task_usage
into the new UTC hourly rollup, sliced per workspace.

* refactor(server): resolve viewing timezone in report handlers

Report handlers resolve the Viewing tz per request (?tz query param,
then user.timezone, then UTC) and pass it to the hourly-rollup queries.
Drop the UseDailyRollup feature flags and the old raw-scan/daily-rollup
dual paths, remove the /api/usage endpoints, and stop the daemon from
reporting and the runtime handler from accepting host timezone.

* refactor(core): switch report queries to viewing timezone

API client and dashboard/runtime queries send ?tz with each report
request, the user schema/types carry the new timezone field, and the
runtime timezone field/mutation is removed.

* feat(views): add viewing timezone preference and UI

Add the useViewingTimezone hook and a Timezone setting in Preferences;
report charts and the dashboard week boundary follow the viewer tz.
Remove the runtime detail timezone editor and its locale strings.

* fix(test): update fixtures and stabilize tests for timezone refactor

The timezone architecture refactor changed several types without
updating dependent test code:

- RuntimeDevice no longer has a timezone field — drop it from the
  create-agent-dialog runtime fixture.
- User now requires a timezone field — add it to the apps/web mockUser
  fixture.
- The PreferencesTab timezone tests asserted on the async save handler
  (PATCH then store update) with a bare expect, racing the mutation's
  settle callback, and timed out querying the Select's ~600-option IANA
  list on a loaded CI runner. Wrap the assertions in waitFor and extend
  the timeout for those three tests.

* docs(timezone): document self-host migration order and trigger invariant

Add a SELF-HOST UPGRADE ORDER runbook to the backfill command's package
comment: applying migrations 100-104 in a single migrate-up drops the
legacy daily rollups before the hourly backfill runs, leaving dashboards
empty until cron catches up.

Add an INVARIANT comment on trg_atq_dirty_hourly noting that agent_id
must be added to the trigger's OF list if it ever becomes mutable,
otherwise dirty buckets for the old agent_id are silently missed.

* style(runtimes): drop trailing blank line in runtime-detail
2026-05-21 15:33:47 +08:00
Jiayuan Zhang
8d30d76300 feat(dashboard): add 1d range to workspace Usage tab (#2837)
* feat(dashboard): add 1d time range to workspace Usage tab

1d means "today" — the natural calendar day from 00:00 UTC, matching the
rollup's bucket_date axis — not the trailing 24 hours. The client-side
dailyCutoffIso filter is now applied in daily dim too so 1d collapses
strictly to today even at the midnight UTC edge where the server's
wall-clock since cutoff would otherwise include yesterday.

Co-authored-by: multica-agent <github@multica.ai>

* fix(dashboard): scope `1d` to today only on aggregate endpoints

The pre-aggregated `byAgent` / `runTime` dashboard endpoints leaked
yesterday into the agent leaderboard and KPI cards for the `1d` time
range because `parseSinceParam(days=1)` returned `now-24h` (wall clock)
and the downstream SQL then applied `DATE_TRUNC('day', @since)`, which
landed on yesterday 00:00 UTC. The PR's client-side `dailyCutoffIso`
filter could only fix the date-bearing daily endpoints; aggregate
responses are already collapsed across dates.

Anchor `parseSinceParam` at UTC start-of-today instead, so `days=N`
covers N natural calendar days (today + N-1 prior). This matches the
frontend `dailyCutoffIso = today - (days-1)` semantic that the
workspace dashboard already assumes, and removes the off-by-one that
previously made `30d` return 31 buckets.

The runtime-detail page uses `parseSinceParamInTZ` (timezone-aware),
which is unchanged — it has no `1d` option.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 08:28:04 +02:00
Jiayuan Zhang
35fc318d68 feat(runtimes): weekly usage dimension + tz-aware aggregation (MUL-2382) (#2822)
* feat(runtimes): weekly usage dimension + tz-aware aggregation (MUL-2382)

Adds a Weekly view to the runtime Usage chart alongside Daily and Hourly,
backed by `aggregateByWeek` on the existing 180-day daily cache (no new
endpoint). Weeks are ISO 8601 Mon–Sun; the in-progress week is rendered at
half opacity and tooltip-labelled "partial · N / 7 days".

Side effects called out in the RFC:

- `sliceWindow` now reads "today" in the runtime's IANA timezone, fixing a
  one-day drift at the window edge when the browser and runtime sit in
  different time zones.
- ActivityHeatmap rows are reordered Mon → Sun to match the rest of the
  Weekly aggregation; "today" is computed in runtime tz so the grid's
  trailing column lines up with the daily rows the backend buckets.

Dimension / period coupling: switching dimension resets the period to that
dimension's default when the active value isn't in its allowed set
(Hourly 7/30, Daily 7/30/90, Weekly 30/90/180).

Unit tests cover weekStart / addDays / tz-aware today, the sliceWindow
boundary, and aggregateByWeek's partial-week math.

Co-authored-by: multica-agent <github@multica.ai>

* fix(runtimes): weekly chart shows trailing calendar weeks (MUL-2382)

aggregateByWeek built one bucket per week-with-data, and the caller
took the last N buckets. With sparse data — old populated weeks plus
empty stretches near today — the slice surfaced the old weeks instead
of the trailing in-window calendar weeks the user selected.

Now aggregateByWeek takes weekCount and emits exactly that many
trailing calendar weeks anchored at today's week in the runtime tz.
Buckets are pre-zeroed so empty in-range weeks render as empty bars;
rows outside the window are dropped.

Co-authored-by: multica-agent <github@multica.ai>

* feat(usage): drop Hourly dim + add Daily/Weekly to workspace dashboard (MUL-2382)

- Remove Hourly from the runtime usage WHEN-chart: segmented control is
  now Daily / Weekly. Drop the HourlyActivityChart component,
  aggregateCostByHour helper, byHour query subscription, and the
  when_tab_hourly i18n key.
- Add the same Daily / Weekly dimension toggle to the workspace-level
  Usage page (dashboard-page.tsx). Time-range linkage matches the runtime
  page: Daily allows 7/30/90 (default 30), Weekly allows 30/90/180
  (default 90); switching dimensions resets `days` when the current value
  isn't in the new dimension's set.
- Reuse `aggregateByWeek` from runtimes/utils for cost / tokens
  (signature relaxed to accept the wider DashboardUsageDaily shape).
  Add `aggregateWeeklyTime` / `aggregateWeeklyTasks` in dashboard/utils
  with identical pre-zeroed trailing-week semantics. Workspace dashboard
  uses the user-chosen timezone (existing TimezoneSelect) as the
  week-boundary tz; runtime page continues to use the runtime's IANA tz.
- New `WeeklyTimeChart` / `WeeklyTasksChart` mirror their daily
  counterparts plus partial-week half-opacity bars and rangeLabel
  tooltips, matching the existing Weekly cost / tokens charts.
- Tests: drop hourly-related setup; add weekly run-time / tasks coverage
  asserting pre-zeroed trailing buckets and the same MUL-2382 sparse
  window-scoping regression we caught on the runtime side.

Co-authored-by: multica-agent <github@multica.ai>

* fix(usage): correct workspace Weekly window + lock tz to UTC (MUL-2382)

Two blocking correctness bugs from Emacs's PR #2822 review:

1. The Weekly chart paints `ceil(days/7)` trailing calendar weeks but the
   API was still asked for exactly `days`. Worst case (today = Sunday on a
   30D request) the leftmost Monday sits 34 days back, so the first week's
   bucket was silently truncated. Over-fetch the per-date queries to
   `weekCount * 7` days when Weekly is active; per-agent rollups stay at
   `days` so the KPI / leaderboard labels keep their advertised window.
   Daily-aggregation surfaces (cost/tokens/time/tasks KPIs and the Daily
   chart) re-scope the over-fetched rows back to `days` so the labels
   stay consistent.

2. The backend dashboard rollup buckets data by UTC `bucket_date` (and the
   raw fallback queries by `DATE(tu.created_at)`, also UTC), but the
   frontend was driving Weekly boundaries from the user-chosen
   `TimezoneSelect`. Near midnight UTC that put cross-boundary rows into
   the wrong calendar week. Lock workspace Weekly to UTC and remove the
   timezone picker from this page; the runtime detail page keeps its own
   `runtime.timezone`-anchored aggregation, which is consistent because
   its rollup is materialized in that runtime's tz.

Verification: pnpm --filter @multica/views test (636 passed),
typecheck clean, lint 0 errors / 13 pre-existing warnings.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 04:24:46 +02:00
Jiayuan Zhang
380c6b5122 feat(usage): add Time and Tasks to daily-trend toggle (MUL-2283) (#2709)
Extends the workspace /usage page Daily tokens chart toggle from
Tokens | Cost to Tokens | Cost | Time | Tasks, so users see daily
run-time and task-count trends alongside spend without leaving the page.

- New SQL `ListDashboardRunTimeDaily`: per-date totals from
  agent_task_queue (terminal tasks only), scoped to workspace and
  optionally project. Same time anchor as ListDashboardAgentRunTime
  so day boundaries line up.
- New handler GET /api/dashboard/runtime/daily + TanStack Query option.
- New DailyTimeChart (single-series, smart h/m/s unit) and
  DailyTasksChart (completed + failed stacked).
- Empty-state is per-metric so a workspace with tokens but no terminal
  runs (or vice-versa) doesn't get a false "no data".
- i18n: en + zh-Hans daily.metric_time / metric_tasks + titles.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 18:51:02 +02:00
Bohan Jiang
4c1bef2e1f feat(usage): mirror Tokens metric toggle onto Usage page Daily chart (MUL-2148) (#2540)
* feat(usage): mirror Tokens metric toggle onto Usage page Daily chart (MUL-2148)

#2537 added the Cost/Tokens metric toggle to the Daily chart inside the
runtime-detail Usage section (packages/views/runtimes/components/
usage-section.tsx). The workspace-level Usage page at /{slug}/usage
imports the same DailyCostChart primitive but renders it from
dashboard-page.tsx without any toggle wrapper, so #2537 only landed on
half of the surface that says "Daily cost".

This PR mirrors the same pattern to dashboard-page.tsx so users see
the toggle wherever a "Daily" chart appears.

Changes
- `packages/views/dashboard/utils.ts`: new `aggregateDailyTokens` helper
  that folds DashboardUsageDaily[] into the same DailyTokenData[] shape
  the DailyTokensChart consumes (mirrors aggregateByDate's dailyTokens
  branch from the runtimes side, adapted to DashboardUsageDaily field
  names).
- `packages/views/dashboard/components/dashboard-page.tsx`: rename
  `DailyCostBlock` → `DailyTrendBlock`, add a Cost/Tokens Segmented
  next to the section title, switch chart and title based on the
  active metric, per-metric empty-state (so a workspace with unmapped
  pricing but recorded tokens still gets a real Tokens chart while
  the Cost view falls through to the empty-state — same convention as
  DailyTab in usage-section.tsx).
- usage.json (en + zh-Hans): split `daily.title` into `title_cost` +
  `title_tokens`, add `metric_cost` + `metric_tokens` toggle labels.

* feat(usage): default Daily chart to Tokens metric

Most users land on /{slug}/usage to gauge "how much agent work
happened" rather than "how much was spent." Tokens is the more
universally meaningful axis on first read (Cost depends on having
pricing mapped for every model and on whether the workspace has
unmaintained models). Cost stays one click away via the same toggle.

Also reorder the Segmented so Tokens sits first, matching the new
default.
2026-05-13 22:07:47 +08:00
Jiayuan Zhang
291c2c7898 feat(usage): reuse runtime timezone picker on the usage page (#2533) (#2546)
* feat(usage): add timezone picker to usage page (#2533)

Extracts the runtime detail page's timezone dropdown into a shared
TimezoneSelect at packages/views/common/timezone-select.tsx and reuses
it in the usage page header, immediately to the right of the 7d / 30d
/ 90d segmented control. Defaults to the browser-resolved zone with
the same "(browser)" suffix rendering as the runtime page.

The runtime-detail TimezoneEditor still owns the PATCH mutation; only
the dropdown UI moved. UI-only — no API client / handler changes.

Co-authored-by: multica-agent <github@multica.ai>

* fix(usage): make header wrap so timezone picker fits on narrow widths

The h-12 PageHeader is a single non-wrapping flex row. Adding the
timezone picker with a 180px min-width pushed the title + project
filter + range switch + tz select past the viewport on narrow and
medium widths. Drop the picker's hard min-width, let the header grow
vertically (h-auto + min-h-12) and let the right toolbar wrap. Wide
viewports still render the original single row.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-13 15:58:53 +02:00
Bohan Jiang
451c46c43f refactor(usage): rename Dashboard → Usage + dynamic per-agent leaderboard (#2511)
The page added in #2462 lived at `/{slug}/dashboard` and was titled
"Dashboard", which collides with the conventional meaning ("personal
landing surface") and doesn't tell new users what the page is for. Its
actual contents — token spend, cost, run time, task counts — map cleanly
onto the OpenAI / Anthropic / Vercel "Usage" surface, so rename to that.

Renames (user-visible)
- Route: `/{slug}/dashboard` → `/{slug}/usage` (web App Router + desktop
  memory router)
- Sidebar entry: label "Dashboard" / "看板" → "Usage" / "用量", icon
  LayoutDashboard → BarChart3 (page header icon swapped in sync)
- Page title in en/zh-Hans
- Reserved-slugs: add `usage` to workspace route segments group;
  `dashboard` stays reserved in the marketing group (back-compat against
  workspace slug collisions + keeps the name free for a future Home page)
- i18n namespace `dashboard` → `usage` across resources-types.ts,
  locales/index.ts, and the moved JSON files
- WORKSPACE_ROUTE_SEGMENTS in editor link-handler
- paths.workspace(slug).dashboard() → .usage(), with matching test
  expectation updates

Per-agent leaderboard polish (`packages/views/dashboard/components/
dashboard-page.tsx`)
- Card title "Cost & run time by agent" → "Leaderboard" with a 4-way
  Segmented control: Tokens / Cost / Time / Tasks
- Active metric drives row order, progress-bar width, and the
  emphasised column header / cell — keeping ranking, visual quantity,
  and column emphasis in lockstep so users always see what's being
  measured
- Default sort = Tokens (most universally meaningful; Cost still one
  click away)
- Project filter dropdown:
  - Show ProjectIcon next to the selected project + each list item;
    FolderKanban as the "All projects" fallback (matches ProjectPicker
    language)
  - alignItemWithTrigger={false} so "All projects" doesn't get pushed
    above the trigger and clipped when the header sits at the top of
    the viewport (was the root cause of "can't re-select All projects"
    once a project was selected)
  - max-h-72 to cap the dropdown when workspaces accrue many projects;
    matches the runtime-detail Select precedent
- Folder name `packages/views/dashboard/*` and `DashboardPage`
  component name intentionally left in place — user-visible rename
  only, no broad code refactor.

Old `/dashboard` routes are not redirected because the page only landed
in #2462 (a few days ago); no real users, external links, or
desktop-tab persistence have settled on it yet.
2026-05-13 14:07:53 +08:00
Bohan Jiang
96695a79c5 feat(dashboard): workspace/project token + run-time dashboard MUL-1882 (#2462)
* feat(dashboard): workspace/project token + run-time dashboard

Add a `/{slug}/dashboard` page showing per-agent token spend and execution
time across the whole workspace, with an optional project filter.

Backend:
  - Three new sqlc queries against task_usage + agent_task_queue: daily
    usage, per-agent usage, per-agent total run-time. All optionally
    scoped to a project via sqlc.narg('project_id'), reaching project
    through the issue join.
  - Handlers under /api/dashboard return the same wire shape the runtime
    page already consumes (model preserved for client-side cost math).

Frontend: - Shared DashboardPage in packages/views/dashboard reusing KpiCard,
    DailyCostChart, ActorAvatar, and estimateCost from the runtime page
    so the visual style and pricing math stay in lock-step.
  - Period selector (7/30/90d), project dropdown, four KPI tiles
    (cost, tokens, run time, tasks), daily cost chart, and a combined
    "cost + run time by agent" list.
  - Routed in both web (app/[slug]/(dashboard)/dashboard) and desktop
    (memory router); sidebar nav entry added under Workspace group.
Co-authored-by: multica-agent <github@multica.ai>

* fix(dashboard): drop stale project filter and stop double-counting tasks

Two issues caught in PR #2462 review:

1. Project filter held the previous selection's UUID across workspace
   switches and project deletions: the dropdown gracefully showed
   "All projects" (because the title lookup missed) while the three
   dashboard queries kept forwarding the dead UUID, leaving the UI
   looking like a full-workspace view but populated with empty
   project-scoped data. Validate the picked UUID against the current
   projects list before passing it to the queries.

2. The "by agent" table read its task count from the token rollup,
   which is grouped per (agent, model). A single task that spans two
   models lands twice and the agent's row reads e.g. "2 tasks" when
   the real count is 1. Prefer `ListDashboardAgentRunTime`'s per-agent
   distinct count when available; fall back to the token aggregate
   only for agents with no terminal run yet (in-flight tasks).

Extract the merge into `mergeAgentDashboardRows` so the precedence
rules are unit-tested directly.

Co-authored-by: multica-agent <github@multica.ai>

* test(dashboard): allocate per-workspace issue.number explicitly

TestDashboardEndpoints creates two issues in the shared fixture
workspace. issue.number defaults to 0 (migration 020), and the table
carries UNIQUE (workspace_id, number), so the second insert raced the
first on the same default and failed in CI.

Allocate MAX(number) + 1 per insert so each row gets a fresh number
without stepping on rows other tests left behind in the same workspace.

Co-authored-by: multica-agent <github@multica.ai>

* feat(dashboard): rollup table + cron-driven aggregation for dashboard

Mirror the per-runtime rollup in `task_usage_daily` (migrations 073/077/082)
to remove the per-request raw aggregation the dashboard was doing.

Migration 084 adds:
  - `task_usage_dashboard_daily` keyed on
    (bucket_date, workspace_id, agent_id, project_id, model) — the
    dimensions the dashboard actually queries, with project_id nullable
    via UNIQUE NULLS NOT DISTINCT (PG15+) so "no-project" buckets
    upsert cleanly.
  - `task_usage_dashboard_rollup_state` watermark table.
  - `task_usage_dashboard_dirty` invalidation queue.
  - Triggers on agent_task_queue DELETE, task_usage DELETE, and
    issue.project_id UPDATE — the cases the updated_at watermark can't
    see. The project_id trigger re-attributes existing rollup rows when
    a user moves an issue across projects.
  - `rollup_task_usage_dashboard_daily_window(from, to)` —
    idempotent recompute primitive (same shape as 077).
  - `rollup_task_usage_dashboard_daily()` cron entry — own advisory
    lock (4244) so it serialises independently of the runtime rollup.
  - `task_usage_dashboard_rollup_lag_seconds()` health helper.

Sqlc queries `ListDashboardUsageDailyRollup` /
`ListDashboardUsageByAgentRollup` read from the new table; the handler
dispatches between rollup and raw on a separate
`UseDailyRollupForDashboard` config flag
(`USAGE_DASHBOARD_ROLLUP_ENABLED` env). Same fail-safe default (false →
raw) so operators can roll out independently of the per-runtime flag.

Bucket date is UTC (the dashboard aggregates across runtimes that may
sit in different tzs; there's no single correct local boundary).

Adds `cmd/backfill_task_usage_dashboard_daily` mirroring the existing
per-runtime backfill — operator runs it once before flipping the flag.

Tests: - TestDashboardEndpoints now also exercises the rollup read path
    (raw vs. rollup, same project-scoped totals).
  - TestDashboardRollupReattributesOnProjectChange verifies the
    issue.project_id trigger enqueues both old + new buckets and the
    next rollup tick zeroes the old project + populates the new one.
Co-authored-by: multica-agent <github@multica.ai>

* fix(dashboard-rollup): close two invalidation gaps

Two leak paths missed by migration 084 review:

1. Issue cascade DELETE — the atq BEFORE DELETE trigger runs AFTER the
   issue row is gone, so `LEFT JOIN issue` returns NULL project_id and
   the original-project bucket never gets cleared (issue 077 calls this
   out for the runtime rollup but didn't need to act on it). Adds an
   `issue BEFORE DELETE` trigger that enqueues using OLD.project_id
   while the issue row is still readable.

2. `LinkTaskToIssue` (quick-create task attaching to a real issue post-
   completion) UPDATEs `agent_task_queue.issue_id` from NULL to a real
   id. Migration 084 only watched DELETE on atq, so usage already
   rolled up under the no-project bucket stayed attributed to NULL
   forever. Extends the atq trigger to fire on UPDATE OF issue_id too,
   enqueueing both OLD (NULL project) and NEW (linked issue's project).

Tests: - TestDashboardRollupClearsOnIssueDelete asserts rollup row drops to
    zero after issue delete + rollup tick.
  - TestDashboardRollupReattributesOnLinkTaskToIssue verifies tokens
    move from the NULL bucket to the project bucket after the UPDATE.
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-13 12:51:16 +08:00