Background
Currently, every /impact metric is cross-wiki by construction: no /api/impact/* endpoint accepts a wiki parameter, ImpactDashboard.vue offers only the days range selector, and the page has no wiki control. For a maintainer of a specific community, "how is my wiki doing" currently means opening the maintainer-only drill-downs and scanning for their wiki's rows — and the non-maintainer tiles cannot answer it at all.
This task adds a page-wide wiki filter: pick a wiki, and every card, tile and drill-down on /impact shows that wiki's numbers. (The planned "Essential work done through the kit" card's mockup assumed such a filter; it applies equally to the existing Key Highlights and Users onboarded
cards, and any future card inherits it.)
Proposed approach
A wiki selector on the /impact page header next to the existing time-range <select>, defaulting to "All wikis", threading an optional wiki parameter through every impact endpoint and query.
- Selector options from data, not a hardcoded list: the distinct wikis the kit has seen (wikisUsingKit already computes the active set; label via the existing sitematrix name map / shortWiki labeler in client/src/components/impact/labels.js).
- Server: each /api/impact/* route accepts ?wiki=<url>; each query in server/db/impact.js gains an AND (@wiki IS NULL OR wiki_url = @wiki) predicate alongside the existing EXCLUSIONS_SQL / CONTRIB_WINDOW_SQL fragments — one new shared fragment, bound everywhere the exclusions already are, so the filter cannot drift per-metric.
- Client: a wiki ref in ImpactDashboard.vue passed to every card/dialog exactly like days is today; drill-down dialogs inherit it so headline counts and drill-down rows stay consistent under the filter (the same equal-by-construction rule the dashboard already maintains).
- Ledger-sourced metrics (verified completions) filter identically — task_verifications rows carry wiki_url.
- Deep-linkable: the selection lives in the URL query, so a maintainer can bookmark their wiki's view.
Implementation inventory
Everything the wiki parameter has to touch, from the code as of 2026-07-30:
Routes (server/routes/impact.js) — all seven accept ?wiki=<url>: /summary, /contributors, /wikis (maintainer-only), /onboarded, /onboarded-users, /onboarded-by-month, /task-completions.
Queries (server/db/impact.js) — every function gets the shared predicate:
- getImpactSummary — three parts: the contributors count, tasksCompleted (delegates to getTaskCompletionsDetail, so it inherits), and the per-task taskMilestones map (distinct-wiki counts — degenerate under a single-wiki filter exactly like the "Wikis using the kit" tile; fold into open question 1).
- wikisUsingKit — feeds the headline tile AND is the natural source for the selector's options.
- getWikiContributors (/wikis drill-down), getContributorsWithWikis, getOnboardedUsers, getOnboardedUsersForMonth.
- getContributorsBreakdown — note the existing allWikis flag ("Show all" re-request): under a wiki filter the LIMIT-15-vs-all distinction collapses to one row; decide whether the flag is ignored or the breakdown hides.
- getTaskCompletionsDetail — two passes to filter: the event pass AND the VERIFIABLE_TASKS ledger overwrite loop (verifiedStmt on task_verifications) added by T433369 phase 5.
Fragment convention: the existing fragments (EXCLUSIONS_SQL, CONTRIB_WINDOW_SQL) use positional ? placeholders with spread bindings, and the file's comment warns better-sqlite3 errors
loudly on bind-count mismatches — the new fragment must follow the same convention (e.g. AND (? IS NULL OR wiki_url = ?) binding the wiki twice, spread alongside ...ADMIN_USERS), not a named @wiki parameter mixed into positional statements.
Selector data source: the existing /wikis endpoint is maintainer-only and returns usernames — the selector cannot use it. Add a lightweight public endpoint (e.g. /api/impact/wiki-options) returning [{ url, label }] from wikisUsingKit + the labels.js labeler, or include the list in /summary's response.
Client surfaces (all currently take :days; each gains :wiki): ImpactDashboard.vue (owner of the ref + URL query sync), KeyHighlights.vue, UsersOnboardedCard.vue, and the four dialogs — WikiContributorsDialog, ContributorWikisDialog, TaskCompletionsDialog, OnboardedUsersDialog (+ MonthOnboardedDialog via UsersOnboardedCard).
Scope note: seven routes, eight query functions, a new options endpoint and seven components — mechanical but wide; the byte-identical-when-unfiltered acceptance criterion is what keeps it safe to review.
Acceptance criteria
- "All wikis" default renders byte-identical numbers to today's dashboard
- With a wiki selected, every tile, card and drill-down on the page reflects only that wiki, and each headline count still equals its drill-down row count
- The selector lists only wikis with kit activity, labeled with friendly names
- The wiki parameter composes with the existing days window and production exclusions
- The selection survives reload (URL query param)
Open questions — fine to determine during implementation
- The "Wikis using the kit" tile is degenerate under a single-wiki filter (always 1 or 0) — hide it, or pin it to the unfiltered value with a label.
- Selector unit and labels: wikis are language editions, so one control covers both readings — decide whether options read as wiki names ("Kai Wikipedia") or language codes ("kai"), and how to sort them.
- Control placement: next to the time-range select on the /impact page header (assumed here), or in the shared app header — the mockup text said "in the header" without specifying which.
- Per-wiki empty states: a wiki with activity but no data for a given tile should show 0, not drop the tile — confirm against how the unfiltered dashboard handles absent metrics today.
Out of scope
- Per-wiki filtering on /analytics (separate dashboard, separate task if wanted).
- Multi-wiki compare views.