- 1) Goal & Preconditions
- 2) High‑level User Flow (Mermaid)
- 3) Widgets — Full Details
- 4) Filters & UI Controls (global)
- 5) Data & Validation Cheatsheet
- 6) API Surface — Full Stubs
- 7) Non‑Functional Requirements
- 8) UX Copy & Errors (EN/AR)
- 9) Edge Cases & Handling
- 10) Acceptance Criteria
- 11) Analytics Events (recommended)
- 12) Implementation Notes & Recommendations
- 13) Implementation Checklist (Dev ticket)
- 14) Appendix — Certification gates
1) Goal & Preconditions
Goal
Provide tenants a single, actionable dashboard summarizing facility certification health, device status, alerts and subscriptions using only data the system stores given current retention rules (Device lastReading, Device×Bucket for current month, MonthlyRollup for historical data).
Preconditions
- Tenant is authenticated and authorized to view tenant resources.
- Tenant has at least one Facility with geolocation (lat/lon recommended, fallback: address string).
- System retention guarantees:
- Device×Bucket rows are retained for current month + purge buffer (month_close + 7d). Do not rely on Device×Bucket for historical trends.
- MonthlyRollup is the canonical, long-term denormalized store for historical, month-on-month aggregator data.
- Alerts store exists and keeps at least 3 months of records for dashboard feeds (recommended).
- Facility objects include: ID, name, type, timezone, lat, lon, currentCertLevel (recommended denorm cache), certExpiryDate, subscriptionStatus.
2) High‑level User Flow (Mermaid)
Provide/attach the Mermaid diagram here when available.
%% mermaid placeholder
flowchart LR
A[Open Dashboard] --> B[KPI Summary]
B --> C[Trends]
B --> D[Devices vs Alerts]
C --> E[Top Failures]
B --> F[Map]
B --> G[Mini Grid]
B --> H[Alerts Feed]
3) Widgets — Full Details
Widget A — Dashboard Summary KPIs
Purpose / UX
Show top-level tenant KPIs as tiles at the top of the dashboard. Tiles: Total Devices, Devices with Alerts (30d), Active Certifications, Total Facilities, Total Assets, Active Subscriptions.
Data Fields & Source
- totalDevices — source: devices table/API. COUNT where tenant_id.
- devicesWithAlerts30d — source: alerts store. COUNT(DISTINCT device_id) WHERE created_at >= now() - 30d.
- activeCertifications — source: monthly_rollup or denorm facility.currentCertLevel (count facilities classified as Active).
- totalFacilities — source: facilities table.
- totalAssets — source: facility hierarchy (assets/locations/spaces). Sum by tenant.
- activeSubscriptions — source: subscriptions table (facility subscription statuses).
Backend logic
- Use cached counts where heavy (summary endpoint caches for 30s–1m).
- For certifications, read denorm facility.currentCertLevel if available; otherwise compute from latest monthly_rollup month.
API Stub
GET /v1/dashboard/summary?tenantId=TEN_1
Response:
{
"totalDevices":124,
"devicesWithAlerts30d":12,
"activeCertifications":5,
"totalFacilities":12,
"totalAssets":34,
"activeSubscriptions":5
}
Acceptance Criteria
Values reflect tenant data at request time. Tiles link to devices list, alerts view, certifications list, facilities list, subscriptions list.
Edge Cases
Tenant with zero facilities: show zeros & CTA to onboard facility.
Widget B — Devices vs Alerts (last 30 days)
Purpose / UX
Bar chart + sparkline: totalDevices vs devicesWithAlerts (30d) with a 30‑day alerts timeseries sparkline. Clicks filter device table to devices with alerts.
Data Fields & Source
- devices: devices table (device.id, device.name, device.status, device.lastReading).
- alertsTimeseries: alerts table (created_at, severity, device_id).
Backend logic
- totalDevices = COUNT(devices)
- devicesWithAlerts = COUNT(DISTINCT device_id WHERE created_at >= now()-30d)
- Timeseries by grouping alerts by DATE(created_at)
API Stub
GET /v1/dashboard/devices-overview?tenantId=TEN_1&days=30
Response:
{
"totalDevices":124,
"devicesWithAlerts":12,
"alertsTimeseries":[{"date":"2025-07-25","count":3}, ...]
}
Acceptance Criteria
Clicking devicesWithAlerts opens a devices table filtered to those that had alerts; include lastReading.
Edge Cases
Toggle to count unique device-days vs raw alerts.
Filters
- Facility (all / specific)
- Severity (info / warn / critical)
- Parameter (CO2, Temp, Humidity, etc.)
- Date range (default 30 days)
Widget C — Certifications Summary & KPIs
Purpose / UX
Show counts by certification state and a small expiring list with CTA to renew.
Data Fields & Source
facility.currentCertLevel (preferred cached field) OR compute from monthly_rollup for latest month. Fields: facilityId, name, certLevel, certExpiryDate, certStartDate, subscriptionStatus.
Backend logic
If facility.currentCertLevel exists, use it; otherwise derive from latest monthly_rollup per facility using certification gates. Count by state.
API Stub
GET /v1/dashboard/certifications?tenantId=TEN_1
Response:
{
"totals": { "platinum":2, "gold":5, "silver":3, "pending":2, "grace":1, "suspended":0 },
"expiring": [{"facilityId":"F3","name":"Shop C","daysRemaining":25}, ...]
}
Acceptance Criteria
Counts reflect latest computed levels. Expiring list ordered by daysRemaining ascending.
Filters
Level (platinum/gold/silver) • Expiring (≤90/≤30/≤7 days)
Widget D — Certification Trend (line chart)
Purpose / UX
Show month-by-month counts per certification level for N months (default 12). Toggle to show/hide levels.
Why use MonthlyRollup
Historical trends MUST use monthly_rollup as Device×Bucket is ephemeral.
Data & Backend
For each month M in window: for each facility fetch monthly_rollup for M, aggregate within_pct across required parameter families, apply gates to classify level, tally counts per level.
API Stub
GET /v1/dashboard/cert-trend?tenantId=TEN_1&months=12
Response:
{
"months": ["2025-08","2025-07", ...],
"series": {
"platinum": [2,3,2,...],
"gold": [5,4,5,...],
"silver": [10,9,11,...],
"paused": [1,1,0,...]
}
}
Acceptance Criteria
For each month, classification equals facility-level gate application from monthly_rollup.
Filters
Months window (3/6/12/24) • Facility type (Retail/Office/School)
Widget E — Top 5 Facilities failing thresholds
Purpose / UX
Table listing worst-performing 5 facilities (lowest within_pct or worst primary failing parameter) for current month.
Data Fields & Source
monthly_rollup (scope='facility', month=currentMonth): param, within_pct, expected, actual, flags.
Backend logic
Compute failureScore (configurable), sort ascending, return top 5. Include primaryFailParam = param with lowest within_pct.
API Stub
GET /v1/dashboard/top-failures?tenantId=TEN_1&limit=5
Response:
[
{ "facilityId":"F1","name":"Shop A","primaryFailParam":"CO2","withinPct":58.4,"expected":720,"actual":432 },
...
]
Acceptance Criteria
Top 5 reflect current month monthly_rollup. Missing monthly_rollup → mark as insufficient data.
Filters
Facility type • Parameter family • Only expected ≥ X
Widget F — Device lastReading mini‑grid
Purpose / UX
Compact card grid showing N primary devices. Each tile: Device name, primary param & value, timestamp (facility local TZ), online/offline badge.
Data Fields & Source
device.lastReading on devices: { param, value, timestamp } and device.lastSeen / device.status.
API Stub
GET /v1/dashboard/device-mini-grid?tenantId=TEN_1&limit=12
Response:
{ "items": [ { "deviceId":"D1","name":"CO2 Sensor 1","param":"CO2","value":412,"timestamp":"2025-08-22T10:00:00Z","status":"online","assetId":"A1" }, ... ] }
Acceptance Criteria
Tile displays lastReading.value and timestamp in facility timezone.
Filters
Facility • Parameter • Status (online/offline)
Widget G — Facility Map (pins color‑coded by certification)
Purpose / UX
Map with facility pins. Color indicates certification status. Hover: facility name, cert level, days remaining, link to facility page.
Data Fields & Source
facilities: facilityId, name, lat, lon, timezone, facility.currentCertLevel (preferred), certExpiryDate, subscriptionStatus. Fallback: compute from latest monthly_rollup.
Backend logic
Return geo points with certLevel and daysRemaining. Pin color rules applied in UI.
API Stub
GET /v1/dashboard/facility-map?tenantId=TEN_1
Response:
[ {"facilityId":"F1","name":"Shop A","lat":25.123,"lon":55.321,"certLevel":"Gold","expiry":"2026-06-01","daysRemaining":285}, ... ]
Filters
Level (All / Platinum / Gold / Silver / Payment Due / Suspended) • Expiring (≤90/30/7 days) • Facility type
Edge Cases
Missing lat/lon → facility appears in side list with CTA to add coordinates.
Widget H — Alerts feed (recent)
Purpose / UX
Rolling feed of recent alerts for tenant with severity badges and quick links.
Data Fields & Source
alerts: alert_id, created_at, device_id, facility_id, param, value, severity, message.
API Stub
GET /v1/facilities/{id}/alerts?tenantId&since=2025-07-01
Response: [ { alert objects... } ]
Filters
Severity • Parameter • Facility • Date range
4) Filters & UI Controls (global)
- Date Range: 7d, 30d, 90d, 12 months or custom.
- Facility: All / specific (multi-select).
- Level: Platinum / Gold / Silver / PaymentDue / Suspended.
- Facility Type: Retail / Office / School / Warehouse / Others.
- Parameter: CO2 / Temperature / Humidity / VOC / PM2.5 / Water Temp / Cold Chain.
- Severity: info / warn / critical.
- Device status: online / offline / unknown.
- Export: CSV/PNG for charts & tables.
5) Data & Validation Cheatsheet
Core tables / stores (canonical)
facilities
| Field | Type | Notes |
|---|---|---|
| id | string | |
| name | string | |
| tenant_id | string | |
| type | string | |
| timezone | string (IANA) | e.g., Asia/Dubai |
| lat | float | |
| lon | float | |
| currentCertLevel | enum | platinum/gold/silver/pending/grace/suspended (denorm) |
| certExpiryDate | date | |
| created_at, updated_at | timestamp |
devices
| Field | Type | Notes |
|---|---|---|
| id, externalId, name, asset_id, facility_id, tenant_id | various | |
| lastReading | json | { param, value, unit, timestamp } |
| lastSeen | timestamp | |
| status | enum | online/offline/unknown |
alerts
| Field | Type | Notes |
|---|---|---|
| id, tenant_id, facility_id, device_id | string | |
| param, value, severity, message | various | |
| created_at | timestamp |
device_x_bucket (ephemeral — current month only)
| Field | Type | Notes |
|---|---|---|
| device_id, param | string | |
| bucket_start_utc | timestamp | |
| value_last, within | float/bool | Purge at month_close + 7d |
monthly_rollup (denormalized, kept forever)
| Field | Type | Notes |
|---|---|---|
| scope | enum | facility/asset |
| scope_id | string | facilityId / assetId |
| month | date | YYYY‑MM‑01 |
| param | string | |
| expected | int | expected buckets/readings |
| actual | int | actual buckets/readings |
| within_count | int | |
| within_pct | float | |
| flags | json | { gate: 'silver', notes: '...' } |
subscriptions
| Field | Type | Notes |
|---|---|---|
| id | string | |
| facility_id | string | |
| status | enum | active/pending_activation/paused/cancelled |
| nextBillingDate | date | |
| graceEndsAt | timestamp |
6) API Surface — Full Stubs (JSON shapes)
All endpoints expect authenticated requests with tenant-scoped tokens. Pagination via limit/offset or cursor.
1) GET /v1/dashboard/summary?tenantId=
Response:
{ totalDevices:int, devicesWithAlerts30d:int, activeCertifications:int, totalFacilities:int, totalAssets:int, activeSubscriptions:int }
2) GET /v1/dashboard/devices-overview?tenantId=&days=30&facilityId=
Response:
{ totalDevices:int, devicesWithAlerts:int, alertsTimeseries:[{date:string,count:int}], devices:[{deviceId,name,lastReading,timestamp,status}] }
3) GET /v1/dashboard/certifications?tenantId=&filter=
Response:
{ totals: { platinum:int, gold:int, silver:int, pending:int, grace:int, suspended:int }, expiring:[{facilityId,name,daysRemaining,expiry}] }
4) GET /v1/dashboard/cert-trend?tenantId=&months=12 — uses MonthlyRollup
Response:
{ months:["YYYY-MM"], series:{ platinum:[int,...], gold:[...], silver:[...], paused:[...] } }
5) GET /v1/dashboard/top-failures?tenantId=&limit=5 — uses MonthlyRollup current month
Response:
[ { facilityId, name, primaryFailParam, withinPct, expected, actual, notes } ]
6) GET /v1/dashboard/device-mini-grid?tenantId=&limit=12&facilityId=¶m=
Response:
{ items:[{ deviceId, name, assetId, param, value, unit, timestamp, status }] }
7) GET /v1/dashboard/facility-map?tenantId=&filters=
Response:
[ { facilityId, name, lat, lon, certLevel, expiry, daysRemaining, type } ]
8) GET /v1/facilities/{id}/alerts?since=&severity=&limit=
Response: [{alert objects...}]
7) Non‑Functional Requirements
Performance
- Dashboard summary API p95 ≤ 300ms (tenants ≤ 100 facilities) when cached (30s).
- Map endpoint (≤500 pins) p95 ≤ 500ms; leverage facility.currentCertLevel cache.
Scalability
- MonthlyRollup indexed by (tenant_id, scope, month). Use precomputed aggregates for trends.
Availability & Reliability
- Alerts feed eventually consistent; show Last updated timestamp on widgets.
Security
- Tenant-scoped tokens limit data exposure; superadmin portfolio endpoints require elevated privileges.
Retention
- Device×Bucket purge: month close + 7d. MonthlyRollup persistent.
8) UX Copy & Errors (EN / AR samples)
KPI tiles
EN: Active Certifications: {n} — Click to view facilities.
AR: الشهادات النشطة: {n} — اضغط لعرض المرافق.
Map tooltip
EN: {FacilityName} — {CertLevel} (expires in {daysRemaining} days)
AR: {FacilityName} — {CertLevel} (تنتهي بعد {daysRemaining} يوم)
Errors
- 500: “Something went wrong — please try again later.” / 500: «حدث خطأ — يرجى المحاولة لاحقًا».
- 403 (tenant not authorized): “You don’t have permission to see this dashboard.” / «ليس لديك إذن لعرض لوحة التحكم هذه».
- Map missing coords: “This facility has no coordinates. Add location to appear on map.” / «هذه المنشأة لا تحتوي على إحداثيات. أضف الموقع للظهور على الخريطة.»
9) Edge Cases & Handling
- Incomplete monthly_rollup: indicate insufficient data; exclude from some aggregates or mark low-confidence.
- Ties in Top‑5: tie-breaker by actual/expected completeness then by severity of primaryFailParam.
- Devices with no lastReading: show “No readings” and lastSeen if available.
- Missing geolocation: show facility in side list with CTA.
- Large tenants: pagination & server-side filtering for device lists; map clustering for >500 pins.
- Timezones: render timestamps in facility’s timezone.
10) Acceptance Criteria (Given / When / Then)
- A. Dashboard loads summary KPIs — Given tenant has facilities; When user opens Dashboard; Then show KPI tiles with counts and each tile links to relevant lists.
- B. Certification Trend correctness — Given monthly_rollup contains months M1..Mn; When user selects 12 months; Then series counts equal facility-level classification derived from monthly_rollup for each month.
- C. Top 5 failures computed from monthly_rollup — Given monthly_rollup for current month is present; When user opens Top‑5 widget; Then show top 5 by lowest within_pct and primaryFailParam.
- D. Map pins color‑coded — Given each facility has cert status cached or computed; When map loads; Then pins colors reflect cert level and hover shows daysRemaining.
- E. Device mini‑grid accuracy — Given device.lastReading exists; When mini‑grid loads; Then tiles display lastReading value and timestamp in facility timezone.
11) Analytics Events (recommended)
| Event | Properties |
|---|---|
| dashboard_viewed | tenantId, userId, timestamp |
| widget_opened | widgetId, tenantId, userId |
| filter_applied | tenantId, userId, filterType, filterValue |
| map_pin_clicked | facilityId, tenantId, userId |
| device_tile_clicked | deviceId, tenantId, userId |
| export_csv | widgetId, tenantId, userId |
12) Implementation Notes & Recommendations
- Cache facility.currentCertLevel: compute nightly from monthly_rollup; store on facilities table to speed map & KPI reads.
- Precompute trend aggregates: maintain cert_trend table per tenant‑month to avoid heavy on‑the‑fly queries.
- Map clustering for large footprints; client‑side pin color mapping.
- API versioning: prefix with /v1/dashboard; follow semantic versioning for incompatible changes.
- Testing: unit tests for classification gates, e2e tests for widget drilldowns, and data‑driven tests for Top‑5 ranking.
13) Implementation Checklist (Dev ticket)
- Build API endpoints (see section 6)
- Add caching for summary endpoint (30s)
- Implement map endpoint + clustering
- Implement device mini‑grid endpoint
- Implement certification trend precompute job (cron/nightly)
- Add analytics instrumentation
- Add unit & integration tests
14) Appendix — Certification gates (default rules)
- IAQ/Odor: Silver ≥ 70% within; Gold ≥ 80%; Platinum ≥ 90%
- Cold/Water: require ≥ 99% within for active certifications (higher priority)
- Expiry warnings: 90 days / 30 days / 7 days thresholds
- Offline thresholds: IAQ/Odor device offline if lastSeen > 2h; CO2 if > 30m