Skip to content

Commit 2ccc80a

Browse files
committed
feat: analytics for labs and kpis metrics
2 parents 4f2cca7 + d8ae19a commit 2ccc80a

34 files changed

Lines changed: 3865 additions & 29 deletions

.github/agents/copilot-instructions.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
Auto-generated from all feature plans. Last updated: 2026-02-24
44

55
## Active Technologies
6+
- TypeScript 5 (strict), Node.js 20, React 18 + Astro 4 (SSR), Prisma Client (MySQL), D3.js v7, `@testing-library/react`, Vitest (002-admin-analytics-dashboard)
7+
- MySQL (via Prisma) — constitution names PostgreSQL but production schema is MySQL (002-admin-analytics-dashboard)
68

79
- **Language**: TypeScript 5.x, strict mode — no `any`, no `@ts-ignore`, no weakening of tsconfig
810
- **Frontend framework**: Astro 4 (SSR) + React 18 islands
@@ -62,11 +64,8 @@ tests/
6264
- Tailwind + Catalyst UI Kit only for styling
6365

6466
## Recent Changes
65-
- 001-csv-download: Added [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
67+
- 002-admin-analytics-dashboard: Added Platform Analytics Dashboard with D3.js charts, React components (SSR + client:load), analytics helpers
6668

67-
- 001-kpi-multi-value-input: Replace fixed before/after KPI input with dynamic multi-value list; add DefaultCollectionDate; expose `results: IKpiResult[]` from BFF
68-
- 001-multi-entry-kpi-charts: Added TypeScript (Astro strict preset) + React 19 islands + Astro 5, React 19, D3 7, Chart.js 4, react-chartjs-2, Tailwind 4
69-
- 001-mcda-custom-analysis: Added TypeScript 5 (strict mode via `astro/tsconfigs/strict`) + Astro 5 (`@astrojs/node` SSR adapter), React 19, Prisma 6, Tailwind CSS 4, Vitest 4
7069

7170
<!-- MANUAL ADDITIONS START -->
7271
<!-- MANUAL ADDITIONS END -->

package-lock.json

Lines changed: 0 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Specification Quality Checklist: Platform Analytics Dashboard
2+
3+
**Purpose**: Validate specification completeness and quality before proceeding to planning
4+
**Created**: 2026-03-17
5+
**Feature**: [spec.md](../spec.md)
6+
7+
## Content Quality
8+
9+
- [x] No implementation details (languages, frameworks, APIs)
10+
- [x] Focused on user value and business needs
11+
- [x] Written for non-technical stakeholders
12+
- [x] All mandatory sections completed
13+
14+
## Requirement Completeness
15+
16+
- [x] No [NEEDS CLARIFICATION] markers remain
17+
- [x] Requirements are testable and unambiguous
18+
- [x] Success criteria are measurable
19+
- [x] Success criteria are technology-agnostic (no implementation details)
20+
- [x] All acceptance scenarios are defined
21+
- [x] Edge cases are identified
22+
- [x] Scope is clearly bounded
23+
- [x] Dependencies and assumptions identified
24+
25+
## Feature Readiness
26+
27+
- [x] All functional requirements have clear acceptance criteria
28+
- [x] User scenarios cover primary flows
29+
- [x] Feature meets measurable outcomes defined in Success Criteria
30+
- [x] No implementation details leak into specification
31+
32+
## Notes
33+
34+
- FR-010 and FR-011 reference Astro/SSR and Astro components — these are architectural constraints from the project constitution, not implementation details. They define the "what" (server-rendered, non-interactive) rather than the "how".
35+
- The Assumptions section documents that the existing ApiClient may need extension for retrieving all KPI results and user counts. This is flagged as a dependency rather than left as a clarification blocker.
36+
- All items pass validation. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Contracts: Platform Analytics Dashboard
2+
3+
**No new external API contracts.**
4+
5+
This feature is a read-only SSR Astro page that consumes existing internal API endpoints via `ApiClient`. It does not expose any new endpoints, commands, or public interfaces.
6+
7+
## Consumed Endpoints (existing)
8+
9+
| Endpoint | Method | Used For |
10+
|----------|--------|----------|
11+
| `/api/v1/labs?fields=projects,kpiresults,transport_modes` | GET | All living labs with KPI results and measures |
12+
| `/api/v1/kpidefinitions` | GET | All KPI definitions |
13+
| `/api/v1/projects` | GET | All measures/projects |
14+
| `/api/v1/users` | GET | All users (requires new `getUsers()` on ApiClient) |
15+
16+
## New ApiClient Method
17+
18+
One new method added to `ApiClient.ts`:
19+
20+
```typescript
21+
async getUsers(options?: { status?: string }): Promise<User[] | null>
22+
```
23+
24+
This method wraps the existing `/api/v1/users` endpoint which already supports status filtering. It follows the same pattern as other ApiClient methods.
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Data Model: Platform Analytics Dashboard
2+
3+
**Phase**: 1 | **Feature**: 002-admin-analytics-dashboard | **Date**: 2026-03-17
4+
5+
---
6+
7+
## Source Entities (read-only — no schema changes)
8+
9+
This feature reads existing data only. No new database tables or Prisma schema changes required.
10+
11+
### Living Labs (`labs`)
12+
13+
| Field | Type | Notes |
14+
|-------|------|-------|
15+
| id | BigInt (PK) | Used as grouping key |
16+
| name | String | Display label in tables/charts |
17+
| country | String? | Display in table |
18+
| created_at | DateTime? ||
19+
| updated_at | DateTime? ||
20+
21+
**Relationships used**:
22+
- `kpiresults[]` → via `getLivingLabs()` populated include
23+
- `living_lab_projects_implementation[]` → via populated include (projects)
24+
25+
### KPI Definitions (`kpidefinitions`)
26+
27+
| Field | Type | Notes |
28+
|-------|------|-------|
29+
| id | BigInt (PK) | Used for mapping results → definitions |
30+
| kpi_number | String | Display label (e.g. "1", "2.1") |
31+
| name | String | Display label |
32+
| parent_kpi_id | BigInt? (FK → self) | `null` = main/parent KPI |
33+
| type | Enum (GLOBAL, LOCAL) | Used for type breakdown |
34+
35+
**Main/parent KPI identification**: `parent_kpi_id IS NULL`
36+
37+
### KPI Results (`kpiresults`)
38+
39+
| Field | Type | Notes |
40+
|-------|------|-------|
41+
| id | BigInt (PK) ||
42+
| kpidefinition_id | BigInt (FK → kpidefinitions) | Links result to definition |
43+
| living_lab_id | BigInt (FK → labs) | Links result to lab |
44+
| value | Float | The submitted measurement |
45+
| date | DateTime | Used for timeline aggregation (year extraction) |
46+
47+
**Accessed via**: `ILivingLabPopulated.kpi_results: IKpiResultGroup[]`, where each group has `results: IKpiResult[]`
48+
49+
### Projects/Measures (`projects` + `living_lab_projects_implementation`)
50+
51+
| Field | Type | Notes |
52+
|-------|------|-------|
53+
| projects.id | BigInt (PK) ||
54+
| projects.name | String | Display label |
55+
| projects.type | String (PUSH, PULL, OTHER) | Used for type breakdown |
56+
| impl.project_id | BigInt (FK) | Links implementation to project |
57+
| impl.living_lab_id | BigInt (FK) | Links implementation to lab |
58+
| impl.start_at | DateTime? | Used for "last updated" computation |
59+
| impl.updated_at | DateTime? | Used for "last updated" computation |
60+
61+
### Users (`users`)
62+
63+
| Field | Type | Notes |
64+
|-------|------|-------|
65+
| id | BigInt (PK) ||
66+
| name | String ||
67+
| email | String ||
68+
| status | String (signup, active, disabled) | Used for active vs pending breakdown |
69+
70+
---
71+
72+
## Computed Analytics Data Structures
73+
74+
These are TypeScript interfaces for data computed in `src/lib/helpers/analytics.ts` and passed as props from the Astro page to components.
75+
76+
### MetricCardData
77+
78+
```typescript
79+
/** Data for a single summary metric card */
80+
interface MetricCardData {
81+
label: string; // e.g. "Living Labs", "Users (active / pending)"
82+
value: string; // Primary display value, e.g. "12", "45 / 3"
83+
icon?: string; // Optional icon identifier
84+
color?: string; // Tailwind color class, e.g. "text-primary"
85+
}
86+
```
87+
88+
### LabKpiTimelineData
89+
90+
```typescript
91+
/** Data for the D3 line chart — one series per living lab */
92+
interface LabKpiTimelineSeries {
93+
labId: number;
94+
labName: string;
95+
color: string;
96+
dataPoints: { year: number; count: number }[];
97+
}
98+
```
99+
100+
### AlertCardData
101+
102+
```typescript
103+
/** Data for a single analytics alert card */
104+
interface AlertCardData {
105+
label: string; // e.g. "Labs with no KPI results"
106+
value: number; // Count
107+
severity: "warning" | "danger" | "info";
108+
items?: string[]; // Optional list of affected entity names
109+
}
110+
```
111+
112+
### LivingLabMetricsRow
113+
114+
```typescript
115+
/** One row in the living lab metrics table */
116+
interface LivingLabMetricsRow {
117+
labId: number;
118+
labName: string;
119+
totalResultEntries: number; // All KPI results (including child KPIs)
120+
kpisCoveredCount: number; // Distinct main/parent KPIs with results
121+
totalMainKpis: number; // Total main/parent KPI definitions
122+
pushMeasuresCount: number;
123+
pullMeasuresCount: number;
124+
lastUpdatedAt: string | null; // ISO date string or null
125+
}
126+
```
127+
128+
### KpiCoverageRow
129+
130+
```typescript
131+
/** One row in the KPI coverage table */
132+
interface KpiCoverageRow {
133+
kpiId: number;
134+
kpiNumber: string; // e.g. "1", "2", "3"
135+
kpiName: string;
136+
kpiType: "GLOBAL" | "LOCAL";
137+
labsWithResultsCount: number; // How many labs have submitted results
138+
totalLabs: number; // Total number of labs as a reference
139+
}
140+
```
141+
142+
### LabMeasuresBarData
143+
144+
```typescript
145+
/** Data for the D3 bar chart — one group per living lab */
146+
interface LabMeasuresBarData {
147+
labName: string;
148+
pushCount: number;
149+
pullCount: number;
150+
}
151+
```
152+
153+
---
154+
155+
## Data Flow Diagram
156+
157+
```
158+
┌─────────────────────────────────────────────────────┐
159+
│ analytics.astro (SSR frontmatter) │
160+
│ │
161+
│ 1. ApiClient.getLivingLabs() → ILivingLabPopulated[] │
162+
│ 2. ApiClient.getKPIs({}) → IKpi[] │
163+
│ 3. ApiClient.getMeasures() → IProject[] │
164+
│ 4. ApiClient.getUsers() → User[] │
165+
│ │
166+
│ ┌───────────────────────────────┐ │
167+
│ │ analytics.ts helpers │ │
168+
│ │ - computeMetricCards() │ │
169+
│ │ - computeLabKpiTimeline() │ │
170+
│ │ - computeAlerts() │ │
171+
│ │ - computeLabMetricsTable() │ │
172+
│ │ - computeKpiCoverageTable() │ │
173+
│ │ - computeLabMeasuresBar() │ │
174+
│ └───────────────────────────────┘ │
175+
│ │
176+
│ Pass computed data as props ↓ │
177+
├─────────────────────────────────────────────────────┤
178+
│ REACT COMPONENTS (SSR-only, no client:* → zero JS) │
179+
│ ┌──────────┐ ┌──────────────┐ ┌────────────────┐ │
180+
│ │MetricCard│ │AnalyticsAlerts│ │LivingLabMetrics│ │
181+
│ │ (×4) │ │ │ │ Table │ │
182+
│ └──────────┘ └──────────────┘ └────────────────┘ │
183+
│ ┌──────────────────┐ │
184+
│ │KPICoverageTable │ │
185+
│ └──────────────────┘ │
186+
├─────────────────────────────────────────────────────┤
187+
│ REACT ISLANDS (client:load — D3 requires DOM) │
188+
│ ┌───────────────────────┐ ┌──────────────────────┐ │
189+
│ │D3LineChartLabKPIs │ │D3BarChartLabMeasures │ │
190+
│ │ Overtime │ │ │ │
191+
│ └───────────────────────┘ └──────────────────────┘ │
192+
└─────────────────────────────────────────────────────┘
193+
```
194+
195+
---
196+
197+
## Mapping: KPI Result → Parent KPI
198+
199+
To determine coverage of main/parent KPIs:
200+
201+
```
202+
For each IKpiResult in a lab's kpi_results:
203+
1. Get kpidefinition_id from the result
204+
2. Look up the IKpi definition
205+
3. If definition.parent_kpi_id is not null → use parent_kpi_id as the "covered parent"
206+
4. If definition.parent_kpi_id is null → this IS a parent KPI, use definition.id
207+
5. Add resolved parent ID to the "covered" set for that lab
208+
```
209+
210+
This ensures a parent KPI is counted as "covered" whether the result was submitted against the parent directly or against any of its children.
242 KB
Loading

0 commit comments

Comments
 (0)