|
| 1 | +# Add category selection to the "Create status badge" modal |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +Coverage/complexity/duplication data can already be tracked per **category** — a free-form |
| 6 | +label (`coverage_runs.category` / `coverage_daily.category`, migration `0003_categories.sql`) |
| 7 | +that partitions independent report series within one project (e.g. "backend" vs "frontend"). |
| 8 | +The main project dashboard already renders one trend card per discovered category |
| 9 | +(`dashboard/src/routes/[owner]/[repo]/+page.svelte`), and the PR-check/baseline endpoint |
| 10 | +already reads a `?category=` query param (`src/routes/baseline.ts`). |
| 11 | + |
| 12 | +The "Create status badge" modal (`BadgeModal.svelte`) and its backend |
| 13 | +(`src/routes/badge.ts`) were never updated for this: the modal only lets a user pick a |
| 14 | +**metric** (coverage, complexity, duplication, etc.) and always produces a badge URL with no |
| 15 | +category, and the badge endpoint always reads the `'default'` category regardless of what's |
| 16 | +requested. Users with categorized projects can't get a status badge for anything but the |
| 17 | +implicit default series. This plan adds a category picker to the modal and wires it through |
| 18 | +to the badge endpoint, including the generated badge's label, for all metric types. |
| 19 | + |
| 20 | +## Backend: `src/routes/badge.ts` |
| 21 | + |
| 22 | +`getLatestCoverage(db, projectId, branch, category)` already accepts a `category` param |
| 23 | +(default `'default'`) — `src/lib/db.ts:274`. `badge.ts` just never passes it through. Change: |
| 24 | + |
| 25 | +- Read `const category = c.req.query('category') ?? 'default';` (same pattern as |
| 26 | + `src/routes/api.ts:35` and `baseline.ts`). |
| 27 | +- Pass it into `getLatestCoverage(c.env.DB, project.id, project.default_branch, category)`. |
| 28 | +- Include the category in the returned `label` when it isn't the default, so the badge itself |
| 29 | + communicates which series it's showing: |
| 30 | + ```ts |
| 31 | + label: category === 'default' ? metricName : `${category} ${metricName}`, |
| 32 | + ``` |
| 33 | +- No new validation needed — an unknown/malformed category simply yields no matching row and |
| 34 | + `getLatestCoverage` returns `null`, which already 404s (consistent with how `baseline.ts`/ |
| 35 | + `api.ts` treat category as an unvalidated read-side filter). |
| 36 | + |
| 37 | +This applies uniformly to every metric type (`coverage`, `branch_coverage`, `complexity`, |
| 38 | +`cognitive`, `duplication`, `maintainability`) since category is orthogonal to metric — one |
| 39 | +category row carries all metric columns. |
| 40 | + |
| 41 | +## Frontend: `dashboard/src/lib/components/BadgeModal.svelte` |
| 42 | + |
| 43 | +1. Add a `defaultBranch: string` prop (needed to query categories for the right branch; the |
| 44 | + badge endpoint itself always uses the project's default branch, so the picker must match). |
| 45 | +2. Add state: |
| 46 | + - `categories = $state<string[]>(['default'])` |
| 47 | + - `selectedCategory = $state('default')` |
| 48 | +3. Fetch categories whenever the selected metric changes, reusing the existing helper |
| 49 | + `fetchTrendByCategory(owner, repo, selectedMetric, defaultBranch, 1)` from |
| 50 | + `dashboard/src/lib/api.ts:25` (already used by the project page to discover categories) — |
| 51 | + no new endpoint required. On response, take `result.categories.map(c => c.category)`; |
| 52 | + fall back to `['default']` on an empty list or fetch error. If the current |
| 53 | + `selectedCategory` isn't in the new list, reset it to `categories[0] ?? 'default'`. |
| 54 | + - Doing this per-metric (rather than once) is intentional: a category with no data for the |
| 55 | + currently selected metric wouldn't produce a working badge anyway (`getLatestCoverage` |
| 56 | + would return a null value for that column), so scoping the picker to |
| 57 | + metric+category combinations that actually have data avoids offering dead combinations. |
| 58 | +4. Add a second `<select>` next to the existing metric select (mirroring its markup/styles at |
| 59 | + lines 151–158), labeled "Category", bound to `selectedCategory`, listing `categories`. |
| 60 | +5. Update the derived badge URL (`badgeEndpointUrl`, lines 32–34) to append |
| 61 | + `?category=${selectedCategory}` only when `selectedCategory !== 'default'`, so existing |
| 62 | + badges for uncategorized projects keep their current clean URL. |
| 63 | +6. Update the alt text / markdown snippet (line 38) to mention the category when non-default, |
| 64 | + e.g. `` `${selectedMetric}${selectedCategory !== 'default' ? ` (${selectedCategory})` : ''} badge` ``, |
| 65 | + matching the label convention used server-side. |
| 66 | + |
| 67 | +## Wiring: `dashboard/src/routes/[owner]/[repo]/+page.svelte` |
| 68 | + |
| 69 | +Pass the new prop at the existing `<BadgeModal ... />` call (lines 143–149): |
| 70 | +```svelte |
| 71 | +<BadgeModal |
| 72 | + owner={data.project.owner_login} |
| 73 | + repo={data.project.repo_name} |
| 74 | + projectId={data.project.id} |
| 75 | + badgeEnabled={data.project.badge_enabled} |
| 76 | + defaultBranch={data.project.default_branch} |
| 77 | + onclose={() => (badgeModalOpen = false)} |
| 78 | +/> |
| 79 | +``` |
| 80 | + |
| 81 | +## Tests: `test/badge.test.ts` |
| 82 | + |
| 83 | +Extend the existing helpers and add cases: |
| 84 | +- Extend `seedCoverage()` to accept a `category` field (default `'default'`), inserting into |
| 85 | + the `category` column. |
| 86 | +- Extend `getBadge()` to accept an optional `category` and append it as a `?category=` |
| 87 | + query param. |
| 88 | +- New cases: |
| 89 | + - A badge for a non-default category returns the correct value when that category has its |
| 90 | + own coverage row (distinct from the `'default'` row for the same project). |
| 91 | + - The label includes the category name when non-default (e.g. `"backend coverage"`), and |
| 92 | + stays as just the metric name when `category` is omitted/`'default'`. |
| 93 | + - Requesting a category with no data 404s even when the `'default'` category has data for |
| 94 | + that metric. |
| 95 | + - Do this for at least one non-coverage metric (e.g. `complexity`/`cyclomatic` or |
| 96 | + `duplication`) to confirm category plumbing isn't coverage-specific. |
| 97 | + |
| 98 | +## Verification |
| 99 | + |
| 100 | +- `npm test` (badge.test.ts additions) to confirm backend category filtering + label logic. |
| 101 | +- `npm run dev` (dashboard + worker together): seed a project with two categories via the CI |
| 102 | + ingest endpoint (or local seed SQL), open "Create status badge", switch the category |
| 103 | + dropdown, and confirm: |
| 104 | + - The metric/category combinations with data render badge previews and correct shields.io |
| 105 | + URLs (`?category=` present only for non-default). |
| 106 | + - Switching metric re-fetches/re-scopes the category list appropriately. |
| 107 | + - Existing single-category (`'default'`) projects are unaffected — URL has no `category` |
| 108 | + query param and label is unchanged from current behavior. |
0 commit comments