13 KiB
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
- 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. - Single analytics source in the app:
analytics_data(954ticketsMetarecords- precomputed aggregations). Everything else enriches it, joined on the RITM number.
- 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).
- Money rule:
finalCost = 0means absence of price, not free work — never counts as money, never gets a size, always a "missing cost" margin leak. - 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), anchoropenedDate, norm hours. - PM response (cresp) =
clientRespMinutes/1440, anchoropenedDate. (Our reply speed, not client wait.) - Avg close =
daysBetween(opened, closed), anchorclosedDate. - OTD = same duration scored vs
otd_day_norms; on-time% = onTime·100/scored. - Assign SLA =
max(0, daysBetween(fulfillment, firstAssigned)), anchorfulfillmentDate. - Preview SLA =
daysBetween(toDoAt, inUatAt), anchorinUatAt. - 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,
:roottokens,card+mono-label+srOnlyskeleton,useInView+useCountUpreveal hooks. Component = folder (index.tsx+.module.scss). - Reuse verbatim:
KpiTile,StateDonut,BucketBars,AssigneeBars,RequestorBars,TrendArea,MonthlyByUser,InsightChart, plusStats-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 frombrand_colors. - FORGE already uses the indigo palette (
#6366f1) matching Let it Snow'sUI.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 loadanalytics_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
- Size algorithm — three coexist (nearest-nominal / largest-fits / Excel-MATCH). Pick one (recommend composite for cards, as the app defaults).
psla_day_normsdefault mismatch (dashboard{XS:1…XXL:15}vs collector{XS:3…XXL:30}). Pick one.- FX — keep fixed GBP-base rates (recommended; live-fetch was retired).
- Palette — keep FORGE indigo, or switch to Husky Fluent blue.
- Scope of charts for v1 — all 29, or the high-value subset first (recommend Overall + SLA heatmaps).
- Brand naming — popup says "FORGE Tasks", product "Let it Snow" → FORGE.
- Boards — SNOW board is core; Jira/ADO boards are later/optional.