@aiquants/daily-report 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes to `@aiquants/daily-report` are documented here.
4
4
 
5
+ ## 0.28.0 (2026-10-05)
6
+
7
+ 0.27.0 の続き: 要求の隔離の許可の一覧化 (別オリジンから受けるのは添付の原本を開く最上位の文書のナビゲーションだけ)、Fetch Metadata の無い要求の `Origin` による判定、拒否の上限付きの記録、添付のどの応答にも枠の方針、配置の格子の単位と E2E のテスト用ハンドルの型の公開、ID 一覧のストリームの行の線形な分割、折り返す文言の改行の規則。どれも README の該当の節が今の契約を記す。
8
+
9
+ ### Security
10
+
11
+ - **別オリジンから受けるのは、添付の原本を開く最上位の文書のナビゲーションだけ**: 要求の隔離は許可の一覧になった。経路ごとの方針 (`RequestIsolationPolicy`) が受けると宣言したナビゲーション (`GET`・`Sec-Fetch-Mode: navigate`・`Sec-Fetch-Dest` がちょうど `document`) だけを通し、それを宣言するのは添付の経路の原本 (インラインとダウンロード。ローダーと同じ厳格なクエリの解釈で決める) だけ。API・SSE・サムネイルは別オリジンからのナビゲーションを受けない。
12
+ 枠 (`iframe`・`frame`)・`fencedframe`・行き先の無いナビゲーション・`HEAD` のナビゲーション (ブラウザーは `HEAD` でナビゲーションしない)・これから仕様に増える行き先は、並べなくても作りから拒む。隔離はハンドラー工場が返す前に 1 つの包みで 4 つの経路へ当て (経路の中の手で置いた判定は無くなった)、添付の経路では形の崩れたクエリも 405 になるメソッドも、別オリジンからなら認証の前に 403 になる (以前は認証の後の 400 か 405)。
13
+ 以前は `object` と `embed` 以外の行き先の GET か HEAD のナビゲーションをどの経路でも通したので、同じサイトの兄弟のサブドメインのページの `<iframe>` が、閲覧者のセッションで、サムネイルのバケットを空にし (閲覧者自身のタイルが 429 になる)、見える添付と見えない添付の応答の時間の差を測り、ID 一覧の全件のクエリを走らせ、SSE の購読を開けた。応答の側の `X-Frame-Options` は枠に描くことを止めるだけで、要求とその仕事は止めない。
14
+ - **Fetch Metadata の無い安全でないメソッドは `Origin` で決める**: ブラウザーは潜在的に信頼できないオリジン (`localhost` 以外の素の HTTP) への要求に `Sec-Fetch-*` を付けないので、ヘッダーの無い要求をすべて通すと、素の HTTP のホストでは兄弟のページのフォームの POST が状態を変えた。`Sec-Fetch-Site` の無い安全でないメソッドは、`Origin` が無ければ通し (ブラウザーのページではない)、要求の宛先のホスト (`new URL(request.url).host`) と同じなら通し、`null`・URL として読めない値・別のホスト・別のポートなら 403 にする (Go の `net/http` の `CrossOriginProtection` と同じ規則)。安全なメソッドは今までどおり通す。隔離を完全にするには、潜在的に信頼できるオリジン (HTTPS) で配る。
15
+ - **添付のどの応答も `X-Frame-Options: SAMEORIGIN`**: 枠への埋め込みの方針は、`nosniff` と `Cross-Origin-Resource-Policy: same-origin` と同じ 1 つの組 (`ATTACHMENT_RESPONSE_SECURITY_HEADERS`) から、失敗の応答 (隔離の 403 を含むどのステータスも。GET と HEAD)・304・2 つの配信の 200 へ付く。以前は 200 だけが持ったので、別オリジンのページは添付の URL を枠に読み込み、塞がれた枠 (見える添付の 200) と描かれた枠 (見えない添付の 404) を見分けられた。
16
+
17
+ ### Added
18
+
19
+ - client の値 `DAILY_REPORT_LAYOUT_LATTICE_PX`: 配置の格子の単位 (4 CSS px)。パッケージ自身の格子の単位そのもので、ホストがビューの周りのバー (ヘッダー・フッターとそれらが分け合うウィンドウ) の大きさをこの倍数にそろえる契約 (README の View height の G-symmetric frame)。公開する配置の値はこれだけで、行の大きさは公開しない。
20
+ - client の型 `DailyReportViewTestHandle`・`DailyReportViewTestReadHandle`・`DailyReportRevealOptions`: E2E のテスト用ハンドル (`__virtualScroll`) の契約とその読み取りの部分、`revealIndex` の揃えの型 (VirtualScroll の `scrollToIndex` の第 2 引数。virtualscroll 3.11.0 の見せる操作 `{ align: "nearest" }` も含む)。型だけの公開なので、バンドルは増えない。ホストのテストは形を書き写さずに読み込む。
21
+
22
+ ### Changed
23
+
24
+ - **拒否は上限付きの記録に残る**: 隔離の拒否は、ハンドラー工場ごとの記録 (`createCrossSiteRejectionRecorder`) が窓 1 つ (`CROSS_SITE_REJECTION_LOG_WINDOW_MS`、60 秒) につき warn の行を 2 行まで書く: 窓の最初の拒否の `cross_site_rejected route=<api|action|sse|attachment> site=<値> mode=<値> dest=<値> method=<メソッド>` と、窓の後の最初の拒否が書く前の窓の数の `cross_site_rejected_suppressed count=<数>` (数えた拒否があるときだけ)。仕様の形 (小文字と `-` だけの 32 文字まで) でない値は、先頭の 32 文字の JSON の文字列で書く。書き先は設定の `logger`、無ければ `[DailyReportIsolation]` を頭に付けた warn の水準の console。受けた要求は窓を開きも閉じもしない。以前は拒否を一切記録しなかったので、同じサイトのページからの探りや CSRF の試みが運用から見えなかった。
25
+ - **ID 一覧のストリームの行の分割は線形**: クライアントは届いたチャンクを復号するたびに、新しく復号した文字だけを改行まで調べ、行が完結したときに欠片を 1 度だけつなぐ (`createNdjsonLineSplitter`)。以前は持ち越した欠け行と新しいチャンクを連結して全体を分け直したので、4,000 件で約 300 KB の行が届き終わるまで、チャンクのたびに同じ前半を調べ直した (同じ形の合成の 16 MB のストリームを 4 KiB のチャンクで分けるベンチで、調べる文字は 1,600 万でなく 6 億、時間は 11.8 ms でなく 287.6 ms)。
26
+ ストリームの終わりの改行の無い行も 1 行として読む (以前は捨てた)。終わりで切れた行は JSON として読めないので、警告 1 件で読み飛ばし、今までどおりカーソルから読み直す。
27
+ - **折り返す文言の改行**: 側面ペインの選択を促す表示と、添付のタイルの表示できない旨の文言は、1 つの規則 (`WRAPPING_LABEL_CLASS_NAME`: `text-wrap: balance` と `word-break: auto-phrase`) で折り返す。行の長さを揃えるので最後の行に 1-2 文字だけが残らず、ホストの文書の `lang` が `ja` なら日本語を文節の境で折り返す (171 px の列で 「プレビューを表示できませ / ん」 が 「プレビューを / 表示できません」 になる)。`auto-phrase` を持たない Firefox と Safari は宣言を捨てて今までどおりに折り返し、`balance` を持たない Safari 17.4 は行の長さを揃えない (README の Browser floor)。
28
+ - **開発者向け**: `pnpm run verify` の単体テストは、リポジトリーの公開の漏洩検査を配布する Markdown (`package.json` の `files` が配る `README.md`・`CHANGELOG.md`) へ検査自身の規則で当てる (`src/shipped-markdown-leaks.spec.ts`)。内部の名前は公開の段ではなく検証の段で落ちる。
29
+ `StreamEntry` と描画ポートの契約の定数 (`DAILY_REPORT_ATTACHMENT_THUMBNAIL_OUTPUT_MEDIA_TYPES`・`DAILY_REPORT_ATTACHMENT_THUMBNAIL_MAX_OUTPUT_BYTES`) はポートのモジュール (`src/server/ports.ts`) に置き、ポートは他のサーバーのモジュールを読み込まない (型の循環を断った。公開の名前と入口は変わらない)。
30
+ DetailList の行の spec は、本物の VirtualScroll の上で、差の和が 0 の測り直しの組 (1 回の ResizeObserver の配達で行 2 つが +40 と −40) の後も行の枠がそれぞれの実測の高さに置き直され、間の行だけが動き、錨が変わらないことを確かめる。直すのは `@aiquants/virtualscroll` の次の版 (CHANGELOG の 3.11.0) で、spec はワークスペースの virtualscroll のソースで走る。ピアの下限はその版の公開と一緒に上げる。
31
+
32
+ ### Breaking
33
+
34
+ - ピアの `@aiquants/virtualscroll` の下限を 3.11.0 へ上げた (`peer-floors.json`)。テスト用のハンドルの `DailyReportRevealOptions` は 3.11.0 の `scrollToIndex` の引数 (`align: "nearest"` を含む) で、DetailList の行が差の和が 0 の測り直しの組の後も木の上端と高さに戻るのは 3.11.0 の行の memo が木の版に従うため。
35
+ Migration: `@aiquants/virtualscroll` を 3.11.0 以降へ上げる。
36
+
5
37
  ## 0.27.0 (2026-10-05)
6
38
 
7
39
  0.26.0 の続き: Fetch Metadata による要求の隔離、ポインター専用のスクロールバー (名前の無い `scrollbar` と `slider` を出さない)、添付の格子の整数 px の列、要求が全員去った描画の結果の保持、SSE リーダーのポート、E2E のテスト用ハンドルの読み取り専用化。どれも README の該当の節が今の契約を記す。
package/README.md CHANGED
@@ -26,8 +26,9 @@ pnpm add @aiquants/daily-report @aiquants/virtualscroll @aiquants/swipe-overlay
26
26
  ```
27
27
 
28
28
  `@aiquants/virtualscroll` / `@aiquants/swipe-overlay` / `@aiquants/resize-panels` / `react` / `react-dom` / `react-router` / `drizzle-orm` / `zod` are **peer dependencies**. React must be **19 or later** (`react` / `react-dom` `>=19`): the rows register their keyboard focus targets with ref callbacks that return a cleanup.
29
- `@aiquants/virtualscroll` must be **3.10.0 or later**: both views pass its scroll-bar option `enableArrowButtonTabStops: false`, since 3.10.0 its one signal that the host scrolls by keyboard itself, under which the scroll bar is pointer-only — hidden from assistive technology, out of the Tab order and never taking focus on a press (the views scroll by keyboard themselves; see [Keyboard, focus and selection](#keyboard-focus-and-selection-list--detaillist)) —; its stylesheet stops the motion of its own parts under `prefers-reduced-motion: reduce` (3.8.2;
29
+ `@aiquants/virtualscroll` must be **3.11.0 or later**: both views pass its scroll-bar option `enableArrowButtonTabStops: false`, since 3.10.0 its one signal that the host scrolls by keyboard itself, under which the scroll bar is pointer-only — hidden from assistive technology, out of the Tab order and never taking focus on a press (the views scroll by keyboard themselves; see [Keyboard, focus and selection](#keyboard-focus-and-selection-list--detaillist)) —; its stylesheet stops the motion of its own parts under `prefers-reduced-motion: reduce` (3.8.2;
30
30
  see [Selection and focus appearance](#selection-and-focus-appearance)); and both views re-record their scroll anchor from its `onScrollAdjust` notification and rely on its edge-keeping snap (3.9.0; see **Host selections and list changes** and [View height](#view-height-host-layout)). The label catalog reuses its 11 engine keys, the names of the scroll bar and its thumb among them (3.10.0; see [Localization](#localization-locale--labels)).
31
+ The test handle's `DailyReportRevealOptions` is its `scrollToIndex` options, which take `align: "nearest"` (3.11.0), and DetailList rows stay at the tree's tops and heights after a batch of re-measurements whose deltas cancel only because its row memo follows the tree revision (3.11.0).
31
32
  That floor is declared once, in `peer-floors.json` (`{ "<peer name>": "<X.Y.Z>" }`), and every publish path runs `scripts/check-peer-floors.mjs` (`pnpm run check:peer-floors`) right after the leak check: each `publish:*` script before its version bump (so a refusal leaves no bumped version behind), and `prepublishOnly` before every `pnpm publish`, a bare one included (for example a re-run after a `publish:*` whose registry step failed).
32
33
  `workspace:^` publishes `^<version>` of the linked workspace package (`node_modules/@aiquants/virtualscroll/package.json`), so while that version is below the floor the check exits 1 and the publish stops (exit 2 for a configuration error, such as a floor that names no `workspace:` peer).
33
34
  `@aiquants/sse` (the SSE wire contract, server response helpers and the reopening client) is a regular **dependency**: it arrives transitively, so consumers do not declare it.
@@ -40,6 +41,7 @@ Every `publish:*` script measures the build that `pnpm run verify` ends with (ve
40
41
  `--write` lowers every entry the build shrank, so a shrink is recorded into the release by construction (no failed publish, no separate baseline commit); a growth is left unrecorded, and `--exact` — besides the budgets, it fails when any measured size differs from its baseline entry in either direction — stops it and prints the `node scripts/check-bundle-size.mjs --write --accept` command that records that build after review.
41
42
  So a release always ships the baseline of its own build: an entry left high after a shrink would loosen every budget derived from it, and one left low would judge the next change against a build nobody shipped. `--exact` cannot be combined with `--write` (exit 2).
42
43
  `pnpm run verify` runs the type checks, Biome (`src/` and `scripts/`), markdownlint, the two count ratchets (docstrings, then the no-fallback guard), the unit tests with per-file coverage floors, the coverage ratchet, the examples, the build and, last, the bundle check.
44
+ The unit tests include the repository's publish leak guard over the markdown the package ships (`src/shipped-markdown-leaks.spec.ts`: every markdown file `package.json` `files` ships — `README.md`, `CHANGELOG.md` and any shipped docs — packed with `package.json` into a tarball and checked by the guard itself, with its own rules), so an internal name in them fails verify instead of stopping a publish; the publish paths still run the guard on the packed tarball, `dist` included.
43
45
  Every source file under `src` (without specs, tests, declarations and test helpers) and every gate script under `scripts` (`scripts/**/*.mjs`) has a committed floor of branch and function coverage in `coverage-floors.json`: Vitest fails a file below its floor, `pnpm run check:coverage` (`scripts/ratchet-coverage.mjs`) fails a file without an entry or an entry without a file, and `pnpm run coverage:ratchet` raises each floor to the measured percentage rounded down after a whole-suite coverage run (a file at 100 % stays at 100; no floor is ever lowered by the script).
44
46
  Each gate script is a two-statement command-line entry around the `main({ packageRoot, argv })` of its module under `scripts/lib/` (the shared exit codes, JSON readers and error descriptions are `scripts/lib/cli.mjs`); the specs run `main` in the test process against temporary packages and keep one subprocess test of each entry, so the scripts' logic is measured by the same floors as `src` (all at 100).
45
47
  A new file enters the ratchet only at 100 % of both metrics: the script records no entry below that, so a file that cannot reach 100 % needs a committed entry with a non-empty `exemption` (the reason, reviewed with the floors), which the script keeps while it raises the floors.
@@ -48,7 +50,7 @@ The guards are the violations of the workspace's docstring and comment language
48
50
  A substitution that the specification or a port contract defines is a reviewed exception of the no-fallback guard instead (`EXCEPTIONS` in `scripts/lib/check-no-fallback.mjs`: the file, the exact expression, the number of `occurrences` it covers and the reason), and its matches are not counted; an exception that matches another number of expressions than it declares fails (exit 1: fewer means the expression was fixed or rewritten, more a new copy that needs its own review), and a malformed list is exit 2.
49
51
  Every TypeScript or JavaScript code block of this README names its source in its info string: an example file, or a `#region` of one, which `src/docs-examples.spec.ts` compares byte for byte (`ts examples/<file>.ts[#<region>]`), or `illustrative` for a fragment that is not type-checked.
50
52
 
51
- **Browser floor**: Chrome 121, Firefox 122, Safari 17.4 (Baseline 2024). The client uses each platform feature below as it is, with no feature detection and no second code path, so on an engine under the floor the listed behaviour fails and nothing else replaces it. Three CSS features of the table are newer than parts of the floor, and the row says what those engines draw:
53
+ **Browser floor**: Chrome 121, Firefox 122, Safari 17.4 (Baseline 2024). The client uses each platform feature below as it is, with no feature detection and no second code path, so on an engine under the floor the listed behaviour fails and nothing else replaces it. Five CSS features of the table are newer than parts of the floor, and the row says what those engines draw:
52
54
 
53
55
  | Feature | Supported from | Used for | Below the floor |
54
56
  | --- | --- | --- | --- |
@@ -58,6 +60,8 @@ Every TypeScript or JavaScript code block of this README names its source in its
58
60
  | CSS `round()` | Chrome 125, Firefox 118, Safari 15.4 | The attachment grid's whole-pixel width `calc(round(down, 100% − 16 (n − 1), n px) + 16 (n − 1))` per column count, which makes every track a whole pixel, the tile frame's whole-pixel height `round(nearest, 100cqi * 320 / 480, 1px)` and the tile's lattice pad `round(up, h, 4px) − h` under its last line (see [Attachment display](#attachment-display)); the views' column origin on the 4 px lattice (see **Origin on the 4 px lattice**) | Chrome 121–124, inside the floor, drop those declarations: the grid fills its container with the fluid tracks (W − 16 (n − 1)) / n (every breakpoint keeps its column count, which no `round()` declaration sets), so tile edges can fall between pixels and the names are fitted less than 1 px narrower than the track; the frame's `aspect-ratio: 480 / 320`, always declared beside its height, sizes the frame at the exact fractional 2t / 3, so the frames of one grid can paint one device pixel apart; a tile stays its frame's fractional height + 48 px tall, so the rows below an image grid can start between device pixels; and the column keeps the `mx-auto` centre |
59
61
  | CSS `calc-size()` | Chrome 129 | The DetailList row bodies' height, their content height rounded up to the 4 px lattice (`calc-size(auto, round(up, size, 4px))`, see **Row slots on the lattice**) | Chrome 121–128, and any engine that does not implement it, drop the declaration: a body keeps its content height, which is on the lattice at a 16 px root and can leave it at other roots, so the DetailList rows below can start between device pixels |
60
62
  | `scrollbar-gutter: stable` | Chrome 94, Firefox 97, Safari 18.2 | The side pane's tab panels keep their scroll bar's width whatever their content's height | Safari 17.4–18.1, inside the floor, ignore it; their default scroll bars overlay the content and take no width |
63
+ | `text-wrap: balance` | Chrome 114, Firefox 121, Safari 17.5 | The short centred labels that can wrap (`WRAPPING_LABEL_CLASS_NAME`: the side pane's selection prompt, an attachment tile's unavailable label) keep their lines about equally long | Safari 17.4, inside the floor, drops the value and wraps them greedily, so a wrapped label can end with a short last line |
64
+ | `word-break: auto-phrase` | Chrome 119 (not in Firefox or Safari) | The same labels break Japanese at phrase boundaries where the host document's `lang` is `ja` | Firefox and Safari drop the declaration and break Japanese between any two characters (the default), so a narrow label can break inside a word |
61
65
  | `<dialog>` with `showModal()` | Chrome 37, Firefox 98, Safari 15.4 | The mobile detail overlay (a modal dialog in the top layer) | Opening the overlay throws a `TypeError` from a layout effect, which React hands to the nearest error boundary |
62
66
 
63
67
  File names are cut at code-point boundaries rather than grapheme boundaries, because `Intl.Segmenter` (Firefox 125) is above the floor (see [Attachment display](#attachment-display)).
@@ -162,6 +166,7 @@ export const loader = (args) => dailyReportServer.sse.loader(args)
162
166
  - `resolveVisibleSourceTypes(request)` — **Optional row-level authorization port.** Returns the `Hub.source_type` values this request may view. Applies uniformly to every server data path: list (ids stream), business-date list, detail, comments, attachment bytes, and SSE. See [Source-type visibility](#source-type-visibility) below.
163
167
  - `enableDevCacheClear` — Gates the dev-only `POST /action` `intent=clearCache` (flush every worker's cache). Default `false` → the handler returns `400` before touching the service. Wire `import.meta.env.DEV` to enable it only in development (any authenticated user could otherwise flush all caches without limit).
164
168
  - `attachmentIdCodec` / `readAttachment` / `attachmentThumbnailRenderer` / `attachmentMaxBytes` — Attachment delivery ports and size limit, owned by the service. See [Attachments](#attachments).
169
+ - `logger` — Optional console-compatible logger (`debug` / `info` / `warn` / `error`) that receives every server log line of the package as it is. Without it each area logs to the console with its own prefix and lowest level, for example `[DailyReportAttachment]` from `info` (see **Log levels** under [Attachments](#attachments)) and `[DailyReportIsolation]` from `warn` (the refusal record of [Request isolation](#request-isolation)).
165
170
  - Primary tuning parameters: `idsTtlMs` (180s) / `businessDateTtlMs` (300s) / `streamKey` / `streamMaxLen` / `loginRedirectPath`, plus the attachment tuning keys listed under [Attachments](#attachments).
166
171
 
167
172
  **Configuration errors** follow one convention on the server and on the client: the message reads `[daily-report] <path> must be <expectation>`, `<path>` being the public key or argument to fix, and ends with `; got <value>` only when it shows a value.
@@ -171,16 +176,25 @@ The three helpers of `src/shared/config-errors.ts` (`configValueError`, `configK
171
176
 
172
177
  ### Request isolation
173
178
 
174
- Every resource route the handlers serve — `api.loader`, `api.action`, `sse.loader` and `attachment.loader` — checks the request's Fetch Metadata first (`isCrossSiteRequest` in `src/server/request-isolation.ts`), before authentication, any rate charge, authorization or service call:
179
+ Every resource route the handlers serve — `api.loader`, `api.action`, `sse.loader` and `attachment.loader` — is wrapped once, where `createDailyReportHandlers` returns it, in the request isolation, which runs first: before authentication, any rate charge, authorization or service call (`isCrossSiteRequest(request, policy)` in `src/server/request-isolation.ts`). `index.loader`, the document route, is not wrapped.
180
+ The check is an allow-list: it names what a route serves from another origin, so everything else — frames (`iframe`, `frame`), `fencedframe`, `object`, `embed`, a navigation without `Sec-Fetch-Dest`, and any destination a later specification adds — is refused by construction.
175
181
 
176
- | `Sec-Fetch-Site` | Answer |
182
+ | Request | Answer |
177
183
  | --- | --- |
178
- | `same-origin`, `none` (a load the user started: the address bar, a bookmark), or no header (a client that is not a browser; every browser of the floor sends it) | Served: authentication and the route's own checks follow |
179
- | `same-site` (another origin of the same site, such as a sibling subdomain), `cross-site` or any other value | 403 before authentication, unless the request is a top-level navigation: `Sec-Fetch-Mode: navigate`, method `GET` or `HEAD`, and a `Sec-Fetch-Dest` other than `object` and `embed` (a link from another page to an original opens it) |
180
-
181
- - **The refusal**: on the API and SSE routes, JSON `{"error":{"message":"Cross-site request rejected"}}` with `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`; on the attachment route, the same body through the loader's own failure builder, so it carries the attachment headers (**Same-origin loads only** in [Attachments](#attachments)). Nothing has authenticated, so no `Set-Cookie` is forwarded, and no log line is written. A cross-site navigation that is not `GET` or `HEAD` (a form `POST` from another site) is refused too, on the attachment route before its method check.
184
+ | `Sec-Fetch-Site: same-origin` or `none` (a load the user started: the address bar, a bookmark) | Served: authentication and the route's own checks follow |
185
+ | Any other `Sec-Fetch-Site` — `same-site` (another origin of the same site, such as a sibling subdomain), `cross-site` or an unknown value | 403 before authentication, unless the request is a top-level document navigation that the route serves: method `GET`, `Sec-Fetch-Mode: navigate`, `Sec-Fetch-Dest` exactly `document`, and a route policy that accepts it. Only the attachment route accepts one, and only for an original (`inline` or `download`, read by the loader's own strict query parser): a link in another page opens or downloads the original. The API, SSE and thumbnail routes accept no navigation from another origin |
186
+ | No `Sec-Fetch-Site` | A safe method (`GET`, `HEAD`, `OPTIONS`) is served. An unsafe method is decided by `Origin`: no `Origin` is served (not a browser page), an `Origin` whose host is the host the request was addressed to (`new URL(request.url).host`, which the adapter builds from `Host`) is served, and `null`, an unparsable value, another host or another port is 403 |
187
+
188
+ - **Requests without Fetch Metadata**: a browser sends no `Sec-Fetch-*` to an origin that is not potentially trustworthy (plain HTTP other than `localhost`), so such a request comes either from a client that is not a browser or from a page of such an origin. A browser always sends `Origin` with an unsafe method (`null` for an opaque origin), so a sibling page's form `POST` to a plain-HTTP host is refused, while a client that is not a browser (no `Origin`) and the host's own pages are served (the rule of Go's `net/http` `CrossOriginProtection`).
189
+ Safe methods change no state, so a no-cors `GET`, which carries no `Origin`, is served.
190
+ Full isolation therefore needs a potentially trustworthy origin (HTTPS): only there does the browser send the Fetch Metadata that also refuses a sibling page's `<img>` and no-cors probes before authentication.
191
+ - **The refusal**: on the API and SSE routes, JSON `{"error":{"message":"Cross-site request rejected"}}` with `Cache-Control: no-store` and `X-Content-Type-Options: nosniff` (`crossSiteRequestRejection`); on the attachment route, the same body through the attachment failure builder, so it carries every attachment security header (**Same-origin loads only** in [Attachments](#attachments)). Nothing has authenticated, so no `Set-Cookie` is forwarded.
192
+ On the attachment route the refusal comes before the query's 400 and the method's 405: a malformed query, a `HEAD` navigation (browsers never navigate with `HEAD`) or a form `POST` from another origin answers 403.
193
+ - **The refusal record**: each handler factory keeps one bounded record of its refusals (`createCrossSiteRejectionRecorder`; state per factory, not per module), written to the configured `logger`, or by default to a console logger at the `warn` level prefixed `[DailyReportIsolation]`.
194
+ The first refusal of a window of `CROSS_SITE_REJECTION_LOG_WINDOW_MS` (60 s) writes one warn line, `cross_site_rejected route=<api|action|sse|attachment> site=<value> mode=<value> dest=<value> method=<method>` (`-` for an absent header; a value outside the specification's shape — lower-case letters and hyphens, at most 32 characters — is written as the JSON string of its first 32 characters, so a sender cannot forge the line's fields); later refusals in the window are only counted.
195
+ The first refusal after the window writes `cross_site_rejected_suppressed count=<N>` for the window before (only when N > 0) and opens a new window with its own line, and a clock that moved back before the window's start opens a new window too. Served requests neither open nor close a window, so an active probe or a CSRF attempt shows in the log at once, at most two lines a minute per factory, however fast the refusals come.
182
196
  - **Why the request side**: the host's session cookie (`SameSite=Lax` in the usual setting) also accompanies requests from other origins of the same site, form submissions (`application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`) and no-cors requests need no preflight, so CORS does not stop them, and React Router checks CSRF only for document requests and single fetch, not for resource routes.
183
- A response policy such as `Cross-Origin-Resource-Policy` decides only whether a response may be read; it does not stop a state change or the authentication, rate and authorization work a request starts. With the check, a page of another origin can neither post to the API (create, update, publish or delete a report, comment, star, mark read) nor probe attachment tokens: a visible and a hidden attachment get the same 403 at the same cost, and the viewer's rate buckets are not spent.
197
+ A response policy such as `Cross-Origin-Resource-Policy` or `X-Frame-Options` decides only whether a response may be read or drawn; it does not stop a state change or the authentication, rate and authorization work a request starts. With the check, a page of another origin can neither post to the API (create, update, publish or delete a report, comment, star, mark read), nor open any of these routes in a frame, nor probe attachment tokens: a visible and a hidden attachment get the same 403 at the same cost, and the viewer's rate buckets are not spent.
184
198
  - **Same origin only**: the package's client calls its endpoints from the page's own origin. A host that serves the API from another origin than the page is refused by this check.
185
199
 
186
200
  ### Source-type visibility
@@ -261,8 +275,10 @@ Attachment bytes are served by `dailyReportServer.attachment.loader` at `GET {ap
261
275
  - **Parsing is strict.** `thumbnail` must be one known variant name, `download` must be exactly `1`, the two cannot be combined, and neither may repeat. Anything else — `?thumbnail=1`, `?thumbnail=true`, `?download=true`, `?download=yes`, `?thumbnail=tile&download=1` — answers **400** `{"error":{"message":"Invalid attachment request"}}` right after authentication, before any port or database query runs, and writes no log line. Other parameters are ignored, and names are case-sensitive (`?Download=1` is an unknown parameter, so the request stays inline).
262
276
  - **The original's type**: the declared type is the row's `file_type` when it is a valid media type, otherwise the type the read port reported. An inline-safe declared type (`image/png`, `image/jpeg`, `image/gif`, `image/webp`, `application/pdf`, `text/plain`) is sent as itself, inline (as an attachment for a download); any other declared type, and a missing one, is sent as `application/octet-stream` with `Content-Disposition: attachment`, because the declared type comes from outside the package and an inline `text/html` or `image/svg+xml` would run script in the host's origin.
263
277
  - **Methods**: the original (inline and download) answers `GET` and `HEAD` (a HEAD reads metadata only and reports the same `Content-Length` as GET); any other method answers **405** with `Allow: GET, HEAD`. The thumbnail answers `GET` only (see **Thumbnail endpoint** below). Both 405s are checked right after the query, before the ports and the token.
264
- - **Same-origin loads only**: a request from another origin's page that is not a top-level `GET` / `HEAD` navigation — an `<img>`, a no-cors `fetch`, a `HEAD` probe from a sibling subdomain — is refused with 403 before authentication ([Request isolation](#request-isolation)), so it learns nothing about a token: a visible and a hidden attachment get the same 403 at the same cost, with no authentication, rate token, visibility resolution or query. A top-level navigation from another site (a link to an original) is served after authentication and authorization as usual.
265
- As defence in depth, every attachment response, success or failure — the original's 200 (inline and download, GET and HEAD), the thumbnail's 200 and 304, and every error status the loader answers (400, 401, 403, 404, 405, 413, 429, 500, 502 and 503, for GET and HEAD alike) — carries `Cross-Origin-Resource-Policy: same-origin` and `X-Content-Type-Options: nosniff`, so the browser lets only pages of the host's own origin read it. Both headers come from one set that every path building an attachment response spreads last, so no status can lose them.
278
+ - **Same-origin loads only**: a request from another origin's page — an `<img>`, a no-cors `fetch`, a `HEAD` probe, an `<iframe>` or `<frame>` from a sibling subdomain, any request for a thumbnail — is refused with 403 before authentication ([Request isolation](#request-isolation);
279
+ on a host served from a potentially trustworthy origin such as HTTPS, where the browser sends Fetch Metadata), so it learns nothing about a token: a visible and a hidden attachment get the same 403 at the same cost, with no authentication, rate token, visibility resolution or query. The one exception is a top-level `GET` document navigation to an original (a link to an attachment in another page), which is served after authentication and authorization as usual.
280
+ As defence in depth, every attachment response, success or failure — the original's 200 (inline and download, GET and HEAD), the thumbnail's 200 and 304, and every error status the route answers (400, 401, 403, 404, 405, 413, 429, 500, 502 and 503, for GET and HEAD alike, the isolation's 403 included) — carries `Cross-Origin-Resource-Policy: same-origin`, `X-Content-Type-Options: nosniff` and `X-Frame-Options: SAMEORIGIN`, so the browser lets only pages of the host's own origin read it or draw it in a frame (a frame of another origin could otherwise tell a blocked 200 from a drawn 404).
281
+ The three headers come from one set (`ATTACHMENT_RESPONSE_SECURITY_HEADERS`) that every path building an attachment response spreads last — the failure builder `attachmentFailureResponse`, which every JSON failure goes through, and the 200 and 304 of both deliveries — so no status can lose them. `Content-Security-Policy: default-src 'none'; sandbox` stays on the two content 200s.
266
282
 
267
283
  **Configuration**
268
284
 
@@ -559,7 +575,7 @@ The client places a thumbnail on this flag alone; it has no switch of its own.
559
575
  **Thumbnail endpoint (`?thumbnail=tile`)**
560
576
 
561
577
  - **GET only**: `HEAD` and every other method answer 405 with `Allow: GET` (checked right after the query). A HEAD response would need a generated body to report the same `Content-Length` as GET.
562
- - **Order**: request isolation (403 for another origin's page, [Request isolation](#request-isolation)) → authenticate → query (400) → method → ports → token → viewer → renderer port → rate admission (below; an empty bucket answers 429 without resolving visibility or running the authorization query)
578
+ - **Order**: request isolation (403 for a request from another origin's page, a navigation included, [Request isolation](#request-isolation)) → authenticate → query (400) → method → ports → token → viewer → renderer port → rate admission (below; an empty bucket answers 429 without resolving visibility or running the authorization query)
563
579
  → authorization (`getAttachmentForUser`, the SQL predicate) → eligibility, identity and ETag → `If-None-Match` (304) → the generation token of a revalidation (below) → not visible / not eligible (404) → cache → generation. The cache, joining a generation and the 304 all come **after** authorization, so a cached preview is never returned to a viewer who cannot see the attachment.
564
580
  - **Rate admission uses two buckets per user and process**. Every token is taken synchronously — before the first `await` on admission, right after authorization's last `await` otherwise — so concurrent requests can never spend one token twice. With L = `attachmentThumbnailRateLimitPerMinute`:
565
581
  - The **generation bucket** holds L tokens and refills L per minute. Every answer except a matching 304 needs one of its tokens. A request without `If-None-Match` can never be a 304, so it spends its token on admission or is answered 429 (`reason=rate_limit`) before authorization.
@@ -597,9 +613,9 @@ The client places a thumbnail on this flag alone; it has no switch of its own.
597
613
  | Status | When |
598
614
  | --- | --- |
599
615
  | 200 | Preview. Headers: the port's `Content-Type`, `Content-Length`, `Cache-Control: private, no-cache` and `ETag` (an unverified preview: `Cache-Control: no-store` and no `ETag`), `X-Content-Type-Options: nosniff`, `Content-Security-Policy: default-src 'none'; sandbox`, `X-Frame-Options: SAMEORIGIN`, `Cross-Origin-Resource-Policy: same-origin`, forwarded `Set-Cookie` |
600
- | 304 | `If-None-Match` matches (carries `Cache-Control`, `ETag`, `X-Content-Type-Options: nosniff`, `Cross-Origin-Resource-Policy: same-origin`, `Set-Cookie`) |
616
+ | 304 | `If-None-Match` matches (carries `Cache-Control`, `ETag`, `X-Content-Type-Options: nosniff`, `Cross-Origin-Resource-Policy: same-origin`, `X-Frame-Options: SAMEORIGIN`, `Set-Cookie`) |
601
617
  | 400 | A query that names no delivery (unknown or repeated variant such as `?thumbnail=1`, combined with `download`), or a malformed token |
602
- | 401 / 403 | Not authenticated / no internal user, or (403, before authentication) a request from another origin's page that is not a top-level `GET` / `HEAD` navigation ([Request isolation](#request-isolation)) |
618
+ | 401 / 403 | Not authenticated / no internal user, or (403, before authentication, the query and the method) a request from another origin's page that the isolation refuses ([Request isolation](#request-isolation): it serves no thumbnail to another origin, not even to a navigation) |
603
619
  | 404 | Ports not injected, not visible, not eligible, signature mismatch, `unsupported`, `too_large`, `not_found`, `denied`, `invalid_path` — one identical body for all |
604
620
  | 405 | Any method other than GET (`Allow: GET`) |
605
621
  | 429 | An empty generation bucket (`reason=rate_limit`) or revalidation bucket (`reason=revalidation_rate_limit`), or a revalidation that is not a matching 304 when no generation token is left after authorization (`reason=rate_limit`) (`Retry-After: 60`) |
@@ -607,10 +623,11 @@ The client places a thumbnail on this flag alone; it has no switch of its own.
607
623
  | 503 | Wait queue full or wait timed out (`queue`), or the generation abandoned by every waiting request (`aborted`; no one receives it) (`Retry-After: 5`) |
608
624
  | 500 | Unexpected exception, including one thrown by a port during generation (logged as `500 attachment=<id> viewer=<id> variant=thumbnail cache=miss reason=port_exception message=<msg>`, never cached), or a port contract violation (`port_contract`: a render result outside the contract, logged with `code=result` for a value that is not an object, `code=failure_reason` for a failure whose reason is neither `unsupported` nor `failed`, or the output check that failed, `code=` `content_type`, `bytes`, `empty`, `too_large`, `signature` or `dimensions`; or a read over `maxBytes`, `code=over_max_bytes`), or a host route that passes no `token` parameter (a route misconfiguration, below) |
609
625
 
610
- Error responses are JSON (`{ "error": { "message": "..." } }`) with `Cache-Control: no-store` (a 404 or 405 without freshness information may otherwise be cached heuristically, `Set-Cookie` included), carry `X-Content-Type-Options: nosniff` and `Cross-Origin-Resource-Policy: same-origin` like every attachment response (**Same-origin loads only** above), and forward `Set-Cookie`. Log lines carry `attachment=<id> viewer=<id> variant=thumbnail`, plus `cache=hit|miss|revalidated` once the cache stage is reached; the 200 and 404 lines of an unverified outcome end with `identity=unverified`.
626
+ Error responses are JSON (`{ "error": { "message": "..." } }`) with `Cache-Control: no-store` (a 404 or 405 without freshness information may otherwise be cached heuristically, `Set-Cookie` included), carry `X-Content-Type-Options: nosniff`, `Cross-Origin-Resource-Policy: same-origin` and `X-Frame-Options: SAMEORIGIN` like every attachment response (**Same-origin loads only** above), and forward `Set-Cookie` (except the isolation's 403, which comes before authentication).
627
+ Log lines carry `attachment=<id> viewer=<id> variant=thumbnail`, plus `cache=hit|miss|revalidated` once the cache stage is reached; the 200 and 404 lines of an unverified outcome end with `identity=unverified`.
611
628
  Unexpected exceptions outside generation go to the loader's shared catch and are logged as `500 attachment=? viewer=? reason=unexpected message=<msg>` (no `variant`).
612
629
  A route that mounts `attachment.loader` without a `token` route parameter is the host's configuration error, on both deliveries: right after the port check the loader throws `[daily-report] params.token must be passed by the route that mounts attachment.loader; declare a "token" route parameter ({apiBasePath}/attachment/{token})`, which that catch logs and answers 500, without decoding an empty token or querying the database.
613
- Rejections up to the renderer-port check (401 / 400 / 405 / 404 for a missing port / 403) are not logged.
630
+ Rejections up to the renderer-port check (401 / 400 / 405 / 404 for a missing port / the 403 of a viewer without an internal user) are not logged by the loader; the request isolation's 403 goes to the handlers' bounded refusal record instead (at most two lines a minute, [Request isolation](#request-isolation)).
614
631
  The package never writes the file path itself; a `port_exception` line includes the port's own error message verbatim, so keep paths out of your port's error messages.
615
632
 
616
633
  **Log levels** (both deliveries; the line formats are fixed):
@@ -683,6 +700,7 @@ The first load of a report that is not cached starts inside the hook's effect, w
683
700
  Mutation failures (save / publish / delete / comment) are surfaced through an error seam: inject `config.onError(info: DailyReportErrorInfo)` to route them into your own toast/notification system, or omit it to use the package's built-in `role="alert"` banner (auto-dismiss + manual close). Failures on a continuation that resolves after a no-reload user switch are suppressed. If you compose `DailyReportActionProvider` yourself instead of using `DailyReportPage`, wrap it in `DailyReportErrorProvider` (both are exported from `@aiquants/daily-report/client`) so `onError` / the banner work.
684
701
 
685
702
  The report id list is **not** part of the loader data: a module-resident NDJSON stream session (`GET {apiBasePath}/ids-stream`, resilient client with cursor resume + exponential backoff) supplies it.
703
+ The client splits the stream into lines as the chunks arrive, examining each decoded character once (a chunk of a 4,000-item line of about 300 KB does not re-read the part of the line already received), and reads a final line without a newline at the end of the stream; a line cut short there is not valid JSON, so it is skipped with one warning and the attempt resumes from its cursor, like any interrupted stream.
686
704
  `createDailyReportClientLoader` warms an existing session (`primeDailyReportIdsStreamSession`) and then bootstraps (`bootstrapDailyReportIdsStreamSession`): when no session exists it is **created at loader time** so the stream fetch runs in parallel with hydration (the dominant cold-load optimization); when one exists, the obfuscated user id is reconciled (a different user destroys and recreates the session before render).
687
705
  If your app overrides `config.apiBasePath`, pass the same value to `createDailyReportClientLoader({ apiBasePath })` — forgetting it costs one wrong-path request on the very first load, self-healed by the page-mount `ensureDailyReportIdsStreamSession`.
688
706
  `DailyReportPage` subscribes via `useDailyReportIdsStream`, rendering the list as soon as the first chunk arrives (on cache misses the server races a fast `TOP 200` first page against the cached full query, so first paint does not wait for the full id scan).
@@ -706,6 +724,7 @@ Each view (List and DetailList) is as tall as the space the host's layout leaves
706
724
  - **G-symmetric frame**: the rows' gutter G is inside the view's box, so the last aligned surface — a List or DetailList row aligned to the bottom, or the List's side pane — sits G = 8 px above the page's bottom edge (where a footer that follows the page starts), the distance from the view-mode toolbar to the first surface at the top.
707
725
  `VirtualScroll` snaps the layer that moves the rows to whole device pixels away from the edge a row is aligned to (the start at position 0 and after an alignment to the top, the end at the maximum position and after an alignment to the bottom; 3.9.0, "Device-pixel snapping" in its README), so an aligned row's surface never comes closer than G to that edge: what the snap adds is less than one device pixel.
708
726
  It adds nothing — the surface sits exactly G from the edge — when the view's height and the row slots are whole numbers of device pixels: the slots are multiples of 4 px (**Row slots on the lattice**), and a host gives the view a height on the 4 px lattice by sizing its own bars on that lattice (for example a window height that is a multiple of 4 under a header and a footer whose heights are rounded up to 4 px).
727
+ That unit is a host contract, exported as `DAILY_REPORT_LAYOUT_LATTICE_PX` (4 CSS px, client entry): it is the package's own lattice unit, not a copy, so a host whose bars are sized from it — or whose own unit is checked to be a multiple of it — stays on the package's lattice.
709
728
  - **Empty list**: an empty view shows exactly one message, `VirtualScroll`'s own empty state with the engine label `labels.noItems` (en "No items", ja 「項目がありません」), which `VirtualScroll` draws over the top of its content, out of the flow, so an empty view keeps the same box. The views render no message of their own; they only theme the engine's (`VIEW_EMPTY_TEXT_THEME_CLASS_NAME` on `VirtualScroll`'s root: 16 px above and below, slate-500 in light and slate-400 in dark: 4.55:1 and 7.66:1 against the page, where the engine's default grey reaches only 4.17:1 in dark).
710
729
 
711
730
  ### Keyboard, focus and selection (List / DetailList)
@@ -863,7 +882,8 @@ Whichever size rule wins the cascade, a selected surface therefore paints nothin
863
882
  - **A key press restyles only what paints the change**: every selector that depends on another element's state ends in the styled element's own class or attribute, and `:has()` sits only on the styled element itself. A featureless subject (`*:`, `group-*`) or an ancestor's `:has(:focus-visible)` would make the browser restyle whole rows or the whole view on each key; `src/client/ui/tailwind-selector-scope.spec.ts` compiles the package's classes and fails on either form.
864
883
  - **Every row is a relayout boundary**: the row frames of both views are `contain: size layout style` (one token, `ROW_CONTAINMENT_CLASS_NAME`), so a change inside one row — a held body rendered after the key's commit, a load that completes, the selection and focus indicators — lays out that row alone and restyles no other.
865
884
  A key move that shifts `VirtualScroll`'s rendering window (it mounts and unmounts rows) is laid out from the items wrapper's containing block. In `@aiquants/virtualscroll` 3.9 that block is a flex item, which Chromium does not make a relayout boundary, so such a shift lays out from the document root: in the app's keyboard harness (run `2026-10-03T23-04-07-660Z`, 4× CPU) a key's layout CPU p50 equals its document-rooted layout CPU p50, 3.99 ms in the List and 8.12 ms in the DetailList.
866
- 3.10.0, the peer floor, puts the wrapper in a relayout boundary of its own (`.aqvs-items-boundary`, a zero-height box with `contain: size layout style`; its README "What a scroll step paints"), from which the same shift is laid out inside the list: in virtualscroll's own probe, 60 one-row shifts in a page of 2,231 layout objects ran 60 partial layouts of 218 objects instead of 60 layouts from the document root, with identical pixels. The views' numbers with the box come from the app's keyboard harness.
885
+ 3.10.0, the peer floor, puts the wrapper in a relayout boundary of its own (`.aqvs-items-boundary`, a zero-height box with `contain: size layout style`; its README "What a scroll step paints"), from which the same shift is laid out inside the list: in virtualscroll's own probe, 60 one-row shifts in a page of 2,231 layout objects ran 60 partial layouts of 218 objects instead of 60 layouts from the document root, with identical pixels.
886
+ In the views the box removes every document-rooted layout of a key's own rendering: in the app's keyboard harness with 3.10.0 no key's rendering lays out from `#document` in any condition, and a key's layout CPU p50 is about 0.2–0.4 ms at 1× CPU on an uncontended host and, at 4× CPU, about 1.1 ms in the List and 6.8–6.9 ms in the DetailList, all of it inside the list (3.99 and 8.12 ms from the document root before). What still lays out from the document root is the List side pane's deferred catch-up to the selection: about once per spaced key, and once per hold when the key is held.
867
887
  Inside the box the rows' overflow is ink overflow, so a scroll the browser makes on its own to reveal an overscan row (a find-in-page match there) cannot move the list and moves the nearest outer scroller instead when the row's box lies outside its view; the views' own reveals do not depend on it (one Tab stop per view, rows focused with `preventScroll`, keyboard focus revealed by the scroller).
868
888
  Size containment makes the frame's size independent of its content: its width is the row box `VirtualScroll` lays out, and its height is the slot P the List writes on the frame or, in the DetailList, that row box itself (`h-full`), whose height `VirtualScroll` takes from the measured row.
869
889
  The DetailList therefore measures each row's body — the frame's direct child, as tall as its content rounded up to the 4 px lattice (**Row slots on the lattice**) — and adds 2G, handing the height to `VirtualScroll` through the scroller's `resizeRow` (see **Host selections and list changes**); measuring the frame would read back the box it fills. A held body is not measured, and a body that is replaced (another load state, a released hold) is observed in its place.
@@ -933,7 +953,7 @@ Whichever size rule wins the cascade, a selected surface therefore paints nothin
933
953
  Plain centring puts the column on a half pixel whenever the space beside it is odd (x = 56.5 in a 1,265 px area), and at a fractional ratio each 2 px stroke then blends into its neighbours.
934
954
  On the lattice, each 2 px stroke of the two-channel indicator — the ring, the separation band and the outline — paints ⌊2 × ratio⌋ full device pixels on the left and right edges (2, 3 and 3 at 1.25, 1.5 and 1.75) with no blended pixel on its inner side, and the 1 px border paints its own colour (measured in Chromium at those ratios, light and dark). An engine without CSS `round()` drops the declaration and keeps the `mx-auto` centre.
935
955
  The block-axis origin and the view's height are the host's: the views start and end where the host's layout puts them.
936
- A host that wants the same whole-pixel strokes on the top and bottom edges at fractional ratios puts both edges on the lattice too — for example with a header and a footer whose heights are their content rounded up to 4 px (`height: calc-size(auto, round(up, size, 4px))`) in a window whose height is a multiple of 4, since a bar sized by its font metrics alone ends on a fraction (such as 30.4375 px); the bottom-aligned surface then sits exactly G from the view's end (**G-symmetric frame** in [View height](#view-height-host-layout)).
956
+ A host that wants the same whole-pixel strokes on the top and bottom edges at fractional ratios puts both edges on the lattice too — for example with a header and a footer whose heights are their content rounded up to the lattice unit `DAILY_REPORT_LAYOUT_LATTICE_PX` (`height: calc-size(auto, round(up, size, 4px))`) in a window whose height is a multiple of it, since a bar sized by its font metrics alone ends on a fraction (such as 30.4375 px); the bottom-aligned surface then sits exactly G from the view's end (**G-symmetric frame** in [View height](#view-height-host-layout)).
937
957
  - **Side pane** (List, desktop layout): the panel group fills the view's root, and the list and the side-pane panels both take its height ([View height](#view-height-host-layout)), so a long report scrolls only inside the pane's article tab and never lengthens the page. The pane sits G inside its panel on all four sides, so its top and bottom edges line up with the first and last cards, and its bottom edge sits exactly G above the page's bottom edge, as far as the toolbar is above the first surface.
938
958
  The List panel ends G after its scroll bar (`LIST_PANEL_END_GUTTER_CLASS_NAME`, `pr-[8px]`), so a 2G = 16 px channel lies between the scroll bar and the pane, and the resize handle that `@aiquants/resize-panels` centres on the panel boundary (a 10 px hit area around a 6 px grip) covers neither the scroll bar nor the pane's edge.
939
959
  The pane's header and its scrolling body end on one edge: the date and author column and the tab list sit in boxes that reserve the same scroll-bar gutter as the article and relations tab panel (`SIDE_PANE_HEADER_BOX_CLASS_NAME` and the panel both compose `SCROLLBAR_GUTTER_CLASS_NAME`, `scrollbar-thin` with `scrollbar-gutter: stable`, which an `overflow: hidden` box reserves too), so with a classic thin scroll bar, a wider one or an overlay one of no width, the header's end and the body's end share one x, in the desktop pane and in the mobile overlay alike.
@@ -944,6 +964,9 @@ Whichever size rule wins the cascade, a selected surface therefore paints nothin
944
964
  - **Scroll bars**: both views theme VirtualScroll's scroll bar through its root class (`VIEW_SCROLL_BAR_THEME_CLASS_NAME`): the track is slate-100 / dark slate-900 and the thumb slate-500 (slate-600 / 400 on hover, slate-700 / 300 while dragged), at least 3:1 against the track in both themes (lowest 4.35 / 3.74); the arrow glyphs reach 6.90 / 6.14 in light and 6.78 / 5.56 in dark on their resting and hover backgrounds.
945
965
  In forced colours the browser replaces every background that is not a system colour with `Canvas`, which would leave the thumb invisible on its track, so the token restates the thumb in system colours with the same selector as each state — `CanvasText` at rest, `Highlight` hovered and dragged, `GrayText` disabled — after the light and dark rules, and gives the track a 1 px inset `CanvasText` outline (an outline: the layout does not change).
946
966
  These rules win over virtualscroll's own forced-colours rules (`@aiquants/virtualscroll` 3.10.0, its README "Forced colours") the same way the token wins over its default colours, so the views' thumb is drawn by this token in every mode.
967
+ - **Line breaks of wrapping labels**: a short centred label that can wrap — the side pane's selection prompt, whose panel narrows to the panel minimum, and an attachment tile's unavailable label, as wide as its track — breaks its lines through one token, `WRAPPING_LABEL_CLASS_NAME`: `text-wrap: balance` keeps the lines about equally long, so no one-glyph last line is left under the others (engines balance only blocks of a few lines, so the token is for labels, not body text),
968
+ and `word-break: auto-phrase` breaks Japanese at phrase boundaries where the host document's `lang` is `ja` (ja 「プレビューを / 表示できません」 instead of 「プレビューを表示できませ / ん」 in a 171 px track) and behaves as `normal` in other languages.
969
+ The token sets nothing else, and the labels that use it declare no other line-breaking property, so nothing cancels it: `src/client/ui/style-tokens.spec.ts` pins the token's two declarations, the side pane's spec the prompt's classes, and the tile's spec compiles the label's classes and checks that its line-breaking declarations are exactly the token's.
947
970
  - **Type and contrast**: no text is smaller than 12 px (`text-xs`); text reaches 4.5:1 and the indicators 3:1 in both themes (section labels slate-500 / slate-400: 4.77 / 7.09). The floating tap-scroll circle hides while the view root carries `data-daily-report-keyboard-focus` (through the class both views pass in `VirtualScroll`'s tap-scroll circle options).
948
971
  - **Action buttons and icons**: the List card's star and read toggles, the star, read, edit and delete buttons of the DetailList header and the side pane, and the trash button of the viewer's comments share one 24 px round target that never shrinks, with a 16 px SVG icon centred in it (4 px on every side), so the icons sit on the 4 px grid without depending on a font. In the List card the markers and toggles stand 4 px apart; the header pills and the source badge are 24 px tall like the targets.
949
972
  The star is a regular five-pointed star, outlined when off and filled gold when on (with a darker gold edge in the light theme); read is a check, unread an 8 px dot, edit a pencil, delete a trash can. Every icon state reaches at least 3.59:1 against each background it sits on, the button's hover background included, in both themes; in forced colours the star is drawn in `CanvasText` (off) and `Highlight` (on). The side pane shows them in the order star, read, edit, delete, centred on the first line of the subject.
@@ -993,7 +1016,7 @@ Whichever size rule wins the cascade, a selected surface therefore paints nothin
993
1016
  The frame is a relayout boundary: its strict containment (size, layout, paint, style) changes neither its size, which comes from its own style alone (the container's full width and the height above, or the aspect ratio), nor what is painted, since paint containment clips at the frame's edge and the skin lies inside it;
994
1017
  and as a positioned box that is not a flex or grid item (its parent is a block wrapper inside the tile link), Chromium lays out what changes inside the frame — the image's intrinsic size arriving on load, a style change of the skin — inside the frame alone, not from the document root.
995
1018
  Only a change inside the frame stops there: a boundary whose own computed style changes is laid out by its parent, from the document root. So the frame's own style never changes after mount — its classes are its box alone (`relative block w-full`: no variant, no paint, no animation) and its inline style is `ATTACHMENT_TILE_FRAME_STYLE` — and every state is drawn inside it, by the skin and the image, which read the frame's attributes.
996
- - **`data-thumbnail-state`** on the frame is `pending` (waiting or loading), `loaded` (the image fades in) or `unavailable` (an icon and `labels.attachmentThumbnailUnavailable` inside the skin; the icon reaches 4.35:1 / 5.56:1 and the label 6.90:1 / 5.58:1 in the light / dark theme). The loader's internal phases are not exposed.
1019
+ - **`data-thumbnail-state`** on the frame is `pending` (waiting or loading), `loaded` (the image fades in) or `unavailable` (an icon and `labels.attachmentThumbnailUnavailable` inside the skin; the icon reaches 4.35:1 / 5.56:1 and the label 6.90:1 / 5.58:1 in the light / dark theme, and the label breaks its lines by **Line breaks of wrapping labels** in [Selection and focus appearance](#selection-and-focus-appearance)). The loader's internal phases are not exposed.
997
1020
  - **The waiting pulse runs only where the tile can be seen**: a `pending` frame's skin pulses (under `prefers-reduced-motion: no-preference`) only while the frame also carries the boolean attribute `data-thumbnail-active`. The frame's load controller decides it (`showActivity`) and the attribute is toggled on the frame element, without a React render, so visibility changes and key moves add no commit; only the skin reads it, so a toggle restyles the skin, never the frame (see **Frame**):
998
1021
  an observed frame (waiting for its 250 ms or for a slot, below) carries the attribute exactly while it is visible, and before its first visibility record it carries none; a granted load sets it and stops observing, so the frame keeps it through the load and, after a failed attempt, through the retry wait — both bounded — until observation starts again (the attribute is removed then) or the frame is released (unmount, a URL change, hiding under `<Activity>`). Once the image has loaded the state is no longer `pending`, so a remaining attribute pulses nothing.
999
1022
  A frame rendered but never seen — a row in the overscan — therefore never pulses: an animation whose element is not visible is not composited, and Chromium would restyle it on the main thread on every frame of an otherwise idle page. A frame that starts from the page's memory (a loaded URL, a remembered failure) is not observed and is only told to hide its activity when it is released.
@@ -1056,7 +1079,8 @@ DOM hooks are `data-*` attributes, test ids and ARIA; class names are styling an
1056
1079
 
1057
1080
  Only the package writes `data-daily-report-keyboard-focus` and `data-daily-report-scrolling`. Hosts may select on them — in tests, or in CSS to adapt their own content inside a view — but never set them. When the pane content matters, wait for `data-displayed-report-id`, not `data-report-id`: during a key's render the frame already names the new selection while the content still shows the previous report.
1058
1081
 
1059
- `[data-testid="daily-report-list"]` and `[data-testid="daily-report-detail-list"]` carry a `__virtualScroll` accessor for end-to-end tests, with the same shape in both views. It is the read-only part of the current `VirtualScroll` handle — `getViewportSize()`, `getScrollPosition()`, `getScrollAnchor()`, `getRange()` and `getFenwickSize()` — plus `findReportIndex(id)`, `getReportItem(index)` and `getReportIds(limit = 20)` over the view's committed list, and one way to move the position, `revealIndex(index, { align, offset })`.
1082
+ `[data-testid="daily-report-list"]` and `[data-testid="daily-report-detail-list"]` carry a `__virtualScroll` accessor for end-to-end tests, with the same shape in both views. It is the read-only part of the current `VirtualScroll` handle — `getViewportSize()`, `getScrollPosition()`, `getScrollAnchor()`, `getRange()` and `getFenwickSize()` — plus `findReportIndex(id)`, `getReportItem(index)` and `getReportIds(limit = 20)` over the view's committed list, and one way to move the position, `revealIndex(index, options)`.
1083
+ The client entry exports the contract as types, so a host's tests import it instead of restating it (type-only, no bundle bytes): `DailyReportViewTestHandle` is the accessor, `DailyReportViewTestReadHandle` its read-only part, and `DailyReportRevealOptions` the options of `revealIndex`, which are the second argument of `VirtualScroll`'s `scrollToIndex` (its alignment and offset, as the peer defines them).
1060
1084
  `revealIndex` goes through the view's scroller (the `VirtualScroll` alignment of `scrollToIndex`, then the anchor's record, like a key move; see **Host selections and list changes**), so a list change right after it keeps the revealed report in place. The accessor has none of the handle's functions that move the position (`scrollToIndex`, `scrollBy`, `scrollTo`, `applyWheel`, `updateItemSize`) and no selection backdoor: select through the UI (a click, the keys). Read it on every use; it is `undefined` while the list is not mounted.
1061
1085
 
1062
1086
  ### Localization (`locale` / `labels`)
@@ -1320,7 +1344,7 @@ Stop signals are **in-band**: the server answers 200 and writes `retry: 86400000
1320
1344
  | --- | --- | --- |
1321
1345
  | Not authenticated | 200, `retry: 86400000` → `event: auth-required` (`Set-Cookie` forwarded) | terminated; calls `config.onSessionExpired()` once; never reconnects |
1322
1346
  | Authenticated but no internal user | 200, `retry: 86400000` → `event: forbidden` | terminated; no navigation (a reload cannot fix it); visible through `sseStatus` |
1323
- | A request from another origin's page ([Request isolation](#request-isolation)) | 403 JSON before authentication (`Cache-Control: no-store`, no `Set-Cookie`) | never met: the package's client connects from the page's own origin |
1347
+ | A request from another origin's page that the isolation refuses ([Request isolation](#request-isolation)) | 403 JSON before authentication (`Cache-Control: no-store`, no `Set-Cookie`), counted in the bounded refusal record | never met: the package's client connects from the page's own origin |
1324
1348
  | Unknown endpoint | 404 | treated as retryable; backoff up to 30 s |
1325
1349
  | Unexpected error before the stream starts (for example the internal-user lookup fails) | 500 with the body `Internal Server Error` (no error detail), `Set-Cookie` forwarded | treated as retryable; backoff up to 30 s |
1326
1350
  | Malformed cursor | 200, `retry: 86400000` → `event: resync-required` | rereads the ids (server cache bypassed) and reopens from the new anchor |
@@ -1362,8 +1386,9 @@ Every entry exports by name (no `export *`), so a module-internal helper never b
1362
1386
  - Components: `DailyReportPage` / `DailyReportResolvedContent` / `DailyReportList` / `DailyReportDetailList` / `DailyReportAttachmentIndicator`, the providers `DailyReportConfigProvider` / `DailyReportActionProvider` / `DailyReportErrorProvider`.
1363
1387
  - Hooks: `useDailyReportConfig` / `useDailyReportActionContext` (`sseStatus`) / `useDailyReportDetail` / `useDailyReportPrefetch` / `useDailyReportComments` / `useDailyReportSseConnection` (returns `DailyReportSseConnectionStatus`) / `useDailyReportIdsStream` (state carries `streamAnchor`).
1364
1388
  - The ids stream: `DailyReportIdsStreamClient` (`resync()`) / `DailyReportIdsStreamStatus` / `primeDailyReportIdsStreamSession` / `bootstrapDailyReportIdsStreamSession` / `ensureDailyReportIdsStreamSession` / `resyncDailyReportIdsStream`; the route helpers `createDailyReportClientLoader` / `dailyReportShouldRevalidate`.
1365
- - Configuration and wording: `defaultDailyReportClientConfig` / `resolveDailyReportLabels` / `DAILY_REPORT_LABEL_CATALOGS` / `DAILY_REPORT_LABEL_KEYS` / `resolveSourceTypeConfig`. No layout value is public: the row geometry follows the host's root font size (see **List slot P**), so rows are located by `[data-daily-report-row]` or the test handle.
1366
- - Types: `DailyReportClientConfigInput` / `DailyReportClientConfigDefaults` / `SourceTypeConfig` / `DailyReportLocale` / `DailyReportLabels` / `DailyReportLabelOverrides` / `DailyReportOperation` / `DailyReportErrorInfo` / `DailyReportSseConnectionStatus`.
1389
+ - Configuration and wording: `defaultDailyReportClientConfig` / `resolveDailyReportLabels` / `DAILY_REPORT_LABEL_CATALOGS` / `DAILY_REPORT_LABEL_KEYS` / `resolveSourceTypeConfig`.
1390
+ - Layout: `DAILY_REPORT_LAYOUT_LATTICE_PX`, the 4 px lattice unit a host sizes its bars on (see **G-symmetric frame**); it is the only public layout value. The row geometry is not public: it follows the host's root font size (see **List slot P**), so rows are located by `[data-daily-report-row]` or the test handle.
1391
+ - Types: `DailyReportClientConfigInput` / `DailyReportClientConfigDefaults` / `SourceTypeConfig` / `DailyReportLocale` / `DailyReportLabels` / `DailyReportLabelOverrides` / `DailyReportOperation` / `DailyReportErrorInfo` / `DailyReportSseConnectionStatus`, and the end-to-end test handle's contract `DailyReportViewTestHandle` / `DailyReportViewTestReadHandle` / `DailyReportRevealOptions` (see [Test hooks](#test-hooks)).
1367
1392
  - **server** (`@aiquants/daily-report/server`, Node.js):
1368
1393
  - Factories: `createDailyReportServer` / `createDailyReportService` (`getDailyReportIdsByExternalId` returns `{ items, streamAnchor }`; `readSseStreamAnchor`; `attachmentDelivery`) / `createDailyReportHandlers` / `defineDailyReportSchema` / `createEpochStore`, the viewer scope `DailyReportViewerScope`.
1369
1394
  - Attachments: `createSharpThumbnailRenderer` / `predictSharpThumbnailWorkingSetBytes` / `DAILY_REPORT_SHARP_THUMBNAIL_WORKING_SET_MODEL` / `DAILY_REPORT_SHARP_THUMBNAIL_DECODE_BUDGET_BYTES` / `DAILY_REPORT_SHARP_THUMBNAIL_DECODE_WORK_MODEL` (the render deadline `renderMs` and the decode-work and walk bounds, for a host's conformance checks) / `DAILY_REPORT_ATTACHMENT_THUMBNAIL_OUTPUT_MEDIA_TYPES` / `DAILY_REPORT_ATTACHMENT_THUMBNAIL_MAX_OUTPUT_BYTES`.
package/dist/client.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from "react";
2
2
  import { RefObject, ReactNode, Context } from "react";
3
3
  import { D as DailyReportItem, a as DailyReportUser, b as DailyReportDetail } from "./types-DkERw8NE.mjs";
4
- import { VirtualScrollLocale, VirtualScrollLabels } from "@aiquants/virtualscroll";
4
+ import { VirtualScrollLocale, VirtualScrollLabels, VirtualScrollHandle } from "@aiquants/virtualscroll";
5
5
  import { UseReopeningEventSourceResult } from "@aiquants/sse/react";
6
6
  import { D as DailyReportSseTerminalEvent, a as DailyReportSseMessage, U as UIComment, b as DailyReportSseStreamAnchor } from "./ids-stream-DvB2h1dj.mjs";
7
7
  import { ShouldRevalidateFunction } from "react-router";
@@ -281,6 +281,15 @@ type DailyReportResolvedContentProps = {
281
281
  userId?: string | null;
282
282
  };
283
283
  declare const DailyReportResolvedContent: ({ userId }: DailyReportResolvedContentProps) => react.JSX.Element;
284
+ type ListNavigationHandle = Pick<VirtualScrollHandle, "getViewportSize" | "getScrollPosition" | "getScrollAnchor">;
285
+ type ListScrollToIndexOptions = NonNullable<Parameters<VirtualScrollHandle["scrollToIndex"]>[1]>;
286
+ type ViewTestReadHandle = ListNavigationHandle & Pick<VirtualScrollHandle, "getRange" | "getFenwickSize">;
287
+ type ViewTestHandle = ViewTestReadHandle & {
288
+ readonly findReportIndex: (id: number) => number;
289
+ readonly getReportItem: (index: number) => DailyReportItem | undefined;
290
+ readonly getReportIds: (limit?: number) => number[];
291
+ readonly revealIndex: (index: number, options: ListScrollToIndexOptions) => void;
292
+ };
284
293
  type DailyReportSseConnectionStatus = UseReopeningEventSourceResult<DailyReportSseTerminalEvent>["status"] | {
285
294
  readonly kind: "resyncing";
286
295
  };
@@ -454,4 +463,5 @@ declare const bootstrapDailyReportIdsStreamSession: (options: {
454
463
  declare const primeDailyReportIdsStreamSession: () => void;
455
464
  declare const resyncDailyReportIdsStream: () => void;
456
465
  declare const useDailyReportIdsStream: () => DailyReportIdsStreamState;
457
- export { DAILY_REPORT_LABEL_CATALOGS, DAILY_REPORT_LABEL_KEYS, DailyReportActionProvider, DailyReportAttachmentIndicator, type DailyReportClientConfigDefaults, type DailyReportClientConfigInput, DailyReportConfigProvider, DailyReportDetailList, type DailyReportErrorInfo, DailyReportErrorProvider, DailyReportIdsStreamClient, DailyReportIdsStreamStatus, type DailyReportLabelOverrides, type DailyReportLabels, DailyReportList, type DailyReportLocale, type DailyReportOperation, DailyReportPage, DailyReportResolvedContent, type DailyReportSseConnectionStatus, type SourceTypeConfig, bootstrapDailyReportIdsStreamSession, createDailyReportClientLoader, dailyReportShouldRevalidate, defaultDailyReportClientConfig, ensureDailyReportIdsStreamSession, primeDailyReportIdsStreamSession, resolveDailyReportLabels, resolveSourceTypeConfig, resyncDailyReportIdsStream, useDailyReportActionContext, useDailyReportComments, useDailyReportConfig, useDailyReportDetail, useDailyReportIdsStream, useDailyReportPrefetch, useDailyReportSseConnection };
466
+ declare const DAILY_REPORT_LAYOUT_LATTICE_PX = 4;
467
+ export { DAILY_REPORT_LABEL_CATALOGS, DAILY_REPORT_LABEL_KEYS, DAILY_REPORT_LAYOUT_LATTICE_PX, DailyReportActionProvider, DailyReportAttachmentIndicator, type DailyReportClientConfigDefaults, type DailyReportClientConfigInput, DailyReportConfigProvider, DailyReportDetailList, type DailyReportErrorInfo, DailyReportErrorProvider, DailyReportIdsStreamClient, DailyReportIdsStreamStatus, type DailyReportLabelOverrides, type DailyReportLabels, DailyReportList, type DailyReportLocale, type DailyReportOperation, DailyReportPage, DailyReportResolvedContent, type ListScrollToIndexOptions as DailyReportRevealOptions, type DailyReportSseConnectionStatus, type ViewTestHandle as DailyReportViewTestHandle, type ViewTestReadHandle as DailyReportViewTestReadHandle, type SourceTypeConfig, bootstrapDailyReportIdsStreamSession, createDailyReportClientLoader, dailyReportShouldRevalidate, defaultDailyReportClientConfig, ensureDailyReportIdsStreamSession, primeDailyReportIdsStreamSession, resolveDailyReportLabels, resolveSourceTypeConfig, resyncDailyReportIdsStream, useDailyReportActionContext, useDailyReportComments, useDailyReportConfig, useDailyReportDetail, useDailyReportIdsStream, useDailyReportPrefetch, useDailyReportSseConnection };
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from "react";
2
2
  import { RefObject, ReactNode, Context } from "react";
3
3
  import { D as DailyReportItem, a as DailyReportUser, b as DailyReportDetail } from "./types-DkERw8NE.js";
4
- import { VirtualScrollLocale, VirtualScrollLabels } from "@aiquants/virtualscroll";
4
+ import { VirtualScrollLocale, VirtualScrollLabels, VirtualScrollHandle } from "@aiquants/virtualscroll";
5
5
  import { UseReopeningEventSourceResult } from "@aiquants/sse/react";
6
6
  import { D as DailyReportSseTerminalEvent, a as DailyReportSseMessage, U as UIComment, b as DailyReportSseStreamAnchor } from "./ids-stream-BR5RSj5u.js";
7
7
  import { ShouldRevalidateFunction } from "react-router";
@@ -281,6 +281,15 @@ type DailyReportResolvedContentProps = {
281
281
  userId?: string | null;
282
282
  };
283
283
  declare const DailyReportResolvedContent: ({ userId }: DailyReportResolvedContentProps) => react.JSX.Element;
284
+ type ListNavigationHandle = Pick<VirtualScrollHandle, "getViewportSize" | "getScrollPosition" | "getScrollAnchor">;
285
+ type ListScrollToIndexOptions = NonNullable<Parameters<VirtualScrollHandle["scrollToIndex"]>[1]>;
286
+ type ViewTestReadHandle = ListNavigationHandle & Pick<VirtualScrollHandle, "getRange" | "getFenwickSize">;
287
+ type ViewTestHandle = ViewTestReadHandle & {
288
+ readonly findReportIndex: (id: number) => number;
289
+ readonly getReportItem: (index: number) => DailyReportItem | undefined;
290
+ readonly getReportIds: (limit?: number) => number[];
291
+ readonly revealIndex: (index: number, options: ListScrollToIndexOptions) => void;
292
+ };
284
293
  type DailyReportSseConnectionStatus = UseReopeningEventSourceResult<DailyReportSseTerminalEvent>["status"] | {
285
294
  readonly kind: "resyncing";
286
295
  };
@@ -454,4 +463,5 @@ declare const bootstrapDailyReportIdsStreamSession: (options: {
454
463
  declare const primeDailyReportIdsStreamSession: () => void;
455
464
  declare const resyncDailyReportIdsStream: () => void;
456
465
  declare const useDailyReportIdsStream: () => DailyReportIdsStreamState;
457
- export { DAILY_REPORT_LABEL_CATALOGS, DAILY_REPORT_LABEL_KEYS, DailyReportActionProvider, DailyReportAttachmentIndicator, type DailyReportClientConfigDefaults, type DailyReportClientConfigInput, DailyReportConfigProvider, DailyReportDetailList, type DailyReportErrorInfo, DailyReportErrorProvider, DailyReportIdsStreamClient, DailyReportIdsStreamStatus, type DailyReportLabelOverrides, type DailyReportLabels, DailyReportList, type DailyReportLocale, type DailyReportOperation, DailyReportPage, DailyReportResolvedContent, type DailyReportSseConnectionStatus, type SourceTypeConfig, bootstrapDailyReportIdsStreamSession, createDailyReportClientLoader, dailyReportShouldRevalidate, defaultDailyReportClientConfig, ensureDailyReportIdsStreamSession, primeDailyReportIdsStreamSession, resolveDailyReportLabels, resolveSourceTypeConfig, resyncDailyReportIdsStream, useDailyReportActionContext, useDailyReportComments, useDailyReportConfig, useDailyReportDetail, useDailyReportIdsStream, useDailyReportPrefetch, useDailyReportSseConnection };
466
+ declare const DAILY_REPORT_LAYOUT_LATTICE_PX = 4;
467
+ export { DAILY_REPORT_LABEL_CATALOGS, DAILY_REPORT_LABEL_KEYS, DAILY_REPORT_LAYOUT_LATTICE_PX, DailyReportActionProvider, DailyReportAttachmentIndicator, type DailyReportClientConfigDefaults, type DailyReportClientConfigInput, DailyReportConfigProvider, DailyReportDetailList, type DailyReportErrorInfo, DailyReportErrorProvider, DailyReportIdsStreamClient, DailyReportIdsStreamStatus, type DailyReportLabelOverrides, type DailyReportLabels, DailyReportList, type DailyReportLocale, type DailyReportOperation, DailyReportPage, DailyReportResolvedContent, type ListScrollToIndexOptions as DailyReportRevealOptions, type DailyReportSseConnectionStatus, type ViewTestHandle as DailyReportViewTestHandle, type ViewTestReadHandle as DailyReportViewTestReadHandle, type SourceTypeConfig, bootstrapDailyReportIdsStreamSession, createDailyReportClientLoader, dailyReportShouldRevalidate, defaultDailyReportClientConfig, ensureDailyReportIdsStreamSession, primeDailyReportIdsStreamSession, resolveDailyReportLabels, resolveSourceTypeConfig, resyncDailyReportIdsStream, useDailyReportActionContext, useDailyReportComments, useDailyReportConfig, useDailyReportDetail, useDailyReportIdsStream, useDailyReportPrefetch, useDailyReportSseConnection };