Files
forge/claude_artifacts/FORGE-parity-spec.md
T

215 lines
13 KiB
Markdown
Raw Normal View History

2026-08-29 11:59:28 +03:00
# FORGE parity spec — rebuilding "Let it Snow" on FORGE (Husky design)
Deep-check of the initial app (`/Users/dmytrotkachenko/WebstormProjects/Let It Snow`)
cross-referenced with `storage-dump.json` (its localStorage export) and the Husky
design template. Goal: a **functionally near-identical** app under FORGE's stack
(Vite+React+TS client · Express+TS server · Postgres `forge`) with a **new view**
built from Husky's hand-rolled chart components.
Sources: 6 discovery agents (analytics charts · KPI/SLA/finance · board behavior ·
data inventory · Husky design · architecture docs). All findings self-verified.
---
## 0. Load-bearing architecture facts
1. **Every board is a READ-ONLY mirror** rebuilt from storage by background scans.
No drag-and-drop write-back to ServiceNow/Jira anywhere (the ADO board has local
DnD only; the one write action is "Create Jira"). → FORGE keeps its model: the
Chrome extension scans and POSTs to `/api/sync`; the web app renders DB state and
links out to ServiceNow/Jira for changes. **No transitions to build.**
2. **Single analytics source** in the app: `analytics_data` (954 `ticketsMeta` records
+ precomputed aggregations). Everything else enriches it, joined on the **RITM number**.
3. **No charting library** in either app — Let it Snow hand-rolls SVG/CSS; Husky does too.
FORGE rebuilds charts as Husky components (or a real lib if we choose; spec assumes Husky-style).
4. **Money rule:** `finalCost = 0` means *absence of price, not free work* — never counts
as money, never gets a size, always a "missing cost" margin leak.
5. **Currency:** display currency (default GBP); **fixed** cross-rates `1 GBP = 1.2 EUR = 20 MXN`.
---
## 1. Feature/view map (initial app)
| Screen | Rebuild target in FORGE |
|---|---|
| Popup (RITM list + Jira tab) | N/A (extension already syncs; optional toolbar) |
| **Dashboard: Kanban board + List view** | **Board page** (7-state columns, filters, card fields, list toggle) |
| **Analytics panel — 3 tabs: Overall / Active / PMs KPI** | **3 analytics pages** (the bulk of "missing statistics") |
| **PM Insights popup** (alert KPIs + drill-downs + Excel) | **Insights page** |
| At-risk / AI Advisor | **OUT OF SCOPE** (user excluded AI) |
| Jira board (read-only mirror) | **Jira board page** (later phase) |
| Size CALC (labor→size estimator) | **Tool** (later phase) |
| Figma links, Teams, Weather | out of scope / optional |
Excluded per DEAD_CODE.md: `melody.js`, removed finance-filter UI, live-FX fetch,
USD from FX cache, Assist/Jira bar-graph toggle (replaced by dual calendars).
---
## 2. Statistics / charts inventory (29) → Husky component mapping
### TAB 1 — Overall (11)
| # | Chart | Data | Husky component |
|---|---|---|---|
| 1 | Opened per month (grouped bars ±YoY, Brand/Market/CGO sub-group, bar/line) | `openedByMonth`/`ByDay`, `ticketsMeta` | new `GroupedBars` (from `MonthlyByUser`) + `TrendArea` |
| 2 | Closed per month | `closedByMonth` | same |
| 3 | Revenue per month (closed, cost/tickets, ±YoY) | `ticketsMeta.finalCost`+FX | `GroupedBars`/`TrendArea` |
| 4 | Lifetime-at-close histogram (6 buckets) + median/avg/closed tiles | `ticketsMeta` open→close | `BucketBars` + `KpiTile`×3 |
| 5 | Opened YoY pie (fair/full) | `openedByDay` | `StateDonut`/`DonutChart` |
| 6 | Closed YoY pie | `closedByDay` | donut |
| 7 | By business unit (per-year, brand drill-down) | `ticketsMeta.businessUnit` | `BucketBars` + expandable |
| 8 | By requester (bars + YoY, market drill-down) | `byRequester` | `RequestorBars` + drill |
| 9 | Ticket-share donut (#/%, top 18 + Others) | `byRequester` | `StateDonut`/`InsightChart` pie |
| 10 | By brand (per-year segments, market drill, YoY) | `ticketsMeta.brand/market` | `BucketBars` segmented |
| 11 | By market | same | `BucketBars` |
### TAB 2 — Active (6) — from live board tickets (status='active')
| # | Chart | Data | Husky component |
|---|---|---|---|
| 12 | Open-not-closed by month | active tickets | `TrendArea`/bars |
| 13 | By status (6 kanban cats) | active | `BucketBars` (state colors) |
| 14 | Age histogram (0-50…200+ days) | active openedAt | `BucketBars` |
| 15 | Time-in-status min/median/max | active | grouped mini-bars |
| 16 | By brand, status-segmented (market drill) | active | stacked `BucketBars` |
| 17 | Jira status durations (workflow order) | `jira_status_map.statusDurations` | `BucketBars` ordered |
### TAB 3 — PMs KPI (12)
| # | Chart | Data | Husky component |
|---|---|---|---|
| 18 | Workload by month (revenue, prev/cur yr, per-PM) | `ticketsMeta` closed cost | `GroupedBars` + `MonthlyByUser` |
| 19 | PM Engagement (backlog/Assist✓/Jira✓/Stale) + **dual daily-activity heatmaps** | live `meta.activity` + `jira movements` | `AssigneeBars` + **new `CalendarHeatmap`** |
| 20 | Missing final cost (ranked PM bars + drill) | `ticketsMeta.finalCost` null | `AssigneeBars` + list |
| 21 | Awaiting PO (ranked, Cost/Avg-d/Max-d, sortable) | finance PO + `sn_waiting_po_meta` + Jira | `AssigneeBars` + table |
| 22 | No-cost by brand/market | derived | `BucketBars`×2 |
| 23 | Awaiting-PO by brand/market | derived | `BucketBars`×2 |
| 24 | **On-Time Delivery** heatmap (PM×size, cur+prev yr) + editable norms | close-days vs `otd_day_norms` | **new `KpiHeatmap`** |
| 25 | **Avg days to close** heatmap | vs `avgdays_day_norms` | `KpiHeatmap` |
| 26 | **Time to assign PM** heatmap | `firstAssignedDate` vs `asla_day_norms` | `KpiHeatmap` |
| 27 | **Time to send preview** heatmap | `inUatAt` vs `psla_day_norms` | `KpiHeatmap` |
| 28 | **Avg TTFR** heatmap | `ttfrMinutes` vs `lisr_ttfr_norms` (hrs) | `KpiHeatmap` |
| 29 | **Avg PM response** heatmap | `clientRespMinutes` vs `lisr_cresp_norms` | `KpiHeatmap` |
**New components needed** (not in Husky): `KpiHeatmap` (PM×size grid vs norms, green/red),
`CalendarHeatmap` (GitHub-style daily activity), `GroupedBars` (multi-year month bars — extend `MonthlyByUser`), a period/granularity control (year/quarter/month + ±YoY).
### KPI definitions (SLA grid #2429)
- **TTFR** = `ttfrMinutes/1440` (days), anchor `openedDate`, norm hours.
- **PM response (cresp)** = `clientRespMinutes/1440`, anchor `openedDate`. (Our reply speed, not client wait.)
- **Avg close** = `daysBetween(opened, closed)`, anchor `closedDate`.
- **OTD** = same duration scored vs `otd_day_norms`; on-time% = onTime·100/scored.
- **Assign SLA** = `max(0, daysBetween(fulfillment, firstAssigned))`, anchor `fulfillmentDate`.
- **Preview SLA** = `daysBetween(toDoAt, inUatAt)`, anchor `inUatAt`.
- Grid cell = `{avgDays, count, onTime, onTimePct}`, per PM×size; "No cost" column norm = 1d.
- Periods: current vs previous Year, and Q1Q4 each.
---
## 3. PM Insights alert KPIs (the Insights page)
Thresholds `insights_thresholds` (days): `unassigned:1, assigned:2, hold:5, wip:7,
customerReplied:3, awaiting:7, lifetime:90, jiraStuck:7, jiraUAT:7, noChase:5,
waitingPo1:7, waitingPo2:14, waitingPo3:21`.
Alert lists: Unassigned · Open/Assigned-long · On-Hold (age OR jiraBreached OR clientOwed)
· WIP-stalled (Jira age) · WIP-no-Jira · Customer-replied · Awaiting-info · Lifetime-monsters
(≥90d) · Inactive-requester · **∑ Total Alerts**. Waiting-PO with L1/L2/L3 escalation +
`waitingPoRevenue`. PM-KPI table: **# Tickets · $ Revenue · ⚠ Rev-at-Risk** (sortable,
hidden PMs from `pm_kpi_settings`). Excel export preserves PM→Size→Ticket grouping.
Support rules: `_jiraBreached`, `_clientOwed` (client replied last ≥1d), `_isAwaitingAgency`
(WIP + last touch by client → "Customer replied").
---
## 4. Board page (7-state kanban + list)
Columns (fixed order): **Unassigned → Open/Assigned → On Hold → [WIP · Customer-replied ·
Awaiting]* → Closed/Awaiting-PO**. `*` middle-3 reorderable via group-by (6 perms).
- **Customer-replied** is a pseudo-status: WIP ticket where `lastActivityBy` ∉ colleague roster.
- Column SLA subtitles ("assign within Nd", "max Nd in progress", …); per-column count.
- Filters: search, brand, market, assignee(+region EU/LATAM sentinels), requester, custom-label,
jira-assignee, jira-status, staleness (stale/updated/inactive/PO/jira-stuck/missing-cost), hide-empty, hide-cost.
- Sort (14): default/number/status-age/lifetime/jira/brand/cost/due/analyzed.
- **16 toggleable card fields** (`card_fields`): stateBadge, shortDesc, description, assignee,
group, brand-market pill, raisedBy, lifetime, dueDate, stateChanged, lastActivity, comments,
createJira, jiraStatus, jiraBar, teamsLink.
- Card click → opens ServiceNow in new tab (no in-app transition). List view = same data as table.
- Waiting-PO detection: synthetic closed card OR Jira "Waiting PO" OR empty PO cell in finance Excel.
---
## 5. Data contract — what to ingest (fixes "not all statistics moved")
FORGE currently seeds only 3 dump keys (`sn_tickets`, `analytics_meta_cache`, `jira_status_map`
→ 966 tickets). The analytics engine needs the **full** dump:
| Dump key | Rows | Feeds |
|---|---|---|
| **`analytics_data.ticketsMeta`** | **954** | THE analytics dataset (SLA/cost/jira/dates) — charts 1-11,18-29 |
| `analytics_data.{openedByMonth,closedByMonth,openedByDay,closedByDay}` | — | time-series (or recompute server-side) |
| `analytics_data.byRequester(Numbers)` | 280 | requester/brand/market drill-downs |
| `analytics_meta_cache` | 965 | closed-ticket enrichment |
| `sn_tickets` | 100 | active board + activity timeline |
| `sn_waiting_po_meta` | 884 | waiting-PO backlog |
| `jira_status_map` | 127 | RITM↔Jira, statusDurations |
| `jira_board_state`/`snapshot` | 1045 | Jira board page |
| `finance_xlsx_data` | 1128 | PO/invoicing/milestones |
| `insights_thresholds`, `pm_kpi_settings` | — | insights + PM roster |
| `fx_rates_cache`, `lis_size_calc_cfg` | — | FX + size model |
| `brand_colors`, `snow_colors`, `sn_states_order` | — | palettes + column order |
| windowLocalStorage `*_norms` (psla/otd/ttfr/cresp/avgdays/asla) | — | SLA target lines |
**Ticket master fields** (from `ticketsMeta`): `number, shortDesc, state, year, brand, market,
businessUnit, assignedTo, openedBy, openedDate, closedDate, firstAssignedDate, firstReplyAt,
fulfillmentDate, toDoAt, inUatAt, ttfrMinutes, clientRespMinutes, finalCost, currencyCode, jiraKey`.
**Coverage:** ~24 months (2024-08 → 2026-08); 45 brands, 41 markets, 8 BUs, 10 PMs, 280 requesters;
GBP/EUR/MXN. Data-quality: businessUnit casing dupes, currency blanks/junk ("NO"/"PART"),
"(Inactive)" suffixes, finalCost is a string, two date formats — **normalize on ingest**.
---
## 6. Design mapping (Husky as template)
- **Stack/tokens:** Husky's SCSS-module system, `:root` tokens, `card`+`mono-label`+`srOnly`
skeleton, `useInView`+`useCountUp` reveal hooks. Component = folder (`index.tsx` + `.module.scss`).
- **Reuse verbatim:** `KpiTile`, `StateDonut`, `BucketBars`, `AssigneeBars`, `RequestorBars`,
`TrendArea`, `MonthlyByUser`, `InsightChart`, plus `Stats`-page archetypes (`ActivityList`,
`IssuesBySeverityCard`, `DonutCard`).
- **Build new (Husky-styled):** `KpiHeatmap`, `CalendarHeatmap`, `GroupedBars`, period control.
- **Palettes:** size `{XS:#14b8a6,S:#1a73e8,M:#f59e0b,L:#10b981,XL:#8b5cf6,XXL:#ef4444}`;
year `['#f97316','#1a73e8','#16a34a','#dc2626','#7c3aed']`; state colors per §4; brand from `brand_colors`.
- **FORGE already uses the indigo palette** (`#6366f1`) matching Let it Snow's `UI.primary` — keep it,
or adopt Husky's Fluent blue. **DECISION NEEDED.**
---
## 7. Phased build plan
- **Phase 0 — Full data ingest (foundation).** Extend `dump-to-archives` + schema/seed to load
`analytics_data.ticketsMeta` (954) + finance + waiting-PO + jira board + thresholds + norms +
size cfg + palettes. Normalize dimensions. This alone restores the data behind every stat.
- **Phase 1 — Server analytics engine.** Compute aggregations from the DB (opened/closed by
month/day, byRequester/brand/market/BU, revenue/month, lifetime buckets, the 6 SLA metrics
PM×size, missing-cost, waiting-PO, engagement). Expose `/api/analytics/*`. Tests.
- **Phase 2 — Overall tab (charts 1-11).** New view on Husky components.
- **Phase 3 — Active tab (12-17) + Board page redesign** (7-state, filters, card fields, list).
- **Phase 4 — PMs KPI tab (18-29):** `KpiHeatmap` + editable norms + engagement + `CalendarHeatmap`.
- **Phase 5 — PM Insights page** (alerts, drill-downs, Excel export).
- **Phase 6 — Jira board page · Size CALC · finance detail** (as desired).
Each phase: engineer→reviewer→verifier; dba for schema; designer for new components.
---
## 8. Decisions to resolve before/within the build
1. **Size algorithm** — three coexist (nearest-nominal / largest-fits / Excel-MATCH). Pick one
(recommend **composite** for cards, as the app defaults).
2. **`psla_day_norms` default mismatch** (dashboard `{XS:1…XXL:15}` vs collector `{XS:3…XXL:30}`). Pick one.
3. **FX** — keep fixed GBP-base rates (recommended; live-fetch was retired).
4. **Palette** — keep FORGE indigo, or switch to Husky Fluent blue.
5. **Scope of charts for v1** — all 29, or the high-value subset first (recommend Overall + SLA heatmaps).
6. **Brand naming** — popup says "FORGE Tasks", product "Let it Snow" → FORGE.
7. **Boards** — SNOW board is core; Jira/ADO boards are later/optional.