diff --git a/.gitignore b/.gitignore
index c7c5be5..516f1a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ test-results/
# UI kit / console (U1+)
node_modules/
ui/**/dist/
+
+# Coverage reports are BUILD OUTPUT: regenerated by `pnpm test`, timestamped, never reviewed.
+coverage/
diff --git a/docs/rfc/goldpath-console.md b/docs/rfc/goldpath-console.md
index 1da22d2..5d57653 100644
--- a/docs/rfc/goldpath-console.md
+++ b/docs/rfc/goldpath-console.md
@@ -62,7 +62,9 @@ custom-develop ON, with the same kit, the same way they add features to the back
landing page: operators open consoles to answer "is anything wrong", not to browse.
- **D3 — Proof bar (UI is claims-are-proofs too).**
Kit: component tests (vitest) on the composites (keyset table paging, verb button's
- refusal surface, state mapping). Console: the smoke drives THREE real apps — one open,
+ refusal surface, state mapping), under a COVERAGE FLOOR that CI enforces (kit 95/90,
+ console 97/85 statements/branches) — the floor exists because the gaps that hid real
+ bugs here were branches nobody had thought to exercise, not tests anyone had deleted. Console: the smoke drives THREE real apps — one open,
one behind the auth floor, one tenant-scoped — because how the console behaves when it
is REFUSED is as much a claim as how it behaves when it is welcome; plus a service that
dies mid-session. Runnable on demand (`.github/workflows/console-smoke.yml`), not only
diff --git a/ui/console/package.json b/ui/console/package.json
index d53b1c4..8d6ccb6 100644
--- a/ui/console/package.json
+++ b/ui/console/package.json
@@ -6,7 +6,7 @@
"type": "module",
"main": "src/index.ts",
"scripts": {
- "test": "vitest run",
+ "test": "vitest run --coverage --coverage.thresholds.statements=97 --coverage.thresholds.branches=85 --coverage.thresholds.functions=75 --coverage.thresholds.lines=97",
"typecheck": "tsc --noEmit",
"dev": "vite",
"e2e": "playwright test"
@@ -16,21 +16,22 @@
"react-dom": "^19.0.0"
},
"devDependencies": {
+ "@goldpath/kit": "link:../kit",
+ "@playwright/test": "1.50.1",
+ "@tailwindcss/vite": "^4.3.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "14.6.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
+ "@vitest/coverage-v8": "2.1.8",
"jsdom": "^25.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "tailwindcss": "^4.0.0",
"typescript": "^5.7.0",
"vite": "^8.1.4",
- "vitest": "^2.1.8",
- "@goldpath/kit": "link:../kit",
- "@tailwindcss/vite": "^4.3.2",
- "tailwindcss": "^4.0.0",
- "@playwright/test": "1.50.1"
+ "vitest": "^2.1.8"
}
}
diff --git a/ui/console/pnpm-lock.yaml b/ui/console/pnpm-lock.yaml
index 61f1657..44fefb6 100644
--- a/ui/console/pnpm-lock.yaml
+++ b/ui/console/pnpm-lock.yaml
@@ -35,6 +35,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.7.0(vite@8.1.5(jiti@2.7.0))
+ '@vitest/coverage-v8':
+ specifier: 2.1.8
+ version: 2.1.8(vitest@2.1.9(jsdom@25.0.1)(lightningcss@1.33.0))
jsdom:
specifier: ^25.0.1
version: 25.0.1
@@ -62,6 +65,10 @@ packages:
'@adobe/css-tools@4.5.0':
resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
@@ -152,6 +159,9 @@ packages:
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
+ '@bcoe/v8-coverage@0.2.3':
+ resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
+
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
@@ -327,6 +337,14 @@ packages:
cpu: [x64]
os: [win32]
+ '@isaacs/cliui@8.0.2':
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+
+ '@istanbuljs/schema@0.1.6':
+ resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
+ engines: {node: '>=8'}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -352,6 +370,10 @@ packages:
'@oxc-project/types@0.139.0':
resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==}
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
'@playwright/test@1.50.1':
resolution: {integrity: sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==}
engines: {node: '>=18'}
@@ -757,6 +779,15 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+ '@vitest/coverage-v8@2.1.8':
+ resolution: {integrity: sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==}
+ peerDependencies:
+ '@vitest/browser': 2.1.8
+ vitest: 2.1.8
+ peerDependenciesMeta:
+ '@vitest/browser':
+ optional: true
+
'@vitest/expect@2.1.9':
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
@@ -794,10 +825,22 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
ansi-styles@5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
@@ -812,11 +855,25 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
baseline-browser-mapping@2.11.3:
resolution: {integrity: sha512-sbT0Ui/CZwyAyy7icT1Gw5P1LKRlFaHwaF6tDCW5YHq2X5SeeZFphBuIagopSfwSSZq3sQcbmEL072yphxm7ew==}
engines: {node: '>=6.0.0'}
hasBin: true
+ brace-expansion@2.1.2:
+ resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==}
+
+ brace-expansion@5.0.8:
+ resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
+ engines: {node: 20 || >=22}
+
browserslist@4.28.7:
resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -841,6 +898,13 @@ packages:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'}
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -848,6 +912,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
@@ -900,9 +968,18 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
+ eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+
electron-to-chromium@1.5.396:
resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==}
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
enhanced-resolve@5.24.3:
resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==}
engines: {node: '>=10.13.0'}
@@ -955,6 +1032,10 @@ packages:
picomatch:
optional: true
+ foreground-child@3.3.1:
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
+ engines: {node: '>=14'}
+
form-data@4.0.6:
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
engines: {node: '>= 6'}
@@ -984,6 +1065,11 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
+ glob@10.5.0:
+ resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ hasBin: true
+
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -991,6 +1077,10 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
@@ -1007,6 +1097,9 @@ packages:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
+ html-escaper@2.0.2:
+ resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
@@ -1023,9 +1116,35 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+ engines: {node: '>=8'}
+
+ istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+
+ istanbul-lib-source-maps@5.0.6:
+ resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
+ engines: {node: '>=10'}
+
+ istanbul-reports@3.2.0:
+ resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
+ engines: {node: '>=8'}
+
+ jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
@@ -1216,6 +1335,13 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+ magicast@0.3.5:
+ resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
+
+ make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -1232,6 +1358,18 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
+ minimatch@10.2.5:
+ resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
+ engines: {node: 18 || 20 || >=22}
+
+ minimatch@9.0.9:
+ resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minipass@7.1.3:
+ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1247,9 +1385,20 @@ packages:
nwsapi@2.2.24:
resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==}
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+
pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
@@ -1336,9 +1485,26 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -1349,10 +1515,30 @@ packages:
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.2.0:
+ resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+ engines: {node: '>=12'}
+
strip-indent@3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@@ -1363,6 +1549,10 @@ packages:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
+ test-exclude@7.0.2:
+ resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==}
+ engines: {node: '>=18'}
+
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1539,11 +1729,24 @@ packages:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+
ws@8.21.1:
resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==}
engines: {node: '>=10.0.0'}
@@ -1570,6 +1773,11 @@ snapshots:
'@adobe/css-tools@4.5.0': {}
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
'@asamuzakjp/css-color@3.2.0':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
@@ -1692,6 +1900,8 @@ snapshots:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
+ '@bcoe/v8-coverage@0.2.3': {}
+
'@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
@@ -1797,6 +2007,17 @@ snapshots:
'@esbuild/win32-x64@0.21.5':
optional: true
+ '@isaacs/cliui@8.0.2':
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: string-width@4.2.3
+ strip-ansi: 7.2.0
+ strip-ansi-cjs: strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: wrap-ansi@7.0.0
+
+ '@istanbuljs/schema@0.1.6': {}
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -1825,6 +2046,9 @@ snapshots:
'@oxc-project/types@0.139.0': {}
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
'@playwright/test@1.50.1':
dependencies:
playwright: 1.50.1
@@ -2110,6 +2334,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@vitest/coverage-v8@2.1.8(vitest@2.1.9(jsdom@25.0.1)(lightningcss@1.33.0))':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@bcoe/v8-coverage': 0.2.3
+ debug: 4.4.3
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-report: 3.0.1
+ istanbul-lib-source-maps: 5.0.6
+ istanbul-reports: 3.2.0
+ magic-string: 0.30.21
+ magicast: 0.3.5
+ std-env: 3.10.0
+ test-exclude: 7.0.2
+ tinyrainbow: 1.2.0
+ vitest: 2.1.9(jsdom@25.0.1)(lightningcss@1.33.0)
+ transitivePeerDependencies:
+ - supports-color
+
'@vitest/expect@2.1.9':
dependencies:
'@vitest/spy': 2.1.9
@@ -2154,8 +2396,16 @@ snapshots:
ansi-regex@5.0.1: {}
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
ansi-styles@5.2.0: {}
+ ansi-styles@6.2.3: {}
+
aria-query@5.3.0:
dependencies:
dequal: 2.0.3
@@ -2166,8 +2416,20 @@ snapshots:
asynckit@0.4.0: {}
+ balanced-match@1.0.2: {}
+
+ balanced-match@4.0.4: {}
+
baseline-browser-mapping@2.11.3: {}
+ brace-expansion@2.1.2:
+ dependencies:
+ balanced-match: 1.0.2
+
+ brace-expansion@5.0.8:
+ dependencies:
+ balanced-match: 4.0.4
+
browserslist@4.28.7:
dependencies:
baseline-browser-mapping: 2.11.3
@@ -2195,12 +2457,24 @@ snapshots:
check-error@2.1.3: {}
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
convert-source-map@2.0.0: {}
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
css.escape@1.5.1: {}
cssstyle@4.6.0:
@@ -2239,8 +2513,14 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
+ eastasianwidth@0.2.0: {}
+
electron-to-chromium@1.5.396: {}
+ emoji-regex@8.0.0: {}
+
+ emoji-regex@9.2.2: {}
+
enhanced-resolve@5.24.3:
dependencies:
graceful-fs: 4.2.11
@@ -2303,6 +2583,11 @@ snapshots:
optionalDependencies:
picomatch: 4.0.5
+ foreground-child@3.3.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
form-data@4.0.6:
dependencies:
asynckit: 0.4.0
@@ -2339,10 +2624,21 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.2
+ glob@10.5.0:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 3.4.3
+ minimatch: 9.0.9
+ minipass: 7.1.3
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
+ has-flag@4.0.0: {}
+
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
@@ -2357,6 +2653,8 @@ snapshots:
dependencies:
whatwg-encoding: 3.1.1
+ html-escaper@2.0.2: {}
+
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
@@ -2377,8 +2675,39 @@ snapshots:
indent-string@4.0.0: {}
+ is-fullwidth-code-point@3.0.0: {}
+
is-potential-custom-element-name@1.0.1: {}
+ isexe@2.0.0: {}
+
+ istanbul-lib-coverage@3.2.2: {}
+
+ istanbul-lib-report@3.0.1:
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+
+ istanbul-lib-source-maps@5.0.6:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ debug: 4.4.3
+ istanbul-lib-coverage: 3.2.2
+ transitivePeerDependencies:
+ - supports-color
+
+ istanbul-reports@3.2.0:
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
+
+ jackspeak@3.4.3:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
jiti@2.7.0: {}
js-tokens@4.0.0: {}
@@ -2527,6 +2856,16 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
+ magicast@0.3.5:
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ source-map-js: 1.2.1
+
+ make-dir@4.0.0:
+ dependencies:
+ semver: 7.8.5
+
math-intrinsics@1.1.0: {}
mime-db@1.52.0: {}
@@ -2537,6 +2876,16 @@ snapshots:
min-indent@1.0.1: {}
+ minimatch@10.2.5:
+ dependencies:
+ brace-expansion: 5.0.8
+
+ minimatch@9.0.9:
+ dependencies:
+ brace-expansion: 2.1.2
+
+ minipass@7.1.3: {}
+
ms@2.1.3: {}
nanoid@3.3.16: {}
@@ -2545,10 +2894,19 @@ snapshots:
nwsapi@2.2.24: {}
+ package-json-from-dist@1.0.1: {}
+
parse5@7.3.0:
dependencies:
entities: 6.0.1
+ path-key@3.1.1: {}
+
+ path-scurry@1.11.1:
+ dependencies:
+ lru-cache: 10.4.3
+ minipass: 7.1.3
+
pathe@1.1.2: {}
pathval@2.0.1: {}
@@ -2661,24 +3019,64 @@ snapshots:
semver@6.3.1: {}
+ semver@7.8.5: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
siginfo@2.0.0: {}
+ signal-exit@4.1.0: {}
+
source-map-js@1.2.1: {}
stackback@0.0.2: {}
std-env@3.10.0: {}
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@5.1.2:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.2.0
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.2.0:
+ dependencies:
+ ansi-regex: 6.2.2
+
strip-indent@3.0.0:
dependencies:
min-indent: 1.0.1
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
symbol-tree@3.2.4: {}
tailwindcss@4.3.3: {}
tapable@2.3.3: {}
+ test-exclude@7.0.2:
+ dependencies:
+ '@istanbuljs/schema': 0.1.6
+ glob: 10.5.0
+ minimatch: 10.2.5
+
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@@ -2809,11 +3207,27 @@ snapshots:
tr46: 5.1.1
webidl-conversions: 7.0.0
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@8.1.0:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 5.1.2
+ strip-ansi: 7.2.0
+
ws@8.21.1: {}
xml-name-validator@5.0.0: {}
diff --git a/ui/console/src/ArchivalPanel.test.tsx b/ui/console/src/ArchivalPanel.test.tsx
index 08f8800..b200220 100644
--- a/ui/console/src/ArchivalPanel.test.tsx
+++ b/ui/console/src/ArchivalPanel.test.tsx
@@ -261,4 +261,45 @@ describe("the archival panel (chain health, retrieval, lifecycle)", () => {
// The previous "verifies" verdict is GONE — an unknown chain must not read as a proven one.
expect(screen.queryByTestId("chain-findings")).toBeNull();
});
+
+ it("a lifecycle verb that never reached the server is NOT reported as applied", async () => {
+ const { client: api } = client({ verb: { status: 503, body: {} } });
+ render();
+
+ await retrieve();
+ await userEvent.click(await screen.findByRole("button", { name: "lift-hold" }));
+ await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "lift-hold" }));
+
+ expect(await screen.findByText(/did not reach the server — it may not have been applied/)).toBeInTheDocument();
+ });
+
+ it("switching archive clears the open entry — a key means nothing outside its archive", async () => {
+ const { client: api } = client({
+ definitions: [
+ { name: "policies", entries: 10, dueBacklog: 0, activeHolds: 0, chainHead: 10, purgedThrough: 0 },
+ { name: "claims", entries: 4, dueBacklog: 0, activeHolds: 0, chainHead: 4, purgedThrough: 0 },
+ ],
+ });
+ render();
+
+ await retrieve();
+ await screen.findByTestId("archive-entry");
+
+ await userEvent.selectOptions(screen.getByLabelText("archive"), "claims");
+
+ // The policies entry must not linger under the claims archive's name.
+ expect(screen.queryByTestId("archive-entry")).toBeNull();
+ expect(screen.queryByText(/No entry for/)).toBeNull();
+ });
+
+ it("a lifted hold shows who lifted it and when — the row stays as evidence", async () => {
+ const { client: api } = client({
+ holds: [hold({ liftedBy: "legal@example.com", liftedAt: "2026-07-27T05:00:00Z" })],
+ });
+ render();
+
+ const row = await screen.findByRole("row", { name: /CASE-2026-7/ });
+ expect(row).toHaveTextContent("lifted by legal@example.com at 2026-07-27T05:00:00Z");
+ expect(within(row).queryByText("Held")).toBeNull();
+ });
});
diff --git a/ui/console/src/BulkPanel.test.tsx b/ui/console/src/BulkPanel.test.tsx
index 8773478..546b950 100644
--- a/ui/console/src/BulkPanel.test.tsx
+++ b/ui/console/src/BulkPanel.test.tsx
@@ -259,4 +259,40 @@ describe("the bulk intake panel (upload → report → four-eyes gate)", () => {
await waitFor(() => expect(screen.queryByRole("button", { name: "approve" })).toBeNull());
expect(screen.getByText("approved")).toBeInTheDocument();
});
+
+ it("a decision that never reached the server is NOT reported as recorded", async () => {
+ // The verb envelope never came back — 503 is neither an ok nor a refusal, and the
+ // operator must not walk away believing the gate moved.
+ const { client: api } = client({ verb: { status: 503, body: {} } });
+ render();
+
+ await openBatch();
+ await userEvent.click(await screen.findByRole("button", { name: "approve" }));
+ await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "approve" }));
+
+ expect(await screen.findByText(/did not reach the server — it may not have been recorded/)).toBeInTheDocument();
+ });
+
+ it("a decided batch carries its evidence: who, when, and why", async () => {
+ const decided = batch({
+ state: "Rejected",
+ decidedAt: "2026-07-27T10:00:00Z",
+ decidedBy: "ops@example.com",
+ decisionNote: "line 3 is malformed",
+ });
+ const { client: api } = client({ batch: decided, batches: [decided] });
+ render();
+
+ await openBatch();
+ const detail = await screen.findByTestId("batch-detail");
+ expect(detail).toHaveTextContent("Rejected by ops@example.com at 2026-07-27T10:00:00Z");
+ expect(detail).toHaveTextContent("line 3 is malformed");
+ });
+
+ it("a definition with nothing in it says so, instead of showing a bare name", async () => {
+ const { client: api } = client({ definitions: [definition({ batchesByState: {}, awaitingApproval: 0 })] });
+ render();
+
+ expect(await screen.findByText("no batches yet")).toBeInTheDocument();
+ });
});
diff --git a/ui/console/src/CampaignPanel.test.tsx b/ui/console/src/CampaignPanel.test.tsx
index 8083696..f99fb5f 100644
--- a/ui/console/src/CampaignPanel.test.tsx
+++ b/ui/console/src/CampaignPanel.test.tsx
@@ -298,4 +298,26 @@ describe("the campaign governor panel", () => {
await waitFor(() => expect(screen.getByTestId("campaign-detail").textContent).not.toBe(before));
});
+
+ it("a verb that never reached the server is NOT reported as applied", async () => {
+ const { client: api } = client({ verb: { status: 503, body: {} } });
+ render();
+
+ await open();
+ await userEvent.click(await screen.findByRole("button", { name: "pause" }));
+ await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "pause" }));
+
+ expect(await screen.findByText(/did not reach the server — it may not have been applied/)).toBeInTheDocument();
+ });
+
+ it("a campaign with no window releases around the clock, and says exactly that", async () => {
+ const always = campaign({ windowStart: null, windowEnd: null, dailyQuota: null });
+ const { client: api } = client({ detail: always, list: [always] });
+ render();
+
+ await open();
+ const detail = await screen.findByTestId("campaign-detail");
+ expect(detail).toHaveTextContent("around the clock");
+ expect(detail).toHaveTextContent("Daily quotanone");
+ });
});
diff --git a/ui/console/src/NotificationPanel.test.tsx b/ui/console/src/NotificationPanel.test.tsx
index 71d7934..2a48c41 100644
--- a/ui/console/src/NotificationPanel.test.tsx
+++ b/ui/console/src/NotificationPanel.test.tsx
@@ -44,6 +44,18 @@ function client(routes: Routes = {}) {
const json = (body: unknown) =>
new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } });
+ // The by-id route must be matched BEFORE the list route it is nested under.
+ if (/\/notifications\/[^/?]+$/.test(url)) {
+ const id = url.split("/").pop();
+ const all = [
+ notification(),
+ notification({ state: "Failed", id: "failed-1", attempts: 3, detail: "the relay refused: mailbox full", failedAt: "2026-07-27T06:02:00Z" }),
+ notification({ state: "Suppressed", id: "suppressed-1", detail: "suppressed by the MaySend hook — suppression is evidence too" }),
+ ...(Array.isArray(routes.list) ? (routes.list as NotificationInfo[]) : []),
+ ];
+ return json(all.find((row) => row.id === id) ?? notification());
+ }
+
if (url.includes("/failures")) return json(routes.failures ?? [notification({ state: "Failed", id: "failed-1", attempts: 3, detail: "the relay refused: mailbox full", failedAt: "2026-07-27T06:02:00Z" })]);
if (url.includes("/suppressions")) return json(routes.suppressions ?? [notification({ state: "Suppressed", id: "suppressed-1", detail: "suppressed by the MaySend hook — suppression is evidence too" })]);
if (url.includes("/notifications")) return json(routes.list ?? [notification()]);
@@ -157,7 +169,7 @@ describe("the notification evidence panel (read-only by contract)", () => {
});
it("says when the body was deleted — the retention promise, kept and recorded", async () => {
- const { client: api } = client({ list: [notification({ bodyDeletedAt: "2026-10-25T00:00:00Z" })] });
+ const { client: api } = client({ list: [notification({ id: "deleted-1", bodyDeletedAt: "2026-10-25T00:00:00Z" })] });
render();
await userEvent.click(await screen.findByRole("button", { name: "a***@e***" }));
@@ -171,4 +183,26 @@ describe("the notification evidence panel (read-only by contract)", () => {
// The table reports its own failure too — the panel-level banner is the first one.
expect((await screen.findAllByRole("alert"))[0]).toHaveTextContent(/notification templates could not be loaded/i);
});
+
+ it("tenant and correlation id appear when the row carries them — and never as empty labels", async () => {
+ const { client: api } = client({
+ list: [notification({ id: "traced-1", tenant: "acme", correlationId: "0af7651916cd43dd8448eb211c80319c" })],
+ });
+ render();
+
+ await userEvent.click(await screen.findByRole("button", { name: "a***@e***" }));
+ const detail = await screen.findByTestId("notification-detail");
+ expect(detail).toHaveTextContent("acme");
+ expect(detail).toHaveTextContent("0af7651916cd43dd8448eb211c80319c");
+ });
+
+ it("a row WITHOUT them shows no tenant or correlation block at all", async () => {
+ const { client: api } = client();
+ render();
+
+ await userEvent.click(await screen.findByRole("button", { name: "a***@e***" }));
+ const detail = await screen.findByTestId("notification-detail");
+ expect(within(detail).queryByText("Tenant")).toBeNull();
+ expect(within(detail).queryByText("Correlation")).toBeNull();
+ });
});
diff --git a/ui/console/src/NotificationPanel.tsx b/ui/console/src/NotificationPanel.tsx
index d124da8..215e498 100644
--- a/ui/console/src/NotificationPanel.tsx
+++ b/ui/console/src/NotificationPanel.tsx
@@ -45,6 +45,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
const [lens, setLens] = useState("all");
const [state, setState] = useState("");
const [template, setTemplate] = useState("");
+ const [selectedId, setSelectedId] = useState(null);
const [selected, setSelected] = useState(null);
const [error, setError] = useState(null);
const [refreshToken, setRefreshToken] = useState(0);
@@ -77,6 +78,24 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
[client, lens, state, template, refreshToken],
);
+ // The open row RE-READS by id: a notification is evidence in MOTION (Requested → Sent
+ // or Failed), so a row captured minutes ago is not what the operator should judge.
+ useEffect(() => {
+ if (!selectedId) {
+ setSelected(null);
+ return;
+ }
+
+ let live = true;
+ client
+ .notification(selectedId)
+ .then((found) => live && setSelected(found))
+ .catch(() => live && setError(`notification ${selectedId} could not be read`));
+ return () => {
+ live = false;
+ };
+ }, [client, selectedId, refreshToken]);
+
const waiting = (templates ?? []).filter((entry) => (entry.oldestRequestedSeconds ?? 0) > 0);
return (
@@ -127,7 +146,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
}`}
onClick={() => {
setLens(entry.key);
- setSelected(null);
+ setSelectedId(null);
}}
>
{entry.label}
@@ -145,7 +164,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
value={state}
onChange={(event) => {
setState(event.target.value);
- setSelected(null);
+ setSelectedId(null);
}}
>
@@ -165,7 +184,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
value={template}
onChange={(event) => {
setTemplate(event.target.value);
- setSelected(null);
+ setSelectedId(null);
}}
>
@@ -192,7 +211,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
{
header: "Recipient (masked)",
cell: (row) => (
-