Files
forge/claude_artifacts/FORGE-parity-spec.md
T
Dmytro Tkachenko 9868b18818 Agents
2026-08-29 12:55:39 +03:00

215 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.