723eb63e - Rename the lowest wait tier to New and cover everything under 12h - #1509
joshuakrueger-dfx wants to merge 9 commits into
Conversation
… hour The support dashboard grouped waiting customers into tiers of 1h, 12h and 24h. A ticket whose customer had been waiting for a few minutes therefore fell below every tier and was reachable through none of the three filter pills, so a clerk only saw it once a full hour had passed. Lower the first tier to 1 minute and label its pill "1 min". Both waitTier and the card now read their thresholds from WAIT_TIER_HOURS instead of repeating them, and waitTierLabel is the single source for the pill text. The section subtitle switches to a minutes wording below one hour, so it no longer renders a fractional hour. The preselected tier stays at 24h (escalations).
|
dfx pr guard EN: Thanks for your contribution! This repository follows A38. A38 pass: author local-CI report accepted for this head. DE: Danke für deinen Beitrag! In diesem Repository gilt A38. A38 pass: Autor-Local-CI-Report für diesen Head akzeptiert. Details
|
Follow-up from review. The subtitle test matched a bare digit and a single spelling of a fractional hour; it now compares the rendered subtitle in full. The lower boundary is checked at 59 seconds, one second below the threshold, rather than at 30. And the pill styles, which after the previous commit were only coupled to WAIT_TIER_HOURS by array index, are built alongside the thresholds again, so a fourth tier cannot destructure undefined. No change in rendered output for the three current tiers.
CONTRIBUTING asks every file a pull request touches to reach 100 % coverage. The overview screen had no test at all before this branch and sat at 66 % statements after the wait-tier tests were added. Cover the rest: the ticket-load error path, the statistics tab including the fallback to computeStatistics and the four stale-response guards, the period selector and trend bars, the limit-request and my-tickets sections with their empty and error states, and the tab switch. Three branches could not be reached from any input, so they are gone instead of being excused. The style and count lists are now tuples tied to the shape of WAIT_TIER_HOURS, which makes a missing entry a compile error rather than a silent fallback, and the unused action prop on Section is removed.
…in pill CONTRIBUTING asks the author to regenerate the affected screenshots so the reviewer sees the visual change in the diff. The committed baseline still showed the 1h pill. Regenerated against a local stack. Two things differ from the old image: the lowest pill now reads "1 min", and the My tickets card shows 0 instead of a dash. The dash came from an account without a verified name, which the screen can no longer be reached with at all — elevated endpoints now require staff KYC clearance, so any account that can open this screen has a name and the card shows a number. Only the overview screenshot is regenerated; the statistics tab is unaffected.
The test called itself "marks a 12h wait as Awaiting reply" but asserted a title that tier 1 and tier 2 share, so a ticket wrongly sorted into tier 1 would have passed. The word dfxYellow did not appear anywhere in the file. It now reads the badge of a 5h, a 13h and a 25h ticket and asserts gray, yellow and red. Raising the middle threshold so a 13h wait falls into tier 1 turns it red.
…the wait tier Lowering the first wait tier to one minute was the wrong instrument: it made the tier scale, which exists to rank urgency, contain nearly every waiting ticket. The thresholds go back to 1h, 12h and 24h, and the real need — seeing a ticket that just came in — gets a section of its own. Newest lists the ten most recently opened tickets, newest first, including the ones we already replied to, and sits after Escalations and Limit requests. It reads the tickets already in state; no extra request. The selected wait tier is now remembered across navigation through the safe storage helper, so opening a ticket and coming back no longer resets the view to 24h. A stored value is only applied if it is one of the current thresholds: browsers that saw the one-minute build have 0.0166 on disk, which would otherwise filter the list with no pill marked active. Also updates the full-stack spec, which CONTRIBUTING requires for a screen change, and regenerates the visual baseline on the same empty data set as before so the diff shows the UI change rather than seed noise.
|
EN: DE: Details
Durations rounded up to whole seconds / Laufzeiten auf ganze Sekunden aufgerundet. Original report / Originalbericht{
"head": "1fb81645f88d4168136d8e3c94f022e6d7fa56ec",
"private": false,
"recorded_at": "2026-09-18T14:06:27Z",
"repo": "DFXswiss/app",
"required": [
"lint"
],
"runs": [
{
"command": "agent a38 job commands --config '{\"env\":{\"CI\":\"true\"},\"npm\":{\"canaries\":[\"react-app-rewired/package.json\",\"react-scripts/package.json\",\"typescript/lib/typescript.js\",\"prettier/package.json\"],\"node_major\":20},\"steps\":[[\"npm\",\"run\",\"lint\"],[\"npm\",\"run\",\"format:md:check\"]]}'",
"duration_s": 47.69464462506585,
"exit_code": 0,
"id": "lint",
"name": "Lint and Markdown format",
"result": "pass",
"timeout_s": 600.0
}
],
"schema": "dfx-local-ci/v1"
} |
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "1fb81645f88d4168136d8e3c94f022e6d7fa56ec",
"pr": 1509,
"repo": "DFXswiss/app",
"runs": [
{
"run_id": 35353627941,
"workflow": ".github/workflows/codeql.yml"
},
{
"run_id": 35353627997,
"workflow": ".github/workflows/handbook-check.yaml"
},
{
"run_id": 35353628036,
"workflow": ".github/workflows/pr-review-bot.yml"
},
{
"run_id": 35353628601,
"workflow": ".github/workflows/pr.yml"
}
]
} |
… write Review found two assertions that could not see the failure they exist for. The My tickets card renders own count and total; asserting that the text contains "2" stayed green with a broken owner filter, because the 2 came from the total. It now compares the rendered value in full. The test for a throwing storage write asserted only the pill ring and the list, so removing the write call entirely would have left it green. It now also pins that the write was attempted with the selected threshold. Breaking the owner filter turns two tests red; removing the write call turns two tests red.
|
EN: DE: Details
Durations rounded up to whole seconds / Laufzeiten auf ganze Sekunden aufgerundet. Original report / Originalbericht{
"head": "1af3477d21c3d3fea9c5b2fc013c490e90d02c67",
"private": false,
"recorded_at": "2026-09-18T14:39:59Z",
"repo": "DFXswiss/app",
"required": [
"lint"
],
"runs": [
{
"command": "agent a38 job commands --config '{\"env\":{\"CI\":\"true\"},\"npm\":{\"canaries\":[\"react-app-rewired/package.json\",\"react-scripts/package.json\",\"typescript/lib/typescript.js\",\"prettier/package.json\"],\"node_major\":20},\"steps\":[[\"npm\",\"run\",\"lint\"],[\"npm\",\"run\",\"format:md:check\"]]}'",
"duration_s": 8.96377283311449,
"exit_code": 0,
"id": "lint",
"name": "Lint and Markdown format",
"result": "pass",
"timeout_s": 600.0
}
],
"schema": "dfx-local-ci/v1"
} |
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "1af3477d21c3d3fea9c5b2fc013c490e90d02c67",
"pr": 1509,
"repo": "DFXswiss/app",
"runs": [
{
"run_id": 35357083554,
"workflow": ".github/workflows/codeql.yml"
},
{
"run_id": 35357083561,
"workflow": ".github/workflows/handbook-check.yaml"
},
{
"run_id": 35357083553,
"workflow": ".github/workflows/pr-review-bot.yml"
},
{
"run_id": 35357083738,
"workflow": ".github/workflows/pr.yml"
}
]
} |
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "1af3477d21c3d3fea9c5b2fc013c490e90d02c67",
"phase": "applied",
"pr": 1509,
"reasons": [],
"repo": "DFXswiss/app",
"state": "ready"
} |
… section The lowest tier covered everything from one hour upward, which overlapped with the tier above it: a pill counting cumulatively sat over a section showing only the remainder. It is now a range — from the first second up to twelve hours — and reads New. The two upper tiers stay cumulative. WAIT_TIER_HOURS becomes WAIT_TIERS, where each tier carries its own lower and upper bound in one place; waitTier, the counters and the list filter all read from there instead of spelling the boundaries out again. The Newest section is removed: the New tier covers what it showed, so keeping both would list the same fresh tickets twice. The preselected tier stays at 24h, so opening the dashboard still shows escalations first.
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "e9b38cbc545c72d613d2e48352e7d5f0e5c10362",
"phase": "applied",
"pr": 1509,
"reasons": [
"CI not green: .github/workflows/codeql.yml (action_required)",
"CI not green: .github/workflows/handbook-check.yaml (action_required)",
"CI not green: .github/workflows/pr-review-bot.yml (action_required)",
"CI not green: .github/workflows/pr.yml (action_required)",
"Required CI check not green: .github/workflows/pr.yml / Lint and Markdown format",
"Required CI check not green: .github/workflows/pr.yml / Build and test",
"Required CI check not green: .github/workflows/pr.yml / Full-stack E2E",
"CI status not green: A38 / report (develop) (failure)"
],
"repo": "DFXswiss/app",
"state": "draft"
} |
|
EN: DE: Details
Durations rounded up to whole seconds / Laufzeiten auf ganze Sekunden aufgerundet. Original report / Originalbericht{
"head": "e9b38cbc545c72d613d2e48352e7d5f0e5c10362",
"private": false,
"recorded_at": "2026-09-18T20:11:13Z",
"repo": "DFXswiss/app",
"required": [
"lint"
],
"runs": [
{
"command": "agent a38 job commands --config '{\"env\":{\"CI\":\"true\"},\"npm\":{\"canaries\":[\"react-app-rewired/package.json\",\"react-scripts/package.json\",\"typescript/lib/typescript.js\",\"prettier/package.json\"],\"node_major\":20},\"steps\":[[\"npm\",\"run\",\"lint\"],[\"npm\",\"run\",\"format:md:check\"]]}'",
"duration_s": 10.628061374882236,
"exit_code": 0,
"id": "lint",
"name": "Lint and Markdown format",
"result": "pass",
"timeout_s": 600.0
}
],
"schema": "dfx-local-ci/v1"
} |
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "e9b38cbc545c72d613d2e48352e7d5f0e5c10362",
"pr": 1509,
"repo": "DFXswiss/app",
"runs": [
{
"run_id": 35389479049,
"workflow": ".github/workflows/codeql.yml"
},
{
"run_id": 35389479152,
"workflow": ".github/workflows/handbook-check.yaml"
},
{
"run_id": 35389478668,
"workflow": ".github/workflows/pr-review-bot.yml"
},
{
"run_id": 35389479815,
"workflow": ".github/workflows/pr.yml"
}
]
} |
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "e9b38cbc545c72d613d2e48352e7d5f0e5c10362",
"pr": 1509,
"repo": "DFXswiss/app",
"runs": [
{
"run_id": 35389479049,
"workflow": ".github/workflows/codeql.yml"
},
{
"run_id": 35389479152,
"workflow": ".github/workflows/handbook-check.yaml"
},
{
"run_id": 35389478668,
"workflow": ".github/workflows/pr-review-bot.yml"
},
{
"run_id": 35389479815,
"workflow": ".github/workflows/pr.yml"
},
{
"run_id": 35393203954,
"workflow": ".github/workflows/ci-on-ready.yaml"
}
]
} |
waitInTier lets NaN through: NaN < minHours is false, so the function falls to the open-ended top tier and returns true. waitTier therefore reported tier 3 for a ticket whose lastMessageDate cannot be parsed, and the row got the red escalation dot. Before this branch the plain NaN >= 24 comparison fell through to tier 0, so this was a regression introduced here. Reject NaN in waitInTier, where every caller passes through. Removing the guard again turns three tests red. The badge test now uses exactly 12h and 24h instead of 13h and 25h, so an off-by-one at the boundary cannot hide, and the pill counters run on a data set where all three values differ, so a swapped slot cannot pass either.
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "e829736d46f51f43b6e7d1d3b1754def9dba9cf2",
"phase": "applied",
"pr": 1509,
"reasons": [
"CI not green: .github/workflows/codeql.yml (action_required)",
"CI not green: .github/workflows/handbook-check.yaml (action_required)",
"CI not green: .github/workflows/pr-review-bot.yml (action_required)",
"CI not green: .github/workflows/pr.yml (action_required)",
"Required CI check not green: .github/workflows/pr.yml / Lint and Markdown format",
"Required CI check not green: .github/workflows/pr.yml / Build and test",
"Required CI check not green: .github/workflows/pr.yml / Full-stack E2E",
"CI status not green: A38 / report (develop) (failure)"
],
"repo": "DFXswiss/app",
"state": "draft"
} |
|
EN: DE: Details
Durations rounded up to whole seconds / Laufzeiten auf ganze Sekunden aufgerundet. Original report / Originalbericht{
"head": "e829736d46f51f43b6e7d1d3b1754def9dba9cf2",
"private": false,
"recorded_at": "2026-09-19T09:30:17Z",
"repo": "DFXswiss/app",
"required": [
"lint"
],
"runs": [
{
"command": "agent a38 job commands --config '{\"env\":{\"CI\":\"true\"},\"npm\":{\"canaries\":[\"react-app-rewired/package.json\",\"react-scripts/package.json\",\"typescript/lib/typescript.js\",\"prettier/package.json\"],\"node_major\":20},\"steps\":[[\"npm\",\"run\",\"lint\"],[\"npm\",\"run\",\"format:md:check\"]]}'",
"duration_s": 48.956841791048646,
"exit_code": 0,
"id": "lint",
"name": "Lint and Markdown format",
"result": "pass",
"timeout_s": 600.0
}
],
"schema": "dfx-local-ci/v1"
} |
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "e829736d46f51f43b6e7d1d3b1754def9dba9cf2",
"pr": 1509,
"repo": "DFXswiss/app",
"runs": [
{
"run_id": 35434824026,
"workflow": ".github/workflows/codeql.yml"
}
]
} |
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "e829736d46f51f43b6e7d1d3b1754def9dba9cf2",
"pr": 1509,
"repo": "DFXswiss/app",
"runs": [
{
"run_id": 35434824026,
"workflow": ".github/workflows/codeql.yml"
},
{
"run_id": 35434823997,
"workflow": ".github/workflows/handbook-check.yaml"
},
{
"run_id": 35434823999,
"workflow": ".github/workflows/pr-review-bot.yml"
},
{
"run_id": 35434824080,
"workflow": ".github/workflows/pr.yml"
}
]
} |
|
EN: DE: Details{
"base": "0c34dea9f15d8b891352723e86587ab590e84a87",
"head": "e829736d46f51f43b6e7d1d3b1754def9dba9cf2",
"pr": 1509,
"repo": "DFXswiss/app",
"runs": [
{
"run_id": 35434824026,
"workflow": ".github/workflows/codeql.yml"
},
{
"run_id": 35434823997,
"workflow": ".github/workflows/handbook-check.yaml"
},
{
"run_id": 35434823999,
"workflow": ".github/workflows/pr-review-bot.yml"
},
{
"run_id": 35434824080,
"workflow": ".github/workflows/pr.yml"
},
{
"run_id": 35436245476,
"workflow": ".github/workflows/ci-on-ready.yaml"
}
]
} |
A clerk could not see a ticket that had just come in: the overview grouped waiting customers into tiers starting at one hour, and anything below that was reachable through none of the filter pills. The lowest tier now starts at the first second and reads
New, and the selected tier survives opening a ticket and coming back.Details
What changed
Newcovers a wait from the first second up to twelve hours;12hand24hstay cumulative. Before, the lowest tier counted cumulatively (≥1h) while the section below it showed only what the escalation tier had not already taken — a pill reading 3 over a list of 1.WAIT_TIER_HOURSbecomesWAIT_TIERS, where each tier carries its lower bound, its upper bound and its label in one place.waitTier(), the pill counters and the list filter all read from there instead of restating the boundaries.12hand opening a ticket no longer drops the view back on return. The value goes through the repository's existingsafe-storagehelper and is applied only if it matches one of the current tiers — otherwise the default stands. That check matters here: browsers that saw an earlier revision of this branch have a now-invalid threshold on disk.Newestsection is removed. TheNewtier covers what it showed; keeping both would list the same fresh tickets twice.waitInTierletNaNthrough —NaN < minHoursis false, so it fell to the open-ended top tier and returned true, and a ticket whoselastMessageDatecannot be parsed got the red escalation dot. Before this branch the plainNaN >= 24comparison fell through to tier 0, so the range model introduced this.waitInTiernow rejectsNaN, which covers every caller.actionprop on the localSectioncomponent is gone — none of the call sites passed it, so its branch could never be exercised and blocked the coverage bar.The preselected tier stays at 24h, so opening the dashboard still shows escalations first. Layout, colours and spacing are unchanged.
Scope
Display and filtering only — no API call, no query, no backend change.
ESCALATION_HOURS,formatElapsed()and the badge colours are untouched;waitTierBadgeClasses()renders tier 0 and tier 1 identically, so the row colouring insrc/components/support/issue-table.tsxdoes not change.Coverage (per CONTRIBUTING)
src/util/support-stats.tssrc/screens/support-dashboard-overview.screen.tsxsrc/screens/support-dashboard-overview.screen.tsxhad no test file at all before this PR. Bringing it to the bar is the larger part of this diff: the ticket-load error path, the statistics tab including the fallback tocomputeStatisticsand its four stale-response guards, the period selector and trend bars, and the limit-request and my-tickets sections with their empty and error states.Tests
Boundaries of the new range are pinned: one second in, 11 h 59 min in, exactly 12 h out. The counters are checked on a data set where the exclusive lowest tier and the cumulative ones differ, so a tier that silently went back to cumulative shows up.
e2e-stack/specs/support-dashboard.spec.tscarries the screen change per CONTRIBUTING: it asserts the tier labels and that the selection survives a reload. Run against the stack: 15/15 passed.Visual baseline
e2e/screenshots/baseline/support-dashboard-overview.spec.ts-support-dashboard-overview-chromium-darwin.pngis regenerated on the same empty data set as the previous baseline, so the diff isolates the change: the pills readNew / 12h / 24hand theNewestsection is gone.Note for whoever runs that spec next:
npm run setupin the API repository is no longer enough. The admin it creates has the role but no verified name, so staff endpoints answer403 STAFF_KYC_REQUIREDand the screen redirects to/staff-kyc-required. A non-emptyuser_data.verifiedNameis required on top; thestaffKycClearancecron picks the account up within a minute.Reality declaration
No entry is due: this PR introduces, removes and changes no fake. The unit tests mock hooks, which is not one of the declared categories, and the visual spec runs against a real local API.
Pre-existing defect found while reviewing this change
Reported per CONTRIBUTING, not fixed here:
loadIssuesinsrc/screens/support-dashboard-overview.screen.tsxhas no generation counter and no mounted guard, unlikeloadStatsright below it (statsRequestId, the pattern this repository uses insrc/hooks/safe.hook.ts). The list is polled every 60 seconds; if one response outlives the interval, the older one can overwrite the newer state, and a late error can replace a fresh success. The code predates this PR and is untouched by it. Fixing it means givingloadIssuesthe same guard, which is a behaviour change in a code path this PR does not otherwise modify; happy to do it here if you would rather not have a separate PR.Symptom (verbatim): "support dashboard zeigt erst tickets ab 1h das muss ab min1 angezeigt werden" — a clerk did not see a freshly opened ticket. A first attempt lowered the lowest tier to one minute and added a separate
Newestsection; the reporter rejected the tier change ("wir brauchen wieder longer than 1h") and later settled on the shape in this PR: the lowest tier becomes a range namedNew, andNewestgoes away.Scale: Every incoming ticket during its first hours — the normal case of the workflow, not an edge case. No data, no migration, no API change.
Smaller fix considered: Setting the lowest threshold to zero would be one line, but it produces a tier that overlaps the one above it and a counter that contradicts the list below. Rejected as too small. Rejected as too large: a separate endpoint, a configurable limit, or the layout rework that was drafted and turned down — the reporter kept the current design deliberately.
Checks reported by the local gate, resolved as false positives:
awaitin frontend code without a stale guard — measured: zero newawaitin production code; every hit is in a test file.jest.setSystemTime), so it does not go stale with the calendar.ci-on-ready.yamltriggers onready_for_review,main-from-develop.ymlonly for pull requests intomain, and CodeQL did run; it reports asAnalyze (actions)andAnalyze (javascript-typescript), while the gate compares the unexpanded matrix job name.Final pass (e829736):
Coherent: Every file serves the one title. The tier definition, the counters, the list filter and the section title all read from the same
WAIT_TIERStable; removingNewestfollows from theNewtier covering that ground, and theNaNguard sits in the one function all of them pass through. Tests, full-stack spec and baseline cover exactly that surface.Nothing extra: No endpoint, no configurable limit, no extra card, no new filter state, no layout change — three redesigns and a polish draft were shown to the reporter and turned down, so the current design stands untouched. The preselected tier deliberately stays at 24h: changing it was my own idea and the reporter decided against it.
customerWaitingHours()still returnsNaNrather thannullfor an unreadable date; pulling that tonullwould change every caller and is reported below instead of built here.Sources closed: Reporter's instructions, each verified in the running app before committing — lowest tier renamed to
Newcovering one second to twelve hours, everything from twelve hours in the tier above,Newestremoved, design left alone, start filter left at 24h. CONTRIBUTING read in full: coverage stated, full-stack spec updated, baseline regenerated, handbook metadata present, reality declaration not due. No linked issues, no reviews, no review comments (0/0/0). Review lanes across two model families produced nine findings in total: four weak test assertions and two overstated body sentences (all fixed), one regression found by the second family and fixed here with a counter-proof, two pre-existing defects reported below and not fixed.Second pre-existing defect, reported and not fixed:
customerWaitingHours()returnsNaNinstead ofnullwhenlastMessageDatecannot be parsed. With the guard above the row is no longer marked escalated, butIssueListstill sees a non-null value and rendersWaiting NaNd. The behaviour predates this branch. Pulling it tonullchanges what every caller sees and belongs in its own change.