@aiquants/daily-report 0.36.0 → 0.38.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 +110 -3
- package/README.md +145 -56
- package/dist/client.d.mts +39 -8
- package/dist/client.d.ts +39 -8
- package/dist/client.js +5 -5
- package/dist/client.mjs +5 -5
- package/dist/{comment-adapter-pzx3O8_h.d.mts → comment-adapter-BsbOiHmX.d.mts} +1 -1
- package/dist/{comment-adapter-BvGGQkBE.d.ts → comment-adapter-CldlCy1v.d.ts} +1 -1
- package/dist/index.d.mts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/search-filters-wire-5MWi6GWu.d.mts +12 -0
- package/dist/search-filters-wire-5MWi6GWu.d.ts +12 -0
- package/dist/server.d.mts +83 -25
- package/dist/server.d.ts +83 -25
- package/dist/server.js +10 -10
- package/dist/server.mjs +10 -10
- package/dist/styles/daily-report.standalone.css +1 -1
- package/package.json +1 -1
- package/dist/{search-marks-wire-DCFuK5yr.d.mts → search-marks-wire-CZRU4q3h.d.mts} +0 -0
- package/dist/{search-marks-wire-DCFuK5yr.d.ts → search-marks-wire-CZRU4q3h.d.ts} +0 -0
- package/dist/{sse-schema-getLS62T.d.mts → sse-schema-byewDmZK.d.mts} +52 -52
- package/dist/{sse-schema-getLS62T.d.ts → sse-schema-byewDmZK.d.ts} +52 -52
package/README.md
CHANGED
|
@@ -83,6 +83,9 @@ Every TypeScript or JavaScript code block of this README names its source in its
|
|
|
83
83
|
|
|
84
84
|
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)).
|
|
85
85
|
|
|
86
|
+
The controls of the search's filter row are the host's, not the package's (`config.search.renderFilterSelect`; see **Filter row** in [Text search](#text-search)): the package's browser code imports no control library — neither `@aiquants/select-box` nor `@aiquants/fuzzy-search`, which `src/client-bundle.structure.spec.ts` checks in the sources and in a fresh build —, so the floor above stays the package's.
|
|
87
|
+
A host control carries its own floor (a control built on `@aiquants/select-box`, for example, needs Firefox 125 for `Intl.Segmenter`), and a control that throws while it renders turns only its own filter cell into `labels.searchFilterUnavailable`.
|
|
88
|
+
|
|
86
89
|
### Tailwind CSS
|
|
87
90
|
|
|
88
91
|
This package has **no hand-written component CSS**, so it ships **no components-only artifact** — only a standalone build for non-Tailwind hosts. Components carry Tailwind utility classes in JSX. Pick one consumption mode:
|
|
@@ -189,28 +192,32 @@ export const loader = (args) => dailyReportServer.attachment.loader(args)
|
|
|
189
192
|
React Router runs a resource route's loader for `HEAD` and drops the body without reading or cancelling it, and an adapter need not abort the request's signal once the response is sent, so body work started for a `HEAD` would run until the process ends. A `GET` whose signal is already aborted starts no body work either.
|
|
190
193
|
- **Single fetch**: React Router answers a single-fetch data request (`<path>.data`) by running the route's loader and reading its whole body before it answers, so a `.data` request to either stream would hold every event, or the whole NDJSON, in server memory until the client leaves.
|
|
191
194
|
The package's request isolation refuses them before authentication ([Request isolation](#request-isolation)): on the SSE route every one, and on the API route those of every endpoint whose body streams — the ids stream's (`ids-stream.data`, with or without a cursor, and its trailing-slash form `ids-stream/_.data`) —, `GET` and `HEAD` alike, with a 404 JSON (`{"error":{"message":"Not Found"}}`, `Cache-Control: no-store` and the routes' security headers, **Security headers** below) thrown the way a loader answers early.
|
|
192
|
-
A refused request authenticates nothing, subscribes to nothing, starts no timer and runs no query. The JSON endpoints (`report`, `business-date`, and `search`, `reports` and `search-
|
|
195
|
+
A refused request authenticates nothing, subscribes to nothing, starts no timer and runs no query. The JSON endpoints (`report`, `business-date`, and `search`, `reports`, `search-marks` and `search-facets` with the `search` block) and the action keep serving single fetch, which React Router's own fetchers use.
|
|
193
196
|
- **The API route's methods**: authentication comes first (401), then the endpoint — an unknown name is 404, an inherited name such as `toString` included — and then the method: one the endpoint does not answer is 405 with `Allow`.
|
|
194
|
-
The endpoint table (`API_ENDPOINT_TABLE` in `src/server/api-endpoint.ts`) states once, per endpoint, what kind of body it answers and what bounds its database work per viewer, and the methods follow from the body (`API_ENDPOINT_METHODS`): `GET` for a JSON body (`report` and `business-date`, and `search`, `reports` and `search-
|
|
197
|
+
The endpoint table (`API_ENDPOINT_TABLE` in `src/server/api-endpoint.ts`) states once, per endpoint, what kind of body it answers and what bounds its database work per viewer, and the methods follow from the body (`API_ENDPOINT_METHODS`): `GET` for a JSON body (`report` and `business-date`, and `search`, `reports`, `search-marks` and `search-facets` with the `search` block, whose ETag is computed from the body, so a `HEAD` would build the body only to discard it) and `GET, HEAD` for a streaming body (the ids stream).
|
|
198
|
+
The API route's refusal of data requests above follows from the same table.
|
|
195
199
|
Each row's `admission` lists at least one bound (the row's type requires it, so an endpoint cannot enter the table without one): `cache` (reads through the SQL result cache, which shares one fetch among identical concurrent reads and keeps the result for its TTL, in at most `cacheMaxBuckets` buckets), `miss-gate` (a read that misses the cache takes one of the viewer's cache-miss slots before its query starts, **Cache misses per viewer** below),
|
|
196
200
|
`refresh-bucket` (a read that bypasses the cache spends the viewer's refresh bucket, **`forceRefresh`** under **Request values** below), `stream-slots` (the body holds one of the viewer's stream slots, **Streams per viewer** below) and `search-limiter` (the endpoint's own search admission, **Requests** in [Text search](#text-search)).
|
|
197
201
|
`cache` alone shares only identical reads: every distinct key — any business date from 0001 to 9999, any id — misses it, so the distinct reads are bounded by `miss-gate`.
|
|
198
|
-
`business-date` and `report` declare `refresh-bucket`, `cache` and `miss-gate`, the ids stream `refresh-bucket`, `stream-slots`, `cache` and `miss-gate`, and `search`, `reports` and `search-
|
|
202
|
+
`business-date` and `report` declare `refresh-bucket`, `cache` and `miss-gate`, the ids stream `refresh-bucket`, `stream-slots`, `cache` and `miss-gate`, and `search`, `reports`, `search-marks` and `search-facets` `search-limiter` each. Every handler admits its bounds after its 400s, and one spec walks the table and proves each declared bound on its handler (`src/server/handlers.json-endpoints.spec.ts`), so a bound that is declared and not enforced fails.
|
|
199
203
|
- **Streams per viewer**: one viewer holds at most `DAILY_REPORT_STREAMS_PER_VIEWER` = 32 streaming responses at once in one worker process, SSE streams and ids streams together (`createStreamSlots` in `src/server/stream-body.ts`: one set per handler factory, keyed by the internal user id, no wait queue). The bound is the package client's own need with headroom: a view holds 2 streams (its SSE connection for its whole life, and its ids stream while it loads or rescans), and a viewer may keep 16 views open across tabs, windows and devices.
|
|
200
204
|
It is not derived from HTTP/1.1's 6 connections per origin, since one HTTP/2 connection carries many streams. A held stream costs about 10 KB of heap and one visibility re-resolution every 60 s, so 32 streams cost about 310 KB and 0.53 re-resolutions a second.
|
|
201
205
|
A stream takes its slot after the checks that decide its status — on the SSE route after the resume cursor and before the visibility is resolved or anything subscribed, on the ids stream after its 400s and the refresh bucket — and gives it back exactly once, at whichever comes first: the request's abort, at any time after the slot was taken, or the settle path that ends its body (the SSE connection's close; the ids stream's end, cancel or error, and a resumed `GET` that fails before its body starts).
|
|
202
206
|
The admission itself ties the slot to the body's signal (`createStreamSlots().admit`), so a request aborted while its route still awaits — the SSE route resolving the visibility, a resumed ids `GET` waiting for its full query — returns its slot at that moment, also on an adapter that never pulls an aborted body again.
|
|
203
|
-
The request after the last slot starts no body work and is answered 503 with `Retry-After: 5`: the SSE route with the text `Too many streams`, the ids stream with `{"error":{"message":"Too many streams"}}`; both clients come back on their own backoff. A `HEAD`, or a `GET` whose signal is already aborted, takes no slot: it is only checked, and gets the status a `GET` would get. A viewer without an internal user takes none (the SSE route answers it `forbidden`, and its ids stream is one empty line without a query).
|
|
207
|
+
The request after the last slot starts no body work and is answered 503 with `Retry-After: 5`: the SSE route with the text `Too many streams`, the ids stream with `{"error":{"message":"Too many streams"}}`; both clients come back on their own backoff. A `HEAD`, or a `GET` whose signal is already aborted, takes no slot: it is only checked, and gets the status a `GET` would get. A viewer without an internal user takes none (the SSE route answers it `forbidden`, and its ids stream is one empty line without a dataset query).
|
|
204
208
|
The refusals are recorded per viewer in windows of 60 s at `warn` on the `[DailyReportStreams]` channel: the window's first as `503 <sse|ids-stream> viewer=<internal id> reason=streams_per_viewer`, the rest counted into one `streams_per_viewer_suppressed count=<N> since=<ISO 8601 time of the window's first refusal> viewer=<internal id> routes=sse:<n>,ids-stream:<n>` line written by the window's own timer at its end.
|
|
205
209
|
The slots are per worker process, so the effective bound across workers is the worker count times 32.
|
|
206
210
|
- **The SQL result cache** (`SqlResultCache`; `createDailyReportServer` builds one per process, which the service and the SSE reader share): `cacheDefaultTtlMs` (an integer of at least 1, 60,000 ms by default) is the TTL of a read that names none and the interval of the expired-bucket sweep, and `cacheMaxBuckets` (an integer of at least 1, 4,096 by default) bounds the buckets by construction.
|
|
207
211
|
A write puts its bucket in as the newest-written and evicts the oldest-written while the cache holds more than the maximum, and the expired buckets are swept at most once per default TTL on the monotonic clock, so a write costs the same whatever the number of buckets (100,000 distinct keys keep the size at the maximum).
|
|
208
212
|
4,096 is about 400 views' live keys: a view keeps about 10 (its ids key, its rows' business date and the DetailList's 6 prefetched dates). An evicted key's next read is a miss and passes the miss gate below.
|
|
209
213
|
- **Cache misses per viewer** (`createCacheMissGate` in `src/server/cache-miss-gate.ts`; the endpoints whose row declares `miss-gate`): a read that misses the cache — a forced read included — takes one of its viewer's slots exactly where its fetch starts and gives it back when the fetch settles, either way; a hit and a read that joins the fetch in flight for its key take none.
|
|
210
|
-
One gate per handler factory, keyed by the internal user id, holds `CACHE_MISS_SLOTS_PER_VIEWER` = 8 fetches at once per viewer — what one view of the package's client starts when it paints: the ids stream's full read, the shown rows' business date and the DetailList's `PREFETCH_MAX_DATES` = 6 prefetched dates — and `CACHE_MISS_SLOTS_PER_WORKER` = 64 per worker process
|
|
211
|
-
|
|
214
|
+
One gate per handler factory, keyed by the internal user id, holds `CACHE_MISS_SLOTS_PER_VIEWER` = 8 fetches at once per viewer — what one view of the package's client starts when it paints: the ids stream's full read, the shown rows' business date and the DetailList's `PREFETCH_MAX_DATES` = 6 prefetched dates — and `CACHE_MISS_SLOTS_PER_WORKER` = 64 per worker process. A freed slot goes to the waiting viewer that holds the fewest slots among the viewers under their own limit (that viewer's oldest waiter;
|
|
215
|
+
the oldest one between viewers holding as many), so a viewer without a slot gets the next freed one even while busy viewers keep every slot of the worker and their queued reads ahead of it (the max-min dispatch of `createConcurrencyGate`, which the thumbnail generation queue shares).
|
|
216
|
+
One viewer's 8 stay below a host driver's default pool of 10 connections, while the worker's 64 do not, so across viewers a miss that holds a slot can still wait in the driver's own first-come queue for a connection: the slots bound the fetches, not the connections. In a simulation of the shipped sizes in front of a first-come pool of 10 (50 ms per miss, every busy viewer keeping 32 misses outstanding), a newcomer's median first read took 84 / 183 / 334 ms beside 2 / 4 / 8 busy viewers, against 84 / 184 / 1,293 ms when the oldest waiter of any viewer under its limit took the slot;
|
|
217
|
+
the rest of that wait is the driver's queue.
|
|
218
|
+
A miss without a free slot waits in that queue, at most `CACHE_MISS_QUEUE_PER_VIEWER` = 256 per viewer (everything two browsers of the viewer can have in flight on the edge's 128 HTTP/2 streams) and `CACHE_MISS_QUEUE_PER_WORKER` = 1,024 per worker, for at most `CACHE_MISS_WAIT_MS` = 60 s, so the package's own client is queued, never refused, unless the database does not answer for a minute.
|
|
212
219
|
A read past the queue or the wait starts no query and is answered 503 `{"error":{"message":"Too many reads"}}` with `Retry-After: 5`; on the ids stream, a resumed `GET`, which waits for its full read before it answers, gets that 503, and a fresh one, which has already answered 200, ends its body with the stream's error line, from which the client resumes on its own backoff.
|
|
213
|
-
A request whose signal is already aborted takes no slot and starts no fetch, an abort while it waits leaves the queue, and a viewer without an internal user is charged nothing
|
|
220
|
+
A request whose signal is already aborted takes no slot and starts no fetch, an abort while it waits leaves the queue, and a viewer without an internal user is charged nothing: its reads run no dataset query, only the lookup of its internal id, which runs before the admission and again inside the read and is not cached while it finds no internal user.
|
|
214
221
|
The refusals are recorded per viewer in windows of 60 s at `warn` on the `[DailyReportReads]` channel: the window's first as `503 <business-date|ids-stream|report> viewer=<internal id> reason=cache_miss_admission`, the rest counted into one `cache_miss_admission_suppressed count=<N> since=<ISO 8601 time of the window's first refusal> viewer=<internal id> endpoints=business-date:<n>,ids-stream:<n>,report:<n>` line written by the window's own timer at its end; the refusal of an aborted request writes nothing.
|
|
215
222
|
The slots and the queues are per worker process, so the effective bounds across workers are the worker count times these.
|
|
216
223
|
- **Security headers**: every response of the API, action and SSE routes, whatever its status — the JSON answers and refusals, the action's answers, the request isolation's refusals, the SSE stream, its terminal frames and its errors, the ids NDJSON — carries `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY` and `Content-Security-Policy: default-src 'none'` (`RESOURCE_RESPONSE_SECURITY_HEADERS` in `src/server/response.ts`, set last by every builder, so no header of its own overrides them).
|
|
@@ -292,13 +299,14 @@ export const loader = (args) => dailyReportServer.attachment.loader(args)
|
|
|
292
299
|
| `[DailyReportRefresh]` | `warn` | the refresh bucket's 429 (**`forceRefresh`** there) |
|
|
293
300
|
| `[DailyReportStreams]` | `warn` | the 503 of **Streams per viewer** there |
|
|
294
301
|
| `[DailyReportReads]` | `warn` | the 503 of **Cache misses per viewer** there |
|
|
295
|
-
| `[DailyReportSearch]` | `warn` | the 429 of the `search`, `reports` and `search-
|
|
302
|
+
| `[DailyReportSearch]` | `warn` | the 429 of the `search`, `reports`, `search-marks` and `search-facets` admissions (**Requests** in [Text search](#text-search)) |
|
|
296
303
|
| `[DailyReportServer]` | `warn` | `createDailyReportServer` without a `redis` provider |
|
|
297
304
|
| `[DailyReportService]` | `info` | the service: SSE publishing (an event over the size bound, a slow or failed publish), a failed read of the stream anchor, the development cache clear |
|
|
298
305
|
| `[DailyReportSseReader]` | `info` | the shared SSE reader's Redis errors |
|
|
299
306
|
| `[DailyReportExternalSource]` | `info` | `transformJsonArray`'s malformed JSON columns of an external source |
|
|
300
307
|
|
|
301
|
-
The client has no logger port: its
|
|
308
|
+
The client has no host logger port: its four channels of the same list write to the console from `info` — `[DailyReportActionProvider]` (the action provider: a failed action, the queue of SSE messages about uncached reports, a cache reload), `[DailyReportDetailLoader]` (the detail loading: a stale update of a report's cache, a failed prefetch of a business date), `[DailyReportIdsStream]` (the ids stream client: a revalidation that gave up, a skipped malformed line or line of an unknown kind, a malformed stream anchor) and `[DailyReportSseConnection]` (the SSE connection hook).
|
|
309
|
+
Every line, the server's and the client's, carries exactly one channel prefix: no message spells a `[DailyReport…]` prefix of its own (`src/shared/logger.call-sites.spec.ts` rejects that form in any string literal outside the logger module). The ids stream client's `warn` option (`DailyReportIdsStreamClientOptions.warn`) is a test seam: a sink injected there takes the channel's place and receives the message and its arguments without a prefix.
|
|
302
310
|
|
|
303
311
|
A 500 is one line: its message (`500 endpoint=<name> elapsed=<ms>ms name=<error name> code=<code or N/A> message=<message>`, `500 action elapsed=<ms>ms name=<error name> message=<message>`) and the error object go in one call, so the error's stack never becomes a line of its own that concurrent requests interleave with.
|
|
304
312
|
A JSON endpoint's answer is traced by the API route alone, as one `debug` line on this logger after the endpoint answered (`<status> endpoint=<name> etag_match=<true|false>`; the default `[DailyReportAPI]` console logger starts at `error`, so without a host logger the line is not written). The JSON builder itself (`jsonResponseWithETag` in `src/server/response.ts`) writes nothing, and no line carries a request header's value or traces an answer given before the endpoint (the 401, 404 and 405), so a client decides neither the number nor the size of the lines.
|
|
@@ -784,7 +792,8 @@ The client places a thumbnail on this flag alone; it has no switch of its own.
|
|
|
784
792
|
A generation's outcome carries this identity only when what it read matched the row's size (**verified**; see **Read port** above); an **unverified** outcome gets no validator and is never cached.
|
|
785
793
|
- **Revalidation**: a verified preview carries `Cache-Control: private, no-cache` and the ETag. `If-None-Match` is compared weakly (`W/` stripped, lists accepted); `*` never matches. A match answers 304 without touching the gate, storage or renderer. Re-mounted thumbnails cost one 304 round trip, and a logout or a visibility change takes effect on the next revalidation.
|
|
786
794
|
An unverified preview is sent with `Cache-Control: no-store` and no `ETag`: the browser neither keeps nor revalidates it, so a later `If-None-Match` can never pin it through 304s, and the next view generates again.
|
|
787
|
-
- **Generation**: a process-local gate with `attachments.thumbnails.concurrency` slots and a
|
|
795
|
+
- **Generation**: a process-local gate with `attachments.thumbnails.concurrency` slots and a wait queue (at most 64 waiting, 8 per user, waiting at most `queueWaitMs`) that hands a freed slot to the waiting user holding the fewest generations (arrival order within a user and between users holding as many), so one user's burst of tiles does not keep another user's first preview waiting behind it. A slot is held while the original is read and rendered. Concurrent requests for the same identity join one generation;
|
|
796
|
+
the joined requests share one read made with the first requester's principal. A request that disconnects withdraws from the shared generation, but its handler still settles with the shared outcome (which no client receives).
|
|
788
797
|
**One decode per content key at a time**: a render that its generation no longer waits for — past its deadline, or after every requester left — keeps its slot until it settles, and while it runs, a new generation of the same identity does not queue for a slot: it first waits for that render to settle and its result to be recorded or dropped, then reads the cache, and only then queues at the gate.
|
|
789
798
|
The wait for the still-running render and the wait for a slot share one `queueWaitMs` budget, and the generation's abort signal ends both; when the budget runs out first, the request is answered 503 `reason=queue` without a second read or decode. A generation that receives its slot also reads the cache again before it reads the original, so a request that waited at the gate behind a still-running render of the same content is answered from that render's result.
|
|
790
799
|
The generation has its own abort signal, which fires only when every joined request has gone, and passes it to the gate wait and to both ports. An abandoned generation leaves the wait queue, and a port that honours the signal settles early and hands the slot to the next queued generation. A read that settles after the abort is discarded before its result is looked at (see **Read port** above) and nothing is rendered; a render that is running at the abort is kept as above.
|
|
@@ -852,7 +861,7 @@ A viewer therefore writes at most two lines per bucket and window, whatever its
|
|
|
852
861
|
|
|
853
862
|
### Text search
|
|
854
863
|
|
|
855
|
-
The screen can search the reports by text. The server side is one optional block of the service configuration, `search`, and the client side is `config.search`; pass the client part only when the server has the block (without it, the `search`, `reports` and `search-
|
|
864
|
+
The screen can search the reports by text and filter the search by author, source type and customer. The server side is one optional block of the service configuration, `search`, and the client side is `config.search`; pass the client part only when the server has the block (without it, the `search`, `reports`, `search-marks` and `search-facets` endpoints answer 404 `Unknown endpoint`).
|
|
856
865
|
|
|
857
866
|
```ts examples/search-wiring.ts
|
|
858
867
|
/**
|
|
@@ -862,11 +871,11 @@ The screen can search the reports by text. The server side is one optional block
|
|
|
862
871
|
* The search is one optional block, `search`: the two index tables (here from `defineDailyReportSearchSchema`; the host's own models of
|
|
863
872
|
* the same shape also work), the readers of the searchable text the detail view does not show (per external source type), and the
|
|
864
873
|
* request limits. With the block, the six save paths rewrite the index inside their transaction (a failed index write fails the save)
|
|
865
|
-
* and the `search`, `reports` and `search-
|
|
874
|
+
* and the `search`, `reports`, `search-marks` and `search-facets` endpoints answer; without it they answer 404. Create the tables and run the initial
|
|
866
875
|
* reconciliation before passing the block in production: while the tables are missing, every save fails.
|
|
867
876
|
* 検索は任意の 1 つのブロック `search` にまとまる。索引の 2 表 (ここでは `defineDailyReportSearchSchema` から。同じ形のホストのモデルでもよい)、
|
|
868
877
|
* 詳細画面に出ない検索できる文字を読む口 (外部ソースの区分ごと)、要求の上限。ブロックを渡すと、保存の 6 つの経路がそのトランザクションの中で索引を書き直し
|
|
869
|
-
* (書けなければ保存も失敗する)、`search`・`reports`・`search-marks` のエンドポイントが答える。渡さなければどれも 404。本番では、表を作って最初の
|
|
878
|
+
* (書けなければ保存も失敗する)、`search`・`reports`・`search-marks`・`search-facets` のエンドポイントが答える。渡さなければどれも 404。本番では、表を作って最初の
|
|
870
879
|
* 突き合わせを流してからブロックを渡す (表が無い間は、どの保存も失敗する)。
|
|
871
880
|
*/
|
|
872
881
|
import {
|
|
@@ -899,7 +908,7 @@ export type SearchWiring = {
|
|
|
899
908
|
* 文字の検索を有効にした日報サーバーを作る処理。
|
|
900
909
|
*
|
|
901
910
|
* @param wiring Base configuration, the hidden-text reader and the limits. 基本設定・隠れた文字を読む口・上限。
|
|
902
|
-
* @returns The server (`DailyReportServer`); its API route answers `search`, `reports` and `search-
|
|
911
|
+
* @returns The server (`DailyReportServer`); its API route answers `search`, `reports`, `search-marks` and `search-facets`. サーバー (`DailyReportServer`。API の経路が `search`・`reports`・`search-marks`・`search-facets` に答える)。
|
|
903
912
|
* @throws {RangeError} When the block is invalid (an unknown key, a missing column, a limit that is not an integer of at least 1, or a reader for a source type `externalSources` does not have). ブロックが不正なとき (知らないキー・列の欠け・1 以上の整数でない上限・`externalSources` に無い区分の読み取りの口)。
|
|
904
913
|
*/
|
|
905
914
|
export const createDailyReportServerWithSearch = ({ base, readNiDetailTexts, limits }: SearchWiring): DailyReportServer =>
|
|
@@ -933,61 +942,124 @@ export const reconcileEveryReport = (server: DailyReportServer, signal: AbortSig
|
|
|
933
942
|
- **What matches.** A query is space-separated keywords; words in double quotes form one keyword that needs every one of its words. The AND / OR toggle combines the keywords (every keyword, or any). A word matches as a substring of the normalized text of one field or of one comment the viewer may read (a word never spans two fields).
|
|
934
943
|
The normalization is fuzzy-search's default (`DEFAULT_NORMALIZE_OPTIONS`) after removing zero-width characters, so full-width and half-width forms, kana and case fold together. Two kinds of substrings do not match: a word that starts with an iteration mark (`々木` does not match `佐々木`) and a word cut inside a grapheme (`デンフ`, half-width kana before its voiced mark, does not match `デンプン`).
|
|
935
944
|
A word typed with a Latin letter also searches its kana readings (romaji, read by fuzzy-search's `queryNeedles` with `convertRomaji` and normalized like a word): Hepburn and IME spellings alike (`shinbun` and `sinbun` both search シンブン, `chokin` and `tyokin` チョキン).
|
|
936
|
-
|
|
937
|
-
A reading is kept only when the whole word is read (`report` adds nothing), it differs from the word and it has at least 3 letter clusters (`SEARCH_READING_MIN_CLUSTERS`: grapheme clusters, not counting digits, symbols and a leading dash, so `ha100` → ハ100, `no12` → ノ12
|
|
938
|
-
A reading of 1 or 2 letter clusters is not added: the index folds hiragana into katakana, so such a reading matches common kana phrasing (on 228,794 reports, `site` → シテ matched the して of 99,636 of them, 43.55 %, in 1,244 ms; `kome` → コメ the コメント of 4,131; `n` → ン 163,571, in 1,302 ms), and digits or a
|
|
945
|
+
Both the word as typed and its normalization must hold a Latin letter: full-width letters count (`DENPUN` searches デンプン), while Roman numerals and `№`, which the normalization turns into Latin letters, are no letters as typed (`Ⅱです`, `№12` and `第Ⅲ期` have no reading), and the two Latin modifier letters the normalization turns into other marks (U+10781 into ː, U+10782 into ˑ) leave no Latin letter after it (`納期ヲ確認` followed by one has no reading).
|
|
946
|
+
A reading is kept only when the whole word is read (`report` adds nothing), it differs from the word and it has at least 3 letter clusters (`SEARCH_READING_MIN_CLUSTERS`: grapheme clusters, not counting digits, symbols and a leading dash, and counting a run of prolonged sound marks once, so `ha100` → ハ100, `no12` → ノ12, `-site` → ーシテ and `ha--` → ハーー are not kept, while `site-` → シテー is), at most 2 per word in fuzzy-search's order (`DAILY_REPORT_SEARCH_READINGS_PER_WORD`: `numadu` searches ヌマズ and ヌマヅ, `konnyaku` コンニャク and コンヤク), and the Latin spelling itself stays a candidate.
|
|
947
|
+
A reading of 1 or 2 letter clusters is not added: the index folds hiragana into katakana, so such a reading matches common kana phrasing (on 228,794 reports, `site` → シテ matched the して of 99,636 of them, 43.55 %, in 1,244 ms; `kome` → コメ the コメント of 4,131; `n` → ン 163,571, in 1,302 ms), and digits, a dash or a run of prolonged sound marks around it do not make it rarer (ハ100 matches 単価は100円, ーシテ matches コピーして, ハーー matches それは――, which the index holds as ソレハーー). Two-character words such as `memo`, `imo` and `kome` are therefore typed in kana.
|
|
939
948
|
A 3-cluster reading of common phrasing can still match many reports (`shitei` → シテイ, the してい of している, matched 60,168, 26.3 %), while ordinary readings match 0.01–1.24 %.
|
|
940
949
|
A reading never reaches kanji (`nouki` searches ノウキ, not 納期; `tanaka` タナカ, not 田中) and folds no long vowel (`kouhi-` searches コウヒー, not コーヒー; `ko-hi-` does). fuzzy-search's homophone spellings work one way: `numazu` searches ヌマズ and not ぬまづ, `otagei` オタゲイ and not をたげい, so type `numadu`, `wotagei` or the kana.
|
|
941
|
-
A word in kana never searches romaji text (デンプン does not find `denpun`), and a word
|
|
950
|
+
A word in kana never searches romaji text (デンプン does not find `denpun`), and a word without a Latin letter, as typed or after normalization, compiles to the plan it had before readings existed.
|
|
951
|
+
Words and keywords are de-duplicated by their whole set of alternatives, readings included, so two words that normalize alike but differ in their readings (`Ⅱです` reads nothing, `iiです` reads イイデス) are both kept: the plan does not depend on the order the words are typed in, and adding a word to an OR query never takes an alternative away.
|
|
942
952
|
- **What is searched.** The fields the detail view shows, resolved the same way (the title, the body, the people masked like the detail, the categories, the customer, the interviewers and the attachment file names), the label names (searched although the views do not draw them), the text the host's readers return for an external source (`externalSourceTexts`: lines the detail view does not show, never the audit actors or raw ids), and the comments.
|
|
943
953
|
A posted comment is searched for a viewer the detail view would show it to (the comment scope allows the report's source type, or the viewer wrote it); an external comment follows the comment scope alone.
|
|
944
|
-
- **
|
|
945
|
-
|
|
954
|
+
- **Filters.** The search can be filtered by author — the author name the list shows (`employeeName`), masked like the detail —, by source type (`Internal` and the `sourceType` of each `externalSources` adapter) and by customer, the customer name the detail shows.
|
|
955
|
+
Each filter takes several values: the values of one filter are ORed, and the filters are ANDed with each other and with the text. An author or customer value passes only the exactly equal value of the index (a report without the value never passes that filter); a source type compares case-insensitively, as the visibility does.
|
|
956
|
+
A blank text — whitespace only, as `String.prototype.trim` reads it (`isBlankSearchText`, the one definition the client and the server share) — with at least one filter is a filters-only search: it lists every report the viewer may read that the filters pass, in the ID list's order, and its answer carries no words, so nothing is highlighted.
|
|
957
|
+
A text that is not blank but leaves no word (zero-width characters only, for example) is refused with the plan's 400, filters or not. Filter values are never highlighted, and a request without filters is the plain text search described above.
|
|
958
|
+
- **Authorization.** The search SQL applies the detail's conditions itself: no deleted report, no other user's draft (by the draft label names), only the viewer's visible source types. The filters' candidates (`search-facets`, under **Requests**) are read under the same conditions.
|
|
959
|
+
- **The index.** Two derived tables per database, `DailyReportSearchDocument` (one row per report: the normalized text, the digest of the raw source, the normalizer version, `row_version` and the two filter values) and `DailyReportSearchComment` (one row per comment, with its author id), their normalized text `nvarchar(max)` in `Latin1_General_100_BIN2` so that `LIKE` compares it as it is, and both deleted with their report (`ON DELETE CASCADE`).
|
|
960
|
+
The filter values are two nullable columns of the document, `author_name` and `customer_name`: `nvarchar(200)` in `Latin1_General_100_BIN2`, so that the filters and their candidates compare values exactly, each with an index of its own (the candidates' `GROUP BY` and the filters' `IN` read them).
|
|
961
|
+
They hold the author name the list shows (`employeeName`) and the customer name the detail shows, both masked like the detail, in their canonical form (`canonicalSearchFilterText`), the text the list shows: the value with `String.prototype.trim` applied (every leading and trailing whitespace, U+3000 and line breaks included), each lone surrogate replaced by U+FFFD (a URL cannot carry one, so the filter could never send it back), and `NULL` when nothing remains.
|
|
962
|
+
Values the list shows alike are therefore one candidate, and every candidate is a value a filter can send unchanged.
|
|
963
|
+
**Host contract**: the author and customer display values (`employeeName` and `customerName` of each `externalSources` adapter's `mapRow`, and the `userTable`'s `displayName` of a report's creator, the author when no `employeeName` is given) must be at most 200 UTF-16 units after trimming.
|
|
964
|
+
A longer one throws `[daily-report] search index: report <id>: the author|customer display value has <n> UTF-16 units after trimming; the host must keep the author and customer display values at most 200 UTF-16 units`, which fails that report's save, and stops the reconciliation at that report's batch (rolled back; the batches before it stay written) on every run until the host shortens the value; the report id in the message names the row to fix.
|
|
965
|
+
The package writes both columns in every save and every reconciliation, so a document table without them fails every save: create them with the table, and add them (with their indexes) to an existing table before deploying a build that writes them. They are nullable, so adding them disturbs no process that does not write them yet.
|
|
946
966
|
The six save paths (create, update, publish, delete, add and delete a comment) rewrite the report's rows inside their own transaction, after taking an update lock on the report row; a failed index write fails the save. Publishing, deleting and the comment paths therefore run in a transaction, which a deadlock retries as a whole.
|
|
947
967
|
- **Reconciliation.** Writes outside the package (a sync, an import) leave the index stale until `service.reconcileSearchIndex({ batchSize, hubIds?, signal?, onProgress? })` runs.
|
|
948
968
|
It walks every report (or the given ones) in batches of 1 to 500, one transaction per batch at the low deadlock priority, takes the same locks as a save, and rewrites only the reports whose digest or normalizer version differ.
|
|
949
969
|
Run it from a command-line process after such writes, for the initial build, and after deploying a build whose normalizer version changed (the version fingerprints the normalization of the whole Basic Multilingual Plane, the Unicode and the ICU versions; the first save or reconciliation of a process computes it, about a second).
|
|
970
|
+
The digest covers the fields, then the comments, then the two filter values in the form the document stores (its `P` section), all as UTF-16 code units (a lone surrogate and U+FFFD differ), so a change of a filter value alone rewrites the document too, while a source value that differs only in leading or trailing whitespace does not. A document written by a build whose digest has no `P` section differs from every digest this build computes:
|
|
971
|
+
after deploying this build onto an existing index (its columns added first, under **The index**), run one reconciliation once every server that writes the database runs this build, which rewrites every document and fills the filter columns. Until then a document that no save has rewritten keeps both filter values `NULL`, so the author and customer filters never pass it and its values are no candidates (the source-type filter reads the report row and passes it).
|
|
972
|
+
A server still on a previous build keeps writing documents without the filter values (a new document gets `NULL`, a rewritten one keeps its old values), so a document it writes after the reconciliation keeps empty or stale filter values until the next reconciliation; run it again after the last such server is gone, and a second run that reports 0 rewrites confirms that every document carries this build's values.
|
|
973
|
+
The author and customer candidates and the author and customer filters read these stored values, not the source rows: the package's saves and the reconciliation refresh them, so a host that changes source rows outside the package (an external source's sync, an import) should run the reconciliation for those reports (`hubIds`), or the candidates and matches keep the old value until then.
|
|
950
974
|
- **Requests.** `GET {apiBasePath}/search?q=<text>&combine=intersection|union` answers column-wise, `{ ids, businessDates, sourceTypes, words }` in the order of the ID list, with an ETag; a missing `q` or `combine` and a query the plan rejects are 400 (the body's `reason` is `empty`, `too-long`, `too-many-sets`, `too-many-words-in-set`, `too-many-words`, `word-too-long` or `blank-word`), and a viewer over `rateLimitPerMinute` or a process at `concurrency` gets 429 with `Retry-After`.
|
|
951
975
|
`words` is the plan's words, keyword by keyword: each word's alternatives `{ text, tier }` (`DailyReportSearchAlternative`), tier 0 the typed word's forms and tier 1 its romaji readings. It depends on `q` and `combine` alone, never on the viewer, and the client reads it strictly with the rest of the answer (at most 16 keywords, 16 words a keyword, 32 words, 6 alternatives a word of 1 to 100 units, the first alternative of tier 0, tiers that never fall and no text twice in a word): a malformed answer is refused whole. It decides which words the highlights mark.
|
|
976
|
+
A filters-only search answers `words: []`, and the client accepts an answer without keywords only for a request whose text is blank (and an answer with keywords only for one whose text is not).
|
|
977
|
+
The filters travel as `author`, `sourceType` and `customer` (`DAILY_REPORT_API_QUERY_PARAMS.searchAuthor`, `searchSourceType` and `searchCustomer`), each parameter repeated once per value and never comma-joined, since a value may hold a comma; the client sends them in canonical order (each filter's values deduplicated and sorted by UTF-16 code units).
|
|
978
|
+
The limits (`DAILY_REPORT_SEARCH_FILTER_LIMITS`) are 16 values per filter, 200 UTF-16 units per value and 300 units for the author and customer values together (a source type is a configured name and does not count). A value is read as sent, never trimmed or dropped, so a request whose filters break a rule is refused whole:
|
|
979
|
+
a blank value, a value not in canonical form (leading or trailing whitespace as `String.prototype.trim` reads it, or a lone surrogate; the form the index stores, under **The index**), a value over 200 units, more than 16 values, the same value twice (source types compared case-insensitively), more than 300 units and a source type that is neither `Internal` nor an `externalSources` adapter's are each a 400 `{ error: { message: "Invalid search filters", reason } }`,
|
|
980
|
+
the `reason` being `filter-blank-value`, `filter-value-not-canonical`, `filter-value-too-long`, `filter-too-many-values`, `filter-duplicate-value`, `filters-too-long` or `filter-unknown-source-type`.
|
|
981
|
+
The 400s come in order, all before any service call: `q` and `combine` (`Invalid search request`), then the filters, then the text (`Invalid search query` with the plan's `reason`). `q` stays required: a blank `q` (`q=` included) with a filter is the filters-only search, and without one it is still the 400 `empty`.
|
|
982
|
+
A request without filter parameters adds no condition to the SQL: its statement and parameters are those of the text search alone.
|
|
952
983
|
`GET {apiBasePath}/reports?ids=<id,id,...>` (distinct canonical ids, at most 100) answers the visible details among them, `{ reports }`. Neither is cached on the server.
|
|
953
984
|
`reports` has an admission of its own from the same factory, never the search's: `REPORTS_CONCURRENCY_PER_SEARCH_CONCURRENCY` (2) × the larger of `concurrency` and `DAILY_REPORT_SEARCH_BATCHES_IN_FLIGHT` (2, the batch reads the client keeps in flight) reads at once per process, of which one viewer holds at most half, so a viewer always passes the client's two concurrent reads (a `concurrency` of 1 admits 4 per process),
|
|
954
985
|
and `REPORTS_RATE_PER_SEARCH_RATE` (10) × `rateLimitPerMinute` reads per viewer and minute, in a bucket apart from the search's, so paging through a large answer neither runs out of reads nor spends the searches.
|
|
955
|
-
Its order is the search's: the ids (400), then a viewer without an internal user answered `{ reports: [] }` with no admission and no query, then the admission with the request's signal (an aborted request takes no slot and returns its token; a full gate is 429 with `Retry-After: 1`, an empty bucket 429 with one window's `Retry-After`), the query, and the slot's release whatever the outcome. Its refusal lines name `route=reports`.
|
|
986
|
+
Its order is the search's: the ids (400), then a viewer without an internal user answered `{ reports: [] }` with no admission and no dataset query, then the admission with the request's signal (an aborted request takes no slot and returns its token; a full gate is 429 with `Retry-After: 1`, an empty bucket 429 with one window's `Retry-After`), the query, and the slot's release whatever the outcome. Its refusal lines name `route=reports`.
|
|
956
987
|
`GET {apiBasePath}/search-marks?q=<text>&combine=intersection|union&ids=<id,id,...>` (the shown answer's query and at most 100 distinct canonical ids, the rows a view draws) answers `{ wordsDigest, reports }` (`DailyReportSearchMarksAnswer`): for each requested report the viewer may read, once, its id, the mask of the keywords it matches (`matchedSets`: bit i for keyword i, for an OR of two or more keywords;
|
|
957
988
|
`null` otherwise) and the marks of every non-empty field value the views draw (`fields`, by key: `subject`, `content`, `author`, `employeeName`, `updatedBy`, `category`, `creationCategory`, `customerName`, `interviewer:<i>:name|affiliation`, `attachment:<i>` and `comment:<id>:author|content`, the comment id as `mergeComments` gives it).
|
|
958
989
|
A field's marks are the fingerprint of the raw value they were computed on (FNV-1a 32 over its UTF-16 units) and flat `[start, end, tier]` triples in UTF-16 offsets of that value; `wordsDigest` is the digest of the answer's `words`.
|
|
959
|
-
|
|
990
|
+
A field's triples are its maximal runs of one tier, overlapping and adjacent occurrences merged into one, so they never overlap and a field carries at most one triple per UTF-16 unit of its value: an answer grows with the length of the values drawn, like the details answer of the same reports, and has no cap (a one-letter word over 100 reports of 21,200 units each answers 1.6–2.9 MB, against 2.2 MB of their details).
|
|
991
|
+
The server merges a field's occurrences per tier in time proportional to their number plus the value's length, whatever length the occurrences cover (`markSearchText` in `src/server/search/search-marks.ts`).
|
|
992
|
+
The server finds the ranges with the index's own processing — the same invisible-character stripping, line splitting and 4,000-unit pieces, mapped back to the raw value through fuzzy-search's position-mapped forms, and on a line longer than 4,000 units, whose pieces may split a grapheme cluster, widened to the raw value's cluster boundaries (`Intl.Segmenter`), so a mark never cuts a character such as 👍🏽 in two — and the keyword mask with the search's own scope conditions in one SQL, so a mark never lands where the search does not match,
|
|
993
|
+
and the browser normalizes nothing (`@aiquants/fuzzy-search` stays out of the client bundle).
|
|
960
994
|
Its admission is a third instance from the same factory, for the same reason as `reports`: `SEARCH_MARKS_CONCURRENCY_PER_SEARCH_CONCURRENCY` (2) × the larger of `concurrency` and `DAILY_REPORT_SEARCH_BATCHES_IN_FLIGHT` reads at once per process (one viewer holds half, never fewer than the client's requests in flight) and `SEARCH_MARKS_RATE_PER_SEARCH_RATE` (10) × `rateLimitPerMinute` per viewer and minute, in a bucket of its own; its refusal lines name `route=search-marks`.
|
|
961
|
-
|
|
962
|
-
|
|
995
|
+
Unlike the search and `reports`, which refuse at once, a search-marks request that finds the gate full or its viewer at its share waits for a slot (`SEARCH_ADMISSION_WAITS`): `DAILY_REPORT_SEARCH_BATCHES_IN_FLIGHT` waiters per viewer and as many as the admission's concurrency in all, for at most `SEARCH_MARKS_WAIT_MS` = 2,000 ms (about twice the slowest search-marks request measured under load, 196–927 ms), and a freed slot goes to the viewer holding the fewest.
|
|
996
|
+
A new answer's first batches therefore wait for the abandoned batches of the previous answer, which hold their slots until their reads end, instead of spending their one retry; a request past the queue or the wait bound gets 429 with `Retry-After: 1`.
|
|
997
|
+
Its order is the search's and the details': 404 without the `search` block, 400 for a missing `q` or a bad `combine`, 400 with the plan's `reason`, 400 for the ids, then a viewer without an internal user answered `{ wordsDigest, reports: [] }` with no admission and no dataset query, then the admission with the request's signal, the marks (one report at a time, yielding to the event loop between reports), the slot's release whatever the outcome, and the JSON. It is not cached on the server either.
|
|
998
|
+
The service takes the request's signal too (`markDailyReportSearchHits(scope, userId, plan, ids, { signal })`) and checks it before each read and before each report's marks, stopping there once the client has left; drizzle cannot cancel a running query, so the slot is held until the read in progress ends. A request whose client left — while it waited, at one of the service's checks or after the service answered — is answered 503 without a body, with `Retry-After` and `Cache-Control: no-store`, and writes no error line;
|
|
999
|
+
the refusal of an aborted request is never recorded as busy, on any of the four search routes (`search-facets` included).
|
|
1000
|
+
Every answer the route builds — the marks, its 400, 404 and 429, and that 503 — is JSON without an ETag and with `Cache-Control: no-store` (`noStoreJsonResponse` in `src/server/response.ts`, serialized once, with the shared security headers): the batches are cut from the rows on screen, so a URL rarely repeats, and the package's client sends no `If-None-Match`; an `If-None-Match` is not read.
|
|
1001
|
+
`GET {apiBasePath}/search-facets` (it reads no parameter) answers the filters' candidates for the viewer, `{ sourceTypes, authors, customers }` (`DailyReportSearchFacets`).
|
|
1002
|
+
`sourceTypes` lists the source types a filter may name (`service.searchFilterSourceTypes`: `Internal`, then each `externalSources` adapter's, in the configured order, distinct case-insensitively: a later one that differs from an earlier one only in case is dropped, since the filter compares source types case-insensitively and would refuse both as a duplicate) that the viewer's scope allows, without a query.
|
|
1003
|
+
`authors` and `customers` are the distinct non-`NULL` filter values of the documents of the reports the viewer may open, read with the search's own scope conditions (another user's draft, a deleted report and a hidden source type lend no value): one `GROUP BY` query per column, the two run one after the other, in the column's order (BIN2: UTF-16 code units) and with no row limit.
|
|
1004
|
+
Its order is the search's: 404 without the `search` block, then a viewer without an internal user answered `{ sourceTypes: [], authors: [], customers: [] }` with no admission and no dataset query, then the admission with the request's signal, the candidates (none, and no query, for a scope that denies every source type), the slot's release whatever the outcome, and the ETag'd JSON (304 for a matching `If-None-Match`).
|
|
1005
|
+
It is not cached on the server, since a cache would keep offering the value of a report that has since become a draft or been deleted.
|
|
1006
|
+
Its admission is a fourth instance from the same factory, with the search's own `concurrency` and `rateLimitPerMinute` and a bucket of its own, so the candidates still load while the viewer's searches hold their slots (a viewer holds at most half of a gate) or after they spent their bucket; its refusal lines name `route=search-facets`.
|
|
1007
|
+
- **The search box** (`config.search: {}` searches on Enter only; `{ debounceMs }`, an integer from 300 to 5000, also after typing stops; `{ renderFilterSelect }` adds the filters, **Filter row** below) is a second row in the toolbar's band, under the view-mode toolbar: the AND / OR toggle, the field, the filter toggle (with `renderFilterSelect` only) and a status line. Enter, Esc and the debounce wait while an IME composes.
|
|
963
1008
|
The row is one 32 px lattice block whose 24 px controls stand at its bottom (`SEARCH_ROW_CLASS_NAME`), so they sit G = 8 under the toolbar's bar and their bottom edge is G above the view's first surface; the band carries the toolbar's start inset G and the active view's end inset, so the field ends where the cards end in both views, and it stacks above the view, so a control's focus outline paints over the view's G strip (**Origin on the lattice** in [Selection and focus appearance](#selection-and-focus-appearance)).
|
|
964
|
-
The toggle names the current mode (`AND` / `OR`) and keeps focus when pressed, so a second Space flips the mode again and the new name is read where focus stays. The clear button sits inside the field's box at its end, in room the field always keeps free, so typing never changes the field's width; it shows while the field holds text or
|
|
1009
|
+
The toggle names the current mode (`AND` / `OR`) and keeps focus when pressed, so a second Space flips the mode again and the new name is read where focus stays. The clear button sits inside the field's box at its end, in room the field always keeps free, so typing never changes the field's width; it shows while there is text to clear (the field holds text, or the committed search's text is not blank) and returns focus to the field.
|
|
1010
|
+
The clear button and Esc clear the text: without a selected filter they release the search, and with filters the search goes on as a filters-only search (with an empty field it then shows no clear button, and Esc does nothing).
|
|
965
1011
|
The status line (searching, the hit count, no hits, a failure) wraps instead of truncating, at most half the row wide, and takes no room while it is empty. The field's and the OR face's boundary and placeholder come from the text-field token (**Text fields** in [Selection and focus appearance](#selection-and-focus-appearance)).
|
|
966
1012
|
The field's description (`labels.searchSyntaxHelp`, also its tooltip) gives the syntax, says that a word in Latin letters also finds its kana reading and that a match found through a reading is underlined with dots.
|
|
967
1013
|
While a search request runs or after it failed, the views keep the list they showed; an answer replaces the list in its order (a report the provider marked removed is left out), and the header shows the hit count. While an answer shows, the rows read their details by id in batches (`reports?ids=`) instead of by business date.
|
|
968
1014
|
A new search scrolls the views to the top, releasing the search returns them to the report that was at the top before it, and the selection stays on a report the answer keeps, else moves to the nearest remaining one (on release, back to the selection before the search). A creation during a search releases it first and selects the new report.
|
|
969
1015
|
Saves, SSE report and comment events and changes of the settled ID list re-query the search quietly after about a second (`If-None-Match`).
|
|
1016
|
+
- **Filter row** (`config.search.renderFilterSelect`, a function the host provides, `DailyReportSearchFilterSelectRenderer`; a value that is not a function is a `RangeError` at render, `[daily-report] config.search.renderFilterSelect must be a function (it renders the control of each filter cell); got <value>`). Without it there is no filter toggle, no filter row and no `search-facets` request.
|
|
1017
|
+
The filter toggle (`data-testid="daily-report-search-filters-toggle"`) is a 24 px button G after the field, sharing its top and bottom edges, named `labels.searchFiltersToggle(n)` with the number of selected values (`Filters`, `Filters (3)`), with `aria-expanded` and, while the row shows, `aria-controls`.
|
|
1018
|
+
It opens the filter row under the search row: a `<fieldset>` the toggle names (`data-testid="daily-report-search-filters"`) with one cell per filter in the order of `DAILY_REPORT_SEARCH_FILTER_FACETS` — author (`labels.author`), source type (`labels.searchFilterSourceType`) and customer (`labels.customer`); `data-testid` `daily-report-search-filter-author`, `-source-type` and `-customer` —,
|
|
1019
|
+
one cell per line below `sm` and the three in one line at widths 2 : 1 : 3 from `sm`, and a status line (`daily-report-search-filters-status`, `aria-live="polite"`) that shows `labels.searchFiltersLoading` while the candidates load the first time, `labels.searchFiltersFailed(failure)` when loading them failed (also after a failed reload, under the previous candidates) and `labels.searchFiltersTooMany` after a refused selection, for as long as the row shows the selection that was refused (a release from outside, such as a creation during a search, or any new selection clears it).
|
|
1020
|
+
Each cell shows its label as a visible caption above the host's control (`data-testid` `daily-report-search-filter-<facet>-caption`, a 16 px line that wraps rather than being cut, 4 px above the control) and is a `<fieldset>` whose `<legend>` is that caption, so the caption names the cell; the control receives the same label as its accessible name.
|
|
1021
|
+
The open row adds to the toolbar's band G, the cells (caption, 4 px and the host's control; below `sm` one per line, G apart) and, while it has something to say, the status line (G + 24 px or more), and the band rounds its whole height up to the block quantum as it does closed: the host's controls decide the open band's height, and the closed band is unchanged.
|
|
1022
|
+
- **Candidates**: every opening loads `search-facets` again (one request at a time: an opening while one is in flight sends none; stopped on unmount and when `apiBasePath` changes), so authors and customers that appear while the screen stays mounted become selectable and values that left the viewer's reports drop out. The previous candidates stay while the new ones load, and a reload that answers the same values in the same order keeps the very same option arrays.
|
|
1023
|
+
A failed first load shows only the status line and is retried at the next opening; a failed reload keeps the previous candidates and shows the failure in the status line. 401 and 403 call `onSessionExpired`; 404 shows `unavailable`, 429 `busy`, and anything else, a malformed answer included, `failed`.
|
|
1024
|
+
The cells render once the candidates are loaded. The author and customer options are sorted with `Intl.Collator(config.locale)` (values the collation ties are ordered by UTF-16 code units) and labelled with their value; the source-type options keep the server's order and are labelled with their badge label from `sourceTypeConfigs` (the source type itself without one).
|
|
1025
|
+
- **The host's control** receives `DailyReportSearchFilterSelectProps` — `facet`, `label`, `options` (`DailyReportSearchFilterOption`, `{ value, label }`: one array per answer, rebuilt only when the answer, `locale` or `sourceTypeConfigs` change, for controls that validate once per array), `value` (the selected values), `onChange(values)` and `locale` — and is controlled: it shows `value` and reports a new selection through `onChange`.
|
|
1026
|
+
A selected value that the latest candidates no longer hold (a reload dropped it) is appended to that cell's `options`, labelled as the candidates label it, so the control can still show and release it; the appended array stays the same while the selection and the answer do.
|
|
1027
|
+
The reported values must be distinct values of `options`: anything else is a host-control bug and throws a `RangeError` (`[daily-report] config.search.renderFilterSelect (<facet>) onChange values must be distinct values of the options the control received; got …`). A selection over the limits is refused, so the control keeps showing the previous `value`, and the status line says why.
|
|
1028
|
+
Each cell renders its control inside an error boundary of its own: a control that throws while it renders turns only its cell into `labels.searchFilterUnavailable` until the row closes, React still reports the error, and the other cells and the search box keep working.
|
|
1029
|
+
Inside that boundary each cell also has a Suspense boundary of its own: a control that suspends (`React.lazy`, `use()`) shows `labels.searchFiltersLoading` in its cell until it is ready, and never reaches a Suspense boundary of the host, so the rest of the screen stays shown.
|
|
1030
|
+
The package's browser code imports no control library, so the control carries its own browser floor (**Browser floor** under [Install](#install)).
|
|
1031
|
+
- **Commits**: a change of the filters commits again with the field's current text and AND / OR mode, cancelling a pending debounce: a text that is not blank searches with the filters, a blank text with filters is a filters-only search, and with neither the search is released. Enter, the debounce and the AND / OR toggle carry the current filters too.
|
|
1032
|
+
The filters belong to the committed search: a change of their canonical form is a new search (the views scroll to the top), the same values in another order are not, the quiet re-query sends them again, and every release clears them — a creation during a search included.
|
|
970
1033
|
- **Highlights.** While an answer shows, the List cards, the side pane and the DetailList rows mark what the server matched with `<mark data-daily-report-search-mark>`: `word` where a typed word's form matched, the normalization included (`デンプン` marks でんぷん), and `reading` where only a romaji reading matched.
|
|
971
1034
|
- **Where**: a List card marks its preview lines and its creator; the side pane its author, subject, customer, body, attachment file names and comments (author and body, each on its own); a DetailList row its subject, customer and body, the header's author and categories, the metadata's author, updater and categories, the interviewers' names and affiliations, its attachment file names and its comments.
|
|
972
1035
|
A file name is marked in both of its forms: in the stem and extension spans of the CSS layout, each with its own part of the name, and in the fitted text, where the synthetic `…` and the elided middle carry no mark (**File name** in [Attachment display](#attachment-display)).
|
|
973
1036
|
- **Which words**: every word of an AND answer and of a one-keyword answer; for an OR of two or more keywords, only the words of the keywords the report matched (`matchedSets`; a word that several keywords share is marked when any of them matched), so a quoted keyword whose words appear only in part marks nothing.
|
|
974
|
-
- **Never marked**: label names (searched, never drawn: a report matched only through its labels shows no mark, and the indicator below shows `Hidden match`); the external
|
|
975
|
-
the List preview's lines after the third
|
|
1037
|
+
- **Never marked**: label names (searched, never drawn: a report matched only through its labels shows no mark, and the indicator below shows `Hidden match`); the texts a host's readers supply for an external source (`externalSourceTexts`), which the views do not display: a report matched only through them shows no mark either, and the indicator shows `Hidden match`; the filter values (a report is marked for its words alone); the markup the List and the side pane strip (a word inside a tag), while the DetailList draws its tags as text and marks them;
|
|
1038
|
+
the List preview's lines after the third (not drawn) and the elided middle of a fitted file name, whose synthetic `…` stands outside every mark; the edit form and the viewer's own draft open in it; placeholders (`-`), dates, visit times, badges, the visually hidden heading and the unknown-author substitute; and a comment still being posted.
|
|
1039
|
+
Text that the browser hides keeps its marks: a List preview line past its one-line clamp, the creator name of a List card and a file-name part of the CSS layout past their ellipsis. In Chromium a clamp or ellipsis that cuts a mark, or falls at its start, is drawn inside the mark, so the mark's fill continues under the `…` while its underline ends at the last character shown (a cut at a mark's start shows a filled `…` alone).
|
|
976
1040
|
An `interviewerWithAffiliation` template that does not insert the name and the affiliation verbatim (it rewrites them, uses one twice, or words itself by their values) draws the pill plain. `LIKE` counts an occurrence inside a grapheme cluster, so such a match marks the whole cluster.
|
|
977
1041
|
- **Look**: one token (`SEARCH_MARK_CLASS_NAME`): yellow-200 under slate-900 (dark yellow-800 under yellow-50; 15.33:1 and 6.61:1 on any surface, since the mark sets its own text colour), a solid underline for a word and a dotted one for a reading (a cue that does not depend on colour), and the system `Mark` / `MarkText` in forced colours.
|
|
978
1042
|
It writes `margin`, `border` and `padding` 0 and inherits the font, so a host's `mark` rule cannot change it and a mark changes no width, no line box and no row height. Accessible names and descriptions flatten the marks, and find-in-page and copying read the same text as without them.
|
|
979
|
-
|
|
980
|
-
|
|
1043
|
+
A reading mark also carries an accessible description (`aria-description`: `labels.searchReadingMarkDescription`, en `Matched by its romaji reading`, ja `ローマ字の読みで一致`), so the weaker match does not rest on the underline's style alone; a word mark carries none, so the common case adds no speech. The attribute's value and the description come from one table keyed by the tier (`SEARCH_MARK_LOOKS` in `src/client/search/search-marked-text.tsx`); a tier outside it stops the render with an error naming the package.
|
|
1044
|
+
- **Arrival**: the screen holds one marks store per shown answer (`search-marks-store.ts`, inside the client bundle), only while a view draws an answer that has words: without `config.search`, before the first answer is drawn, for a filters-only answer (it has no words, so nothing is marked and no match indicator shows), after the release and for a view rendered on its own there is none, and the DOM is the plain one.
|
|
1045
|
+
A row wraps its subtree in the marks scope (`DailyReportSearchMarksScope`), which subscribes to the report's marks while the row is mounted and hands them to its leaves. The row body reads no marks, so an arrival re-renders only the scope, the marked leaves (`SearchMarkedText`) and the match indicator — never the row's body, header, attachment list or comment section, and never another row; a newly mounted row draws its text plain first and its marks in a deferred render.
|
|
1046
|
+
The requests are decided at send time (`createIdBatchScheduler` in `src/client/search/id-batch-scheduler.ts`): a subscribed report waits in one queue, and each free request slot — two in flight at most — takes the newest waiting reports that a row still subscribes to, at most 100 a request, never cached. A report whose rows left while it waited is dropped, so a scroll or a held key sends nothing for the rows it passed, and the window it stops on goes out in the first free slot.
|
|
1047
|
+
A request whose rows have all left is aborted (the server then stops computing its marks; **Requests** above), and a row that leaves before its marks were sent, or whose request was aborted, asks again when it returns.
|
|
1048
|
+
The store keeps one record per report (its marks, the send order that brought them, the epoch it was asked in, its 429 retry and its revalidations) and forgets the oldest records of reports no row subscribes to beyond `SEARCH_MARKS_RECORD_BOUND` = 400 (2 × the two requests in flight × 100: the answers still in flight and as many rows just left), so its memory follows the drawn window, not the distance scrolled; a forgotten report asks again when a row subscribes to it.
|
|
981
1049
|
Answers apply in send order: a late answer to an earlier request (of a stale epoch, or sent before an edit and overtaken by the edit's revalidation) never overwrites the marks a later request brought.
|
|
982
1050
|
When a new search replaces the shown answer, a row that stays mounted draws its text plain until the new answer's marks arrive, never the previous answer's marks or matched-keyword count.
|
|
983
1051
|
A field is painted only when its marks were computed on the value drawn (the fingerprint): after a save or an SSE update the old ranges are not painted and the field asks once for fresh marks. An answer whose `wordsDigest` differs from the shown answer's words is dropped; a quiet re-query of the same search makes the marks stale and the mounted rows ask again;
|
|
984
|
-
401 and 403 call `onSessionExpired
|
|
985
|
-
|
|
986
|
-
|
|
1052
|
+
401 and 403 call `onSessionExpired`; a 429 is asked again once, after its `Retry-After`, for the reports a row still subscribes to then (one whose rows left meanwhile asks again when a row returns); any other failure leaves those rows plain for as long as the shown answer stays the same.
|
|
1053
|
+
Disposing the store — a new search, or React hiding the screen inside an `<Activity>` — aborts its requests, cancels its retry timers and gives every report they held back, so a store React mounts again asks for them again.
|
|
1054
|
+
- **Match indicator.** There is no score: an answer can tell only how many of its keywords a report matched, and only an OR of two or more keywords varies it. While the marks store exists, the side pane's author line (after the source badge) and the end of a DetailList row's header hold a slot in the contact map's score colours (`SEARCH_INDICATOR_CLASS_NAME`: the page's text face at the package's 12 px floor with tabular figures, on a 16 px line;
|
|
1055
|
+
slate-600 / dark slate-300, 7.56:1 on white and 12.02:1 on slate-900), as wide as the longest text the answer can show (an invisible placeholder), so nothing reflows when the marks arrive.
|
|
1056
|
+
The side pane's author line wraps with a 4 px gap, the label kept whole on one line (`SIDE_PANE_AUTHOR_LINE_CLASS_NAME`), so in a pane too narrow for the author and the slot the slot drops to a 16 px line of its own instead of crushing the author. List cards show none.
|
|
987
1057
|
- `labels.searchMatchedKeywords(k, n)` (en `Match 2/3`, ja `一致 2/3`; `data-daily-report-search-matched-keywords="k/n"`) shows for an OR of two or more keywords once the report's marks arrive, k being the set bits of `matchedSets`.
|
|
988
|
-
- `labels.searchMatchOutsideView` (en `Hidden match`, ja `表示外で一致`; `data-daily-report-search-outside-view`) shows when the report matched (k ≥ 1, or no mask), the view is not editing it,
|
|
989
|
-
|
|
990
|
-
|
|
1058
|
+
- `labels.searchMatchOutsideView` (en `Hidden match`, ja `表示外で一致`; `data-daily-report-search-outside-view`) shows when the report matched (k ≥ 1, or no mask), the view is not editing it, and the view's leaves paint none of its marks: a report matched only through its labels, its external texts or fields the view does not draw says why it is listed.
|
|
1059
|
+
What the view draws is read from the leaves themselves: inside the row's marks scope every leaf (`SearchMarkedText`) registers, while it is drawn, whether its field's marks are current for the value drawn and how many marks it paints, in the row's tally (`drawn-search-marks-tally.ts`), and the token shows only when every registration is current for the marks the scope settled and their sum is 0.
|
|
1060
|
+
So a field the view does not draw at that moment never counts: the side pane on its Related tab (`tabRelations`; the article is not mounted then), a comment the viewer is deleting, an interviewer pill a host template draws plain and the elided middle of a fitted file name all show the token when the report matched only there. A file-name part that a CSS ellipsis hides counts as drawn, because its marks are painted and the browser decides what it hides.
|
|
1061
|
+
- Both tokens are `aria-hidden` with a `title`, and one visually hidden element holds their sentences (`labels.searchMatchedKeywordsDescription(k, n)`: en `Matches 2 of 3 keywords`; `labels.searchMatchOutsideViewDescription`: en `Matched in fields this view does not show, such as labels or details`).
|
|
1062
|
+
A DetailList row's description (`aria-describedby`) ends with that element while the screen holds a marks store, so moving through the rows with the arrow keys reads the sentences each row shows.
|
|
991
1063
|
|
|
992
1064
|
## Client wiring
|
|
993
1065
|
|
|
@@ -1061,7 +1133,8 @@ Every context behind an exported provider defaults to `null` and is read only th
|
|
|
1061
1133
|
The badge's own retrying and failed contents are live regions only where the page does not announce them: `DailyReportIdsStreamStatus` placed alone keeps its `role="status"`.
|
|
1062
1134
|
|
|
1063
1135
|
The provider takes no items: it derives its rows from the module-resident ids stream session (below), so establish the session first — `createDailyReportClientLoader` in the route, or `ensureDailyReportIdsStreamSession({ apiBasePath, userKey })` on mount, as `DailyReportPage` does — and mount one provider per user (`key={userId}`), which also starts the new user with an empty editing store (**Editing** in [Keyboard, focus and selection](#keyboard-focus-and-selection-list--detaillist)).
|
|
1064
|
-
The provider builds its actions once — the functions, the ledgers of the optimistic state and the viewing user, in one value whose identity changes only with the viewing user — and hands out its state apart: the rows (`items
|
|
1136
|
+
The provider builds its actions once — the functions, the ledgers of the optimistic state and the viewing user, in one value whose identity changes only with the viewing user — and hands out its state apart: the rows (`items`, a persistent list read by its length, the report at a position (`at`) and iteration, not an array: **Rows follow the stream by its changes** below), the version that announces a change of the ledgers, and the SSE state (`isSseEnabled`, `sseStatus`).
|
|
1137
|
+
Each function calls the implementation of the last committed render, so a function kept from an earlier render does what the current one does.
|
|
1065
1138
|
The ledgers are read-only views (`DailyReportLedgerView`: a frozen `{ current }` of a read-only map) whose contents only the provider's one writer changes, and the writer advances `version` in the same call, so no ledger changes without a re-render of what reads it through `version`: a comment whose post fails disappears from the list and a comment whose deletion fails comes back, while the notice reports the failure.
|
|
1066
1139
|
The package's rows, List cards, DetailList rows and report cards, the side pane's content and the edit form read only the actions, so an ids stream publish, an SSE message or a change of the connection status re-renders none of them; of a report's parts, a version bump re-renders only its comment section (`ReportComments`). `useDailyReportActionContext()` returns the actions and the state as one value, the same value on a render where the state did not change; its caller re-renders whenever the state changes.
|
|
1067
1140
|
|
|
@@ -1080,8 +1153,11 @@ If your app overrides `config.apiBasePath`, pass the same value to `createDailyR
|
|
|
1080
1153
|
The list itself is not part of a publish, so a publish costs the size of its change, never the list's length; a reader that needs the whole list builds it when it reads (`DailyReportIdsStreamClient.readItems()`: the list as of the last publish, with the changes still waiting in the coalescing window taken back).
|
|
1081
1154
|
`DailyReportActionProvider` keeps its rows in the canonical order (business date descending, then id descending, reports without a date last) and applies only the changes since the revision it last derived, folded into one however many publishes a render receives.
|
|
1082
1155
|
A change is located by reading a few rows, never by walking the list: rows that arrive after the old last row are placed by one comparison with it (a publish of Δ such rows reads at most Δ + 1 rows, the stream client included), and a row that arrives out of order, a changed row, a removed row and a held row that a settled stream lets go are each found by binary search (⌈log2 n⌉ reads); the user's own holds, releases and removals read the held rows' table and one binary search the same way.
|
|
1083
|
-
The
|
|
1084
|
-
|
|
1156
|
+
The rows are a persistent list (`ReportRowList` in `src/client/contexts/report-row-list.ts`): immutable chunks of `REPORT_ROW_CHUNK_SIZE` = 1,024 rows behind one spine, every chunk full but the last, and a derivation's edits make a new version in one pass that shares every chunk they leave alone (`editReportRowList`), once per publish however often React runs the render again.
|
|
1157
|
+
A publish that appends rows — what the stream mostly delivers — writes the spine (⌈n / 1,024⌉ references), the last chunk's rows and the new rows, never the n references of the list (4,128 references for 4,000 rows appended to 80,000, where a copy of the list wrote 84,000); edits that only replace rows copy the spine and those rows' chunks; a removal or an insertion inside the list re-packs the rows from the first chunk it changes, once.
|
|
1158
|
+
The rows are written by position with no `concat`, `filter`, `flat`, slice or spread of the list: `Array.prototype.concat` is fast only while the page-wide `Symbol.isConcatSpreadable` protector holds, which one assignment by any library breaks for the page's lifetime (apache-arrow assigns it on its prototypes, for one), and the slow path then visits every row.
|
|
1159
|
+
The views, the keyboard, the action state (`items`) and the search's list derivation read rows only by their length, the report at a position (`at`, two array reads) and iteration (`ReportRows` in `src/client/utils/report-index.ts`), so a plain array (a search answer) and the provider's list read alike; a search for one id walks the chunks by position (`findReportIndex`), without a call per row.
|
|
1160
|
+
While an answer shows, the search's list derivation (`deriveSearchListItems` in `src/client/search/search-list-items.ts`) examines only the chunks a publish wrote (for an append, the last chunk and the new rows' chunks), since the chunks a version shares were examined before, and returns the previous list itself when the publish changed none of the answer's reports, so such a publish re-renders neither the views nor their rows.
|
|
1085
1161
|
React runs a render-phase update again whenever it restarts the render — a transition that carries a publish is thrown away and restarted from the root by every key a held key repeats —, so one base and one set of inputs (the source's revision and change chain, the settledness and the deletion marks) are derived once and reused, the same rows object, by every rerun (`deriveReportRowsOnce`).
|
|
1086
1162
|
The deletion marks are a read-only set that the provider replaces on every change, so that set's identity is an exact part of the key. Without it, one publish under a held key was derived and copied once per restart (21 to 25 copies in a concurrent root, about 320 KB each at 80,000 rows).
|
|
1087
1163
|
It reads the whole list (once) and derives from it only on its first render, after the session is replaced, when it falls more than 32 publishes behind, and after the development cache clear drops the tombstones (which brings back the reports they hid).
|
|
@@ -1152,8 +1228,9 @@ The keys are delegated to each view's **list**: the element that holds the view'
|
|
|
1152
1228
|
- **One Tab order rule for both views: only the Tab-stop row's controls are in the Tab order.** The Tab-stop row is the row that owns focus (below) while it is inside the visible range, otherwise the selected row while it is visible, otherwise the first visible row (until the view has reported its visible range: the focus owner, then the selection).
|
|
1153
1229
|
Every focusable control the package draws inside a row reads the rule: the List card's primary button, ★ and 既読, the DetailList row's ★, 既読, 編集 (on the viewer's own reports) and 削除, the comment controls (delete and its confirmation, the comment field and its send button), the edit form's fields and buttons, and the attachment links keep their natural order in the Tab-stop row and have `tabIndex=-1` in every other row. The stop row's own element is a stop too where it takes focus: a DetailList row (`tabIndex=0`) and a List row frame whose card is not shown.
|
|
1154
1230
|
So the Tab path through a view is one row's controls long, however many rows are rendered: crossing the List takes exactly 3 presses (the primary button, ★ and 既読). Because the focus owner is the stop, a control reached by pointer continues within its own row, and a row in edit mode keeps its form in the Tab order.
|
|
1155
|
-
The view-mode tab panel that holds a view is no Tab stop: a panel is a stop
|
|
1156
|
-
The
|
|
1231
|
+
The view-mode tab panel that holds a view is no Tab stop: a panel is a stop when its first meaningful content is not focusable, or when it is a scroll container that Tab must reach so the keys can scroll it (WAI-ARIA APG Tabs; `TabsContent` takes a required `tabStop`), and a view's content begins with its own stop, the cursor row.
|
|
1232
|
+
The panel carries no `tabindex` at all (not −1, which would let a pointer press on blank view space focus the panel, an ancestor of the list where the view's keys never arrive), so one Tab from the last control before the view reaches the cursor row, and `Ctrl+Home` from a row returns to that control.
|
|
1233
|
+
The side pane's article and relations panels are stops of their own: each is the pane's scroll box, which Tab must reach so the keys scroll it, although the article holds buttons, the comment field and attachment links further down.
|
|
1157
1234
|
The scroll bar is pointer-only in both views, like a native scroll bar, because the keys above already scroll the list: both views pass `enableArrowButtonTabStops: false`, `@aiquants/virtualscroll`'s one signal that the host scrolls by keyboard itself (3.10.0, its README "Scrollbar accessibility and the Tab order").
|
|
1158
1235
|
The bar carries `aria-hidden="true"`, so assistive technology sees no `scrollbar` and no `slider` in either view, named or not; its arrow buttons have `tabIndex=-1` and the bar and its thumb no `tabindex`, so the bar adds no Tab stop; and a press anywhere on the bar has its default prevented, so it moves no focus, neither onto a part of the bar nor away from the row that holds it. Every pointer interaction still scrolls: the arrows on a press and while held, the track, the thumb and the tap-scroll circle.
|
|
1159
1236
|
The bar and its thumb still carry their names (the engine keys `verticalScrollBar` and `verticalScrollThumb`, below), which the hidden bar does not expose.
|
|
@@ -1239,7 +1316,7 @@ The keys are delegated to each view's **list**: the element that holds the view'
|
|
|
1239
1316
|
The position text reads the row's place from the same context as the row frame, so an insert or a delete before the rendered window re-renders the frames and these texts, never a card's body.
|
|
1240
1317
|
The total the position names is the size of the set the rows belong to — every row frame's `aria-setsize` and the header's total count read the same value, which `DailyReportResolvedContent` decides once for both views (`resolveRowSetSize`):
|
|
1241
1318
|
while the ids stream delivers, the total the server declared on the stream's first line (never fewer than the rows shown, and unchanged as chunks arrive); before the server has declared it, unknown — `aria-setsize="-1"`, the header shows `labels.totalCountLoading`, and the position reads `labels.listRowPositionInUnknownTotal` (en "Item 3", ja 「3 件目」); once the scan has completed, the rows shown.
|
|
1242
|
-
A host that renders `DailyReportList` or `DailyReportDetailList` itself passes that size as `rowSetSize` (−1 while unknown), or omits it when its rows are the whole set; the views' `VirtualScroll` always counts the rows given. A DetailList row is named by its report heading (date, author and subject; `aria-labelledby`) and described by the row state and the markers its header paints.
|
|
1319
|
+
A host that renders `DailyReportList` or `DailyReportDetailList` itself passes that size as `rowSetSize` (−1 while unknown), or omits it when its rows are the whole set; the views' `VirtualScroll` always counts the rows given. A DetailList row is named by its report heading (date, author and subject; `aria-labelledby`) and described by the row state and the markers its header paints, and, while the screen holds a search marks store, by the match indicator's hidden sentences last (**Match indicator** in [Text search](#text-search)).
|
|
1243
1320
|
The markers are the attachment marker (`<labels.attachments>: <count>`) and the source badge (its full `name` when the configuration gives one, otherwise its label); a description references only the markers that are painted. A row without content (loading, failed, missing, editing) is named by its ISO business date, with `aria-busy="true"` while loading. At most one element per view carries `aria-current`. ★, 既読, 編集 and 削除 carry `aria-label` equal to their `title` and no `aria-pressed`.
|
|
1244
1321
|
- **Document structure**: every report has a heading at `config.headingLevel` (an integer from 2 to 5, default 3; pick the level that continues the host page's outline). A DetailList card starts with a visually hidden heading `<date> <author> <subject>`, which names the row.
|
|
1245
1322
|
The side pane and the mobile overlay start with two label / value pairs: `labels.businessDate`, whose value is the report heading (the business date alone), and `labels.author` with the author's name; each label and each value is read once, and the source badge sits beside the pair, outside it. The overlay names its dialog by the heading and the author's value together (`<date> <author>`, the name of the List card).
|
|
@@ -1389,7 +1466,7 @@ Whichever size rule wins the cascade, a selected surface therefore paints nothin
|
|
|
1389
1466
|
The tile around the frame is on the lattice anyway: its last line carries the pad δ(t) = ⌈h / q⌉ · q − h (0 to q − 1 px, q the block quantum of **Row slots on the lattice**) as a bottom margin, so every tile is ⌈h / q⌉ · q + 48 px tall, every tile row top and every grid's height are multiples of q, and what follows a grid stays on whole device pixels (**Tile** in [Attachment display](#attachment-display)).
|
|
1390
1467
|
A bordered box counts its border in its inset, so that its content edge sits on the grid measured from the visible edge: border + padding is on the ladder — 16 for the card surfaces, the error panel, the sides of the error notice and the mobile overlay's close button (1 + 15), 8 for the form fields, the comment input and the top and bottom of the error notice (1 + 7), 4 for the bordered tab bar, the development box and the top and bottom of the text input (1 + 3) — or equals a 12 px corner.
|
|
1391
1468
|
**The 12 px tier**: every panel with a 12 px corner has one inset, 12, equal to its corner, so its content box's corner is concentric with the panel's — the reading panel, the DetailList metadata column and the DetailList skeleton's inner panel (`p-3`, no border), the placeholder panel, a posted comment and the bordered pill (1 + 11); a 24 px pill has the same 12 px padding at its round ends. The tab bars are not in that tier: their 4 px inset makes them concentric with their 8 px tabs (12 − 4 = 8).
|
|
1392
|
-
The only other exceptions are the visually hidden text (Tailwind's `sr-only`, and inside virtualized rows the transparent 1 px box of `VISUALLY_HIDDEN_CLASS_NAME`, which clips nothing) and the 24 px the search field keeps free at its end for the clear button inside its box. `src/client/ui/spacing-ladder.spec.ts` checks every class the package writes, that every bordered box's border + padding is on the ladder or equals its corner, and that every 12 px-corner panel's inset is 12.
|
|
1469
|
+
The only other exceptions are the visually hidden text (Tailwind's `sr-only`, and inside virtualized rows and for every attachment file name the transparent 1 px box of `VISUALLY_HIDDEN_CLASS_NAME`, which clips nothing) and the 24 px the search field keeps free at its end for the clear button inside its box. `src/client/ui/spacing-ladder.spec.ts` checks every class the package writes, that every bordered box's border + padding is on the ladder or equals its corner, and that every 12 px-corner panel's inset is 12.
|
|
1393
1470
|
- **Origin on the lattice**: the views' centred column (at most 72 rem, `max-w-6xl`, centred with `mx-auto`) starts at the centring offset rounded down to a multiple of the block quantum q (`VIEW_COLUMN_ORIGIN_CLASS_NAME`: `ml-[max(0px,round(down,calc((100%-72rem)/2),var(--aqdr-lattice-block)))]`, part of `VIEW_COLUMN_CLASS_NAME`, which carries q itself, so the status screen starts there too), less than q left of the exact centre, and keeps q from the frame's top and side edges.
|
|
1394
1471
|
q CSS px is a whole number of device pixels at every ratio of the lattice's domain (4 px at the multiples of 1/4, k device pixels at the ratio k/4; 8 px at the odd multiples of 1/8), so every inline edge built on the origin — G, the ladder steps of 8 and 16, the card, the row frame — starts on a whole device pixel: on a 412 px phone at 2.625 a DetailList surface's left edge is 16 px in, at device pixel 42.
|
|
1395
1472
|
Below the column's top inset the view-mode toolbar sits in its band, as tall as its content rounded up to q (`VIEW_TOOLBAR_BAND_CLASS_NAME`; the search row, when configured, is inside the band: one more 32 px block, `SEARCH_ROW_CLASS_NAME`), so the views start q + the band below the column's top — 4 + 32 = 36 px at the multiples of 1/4 and 8 + 32 = 40 px at the odd eighths, or 4 + 64 = 68 px and 8 + 64 = 72 px with the search row — on the lattice at every ratio.
|
|
@@ -1422,7 +1499,7 @@ Whichever size rule wins the cascade, a selected surface therefore paints nothin
|
|
|
1422
1499
|
`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;
|
|
1423
1500
|
and `overflow-wrap: anywhere` breaks inside a phrase or a word only when it does not fit on a line by itself, so a label never crosses its frame at any width, root font size or font. It also counts those breaks in the label's min-content width, so a flex item's automatic minimum width no longer holds the label at its longest phrase (the prompt is a flex item, and the tile label's text is the anonymous flex item of its box: the property is inherited, which is how it reaches that item, where a `min-width` on the label's box would not). Where a phrase fits, the lines are the same as without it.
|
|
1424
1501
|
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 three declarations, the side pane's spec the prompt's classes and the panel minimum derived from the compiled insets and the prompt's last phrase, the tile's spec compiles the label's classes and checks that its line-breaking declarations are exactly the token's, and the search box's spec renders every status message in both locales and checks that the line carries the token and no truncating class.
|
|
1425
|
-
- **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).
|
|
1502
|
+
- **Type and contrast**: no text is smaller than 12 px (`text-xs`; a scan of every module under `src/client` rejects a smaller arbitrary size, `src/client/components/report-views.spec.tsx`); 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).
|
|
1426
1503
|
- **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.
|
|
1427
1504
|
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.
|
|
1428
1505
|
Unread is shown by the dot on the read toggle and by the row state in the description; no surface changes its border or its layout for it (a card's border is the same 1 px in every state).
|
|
@@ -1460,6 +1537,8 @@ Whichever size rule wins the cascade, a selected surface therefore paints nothin
|
|
|
1460
1537
|
— after dropping that font's measure, since the glyph widths change while the font string stays the same; other font loads re-render nothing.
|
|
1461
1538
|
Outside a grid (the rows of non-image attachments) and until the grid's width is known, the name truncates with CSS: the stem ellipsizes and the extension, kept up to half the width, follows it.
|
|
1462
1539
|
The visible name is hidden from assistive technology either way, and a visually hidden copy of the whole name gives each link one unbroken name (no space before the extension); `title` holds the whole name too.
|
|
1540
|
+
Every hidden copy of a file name, the download link's included, is `HiddenFileName`: `VISUALLY_HIDDEN_CLASS_NAME` with `contain: layout`. It clips nothing, so it never makes a DetailList row repaint on a scroll step that shifts the window, and its layout containment keeps the long one-line name from widening a scroll box outside the virtualized rows (the side pane, the mobile overlay).
|
|
1541
|
+
The attachment section still holds content that clips — the visible name's ellipsis, a fitted name's `overflow: hidden`, the size note's ellipsis and the thumbnail frame's rounded corners — so a DetailList row with attachments is still repainted on such a step.
|
|
1463
1542
|
While a search answer shows, the visible name carries the search marks of its `attachment:<i>` key (i, the attachment's index in the report, taken before the split into tiles and rows; **Highlights** in [Text search](#text-search)): in the CSS layout the stem span and the extension span each draw their own part of the raw name, so a match across the dot marks both;
|
|
1464
1543
|
a fitted name maps back to the raw name — unit for unit when the whole name fits, otherwise by the NFC runs it was cut from (`fitFileName` returns a `DisplayText`), so a mark on a decomposed name covers the whole composed character — and its synthetic `…` and the elided middle carry no mark.
|
|
1465
1544
|
The visually hidden copy, `title` and `data-attachment-file-name` stay plain.
|
|
@@ -1565,27 +1644,28 @@ locale**: English (`"en"`, the default) and Japanese (`"ja"`). Pick the language
|
|
|
1565
1644
|
```
|
|
1566
1645
|
|
|
1567
1646
|
- **One surface for all wording.** The catalog covers the 11 engine chrome keys of
|
|
1568
|
-
`@aiquants/virtualscroll` plus
|
|
1647
|
+
`@aiquants/virtualscroll` plus 108 own keys: field headings, the page title (`title`, passed to
|
|
1569
1648
|
`renderHeader` and used as the accessible name of both lists), the lists' keyboard help, the row state and the List card's position read to
|
|
1570
|
-
screen readers, tabs, buttons, placeholders, the `confirm()` text, stream badges, the search box and the search's match indicator, the status screen and the page's status region
|
|
1649
|
+
screen readers, tabs, buttons, placeholders, the `confirm()` text, stream badges, the search box, its filter row and the search's match indicator, the status screen and the page's status region
|
|
1571
1650
|
and the mutation-failure message with its repeat count. The engine part of each catalog is `VIRTUAL_SCROLL_LABEL_CATALOGS[locale]`, reused by
|
|
1572
1651
|
value, so the scroll arrows, the names of the scroll bar and its thumb, and the "No items" text always speak the same language as the rest.
|
|
1573
1652
|
- **Forwarded to every embedded `VirtualScroll`.** Both the List and the DetailList pass `locale`
|
|
1574
1653
|
and the 11 engine keys of the resolved labels to their `VirtualScroll`. The engine label object keeps
|
|
1575
1654
|
its identity while its values are unchanged, so rebuilding `config.labels` on every render does not
|
|
1576
1655
|
re-render the memoized list subtree.
|
|
1577
|
-
- **Formatter keys.**
|
|
1656
|
+
- **Formatter keys.** Nineteen keys take arguments and are functions: `totalCount(count)`,
|
|
1578
1657
|
`debounce(milliseconds)`, `streamProgressCount(loaded)`, `streamProgressRatio(loaded, total, percent)`,
|
|
1579
1658
|
`streamRetrying(loaded)`, `reportLoadFailed(reportHubId)`, `reportSummaryLoadFailed(reportHubId)`,
|
|
1580
1659
|
`interviewerWithAffiliation(name, affiliation)`, `operationFailed(operation)` (`operation` is a
|
|
1581
1660
|
`DailyReportOperation`: `create` / `update` / `publish` / `delete` / `addComment` / `deleteComment` /
|
|
1582
1661
|
`toggleStar` / `toggleRead`), `noticeRepeated(count)`, `rowState({ isRead, isStarred })`, `listRowPosition(position, total)`, `listRowPositionInUnknownTotal(position)`,
|
|
1583
|
-
`searchHitCount(count)`, `searchFailed(failure)` (`failure` is `invalid` / `busy` / `unavailable` / `failed`), `searchMatchedKeywords(matched, total)
|
|
1662
|
+
`searchHitCount(count)`, `searchFailed(failure)` (`failure` is `invalid` / `busy` / `unavailable` / `failed`), `searchMatchedKeywords(matched, total)`, `searchMatchedKeywordsDescription(matched, total)`, `searchFiltersToggle(count)` and `searchFiltersFailed(failure)` (`failure` is `busy` / `unavailable` / `failed`). An override of such a key must be a function too.
|
|
1584
1663
|
- **Digit grouping follows the UI locale, not the runtime.** The built-in formatters group numbers with
|
|
1585
|
-
the BCP 47 tag bound to their locale (`en` → `"en-US"`, `ja` → `"ja-JP"`)
|
|
1664
|
+
the BCP 47 tag bound to their locale (`en` → `"en-US"`, `ja` → `"ja-JP"`), through one `Intl.NumberFormat` per
|
|
1665
|
+
locale built once (the row positions and counts re-render on every stream publish); a host override receives
|
|
1586
1666
|
the raw number and formats it itself.
|
|
1587
1667
|
- **Fail-fast**: an unsupported `locale` (`"EN"`, `"ja-JP"`, `"fr"`, `""`, `null`, ...) throws a
|
|
1588
|
-
`RangeError` at render. So do an unknown `labels` key (a key error that lists the
|
|
1668
|
+
`RangeError` at render. So do an unknown `labels` key (a key error that lists the 119 keys), a string
|
|
1589
1669
|
key whose value is not a string with non-whitespace content, and a formatter key whose value is not a
|
|
1590
1670
|
function. An `undefined` value keeps the catalog value.
|
|
1591
1671
|
- **`config` has a closed key set**: `apiBasePath`, `ssePath`, `draftLabelName`, `locale`, `labels`,
|
|
@@ -1623,7 +1703,7 @@ locale**: English (`"en"`, the default) and Japanese (`"ja"`). Pick the language
|
|
|
1623
1703
|
grouping follows `locale` (`en` → `"en-US"`, `ja` → `"ja-JP"`) and dates stay in ISO form. Packages
|
|
1624
1704
|
that do format dates (for example `@aiquants/period-slider`) name that separate axis `formatLocale`.
|
|
1625
1705
|
|
|
1626
|
-
Exported API (`@aiquants/daily-report/client`): `DAILY_REPORT_LABEL_KEYS` (all
|
|
1706
|
+
Exported API (`@aiquants/daily-report/client`): `DAILY_REPORT_LABEL_KEYS` (all 119 keys, the 11 engine keys
|
|
1627
1707
|
first), `DAILY_REPORT_LABEL_CATALOGS` (the frozen catalogs), `resolveDailyReportLabels(locale, labels)`
|
|
1628
1708
|
(the effective frozen labels — the catalog object itself when `labels` is `undefined`), the resolved
|
|
1629
1709
|
`locale` / `labels` on `useDailyReportConfig()`, and the types `DailyReportLocale` (= `VirtualScrollLocale`),
|
|
@@ -1644,14 +1724,14 @@ first), `DAILY_REPORT_LABEL_CATALOGS` (the frozen catalogs), `resolveDailyReport
|
|
|
1644
1724
|
| `noItems` | engine: the one message of an empty List or DetailList, over the top of the list | No items | 項目がありません |
|
|
1645
1725
|
| `id` | DetailList card: id chip | ID | ID |
|
|
1646
1726
|
| `businessDate` | business date heading | Business date | 営業日 |
|
|
1647
|
-
| `author` | author heading | Author | 作成者 |
|
|
1727
|
+
| `author` | author heading; the label of the search filter row's author cell | Author | 作成者 |
|
|
1648
1728
|
| `visitTime` | visit time heading | Visit time | 訪問時間 |
|
|
1649
1729
|
| `createdAt` | created-at heading | Created at | 作成日時 |
|
|
1650
1730
|
| `updatedAt` | updated-at heading | Updated at | 更新日時 |
|
|
1651
1731
|
| `updatedBy` | updated-by heading | Updated by | 更新者 |
|
|
1652
1732
|
| `category` | DetailList: label of the category chip and of the metadata column's category pair | Category | 区分 |
|
|
1653
1733
|
| `creationCategory` | DetailList: label of the creation-category chip and of the metadata column's creation-category pair | Creation category | 作成区分 |
|
|
1654
|
-
| `customer` | customer heading | Customer | 顧客 |
|
|
1734
|
+
| `customer` | customer heading; the label of the search filter row's customer cell | Customer | 顧客 |
|
|
1655
1735
|
| `subject` | subject heading | Subject | 件名 |
|
|
1656
1736
|
| `content` | content heading | Content | 内容 |
|
|
1657
1737
|
| `interviewers` | interviewer list heading | Attendees | 面談者 |
|
|
@@ -1678,7 +1758,7 @@ first), `DAILY_REPORT_LABEL_CATALOGS` (the frozen catalogs), `resolveDailyReport
|
|
|
1678
1758
|
| `devSseToggle` | dev toolbox: the SSE switch's accessible name (`aria-label` on the `role="switch"` button) and tooltip | Toggle SSE real-time sync | SSE リアルタイム同期切替 |
|
|
1679
1759
|
| `devReload` | dev toolbox: reload tooltip | Reload data | データ再取得 (リロード) |
|
|
1680
1760
|
| `devClearCache` | dev toolbox: clear-cache tooltip | Clear the cache and reload | キャッシュ破棄 & 再取得 |
|
|
1681
|
-
| `autoRead` | auto-read switch label | Auto-read | 自動既読 |
|
|
1761
|
+
| `autoRead` | auto-read switch label, tied to its switch by an id of its own (`useId`, so two mounted screens never share one) | Auto-read | 自動既読 |
|
|
1682
1762
|
| `autoReadTitle` | auto-read switch: its description (`aria-describedby`) and tooltip | Mark reports as read automatically while viewing them | 日報閲覧時の自動既読処理切替 |
|
|
1683
1763
|
| `loadErrorBadge` | header annotation on load failure | Error | エラー |
|
|
1684
1764
|
| `loadError` | status panel on load failure; the page's status region announces it as an alert | Failed to load the daily reports. | 日報データの読み込みに失敗しました。 |
|
|
@@ -1714,6 +1794,10 @@ first), `DAILY_REPORT_LABEL_CATALOGS` (the frozen catalogs), `resolveDailyReport
|
|
|
1714
1794
|
| `commentDeleteConfirm` | comment delete confirmation | Delete | 削除する |
|
|
1715
1795
|
| `unknownUser` | author name when none is known (legacy / relational comment without a name, optimistic data of a user without a name) | Unknown | 不明なユーザー |
|
|
1716
1796
|
| `sourceInternal` | built-in `Internal` badge (only when `sourceTypeConfigs` is omitted) | Original | オリジナル |
|
|
1797
|
+
| `searchFilterSourceType` | search filter row: the label of the source-type cell (the author and customer cells use `author` and `customer`) | Source | ソース区分 |
|
|
1798
|
+
| `searchFiltersLoading` | search filter row: the status line while the candidates load | Loading filters... | 絞り込みの候補を読み込み中... |
|
|
1799
|
+
| `searchFiltersTooMany` | search filter row: the status line after a selection over the filter limits was refused | No more filter values can be selected | これ以上は絞り込みの値を選べません |
|
|
1800
|
+
| `searchFilterUnavailable` | search filter row: a cell whose host control threw while it rendered | This filter cannot be shown | この絞り込みを表示できません |
|
|
1717
1801
|
| `totalCount` | header annotation: the size of the views' set (the declared total while the ids stream delivers, the rows once it completes) | `(1234)` → Total: 1,234 | `(1234)` → 総件数: 1,234 件 |
|
|
1718
1802
|
| `debounce` | header annotation | `(300)` → Debounce: 300 ms | `(300)` → デバウンス: 300 ms |
|
|
1719
1803
|
| `streamProgressCount` | stream badge before the total is known | `(1234)` → Loading 1,234 | `(1234)` → 読み込み中 1,234 件 |
|
|
@@ -1727,6 +1811,7 @@ first), `DAILY_REPORT_LABEL_CATALOGS` (the frozen catalogs), `resolveDailyReport
|
|
|
1727
1811
|
| `rowState` | visually hidden row state, the first description of a DetailList row and the second of the List card's primary button | `({ isRead: false, isStarred: true })` → Unread, starred | `({ isRead: false, isStarred: true })` → 未読、スター付き |
|
|
1728
1812
|
| `listRowPosition` | visually hidden position of a List card in the list, the first description of its primary button (`position` is 1-based; `total` is the size of the views' set) | `(3, 1234)` → 3 of 1,234 | `(3, 1234)` → 全 1,234 件中 3 件目 |
|
|
1729
1813
|
| `listRowPositionInUnknownTotal` | the same position while the size of the views' set is unknown (the ids stream has not declared its total yet), in place of `listRowPosition` | `(3)` → Item 3 | `(3)` → 3 件目 |
|
|
1814
|
+
| `searchFiltersToggle` | search box: the filter toggle, with the number of selected filter values (no number at 0; digit grouping) | `(0)` → Filters; `(3)` → Filters (3) | `(0)` → 絞り込み; `(3)` → 絞り込み (3) |
|
|
1730
1815
|
|
|
1731
1816
|
### External Source Badge Configuration (`sourceTypeConfigs`)
|
|
1732
1817
|
|
|
@@ -1878,17 +1963,20 @@ A type the closure brings in is published under a name that carries `DailyReport
|
|
|
1878
1963
|
- **client** (`@aiquants/daily-report/client`, React):
|
|
1879
1964
|
- Components: `DailyReportPage` / `DailyReportResolvedContent` / `DailyReportList` / `DailyReportDetailList` / `DailyReportAttachmentIndicator`, the providers `DailyReportConfigProvider` / `DailyReportActionProvider` / `DailyReportErrorProvider` (required above the action provider: [Client wiring](#client-wiring)),
|
|
1880
1965
|
and their props types `DailyReportPageProps` / `DailyReportResolvedContentProps` / `DailyReportListProps` / `DailyReportDetailListProps` (with `DailyReportDetailListScrollRestore`, the scroll anchor and row heights it keeps across unmounts) / `DailyReportAttachmentIndicatorProps`.
|
|
1881
|
-
- Hooks: `useDailyReportConfig` / `useDailyReportActionContext` (the provider's actions — functions that keep their identity while the provider is mounted, the ledgers and `user` — and its state: `items`, `version`, `isSseEnabled`, `sseStatus`, and for the search `searchInvalidation`, `isReportRemoved`, `removedVersion`; `updateReport` takes both `title` and `content`) / `useDailyReportDetail` / `useDailyReportPrefetch` / `useDailyReportComments` /
|
|
1966
|
+
- Hooks: `useDailyReportConfig` / `useDailyReportActionContext` (the provider's actions — functions that keep their identity while the provider is mounted, the ledgers and `user` — and its state: `items` (the rows as a persistent list, read by `length`, `at` and iteration), `version`, `isSseEnabled`, `sseStatus`, and for the search `searchInvalidation`, `isReportRemoved`, `removedVersion`; `updateReport` takes both `title` and `content`) / `useDailyReportDetail` / `useDailyReportPrefetch` / `useDailyReportComments` /
|
|
1882
1967
|
`useDailyReportSseConnection` (returns `DailyReportSseConnectionStatus`) / `useDailyReportIdsStream` (state carries `loadedCount`, `streamAnchor`, and `itemsRevision` / `itemsChanges`, the revision of the session's list and its latest net changes; the list itself is not part of the state).
|
|
1883
1968
|
Their types: the action context's `DailyReportActionContextType` (what `useDailyReportActionContext` returns) / `DailyReportActions` (the functions, the ledgers and the viewing user) / `DailyReportActionFunctions` / `DailyReportActionLedgers` (with `DailyReportLedgerView`, a ledger's read-only `{ current }`) / `DailyReportActionState` / `DailyReportActionBusinessDate` (the business date an action sends), `DailyReportDetailResource` (the detail source the screen provides while it shows a search answer),
|
|
1884
1969
|
`UseDailyReportSseConnectionOptions`, the ids stream's `DailyReportIdsStreamState` / `DailyReportIdsStreamPhase` / `DailyReportIdsStreamClientOptions` / `DailyReportIdsStreamItemsChange` (one net change) / `DailyReportIdsStreamItemReplacement` (one value change inside it), and `DailyReportSearchFailure` (why a search failed, as the search box tells it).
|
|
1885
1970
|
- The ids stream: `DailyReportIdsStreamClient` (`resync()`, `has(reportHubId)`, `readItems()` — the list as of the last publish, built when it is read; its `now` option is the monotonic clock of the publish window) / `DailyReportIdsStreamStatus` / `primeDailyReportIdsStreamSession` / `bootstrapDailyReportIdsStreamSession` / `ensureDailyReportIdsStreamSession` / `resyncDailyReportIdsStream`; the route helpers `createDailyReportClientLoader` / `dailyReportShouldRevalidate`.
|
|
1886
1971
|
- Configuration and wording: `defaultDailyReportClientConfig` / `resolveDailyReportLabels` / `DAILY_REPORT_LABEL_CATALOGS` / `DAILY_REPORT_LABEL_KEYS` / `resolveSourceTypeConfig`.
|
|
1972
|
+
- The search's filter controls: `DAILY_REPORT_SEARCH_FILTER_FACETS` with its type `DailyReportSearchFilterFacet` (the filters in the filter row's order, `author`, `sourceType` and `customer`), and the contract of the control a host renders in each filter cell: `DailyReportSearchFilterSelectRenderer` (what `config.search.renderFilterSelect` is) /
|
|
1973
|
+
`DailyReportSearchFilterSelectProps` (what it receives) / `DailyReportSearchFilterOption` (one candidate, `{ value, label }`).
|
|
1887
1974
|
- Layout: `DAILY_REPORT_LATTICE_BLOCK_QUANTUM_CLASS_NAME`, the hook that carries the block quantum q (`--aqdr-lattice-block`, 4 px or 8 px by ratio) on the document root, which a host's bars round up to, and `DAILY_REPORT_LAYOUT_LATTICE_PX`, the 4 px unit u that q is built from (see **G-symmetric frame**); they are the only public layout values. 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.
|
|
1888
1975
|
- Types: `DailyReportClientConfigInput` / `DailyReportClientConfigDefaults` / `DailyReportClientConfig` (the resolved configuration `useDailyReportConfig` returns) / `DailyReportHeaderProps` (what `renderHeader` receives) / `DailyReportSearchSettingInput` / `DailyReportSearchSetting` (`config.search` as passed and as resolved) / `SourceTypeConfig` / `DailyReportLocale` / `DailyReportLabels` / `DailyReportLabelOverrides` / `DailyReportOperation` / `DailyReportErrorInfo` / `DailyReportSseConnectionStatus`,
|
|
1889
1976
|
and the end-to-end test handle's contract `DailyReportViewTestHandle` / `DailyReportViewTestReadHandle` / `DailyReportRevealOptions` / `DailyReportListNavigationHandle` (the read-only getters of the views' `VirtualScroll` handle that `DailyReportViewTestReadHandle` builds on) (see [Test hooks](#test-hooks)).
|
|
1890
1977
|
- **server** (`@aiquants/daily-report/server`, Node.js):
|
|
1891
|
-
- Factories: `createDailyReportServer` / `createDailyReportService` (`getDailyReportIdsByExternalId` returns `{ items, streamAnchor, snapshot }`; `readSseStreamAnchor`; `attachmentDelivery`, `null` without the `attachments` block; `titleMaxLength`;
|
|
1978
|
+
- Factories: `createDailyReportServer` / `createDailyReportService` (`getDailyReportIdsByExternalId` returns `{ items, streamAnchor, snapshot }`; `readSseStreamAnchor`; `attachmentDelivery`, `null` without the `attachments` block; `titleMaxLength`;
|
|
1979
|
+
and for the search `searchLimits`, `null` without the `search` block, `searchFilterSourceTypes` (the source types a filter may name), `searchDailyReports` (it takes `DailyReportSearchCriteria`), `getDailyReportDetailsByIdsByExternalId`, `markDailyReportSearchHits` (it takes the request's `{ signal }` and throws its reason at its next check after an abort), `listDailyReportSearchFacets`, `reconcileSearchIndex`) / `createDailyReportHandlers` / `defineDailyReportSchema` / `defineDailyReportSearchSchema` / `createEpochStore`, the viewer scope `DailyReportViewerScope`.
|
|
1892
1980
|
Their types: `DailyReportService` (what `createDailyReportService` returns and `DailyReportHandlersConfig.service` takes), `DailyReportEpochStore`, `DailyReportVisibleSourceTypeSet` (a viewer scope's normalized set), `DailyReportResourceRouteArgs` (the arguments of a resource route's loader or action that the handlers read), `DailyReportSseReaderConfig`, and `DailyReportSqlResultCacheQueryOptions` with `DailyReportSqlResultCacheMissAdmission` (the admission a read passes for the fetch its miss starts).
|
|
1893
1981
|
- 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`.
|
|
1894
1982
|
- SSE and caching: `DailyReportSseReader` / `readDailyReportSseStreamEntry(raw, logger)` (reads one Redis stream entry, `{ id, message }` as the client returns it, once: `{ entry, json }`, the frozen `StreamEntry` the shared reader hands every subscriber and the entry's parsed JSON value; for a host whose own reader keeps the same contract) /
|
|
@@ -1901,6 +1989,7 @@ A type the closure brings in is published under a name that carries `DailyReport
|
|
|
1901
1989
|
for attachments `DailyReportAttachmentsConfig` / `DailyReportAttachmentThumbnailsConfig` (the `attachments` block and its `thumbnails`) / `DailyReportAttachmentDelivery` / `DailyReportAttachmentThumbnailDelivery` (the delivery's resolved thumbnails) / `DailyReportReadAttachment` / `DailyReportAttachmentBytes` (a successful read) / `DailyReportAttachmentError` / `DailyReportAttachmentFailure` / `DailyReportAttachmentThumbnailRenderer` /
|
|
1902
1990
|
`DailyReportAttachmentThumbnail` / `DailyReportAttachmentThumbnailError` / `DailyReportAttachmentThumbnailFailure` (the renderer's failure and its kinds) / `DailyReportAttachmentThumbnailSourceFormat` / `DailyReportAttachmentThumbnailOutputMediaType` / `SharpLike` / `DailyReportSharpPipelineLike` / `DailyReportSharpHeaderLike` (the parts of sharp the renderer reads),
|
|
1903
1991
|
and for the text search `DailyReportSearchConfig` (the `search` block) / `DailyReportSearchSourceTextReader` / `DailyReportSearchTables` with its tables `DailyReportSearchDocumentTable` / `DailyReportSearchCommentTable` / `DailyReportSearchSqlExecutor` (drizzle's `execute`) / `DailyReportSearchReconcileOptions` / `DailyReportSearchReconcileProgress` / `DailyReportSearchReconcileSummary` / `DailyReportSearchPlan`
|
|
1904
|
-
(a compiled query: `DailyReportSearchSet` keywords of `DailyReportSearchWord` words, each with its `DailyReportSearchAlternative` alternatives `{ text, tier }` and their `DailyReportSearchAlternativeTier`, 0 for the typed word's forms and 1 for a romaji reading)
|
|
1992
|
+
(a compiled query: `DailyReportSearchSet` keywords of `DailyReportSearchWord` words, each with its `DailyReportSearchAlternative` alternatives `{ text, tier }` and their `DailyReportSearchAlternativeTier`, 0 for the typed word's forms and 1 for a romaji reading),
|
|
1993
|
+
`DailyReportSearchCriteria` (what `searchDailyReports` takes: `{ plan, filters }`, the plan `null` for a filters-only search) / `DailyReportSearchFilters` (the parsed filters, each `null` when unset: the source types as a lower-case set, the authors and customers as `DailyReportSearchNameFilter`, a non-empty tuple) / `DailyReportSearchFacets` (the answer of `search-facets`).
|
|
1905
1994
|
|
|
1906
1995
|
MIT
|