# 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 #24–29) - **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 Q1–Q4 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.