@aiquants/daily-report 0.44.1 → 0.46.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 +50 -1
- package/README.md +122 -68
- package/dist/client.d.mts +3 -3
- package/dist/client.d.ts +3 -3
- package/dist/client.js +5 -5
- package/dist/client.mjs +4 -4
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/server.d.mts +11 -5
- package/dist/server.d.ts +11 -5
- package/dist/server.js +11 -11
- package/dist/server.mjs +11 -11
- package/package.json +4 -3
- /package/dist/{sse-schema-BcjRPJjN.d.ts → sse-schema-BmhpWHVK.d.ts} +0 -0
- /package/dist/{sse-schema-sKBorzg9.d.mts → sse-schema-CM_YM7xU.d.mts} +0 -0
package/README.md
CHANGED
|
@@ -105,6 +105,9 @@ label catalog reuses its 11 engine keys, the names of the scroll bar and its thu
|
|
|
105
105
|
`DailyReportRevealOptions`, the options of the test handle's `revealIndex`, is `@aiquants/virtualscroll`'s `scrollToIndex` options type, which takes
|
|
106
106
|
`align: "nearest"` since 3.11.0, and DetailList rows stay at the tree's tops and heights after a batch of re-measurements whose deltas cancel only
|
|
107
107
|
because its row memo follows the tree revision (3.11.0).
|
|
108
|
+
The peer floor of `@aiquants/swipe-overlay` is **1.2.6**: install 1.2.6 or later.
|
|
109
|
+
The mobile overlay, the comment delete confirmation and the built-in error notice leave `Escape` to its `Escape` stack, which lets an `Escape` that
|
|
110
|
+
cancels an IME composition pass; the mobile overlay's entry listens on the document of the overlay's element, an iframe's document included (1.2.6).
|
|
108
111
|
`@aiquants/sse` (the SSE wire contract, server response helpers and the reopening client) is a regular **dependency**: it arrives transitively, so
|
|
109
112
|
consumers do not declare it.
|
|
110
113
|
The server entry needs **Node.js 20.3 or later** (`engines.node` `>=20.3.0`): the thumbnail stage deadlines combine the generation's signal with a
|
|
@@ -126,7 +129,7 @@ of the floor, and the row says what those engines draw:
|
|
|
126
129
|
| `:has()` | Chrome 105, Firefox 121, Safari 15.4 | The keyboard-focus outline of a List card whose primary button holds keyboard focus (the card's surface reads its direct-child button). The selection ring, its forced-colours outline and the active row's outline come from the row frame's own `aria-selected` and `data-daily-report-active` and do not use `:has()` | A List card whose primary button holds keyboard focus (after a pointer hand-back, a key on the button) shows no focus outline; the selection ring and the active row's outline still show, and the DetailList is unaffected |
|
|
127
130
|
| Container size queries and container query units (`cqi`) | Chrome 105, Firefox 110, Safari 16 | The attachment grid's column count and its whole-pixel width (both read from the grid's own size container); the tile frame's height and the tile's lattice pad (`100cqi` of the tile root, an inline-size container as wide as its track); the view-mode toolbar's end inset in the single-column List | One attachment column at every width; frames sized by `aspect-ratio` alone and tiles without the pad (below); the toolbar's end edge overhangs the List's scroll bar by 8 px |
|
|
128
131
|
| CSS `round()` | Chrome 125, Firefox 118, Safari 15.4 | The attachment grid's whole-pixel width `calc(round(down, 100% − 16 (n − 1), n px) + 16 (n − 1))` per column count, which makes every track a whole pixel, the tile frame's whole-pixel height `round(nearest, 100cqi * 320 / 480, 1px)` and the tile's lattice pad `round(up, h, var(--aqdr-lattice-block)) − h` under its last line (see [Attachment display](#attachment-display)); the views' column origin on the lattice (see **Origin on the lattice**) | Chrome 121–124, inside the floor, drop those declarations: the grid fills its container with the fluid tracks (W − 16 (n − 1)) / n (every breakpoint keeps its column count, which no `round()` declaration sets), so tile edges can fall between pixels and the names are fitted less than 1 px narrower than the track; the frame's `aspect-ratio: 480 / 320`, always declared beside its height, sizes the frame at the exact fractional 2t / 3, so the frames of one grid can paint one device pixel apart; a tile stays its frame's fractional height + 48 px tall, so the rows below an image grid can start between device pixels; and the column keeps the `mx-auto` centre |
|
|
129
|
-
| CSS `calc-size()` | Chrome 129 | The DetailList row bodies', the error notice's and the view-mode toolbar band's height, their content height rounded up to the block quantum of the device-pixel lattice (`calc-size(auto, round(up, size, var(--aqdr-lattice-block)))`: 4 px, 8 px at the odd multiples of 1/8; see **Row slots on the lattice** and **Origin on the lattice**) | Chrome 121–128, and any engine that does not implement it,
|
|
132
|
+
| CSS `calc-size()` | Chrome 129 | The DetailList row bodies', the error notice's and the view-mode toolbar band's height, their content height rounded up to the block quantum of the device-pixel lattice (`calc-size(auto, round(up, size, var(--aqdr-lattice-block)))`: 4 px, 8 px at the odd multiples of 1/8; see **Row slots on the lattice** and **Origin on the lattice**) | Chrome 121–128, and any engine that does not implement it, find the declaration invalid at computed-value time (it holds a `var()`) and give `height` its initial `auto`: a body keeps its content height, which is on the 4 px lattice at a 16 px root and can leave it at other roots and at the odd multiples of 1/8, so the DetailList rows below can start between device pixels; the toolbar's band keeps its content height, so at the odd multiples of 1/8 the views start half a device pixel off whenever that height is not a multiple of 8 px (the toolbar alone is 32) |
|
|
130
133
|
| `scrollbar-gutter: stable` | Chrome 94, Firefox 97, Safari 18.2 | The side pane's tab panels keep their scroll bar's width whatever their content's height | Safari 17.4–18.1, inside the floor, ignore it; their default scroll bars overlay the content and take no width |
|
|
131
134
|
| `scrollbar-color` | Chrome 121, Firefox 64 (not in Safari 17.4) | The desktop side pane's native scroll bar wears the views' resting thumb and track (**Scroll bars** in [Selection and focus appearance](#selection-and-focus-appearance)); the mobile overlay keeps the browser's own bar | Safari, inside the floor, ignores it and draws its default scroll bar there; the bar's width does not change |
|
|
132
135
|
| `text-wrap: balance` | Chrome 114, Firefox 121, Safari 17.5 | The short labels that can wrap (the side pane's selection prompt, an attachment tile's unavailable label, the search box's failure line) keep their lines about equally long | Safari 17.4, inside the floor, drops the value and wraps them greedily, so a wrapped label can end with a short last line |
|
|
@@ -536,7 +539,8 @@ its payload — with one strict parser each, after authentication and before any
|
|
|
536
539
|
read by one encoder and one strict reader inside the package, so a renamed parameter fails their round trip instead of turning every resume into a
|
|
537
540
|
full replay) and the pattern of the echo id (`DAILY_REPORT_CLIENT_TEMP_ID_PATTERN`).
|
|
538
541
|
|
|
539
|
-
Every answer carries its `intent` (`deleteComment`'s too),
|
|
542
|
+
Every answer carries its `intent` (`deleteComment`'s too) and, but for the development-only `clearCache` (`{"status":"OK","intent":"clearCache"}`),
|
|
543
|
+
the echo id and the report's id as a JSON number, the same id domain every SSE event uses
|
|
540
544
|
(an integer from 1 to 2^53 − 1), beside the intent's own values (the created or published report, the toggled statuses, the posted comment, the
|
|
541
545
|
deleted comment's id, also a JSON number). The server builds each 200 answer from its command alone, so it cannot answer one intent with another's
|
|
542
546
|
shape.
|
|
@@ -581,9 +585,9 @@ its payload — with one strict parser each, after authentication and before any
|
|
|
581
585
|
since sources may call their drafts by different names). `createDailyReportService` copies and freezes the list, and throws
|
|
582
586
|
`[daily-report] draftLabelNames must be one or more non-blank label names; got <value>` for anything but one or more non-blank strings without an
|
|
583
587
|
exact duplicate, an omitted key and `[]` included: the names decide who may see a report, so there is no default name to fall back on.
|
|
584
|
-
A report is a draft while it carries a label of one of these names, and every decision about a draft reads the names inside SQL at the moment
|
|
585
|
-
made (the label table's collation ignores case, width and trailing spaces, so a name is never compared in JavaScript, and the process
|
|
586
|
-
id, which would go stale when a label is re-created):
|
|
588
|
+
A report is a draft while it carries a label of one of these names, and every decision about a draft label reads the names inside SQL at the moment
|
|
589
|
+
it is made (the label table's collation ignores case, width and trailing spaces, so a label's name is never compared in JavaScript, and the process
|
|
590
|
+
keeps no label id, which would go stale when a label is re-created):
|
|
587
591
|
every read shows a draft to its author only (one correlated `NOT EXISTS` over the report-label relation joined to the labels by name, so a report
|
|
588
592
|
with several draft labels still reads as one row), and the audience of a draft's SSE events — its content on `report-create`, `report-update` and
|
|
589
593
|
`report-publish`, and its comments' `comment-add` and `comment-delete` — is its author alone while it is a draft (one indexed one-row read with the
|
|
@@ -592,7 +596,8 @@ its payload — with one strict parser each, after authentication and before any
|
|
|
592
596
|
fails, writes nothing and names the label (a report without it would be visible to every viewer). `publishDailyReport` removes every draft-named
|
|
593
597
|
label of the report, by name and inside its transaction, and addresses its `report-publish` event to the author alone when the report is still a
|
|
594
598
|
draft after the transaction. `getDraftLabelId()` reads the canonical label's id by its name on every call (`null` when no label has that name).
|
|
595
|
-
A report whose creation category (`creationCategory`) is one of the names
|
|
599
|
+
A report whose creation category (`creationCategory`) is one of the names (compared in JavaScript, as an exact string: case, width and trailing
|
|
600
|
+
spaces count) shows the canonical label in its labels, with that label's own id, read by
|
|
596
601
|
name in the same read and only when a report of the read needs it (none when no label has the name); the read rule reads labels alone, so such a
|
|
597
602
|
report without a draft label is not hidden.
|
|
598
603
|
Every report detail the server sends — the `report`, `business-date` and `reports` answers, the action's answers that carry a report and the SSE
|
|
@@ -600,7 +605,7 @@ its payload — with one strict parser each, after authentication and before any
|
|
|
600
605
|
shared draft-label test, beside the hub row) or true when its creation category is one of the names; a row whose flag reads as anything but 1 or 0
|
|
601
606
|
throws.
|
|
602
607
|
The client compares no name: the views open the viewer's own draft in the editor by `isDraft` alone, so a draft named by any of the names, or by a
|
|
603
|
-
variant the collation equates, behaves as one ([Client wiring](#client-wiring)). The SSE stream's key carries the wire version (`<
|
|
608
|
+
variant the collation equates, behaves as one ([Client wiring](#client-wiring)). The SSE stream's key carries the wire version (`<redisKeyPrefix>:sse-stream:v4`; [Realtime architecture](#realtime-architecture)),
|
|
604
609
|
so an entry written before `isDraft` existed is never read.
|
|
605
610
|
- `resolveVisibleSourceTypes(request, { reason })` — **Required row-level authorization port.** Returns the `Hub.source_type` values this request may
|
|
606
611
|
view, or the explicit opt-out `{ unrestricted: "<reason>" }` of a deployment that does not restrict by source type. Applies uniformly to every server
|
|
@@ -611,9 +616,10 @@ its payload — with one strict parser each, after authentication and before any
|
|
|
611
616
|
- `loginRedirectPath` — **Required**: the path of the host's sign-in route, where the page loader (`index.loader`) redirects a reader who is not
|
|
612
617
|
signed in. `createDailyReportServer` and `createDailyReportHandlers` stop with a `RangeError` for anything but a non-blank text
|
|
613
618
|
(`[daily-report] loginRedirectPath must be the non-blank path of the host's sign-in route; got <value>`): the package names no host route of its own.
|
|
614
|
-
- `enableDevCacheClear` — Gates the dev-only `POST /action` `intent=clearCache` (flush
|
|
619
|
+
- `enableDevCacheClear` — Gates the dev-only `POST /action` `intent=clearCache` (flush this process's SQL result and user-id caches and, with Redis,
|
|
620
|
+
advance only the epoch of the id lists, so other workers keep their business-date and detail caches). Default `false` → the action answers
|
|
615
621
|
`400` (`Invalid intent`) and never calls `service.clearCache()`. Wire `import.meta.env.DEV` to enable it only in development (any authenticated user
|
|
616
|
-
could otherwise flush
|
|
622
|
+
could otherwise flush these caches without limit).
|
|
617
623
|
- `attachments` — Optional attachment delivery block (`DailyReportAttachmentsConfig`), owned by the service: the id codec and the read port it always
|
|
618
624
|
carries, the size limit and the original route's tuning, and the optional `thumbnails` with the renderer port and its tuning. See
|
|
619
625
|
[Attachments](#attachments).
|
|
@@ -638,7 +644,7 @@ its payload — with one strict parser each, after authentication and before any
|
|
|
638
644
|
| `[DailyReportReads]` | `warn` | the 503 of **Cache misses per viewer** in [The API route](#the-api-route) |
|
|
639
645
|
| `[DailyReportSearch]` | `warn` | the 429 of the `search`, `reports`, `search-marks` and `search-facets` admissions ([Requests](#requests)) |
|
|
640
646
|
| `[DailyReportServer]` | `warn` | `createDailyReportServer` without a `redis` provider |
|
|
641
|
-
| `[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 |
|
|
647
|
+
| `[DailyReportService]` | `info` | the service: SSE publishing (no Redis client, an event over the size bound, a slow or failed publish), a failed read of the stream anchor, the create's `info` trace (`createDailyReport called`, `Starting transaction`, `Hub created` with the inserted hub row, `Internal created`, `Label assigned`) and its `error` (`Error in createDailyReport`), an id list read out of stream order (`ids array is not in stream order; re-sorting …` at `warn`), a report whose own assembly failed and its suppressed repeats (`<read>: report <id> passed the visibility predicate …`, `failed_report_suppressed …`; [Requests](#requests)), the failed presence writes (`markAttachmentMissing failed`, `markAttachmentPresent failed`), the development cache clear |
|
|
642
648
|
| `[DailyReportSseReader]` | `info` | the shared SSE reader's Redis errors |
|
|
643
649
|
| `[DailyReportExternalSource]` | `info` | `transformJsonArray`'s malformed JSON columns of an external source |
|
|
644
650
|
|
|
@@ -659,11 +665,18 @@ its payload — with one strict parser each, after authentication and before any
|
|
|
659
665
|
The JSON answers themselves write nothing, and no line carries a request header's value or traces an answer given before the endpoint (the 401, 404
|
|
660
666
|
and 405), so a client decides neither the number nor the size of the lines.
|
|
661
667
|
- Primary tuning parameters: `idsTtlMs` (180 s) / `businessDateTtlMs` (300 s) / `cacheDefaultTtlMs` (60 s) / `cacheMaxBuckets` (4,096) /
|
|
662
|
-
`cacheMaxBytes` (512 MiB per process; both **The SQL result cache** in [The API route](#the-api-route)) / `
|
|
668
|
+
`cacheMaxBytes` (512 MiB per process; both **The SQL result cache** in [The API route](#the-api-route)) / `streamMaxLen` (10,000
|
|
663
669
|
entries); the attachment tuning lives in the `attachments` block ([Attachments](#attachments)).
|
|
664
670
|
Every number among them is an integer of at least 1, and only an omitted one takes its default: `0`, a negative number, `NaN`, a fraction or 2^53
|
|
665
671
|
stops `createDailyReportService` and `createDailyReportServer` with a `RangeError` that names the key
|
|
666
672
|
(`[daily-report] idsTtlMs must be an integer >= 1; got 0`).
|
|
673
|
+
- `redisKeyPrefix` (`daily-report` by default) — the prefix of every Redis key of the service: the SSE stream `<prefix>:sse-stream:v4` and the
|
|
674
|
+
cross-worker epochs `<prefix>:ids:epoch`, `<prefix>:date-epoch:<business date>` and `<prefix>:detail-epoch:<report id>`. Environments that share
|
|
675
|
+
one Redis (development and production, say) each pass their own prefix, so that neither reads the other's notifications nor drops the other's
|
|
676
|
+
cached reads. The prefix is one or more characters without whitespace or control characters, neither
|
|
677
|
+
starting nor ending with `:`, and only an omitted one takes the default: any other value stops `createDailyReportService` and
|
|
678
|
+
`createDailyReportServer` with a `RangeError` (`[daily-report] redisKeyPrefix must be a Redis key prefix of …; got ""`), and a configuration that
|
|
679
|
+
carries `streamKey` stops with a `TypeError` that names `redisKeyPrefix` instead.
|
|
667
680
|
|
|
668
681
|
#### Configuration errors
|
|
669
682
|
|
|
@@ -769,7 +782,8 @@ createDailyReportServer({
|
|
|
769
782
|
readViewerKey: async (request, { reason }) => (reason === "initial" ? yourAuth.viewerIdOf(request) : yourSession.viewerIdOf(request)),
|
|
770
783
|
resolve: async (request, { reason }) => {
|
|
771
784
|
const grants = await yourAuthz.grantsFor(request, { skipCache: reason !== "initial" })
|
|
772
|
-
|
|
785
|
+
// `Internal` is the package's own reports: every author needs its read, or creating a report answers 403.
|
|
786
|
+
return grants.canReadLegacy ? ["Internal", "legacy"] : ["Internal"]
|
|
773
787
|
},
|
|
774
788
|
}),
|
|
775
789
|
})
|
|
@@ -831,9 +845,9 @@ reason is the audit trail, so `grep -rn "unrestricted:"` in the host lists every
|
|
|
831
845
|
- **How it reaches the service**: the resolved set is wrapped in a `DailyReportViewerScope` and passed as the **first** argument of every gated
|
|
832
846
|
service method, the create and the three author mutations included.
|
|
833
847
|
The class is nominal (it holds a private field), so `{ visible: null }`, `{ ...scope, visible: null }` and `new DailyReportViewerScope(...)` are all
|
|
834
|
-
type errors — the only ways to obtain one are `DailyReportViewerScope.restrictTo(tokens)` — which accepts `readonly string[]`
|
|
835
|
-
handed a nullish value and quietly widen — and
|
|
836
|
-
explicit opt-out, with its reason.
|
|
848
|
+
type errors — the only ways to obtain one are `DailyReportViewerScope.restrictTo(tokens)` — which accepts `readonly string[]` or the per-dimension
|
|
849
|
+
`{ read, comment? }` (`DailyReportVisibilityDimensions`) only, so it cannot be handed a nullish value and quietly widen — and
|
|
850
|
+
`DailyReportViewerScope.unrestricted(auditReason)`, which the handlers call only for the port's explicit opt-out, with its reason.
|
|
837
851
|
Because a bare `null` is not spellable as a visibility value, `grep -rn "DailyReportViewerScope.unrestricted("` enumerates every unfiltered read in
|
|
838
852
|
a codebase, each carrying a written reason.
|
|
839
853
|
- **Failure**: a port that throws — an authorization-store outage, or a viewer who is gone (signed out or disabled) — or answers outside the contract
|
|
@@ -974,7 +988,7 @@ answers 404 (it never falls back to the original).
|
|
|
974
988
|
| `attachments.read` | required in the block | Reads the original bytes (`DailyReportReadAttachment`, **Read port** below). |
|
|
975
989
|
| `attachments.maxBytes` | 32 MiB | Upper bound for an original, for every delivery. Keep it below the read port's wire limit. Integer ≥ 1. |
|
|
976
990
|
| `attachments.rateLimitPerMinute` | 60 | Original downloads per user per minute; a token is spent before authorization. Integer ≥ 1. |
|
|
977
|
-
| `attachments.concurrency` | 4 | Simultaneous original reads. A slot is held while the original is referenced (below: until its body has been handed over, the client cancels it, or the client stops reading for 60 s), and one viewer holds at most half of the slots, rounded up (⌈n / 2⌉: 2 of the default 4). A request that finds no free slot, or whose viewer already holds that share, fails fast with 503 (`reason=concurrency`, `Retry-After: 5`). Integer ≥ 1. |
|
|
991
|
+
| `attachments.concurrency` | 4 | Simultaneous original reads. A slot is held while the original is referenced (below: until its body has been handed over, the client cancels it, or the client stops reading for 60 s), and one viewer holds at most half of the slots, rounded up (⌈n / 2⌉: 2 of the default 4). A request that finds no free slot, or whose viewer already holds that share, fails fast with 503 `Too many concurrent downloads` (`reason=concurrency`, `Retry-After: 5`). Integer ≥ 1. |
|
|
978
992
|
| `attachments.thumbnails.renderer` | required in `thumbnails` | Renders the preview (`DailyReportAttachmentThumbnailRenderer`: `{ id, render }`; the package's sharp implementation is `createSharpThumbnailRenderer`). Validated when the service is created (**Thumbnail renderer port** below). |
|
|
979
993
|
| `attachments.thumbnails.rateLimitPerMinute` | 120 | Thumbnail generations per user per minute, in a bucket separate from originals; revalidations (requests with `If-None-Match`) pay from a second bucket of ten times this value. Both admit before authorization, and an empty bucket answers 429 at once; a revalidation that does not end in a 304 also pays a generation token, right after authorization (see **Rate admission** below). Integer ≥ 1. |
|
|
980
994
|
| `attachments.thumbnails.concurrency` | 2 | Simultaneous thumbnail generations (read + render), with a wait queue. Integer ≥ 1. |
|
|
@@ -1060,7 +1074,7 @@ type DailyReportReadAttachment = (
|
|
|
1060
1074
|
| `invalid_path` | A scheme or a path the port does not accept | 404 | 404 |
|
|
1061
1075
|
| `too_large` | The object is over `maxBytes`; only the object's size may decide it, never a transient refusal | 413 | 404, cached only when the row's recorded size backs it (below) |
|
|
1062
1076
|
| `unavailable` | The storage cannot be reached, a read ended early, or the package's `signal` aborted the read | 502 | 502 |
|
|
1063
|
-
| `busy` | The storage refuses for now (a load limit, such as a per-connection stream limit that refuses at once) | 503 with `Retry-After: 5` | 503 with `Retry-After: 5` |
|
|
1077
|
+
| `busy` | The storage refuses for now (a load limit, such as a per-connection stream limit that refuses at once) | 503 `Attachment storage busy` with `Retry-After: 5` | 503 `Thumbnail temporarily unavailable` with `Retry-After: 5` |
|
|
1064
1078
|
| `deadline` | A storage call passed the port's own deadline (a per-call deadline, a stalled stream) | 504 | 504 |
|
|
1065
1079
|
|
|
1066
1080
|
`busy`, `deadline` and `unavailable` are transient: a thumbnail generation concludes nothing from them, caches nothing and marks nothing missing.
|
|
@@ -1107,7 +1121,8 @@ type DailyReportReadAttachment = (
|
|
|
1107
1121
|
- A read that has already been aborted when it settles is discarded whatever its kind, for an original download and a thumbnail generation alike: 503
|
|
1108
1122
|
(`reason=aborted`) and no missing- or present-object record, so an aborted `unavailable` is never logged as a storage outage (502) and a racing
|
|
1109
1123
|
`not_found` never marks the object missing.
|
|
1110
|
-
The original download logs `503 attachment=<id> viewer=<id> reason=aborted
|
|
1124
|
+
The original download logs `503 attachment=<id> viewer=<id> reason=aborted` and answers `Attachment request aborted` without `Retry-After`; the
|
|
1125
|
+
thumbnail generation also caches nothing and does not render. No
|
|
1111
1126
|
disconnected client receives either 503. An original request that is already aborted when it arrives gets the same 503 without reading.
|
|
1112
1127
|
- A thumbnail read still running at its deadline (`readMs`) is answered with 504 (`reason=read_timeout`) without waiting for it; nothing is cached and
|
|
1113
1128
|
the object is not marked missing.
|
|
@@ -1252,8 +1267,9 @@ type DailyReportAttachmentThumbnailRenderer = {
|
|
|
1252
1267
|
A render still running at its deadline is answered with 502 (`reason=render_timeout`) without waiting for it. A renderer that cannot stop
|
|
1253
1268
|
(in-process libvips) is still valid: the response is bounded by the deadline, but the generation slot stays held until the render settles
|
|
1254
1269
|
(`render_overrun ms=<elapsed>`; a render that has still not settled at twice its budget is logged once as `render_stuck ms=<elapsed>`).
|
|
1255
|
-
What a render has paid for is kept, whoever still waits for it. The generation stops waiting for its render at the deadline (the 502 above) or
|
|
1256
|
-
|
|
1270
|
+
What a render has paid for is kept, whoever still waits for it. The generation stops waiting for its render at the deadline (the 502 above) or, once
|
|
1271
|
+
every request waiting for that content has gone (the abort reaches only the renderer, through `signal`), when the render settles or the deadline
|
|
1272
|
+
passes, whichever comes first (503 `reason=aborted`, which no client receives); when the render then settles with a
|
|
1257
1273
|
thumbnail that passes the output checks below, or with `unsupported`, and the generation's read matched the row's size (**verified**, see **Read
|
|
1258
1274
|
port** above), the outcome is cached before the slot is released.
|
|
1259
1275
|
A result that settles after the generation stopped waiting is classified by the same rule as one that settles in time, so only those conclusions are
|
|
@@ -1595,7 +1611,7 @@ Both deliveries log at fixed levels, with fixed line formats:
|
|
|
1595
1611
|
| --- | --- |
|
|
1596
1612
|
| `info` | 200, 304, the 503 nobody receives (`reason=aborted`), 404 `not_eligible`, and the content-determined 404s (`signature`, `unsupported`, `too_large`; cached when verified, `too_large size=unrecorded` on a row without a recorded size) |
|
|
1597
1613
|
| `warn` | 404 `not_visible` (a sign of token enumeration), the storage 404s (`not_found`, `denied`, `invalid_path`), 413 (`db_size`, and the port's `too_large` on an original), the first 429 of a viewer's window per bucket and the window's `rate_limit_suppressed` / `revalidation_rate_limit_suppressed` line, the busy 503s (`queue`, `concurrency`, the storage's `busy`), every 502, the 504 of a storage read past a deadline (`deadline`, `read_timeout`), port settlements after a deadline (`read_overrun`, `render_overrun`), ports still unsettled at twice their stage budget (`read_stuck`, `render_stuck`) and thumbnail reads that do not match the row's size (`size_mismatch`: another length, or a `too_large` on a row whose size is known) |
|
|
1598
|
-
| `error` | 500 (`port_exception`, `port_contract` including a read over `maxBytes` and an original's `HEAD` that declares a size over it (`code=over_max_bytes`), an original's `GET` read whose declared `size` is not the length of its bytes (`code=size_mismatch`) and a read failure reason outside the union (`code=failure_reason`), the loader's `unexpected`, a route without the `token` parameter among them) and
|
|
1614
|
+
| `error` | 500 (`port_exception`, `port_contract` including a read over `maxBytes` and an original's `HEAD` that declares a size over it (`code=over_max_bytes`), an original's `GET` read whose declared `size` is not the length of its bytes (`code=size_mismatch`) and a read failure reason outside the union (`code=failure_reason`), the loader's `unexpected`, a route without the `token` parameter among them) and a `markAttachmentMissing` / `markAttachmentPresent` call that rejects (`<method> failed attachment=<id> message=<msg>`); a failed database write inside those calls is caught by the service and logged on `[DailyReportService]`, not here |
|
|
1599
1615
|
|
|
1600
1616
|
`error` is left to failures of the server itself, so an alert on `error` does not fire on user traffic or on upstream storage states.
|
|
1601
1617
|
|
|
@@ -1628,17 +1644,19 @@ and without the retry the victim would answer 500 and lose what it wrote.
|
|
|
1628
1644
|
|
|
1629
1645
|
### Realtime architecture
|
|
1630
1646
|
|
|
1631
|
-
1. Mutation services publish to Redis Stream (`<
|
|
1647
|
+
1. Mutation services publish to Redis Stream (`<redisKeyPrefix>:sse-stream:v4`, `daily-report:sse-stream:v4` by default: the key carries the wire version of the
|
|
1632
1648
|
stream's messages and their embedded report, so a deployment that changes a required key of them reads no entry an older version wrote, and the stream of the previous version
|
|
1633
1649
|
stops growing and can be deleted by hand; a host that reads or writes the stream itself, such as an integration test, builds the key with the
|
|
1634
|
-
server entry's `
|
|
1650
|
+
server entry's `buildDailyReportRedisKeys(redisKeyPrefix).sseStream` instead of restating the version) in sequence:
|
|
1635
1651
|
`invalidate -> epoch increment -> SSE publish`.
|
|
1636
1652
|
2. `DailyReportSseReader` uses a single blocking `xRead` loop to fan-out events to all SSE connections (eliminating per-connection Redis TCP
|
|
1637
1653
|
connections). `createDailyReportServer` creates it; `createDailyReportHandlers` takes the reader as `sseReader` typed by the port
|
|
1638
1654
|
`DailyReportSseReaderPort` — `ready()` and `subscribe(onEntry, onError)`, with the contract under **`connected` frame** below — which is the type a
|
|
1639
1655
|
host's own reader implements: a plain object with the two methods is accepted, while the class type, which has private fields, would refuse one.
|
|
1640
|
-
The reader reads each live entry once per process and hands every subscriber the same
|
|
1641
|
-
`{ id, message }
|
|
1656
|
+
The reader reads each live entry once per process and hands every subscriber the same value (`DailyReportSseStreamEntry`:
|
|
1657
|
+
`{ id, message }`, frozen one level deep — the entry and its `message` object; an entry whose `message` is `null` is not frozen, and `parsed` and
|
|
1658
|
+
`published` are not frozen either): `message` is the entry's `data` field parsed as JSON and validated by the stream form
|
|
1659
|
+
`dailyReportSseStreamMessageSchema` (the
|
|
1642
1660
|
only form that declares the server-only fields: the six types that may be addressed — status-update, comment-add, comment-delete, report-create,
|
|
1643
1661
|
report-update and report-publish — carry the recipient's internal id `recipientRawUserId`, and the three that embed a report also the writer's,
|
|
1644
1662
|
`actorRawUserId`) — the validated message (`parsed`) and the JSON value as published (`published`, which only the recipient filter reads) — or
|
|
@@ -1923,8 +1941,11 @@ source types. The filters' candidates (`search-facets`, under **Requests**) are
|
|
|
1923
1941
|
#### The index
|
|
1924
1942
|
|
|
1925
1943
|
Two derived tables per database, `DailyReportSearchDocument` (one row per report: the normalized text, the digest of the raw source, the normalizer
|
|
1926
|
-
version, `row_version` and the two filter values) and `DailyReportSearchComment` (one row per comment, with its author id
|
|
1944
|
+
version, `row_version` and the two filter values) and `DailyReportSearchComment` (one row per searchable comment, with its author id: a comment whose
|
|
1945
|
+
normalized text is empty gets no row, and the ordinals count only the kept comments), their normalized text
|
|
1927
1946
|
`nvarchar(max)` in `Latin1_General_100_BIN2` so that `LIKE` compares it as it is, and both deleted with their report (`ON DELETE CASCADE`).
|
|
1947
|
+
The document's `indexed_at` (`datetime2`, `NOT NULL`) is written by a rewrite (`SYSUTCDATETIME()`) but left out of the insert of a new document, so
|
|
1948
|
+
the host's table must give it a default (such as `DEFAULT (sysutcdatetime())`).
|
|
1928
1949
|
The filter values are two nullable columns of the document, `author_name` and `customer_name`: `nvarchar(200)` in `Latin1_General_100_BIN2`, so that
|
|
1929
1950
|
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).
|
|
1930
1951
|
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
|
|
@@ -1951,6 +1972,8 @@ the comment paths run in a transaction even without the `search` block, which a
|
|
|
1951
1972
|
|
|
1952
1973
|
Writes outside the package (a sync, an import) leave the index stale until
|
|
1953
1974
|
`service.reconcileSearchIndex({ batchSize, hubIds?, signal?, onProgress? })` runs.
|
|
1975
|
+
An aborted `signal` stops it between batches by rejecting with the signal's reason, so an aborted run returns no counters (`onProgress` has had
|
|
1976
|
+
those of each finished batch); a run that ends returns the counters and the elapsed time.
|
|
1954
1977
|
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
|
|
1955
1978
|
save, and rewrites only the reports whose digest or normalizer version differ.
|
|
1956
1979
|
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
|
|
@@ -2178,8 +2201,9 @@ edges; with the default labels the field starts and ends on the 4 px lattice.
|
|
|
2178
2201
|
The reserved label is the generated content of a hidden element, so it is in neither the toggle's accessible name nor its text.
|
|
2179
2202
|
The field's description (`labels.searchSyntaxHelp`, also its tooltip) gives the syntax, says that a word in Latin letters also finds its kana reading
|
|
2180
2203
|
and that a match found through a reading is underlined with dots.
|
|
2181
|
-
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
|
|
2182
|
-
|
|
2204
|
+
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 only the
|
|
2205
|
+
answer holds is left out when the provider marked it removed, while a report the stream holds shows as the stream holds it), and the header shows
|
|
2206
|
+
the hit count. While an answer shows, the rows read their details by id in batches (`reports?ids=`)
|
|
2183
2207
|
instead of by business date, and an answer goes into the per-report cache only, never into a business date's cached list.
|
|
2184
2208
|
Each view leads its rows' inputs: it keeps its rendered rows and 24–40 rows beyond each rendered edge subscribed to the details and the marks, in
|
|
2185
2209
|
chunks of 8 rows (32 on each side after a jump; **Highlights** below), so the rows a held key mounts find their details already loaded.
|
|
@@ -2315,7 +2339,8 @@ While an answer shows, the List cards, the side pane and the DetailList rows mar
|
|
|
2315
2339
|
The sentences are built from what the row's leaves registered, each heading being the field's own label (the header's and the metadata's author are
|
|
2316
2340
|
one `author`, a customer name is `customer`, every interviewer, attachment and comment key its section's heading), so they name only what the view
|
|
2317
2341
|
draws; **Match indicator** below says where each view puts them.
|
|
2318
|
-
- **Arrival**: the screen holds one marks store per
|
|
2342
|
+
- **Arrival**: the screen holds one marks store per answered search (inside the client bundle; a new answer whose text, combine mode and words equal
|
|
2343
|
+
the shown one's keeps the store, its filters aside), only while a view draws an answer that has words: without
|
|
2319
2344
|
`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),
|
|
2320
2345
|
after the release and for a view rendered on its own there is none, and the DOM is the plain one.
|
|
2321
2346
|
A row hands its marks scope the marks of the unit that drew its card: the row reads the report's details and marks through its input source from
|
|
@@ -2383,9 +2408,9 @@ While an answer shows, the List cards, the side pane and the DetailList rows mar
|
|
|
2383
2408
|
#### Match indicator
|
|
2384
2409
|
|
|
2385
2410
|
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
|
|
2386
|
-
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
|
|
2387
|
-
|
|
2388
|
-
|
|
2411
|
+
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 quiet text
|
|
2412
|
+
colour (the page's text face at the package's 12 px floor with tabular figures, on a 16 px line; 7.73:1 on the white card and overlay, 9.01:1
|
|
2413
|
+
on the dark card and 8.62:1 on the dark overlay's slate-900), as wide as the longest text the answer can show (an invisible placeholder), so
|
|
2389
2414
|
nothing reflows when the marks arrive.
|
|
2390
2415
|
The side pane's author line wraps, its parts 8 px apart along a line and its wrapped lines 4 px apart (`gap-x-2 gap-y-1`), the label kept whole on one
|
|
2391
2416
|
line; only the gap spaces the parts (the source badge has no margin of its own), so a part that wraps starts at the line's start, and in a pane too
|
|
@@ -2461,7 +2486,8 @@ one definition of a draft (`draftLabelNames` in [DI ports](#di-ports)), so `conf
|
|
|
2461
2486
|
unknown key.
|
|
2462
2487
|
An optimistic report the viewer creates is a draft with no labels until the server's answer brings its labels (the draft label among them); an
|
|
2463
2488
|
optimistic publish clears `isDraft` at once and keeps the labels until the answer, and a failed publish restores `isDraft`.
|
|
2464
|
-
The package never navigates by itself: when the session is gone — the SSE stream ends with `auth-required`, or the ids stream
|
|
2489
|
+
The package never navigates by itself: when the session is gone — the SSE stream ends with `auth-required`, or the ids stream, a `search` request,
|
|
2490
|
+
the filters' candidates (`search-facets`) or the marks (`search-marks`) answer 401 / 403 — it
|
|
2465
2491
|
calls `onSessionExpired()` exactly once per occurrence and stops. A typical host reloads the route (`navigate(0)`) so its own loader redirects to the
|
|
2466
2492
|
login page.
|
|
2467
2493
|
`DailyReportConfigProvider` takes the same required `config`, and every component and hook of the package that reads the configuration needs it above:
|
|
@@ -2471,7 +2497,7 @@ configuration renders silently.
|
|
|
2471
2497
|
`config.headingLevel` is the level of each report's heading: an integer from 2 to 5 (default 3), so that the section headings inside a report, one
|
|
2472
2498
|
level lower, stay within `h6`. Pick the level that continues your page's outline (see
|
|
2473
2499
|
[Document structure](#keyboard-focus-and-selection-list--detaillist)); any other value (`1`, `6`, `3.5`, `"3"`, `null`) throws a `RangeError` at
|
|
2474
|
-
render (`[daily-report] config.headingLevel must be
|
|
2500
|
+
render (`[daily-report] config.headingLevel must be one of 2, 3, 4, 5 (the level of each report's heading); got 6`).
|
|
2475
2501
|
|
|
2476
2502
|
`DailyReportPage` is layout-agnostic (header rendering via `renderHeader` slot, error boundaries/footer control managed by the app). For fine-grained
|
|
2477
2503
|
usage, `DailyReportActionProvider`, `DailyReportResolvedContent`, `useDailyReportDetail`, etc. can be imported individually.
|
|
@@ -2481,7 +2507,9 @@ lead while a search answer is shown, the List's side pane and the hook — so on
|
|
|
2481
2507
|
reader's subscription starts the load and its last reader's leaving stops it (an answer that arrives after that is dropped), and a cache write
|
|
2482
2508
|
reaches the readers of its own report only, once per answer. The hook copies the record into state at default priority. On the first render after
|
|
2483
2509
|
the id changes it already returns that id's starting state: its cached report (one definition, also used by the load: the report cache, expired
|
|
2484
|
-
entries included, then the business date's cached list
|
|
2510
|
+
entries included, then the business date's cached list; when another reader already subscribes to the id, that first render returns its record, and
|
|
2511
|
+
otherwise it reads the report cache alone, so a report only the list holds arrives once the hook subscribes), `isLoading` only when nothing is
|
|
2512
|
+
cached, no error and no refetch yet — never the previous
|
|
2485
2513
|
id's report, error or loading flags. An expired cached report is therefore drawn at once, never preceded by `null`, and refetched behind it. An id of
|
|
2486
2514
|
0 or less fetches nothing and returns no report (0 also returns an `Invalid ID` error). A row and the side pane that show one report share its
|
|
2487
2515
|
record, so they never run two loads of it at once, and readers waiting on one load of a report not shown yet receive its failure once each. A
|
|
@@ -2587,10 +2615,11 @@ package does nothing silently outside its provider.
|
|
|
2587
2615
|
|
|
2588
2616
|
The provider takes no items: it derives its rows from the module-resident ids stream session (below), so establish the session first —
|
|
2589
2617
|
`createDailyReportClientLoader` in the route, or `ensureDailyReportIdsStreamSession({ apiBasePath, userKey })` on mount, as `DailyReportPage` does —
|
|
2590
|
-
and mount one provider per user (`key={userId}`), which also starts the new user with an empty editing store (**Editing** in
|
|
2618
|
+
and mount one provider per user (`key={userId ?? "anonymous"}`), which also starts the new user with an empty editing store (**Editing** in
|
|
2591
2619
|
[Keyboard, focus and selection](#keyboard-focus-and-selection-list--detaillist)).
|
|
2592
2620
|
The provider builds its actions once — the functions, the ledgers of the optimistic state and the viewing user, in one value whose identity changes
|
|
2593
|
-
only
|
|
2621
|
+
only when the `user` prop's object changes (keep `user` referentially stable) — and hands out its state apart: the rows (`items`, a persistent
|
|
2622
|
+
list read by its length, the report at a position (`at`)
|
|
2594
2623
|
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
|
|
2595
2624
|
(`isSseEnabled`, `sseStatus`).
|
|
2596
2625
|
Each function calls the implementation of the last committed render, so a function kept from an earlier render does what the current one does.
|
|
@@ -2638,16 +2667,16 @@ the failure count.
|
|
|
2638
2667
|
A line refused on every attempt ends a first scan in `failed` (a revalidation gives up with its error and keeps its data), never in `complete`, so the
|
|
2639
2668
|
completion's pruning never runs after a refused line. A line whose `kind` is not `"chunk"` or `"error"`, a line without `kind` included, is skipped
|
|
2640
2669
|
with a warning as a line of an unknown kind, so the wire can grow by new kinds.
|
|
2641
|
-
The client splits the stream into lines as the chunks arrive, examining each
|
|
2642
|
-
of the line already received), and reads a final line without a newline at the end of the stream; a
|
|
2643
|
-
the attempt like any refused line, and the next attempt resumes from the last accepted line's cursor.
|
|
2670
|
+
The client splits the stream into lines as the chunks arrive, examining each byte once (a chunk of a long line does not re-read the part
|
|
2671
|
+
of the line already received, and a line is decoded once, when it is complete), and reads a final line without a newline at the end of the stream; a
|
|
2672
|
+
line cut short there is not valid JSON, so it ends the attempt like any refused line, and the next attempt resumes from the last accepted line's cursor.
|
|
2644
2673
|
`createDailyReportClientLoader` warms an existing session (`primeDailyReportIdsStreamSession`) and then bootstraps
|
|
2645
2674
|
(`bootstrapDailyReportIdsStreamSession`): when no session exists it is **created at loader time** so the stream fetch runs in parallel with hydration
|
|
2646
2675
|
(the dominant cold-load optimization); when one exists, the obfuscated user id is reconciled (a different user destroys and recreates the session
|
|
2647
2676
|
before render).
|
|
2648
2677
|
If your app overrides `config.apiBasePath`, pass the same value to `createDailyReportClientLoader({ apiBasePath })` — forgetting it costs one
|
|
2649
2678
|
wrong-path request on the very first load, self-healed by the page-mount `ensureDailyReportIdsStreamSession`.
|
|
2650
|
-
`DailyReportPage` renders the list as soon as the first chunk arrives (on cache misses the server races a fast `TOP
|
|
2679
|
+
`DailyReportPage` renders the list as soon as the first chunk arrives (on cache misses the server races a fast `TOP 30` first page against the cached
|
|
2651
2680
|
full query, so first paint does not wait for the full id scan).
|
|
2652
2681
|
|
|
2653
2682
|
- **An items publish re-renders only what it changes, behind the keys.** The package's own components read the session through narrow views
|
|
@@ -2807,7 +2836,7 @@ the resize handle and the side pane lie outside the list.
|
|
|
2807
2836
|
| --- | --- | --- | --- |
|
|
2808
2837
|
| `ArrowUp` / `ArrowDown` | Makes the previous / next report active and selected, and reveals it by the nearer edge | Makes the previous / next report active and selected, and aligns it to the top (the reading start) | Yes, also at the ends (no wrap-around) |
|
|
2809
2838
|
| `Home` / `End` | First / last report, aligned to the top / bottom | First / last report, aligned to the top / bottom (to the top when the last row is taller than the viewport) | Yes |
|
|
2810
|
-
| `PageDown` / `PageUp` | The farthest row that still fits in the viewport together with the active row, revealed by the nearer edge
|
|
2839
|
+
| `PageDown` / `PageUp` | The farthest row that still fits in the viewport together with the active row, revealed by the nearer edge; an active row that reaches past the viewport's bottom (`PageDown`) or top (`PageUp`) is first scrolled by min(overflow, ⌊0.875 × viewport⌋) without changing the selection | The same | Yes |
|
|
2811
2840
|
| `Enter` / `Space` on the composite | Activates the active row like a press of its card's primary button: selects it (on a mobile layout it also opens the detail overlay) | Selects the active row without scrolling | Yes, also without an active row (the page does not scroll) |
|
|
2812
2841
|
| `Enter` / `Space` on a control of the active row | That control's own action | The same | Native |
|
|
2813
2842
|
| `Escape` on a control of the active row, a field or button of an in-row form included | Focus returns to the composite; the active row does not change (below: where `Escape` keeps its own meaning) | The same | Yes |
|
|
@@ -2847,8 +2876,8 @@ the resize handle and the side pane lie outside the list.
|
|
|
2847
2876
|
accumulated. While a stream lasts, the desktop side pane keeps showing the report the stream started from (see **List side pane** below), and rows
|
|
2848
2877
|
get their bodies only in the frames without a key, far rows behind the travel direction after the key is released (see **Rows mount in two
|
|
2849
2878
|
passes** below).
|
|
2850
|
-
- **Ignored keys**: `Alt` / `Ctrl` / `Meta` / `Shift` combinations other than `Ctrl+Home` / `Ctrl+End` alone
|
|
2851
|
-
their browser meaning); an event whose default is already prevented; IME composition (`isComposing` or `keyCode` 229);
|
|
2879
|
+
- **Ignored keys**: `Alt` / `Ctrl` / `Meta` / `Shift` combinations other than `Ctrl+Home` / `Ctrl+End` alone and `Shift+Tab` (so `Ctrl+PageDown`
|
|
2880
|
+
and `Shift+End` keep their browser meaning); an event whose default is already prevented; IME composition (`isComposing` or `keyCode` 229);
|
|
2852
2881
|
keys whose target is neither the view's composite nor a control inside one of its rows (the scroll bar, the side pane, the mobile overlay); the
|
|
2853
2882
|
movement keys and the exits from an input region — a `form` (every control of the in-row edit form and of the comment form, their
|
|
2854
2883
|
buttons included) or an editable element (`input`, `textarea`, `select`, `contenteditable` other than `"false"`, `audio[controls]`,
|
|
@@ -2929,8 +2958,9 @@ the resize handle and the side pane lie outside the list.
|
|
|
2929
2958
|
control before or after the list.
|
|
2930
2959
|
- **A key commits once and forces no layout.** A key move sets the active row, the selection, the scroll and the remembered selection in one
|
|
2931
2960
|
synchronous commit (`flushSync`) inside its keydown: a destination outside the rendered window gets the alignment scroll (`scrollToIndex`) in that
|
|
2932
|
-
same commit, which renders it, and `VirtualScroll` names it in the same render.
|
|
2933
|
-
|
|
2961
|
+
same commit, which renders it, and `VirtualScroll` names it in the same render. A movement key on the composite calls no `focus()`
|
|
2962
|
+
(DOM focus is already there; one pressed from a row's control hands focus back to the composite once, and Escape and the `Ctrl+Home` / `Ctrl+End`
|
|
2963
|
+
exits move focus by design) and reads no layout (the exits and Tab's reveal do), so its task forces no style recalculation and no layout of its own.
|
|
2934
2964
|
The commit renders the two row frames whose state changed (the old and the new active row, and the old and the new selection), `VirtualScroll` once
|
|
2935
2965
|
(the active index and, when the key scrolled, the new window) through the view's `VirtualScroll` wrapper, and the host component that owns the
|
|
2936
2966
|
controlled selection (the List's `selectedReportHubId`, the DetailList's `selectedItemId`); the view itself, report rows, card bodies and the row
|
|
@@ -3046,7 +3076,8 @@ the resize handle and the side pane lie outside the list.
|
|
|
3046
3076
|
| The active report leaves the list (a delete, an SSE removal, a stale removal, a search answer that replaces the list) | The report now at its index, clamped to the new length; none in an empty list |
|
|
3047
3077
|
| The selected report is deleted from the side pane or the mobile overlay | The report now at the deleted report's index |
|
|
3048
3078
|
|
|
3049
|
-
The row under a press is recorded at the press (mouse and pen) and dropped
|
|
3079
|
+
The row under a press is recorded at the press (mouse and pen) and dropped when the press ends (`pointerup` / `pointercancel` on the document), by a
|
|
3080
|
+
press outside the view, by every `focusin` in the view and by every handled key, so a press whose
|
|
3050
3081
|
focus never entered the composite — a List card's primary button takes the click's focus — cannot decide a later entry: a far key after a card
|
|
3051
3082
|
click moves from the clicked row and stays on the key's destination.
|
|
3052
3083
|
A press in the List's row gutter moves no focus, so it changes nothing; a touch scroll fires no click and no focus, so it changes nothing either.
|
|
@@ -3400,7 +3431,8 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3400
3431
|
error — rounds its content height up to q (`height: calc-size(auto, round(up, size, var(--aqdr-lattice-block)))`), so rem line boxes at a root other
|
|
3401
3432
|
than 16 px (a 25 px line at a 20 px root) and a body of an odd multiple of 4 px at an odd eighth leave the next row on whole device pixels; at a 16
|
|
3402
3433
|
px root and a quarter ratio the content is already on the lattice.
|
|
3403
|
-
Engines without `calc-size()`
|
|
3434
|
+
Engines without `calc-size()` accept the declaration when parsing it (it holds a `var()`), find it invalid at computed-value time and give
|
|
3435
|
+
`height` its initial `auto`, the content height (see the browser floor table).
|
|
3404
3436
|
Measured in Chromium 148 at 412 × 915 and 2.625: with bodies rounded to 4 px, a DetailList row of 980 px ended at 6,478.5 device pixels and the
|
|
3405
3437
|
surface edge of the next row blended into two rows of device pixels; rounded to q, the revealed row's surface top sits at device pixel 273 in every
|
|
3406
3438
|
reveal.
|
|
@@ -3445,7 +3477,7 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3445
3477
|
on it.
|
|
3446
3478
|
- **A key press restyles only what paints the change**: every selector that depends on another element's state ends in the styled element's own class
|
|
3447
3479
|
or attribute, and `:has()` sits only on the styled element itself. A featureless subject (`*:`, `group-*`) or an ancestor's `:has(:focus-visible)`
|
|
3448
|
-
would make the browser restyle whole rows or the whole view on each key; `src/client/ui/tailwind-selector-scope.spec.ts` compiles the package's
|
|
3480
|
+
would make the browser restyle whole rows or the whole view on each key; `src/client/ui/tailwind-selector-scope.guard.spec.ts` compiles the package's
|
|
3449
3481
|
classes and fails on either form.
|
|
3450
3482
|
- **Every row is a relayout boundary**: the row frames of both views are `contain: size layout style` (one token), so a change inside one row — a held
|
|
3451
3483
|
body rendered after the key's commit, a load that completes, the selection and focus indicators — lays out that row alone and restyles no other.
|
|
@@ -3521,7 +3553,7 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3521
3553
|
`--primary`, `--background` or `--muted`.
|
|
3522
3554
|
The switch track is slate-500 when off and blue-600 when on (dark: slate-400 / blue-400) under a white thumb (dark: slate-950). The selected tab is
|
|
3523
3555
|
a white surface (dark: slate-950) with slate-900 text (dark: slate-100) and a straight 2 px blue-600 bar (dark: blue-400) along the straight part of
|
|
3524
|
-
its bottom edge, the bar being the cue that does not depend on colour. Unselected labels are
|
|
3556
|
+
its bottom edge, the bar being the cue that does not depend on colour. Unselected labels are in the quiet text colour (below).
|
|
3525
3557
|
The bar is an `::after` box at the tab's bottom edge, inset on each side by the tab's own corner radius (`--radius-lg`), so it never runs into the
|
|
3526
3558
|
rounded corners: it is 2 px thick along its whole length at every device pixel ratio (a bottom border on a rounded box thins and rises along both
|
|
3527
3559
|
corners), and it stays on the straight part for any host `--radius`. It is out of the flow and takes no space, so selecting a tab moves nothing and
|
|
@@ -3529,7 +3561,7 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3529
3561
|
In forced colours the thumb is `ButtonText`, the track has a `ButtonText` border and the on track a `Highlight` fill, and the selected bar is
|
|
3530
3562
|
`Highlight`, painted as declared (`forced-color-adjust: none` on the bar), so the state survives the colour override.
|
|
3531
3563
|
The package's error messages — the status panel's load-error message and its badge in the host's header, and the side pane's report-load error —
|
|
3532
|
-
take their colour from one token (red-
|
|
3564
|
+
take their colour from one token (red-800, dark red-300); the badge sits on the host's header, whose colour is the host's, so it has no pair below.
|
|
3533
3565
|
`src/client/ui/ui-state-contrast.spec.ts` computes every pair below from the compiled CSS and the palette (the toolbar surface is slate-100 at 60 %
|
|
3534
3566
|
over the slate-50 page, dark slate-900 at 60 % over slate-950; the card surface is white, dark slate-900 at 70 % over the page; the overlay panel is
|
|
3535
3567
|
white, dark slate-900):
|
|
@@ -3543,12 +3575,13 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3543
3575
|
| Selected tab bar / selected tab | 5.25 | 7.64 | 3 |
|
|
3544
3576
|
| Selected tab bar / the side pane's tab track | 4.79 | 5.54 | 3 |
|
|
3545
3577
|
| Selected tab bar / toolbar surface | 4.88 | 7.16 | 3 |
|
|
3546
|
-
| Unselected label / the side pane's tab track |
|
|
3547
|
-
| Unselected label / toolbar surface | 7.
|
|
3548
|
-
| Selected label / selected tab | 17.83 | 18.40 |
|
|
3549
|
-
|
|
|
3550
|
-
| Error text /
|
|
3551
|
-
| Error text /
|
|
3578
|
+
| Unselected label / the side pane's tab track | 7.06 | 7.07 | 7 |
|
|
3579
|
+
| Unselected label / toolbar surface | 7.19 | 9.13 | 7 |
|
|
3580
|
+
| Selected label / selected tab | 17.83 | 18.40 | 7 |
|
|
3581
|
+
| Empty view's message / page surface | 7.39 | 9.75 | 7 |
|
|
3582
|
+
| Error text / card surface (the desktop side pane) | 8.36 | 9.70 | 7 |
|
|
3583
|
+
| Error text / overlay panel (the side pane's content on a phone) | 8.36 | 9.29 | 7 |
|
|
3584
|
+
| Error text / the load-error panel (the card surface over the page frame) | 8.36 | 9.70 | 7 |
|
|
3552
3585
|
|
|
3553
3586
|
- **Text fields**: the comment input, the edit form's `Input` and `Textarea` and the search field, and the outline of the search band's toggles in
|
|
3554
3587
|
their field face (the OR toggle and the closed filter toggle), take their boundary and placeholder from one token: a 1 px slate-500 border in both
|
|
@@ -3576,13 +3609,13 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3576
3609
|
so the content edge is 16 px from the visible edge and its corner concentric with the surface's) and `shadow-sm`.
|
|
3577
3610
|
Two more surfaces have one token each, and no other place writes their classes: the placeholder panel (the side pane's selection prompt and its
|
|
3578
3611
|
loading and empty tabs; a dashed 1 px border, the 12 px corner of an inner panel and the 12 px inset of every such panel (1 + 11), slate-50 / dark
|
|
3579
|
-
slate-900 at 40 %,
|
|
3612
|
+
slate-900 at 40 %, text in the quiet text colour) and the reading panel (a report's content in the side pane and in the DetailList card;
|
|
3580
3613
|
a 12 px corner and a concentric 12 px inset, slate-50 / dark slate-800 at 60 %, slate-700 / dark slate-200 text on 24 px lines;
|
|
3581
3614
|
in the side pane it reserves three reading lines while its report loads, `min-h-24`, the inset twice and three line boxes of its own `p-3` and
|
|
3582
3615
|
`leading-6`: 12 + 3 × 24 + 12 = 96 px at the 16 px root, and its skeleton is three 24 px line boxes, each centring a 16 px bar on the text line it
|
|
3583
3616
|
stands for, so a report of up to three lines keeps the panel's height when it loads, at every root font size).
|
|
3584
3617
|
The scroll bar's business-day bubble is the bordered pill (24 px tall, its text 12 px in = 1 px border + 11 px padding, the radius of its round
|
|
3585
|
-
ends). `src/client/ui/border-style.spec.ts` checks these rules, a source scan included.
|
|
3618
|
+
ends). `src/client/ui/border-style.guard.spec.ts` checks these rules, a source scan included.
|
|
3586
3619
|
- **Motion**: everything that moves animates only transform or opacity under `motion-safe` — the hover lift, the thumbnail fade, the loading pulse (on
|
|
3587
3620
|
a thumbnail frame's skin, only while the frame may show its load activity, see [Attachment display](#attachment-display)), the switch's thumb, the
|
|
3588
3621
|
entrance of the comment delete confirmation and of the error notice, the mobile overlay's slide and its backdrop.
|
|
@@ -3592,7 +3625,7 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3592
3625
|
So only four run until their state ends, each only while something loads or reconnects and none behind a measured key hold: the spinning icon of the
|
|
3593
3626
|
stream's retrying and revalidating badges, the List row's loading skeleton, the side pane's loading skeleton (its author and article lines) and the
|
|
3594
3627
|
waiting pulse of a visible thumbnail frame's skin.
|
|
3595
|
-
The progress pill's dot stands still: the count and the percentage beside it change on every publish. `src/client/ui/motion-policy.spec.ts` allows
|
|
3628
|
+
The progress pill's dot stands still: the count and the percentage beside it change on every publish. `src/client/ui/motion-policy.guard.spec.ts` allows
|
|
3596
3629
|
exactly those places, each with its reason and its count of class positions, in the sources and in the shipped CSS (an `--animate-*` value with
|
|
3597
3630
|
`infinite`, or `animation-iteration-count: infinite`), so a new animation that never ends fails until it is listed with a reason.
|
|
3598
3631
|
The one exception is the mobile overlay layer: its right edge follows the host's pinned-chrome offset (`--aqdr-overlay-right`) over 0.5 s, also
|
|
@@ -3628,7 +3661,7 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3628
3661
|
(the circle and every element inside it, overriding the visual's inline transitions; the bar's circle wrapper; the arrow buttons; the thumb; the
|
|
3629
3662
|
scroll-to-edge buttons and their overlay), so the circle follows a drag without easing and the other parts change state at once.
|
|
3630
3663
|
While the list scrolls (`data-daily-report-scrolling` on the view's scroll container) the hover lift and its shadow are held still.
|
|
3631
|
-
`src/client/ui/motion-policy.spec.ts` checks the shipped CSS, and scans the sources so that no smooth scroll behaviour, no `animate()` call and no
|
|
3664
|
+
`src/client/ui/motion-policy.guard.spec.ts` checks the shipped CSS, and scans the sources so that no smooth scroll behaviour, no `animate()` call and no
|
|
3632
3665
|
inertia option other than one chosen by `inertiaOptionsFor` is written outside `src/client/ui/motion.ts`,
|
|
3633
3666
|
and so that only the files of an allowlist with a written reason ask for an animation frame: the header total's easing, which must read the
|
|
3634
3667
|
reduced-motion preference, and two uses that move nothing (the held key's one move per frame and the attachment grid's track width handed to the
|
|
@@ -3661,7 +3694,7 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3661
3694
|
px inset makes them concentric with their 8 px tabs (12 − 4 = 8).
|
|
3662
3695
|
The only other exceptions are the visually hidden text (Tailwind's `sr-only`, and inside virtualized rows and for every attachment file name the
|
|
3663
3696
|
package's transparent 1 px box, which clips nothing and contains its own layout) and the 48 px the search field keeps free at its end for the
|
|
3664
|
-
searching indicator and the clear button inside its box. `src/client/ui/spacing-ladder.spec.ts` checks every class the package writes, that every
|
|
3697
|
+
searching indicator and the clear button inside its box. `src/client/ui/spacing-ladder.guard.spec.ts` checks every class the package writes, that every
|
|
3665
3698
|
bordered box's border + padding is on the ladder or equals its corner, and that every 12 px-corner panel's inset is 12.
|
|
3666
3699
|
- **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
|
|
3667
3700
|
down to a multiple of the block quantum q (`ml-[max(0px,round(down,calc((100%-72rem)/2),var(--aqdr-lattice-block)))]`, on the column that carries q
|
|
@@ -3991,7 +4024,7 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
3991
4024
|
frame's own style never changes after mount — its classes are its box alone (`relative block w-full`: no variant, no paint, no animation) and its
|
|
3992
4025
|
inline style is that frame style — and every state is drawn inside it, by the skin and the image, which read the frame's attributes.
|
|
3993
4026
|
- **`data-thumbnail-state`** on the frame is `pending` (waiting or loading), `loaded` (the image fades in) or `unavailable` (an icon and
|
|
3994
|
-
`labels.attachmentThumbnailUnavailable` inside the skin; the icon reaches 4.35:1 / 5.56:1 and the label
|
|
4027
|
+
`labels.attachmentThumbnailUnavailable` inside the skin; the icon reaches 4.35:1 / 5.56:1 and the label 7.06:1 / 7.07:1 in the light / dark theme,
|
|
3995
4028
|
and the label breaks its lines by **Line breaks of wrapping labels** in [Selection and focus appearance](#selection-and-focus-appearance)). The
|
|
3996
4029
|
loader's internal phases are not exposed.
|
|
3997
4030
|
- **The waiting pulse runs only where the tile can be seen**: a `pending` frame's skin pulses (under `prefers-reduced-motion: no-preference`) only
|
|
@@ -4037,7 +4070,8 @@ luminance by 0.026 in the light theme, and a `shadow-md` brings the ring down to
|
|
|
4037
4070
|
unchanged, after authorization); the revalidation waits for no settle time and takes no slot, and at most one runs per mounted frame. If it fails,
|
|
4038
4071
|
the page forgets that the URL loaded, counts one failed attempt, and the frame goes on with the retry wait.
|
|
4039
4072
|
A URL whose attempts are all used up shows `unavailable` at once, without a request, for 60 s after its last failure (one rate-limit window); after
|
|
4040
|
-
that,
|
|
4073
|
+
that, the next frame that starts on the URL forgets them and runs a new series of up to the same number of attempts. A URL between attempts keeps
|
|
4074
|
+
its failure count and its retry time across remounts.
|
|
4041
4075
|
- **One writer of `src`**: React never renders the image's `src`. A layout effect projects it from the frame's state before paint on every commit (the
|
|
4042
4076
|
URL while a load is granted and once it has loaded, none while waiting) and writes the attribute only when it differs, because rewriting the same
|
|
4043
4077
|
value makes the browser drop the loaded image and fetch it again.
|
|
@@ -4077,7 +4111,7 @@ DOM hooks are `data-*` attributes, test ids and ARIA; class names are styling an
|
|
|
4077
4111
|
| `[role="treegrid"]` | Each view's composite: `VirtualScroll`'s content element, the view's one Tab stop and the element that holds DOM focus while the keys move; its `id` prefixes the row ids, `aria-activedescendant` names the active row's `id` (written by `@aiquants/virtualscroll`), `aria-rowcount` is the set's size, and `data-aqvs-active-item` is `visible` or `outside` while there is an active row |
|
|
4078
4112
|
| `[data-daily-report-row="<id>"]` | Row frame of both views (`role="row"` with `id`, `aria-rowindex`, `aria-posinset`, `aria-setsize`, `aria-level` and an explicit `aria-selected`; the set size is the declared total while the ids stream delivers and `-1` before it is declared, see **Screen readers**), in every load state |
|
|
4079
4113
|
| `[data-daily-report-active]` | The active row's frame (the row the composite's `aria-activedescendant` names) |
|
|
4080
|
-
| `[data-daily-report-row-surface]` | Surface of a row frame (`role="gridcell"`): its direct child that paints the selection ring and the focus outline, in every load state (opaque and never animated;
|
|
4114
|
+
| `[data-daily-report-row-surface]` | Surface of a row frame (`role="gridcell"`): its direct child that paints the selection ring and the focus outline, in every load state (opaque and never animated; the List's loading skeleton pulses inside it, the DetailList's does not pulse) |
|
|
4081
4115
|
| `[data-daily-report-row-shell]` | The surface of a shell, the card's surface without content: a row mounted outside the visible range whose body is still to come (**Rows mount in two passes**) |
|
|
4082
4116
|
| `[data-daily-report-card]` | List card surface (present while the card is shown) |
|
|
4083
4117
|
| `button[data-daily-report-button="<id>"]` | List card primary button (the click target that selects, and the pointer hand-back target; never a Tab stop, `tabIndex=-1`) |
|
|
@@ -4099,7 +4133,8 @@ DOM hooks are `data-*` attributes, test ids and ARIA; class names are styling an
|
|
|
4099
4133
|
| `[data-daily-report-stream-status]` | The header's ids-stream badge container (persistent; empty while there is nothing to show) |
|
|
4100
4134
|
| `[data-daily-report-status-panel]` | The status screen's panel (also `data-testid="daily-report-status-panel"`), the last of the page's focus successors |
|
|
4101
4135
|
| `[data-daily-report-view-tabs]` | The view-mode tab list (its selected tab is the focus successor of a list without rows) |
|
|
4102
|
-
| `data-
|
|
4136
|
+
| `[data-skeleton]` | A loading placeholder: the List's skeleton card (the surface of a List row whose report is still loading) and the List side pane's placeholder lines (the DetailList's skeleton carries none) |
|
|
4137
|
+
| `data-testid` | `daily-report-root`, `daily-report-list`, `daily-report-detail-list`, `daily-report-panel-group` (the desktop List's panel group: the list and the side pane), `daily-report-side-pane`, `daily-report-error-banner` (the error notice), `daily-report-status-panel` (the status screen's panel, `data-phase` `loading` / `failed`), `daily-report-load-error-retry`, `daily-report-stream-announcer` (the page's status region, never focusable), `daily-report-ids-stream-status` and `daily-report-ids-stream-retry` (the badge's content and its retry button), `daily-report-ids-stream-status-announcer` (the badge's own live regions, rendered only while it announces its phases), and the attachment ids above |
|
|
4103
4138
|
|
|
4104
4139
|
Only the package writes `data-daily-report-keyboard-focus`, `data-daily-report-scrolling`, `data-daily-report-active`, `data-daily-report-row-shell` and `data-daily-report-row-focus`. Hosts may select on them — in tests, or in CSS to adapt
|
|
4105
4140
|
their own content inside a view — but never set them. When the pane content matters, wait for `data-displayed-report-id`, not `data-report-id`: during
|
|
@@ -4305,6 +4340,19 @@ first), `DAILY_REPORT_LABEL_CATALOGS` (the frozen catalogs), `resolveDailyReport
|
|
|
4305
4340
|
| `searchFiltersLoading` | search filter row: the status line while the candidates load | Loading filters... | 絞り込みの候補を読み込み中... |
|
|
4306
4341
|
| `searchFiltersTooMany` | search filter row: the status line after a selection over the filter limits was refused | No more filter values can be selected | これ以上は絞り込みの値を選べません |
|
|
4307
4342
|
| `searchFilterUnavailable` | search filter row: a cell whose host control threw while it rendered | This filter cannot be shown | この絞り込みを表示できません |
|
|
4343
|
+
| `searchInputLabel` | search box: the text field's name (`aria-label`) | Search reports | 日報を検索 |
|
|
4344
|
+
| `searchPlaceholder` | search box: the text field's placeholder | Search | 検索 |
|
|
4345
|
+
| `searchCombineAnd` | search box: the combine toggle's label while it asks for every keyword | AND | AND |
|
|
4346
|
+
| `searchCombineOr` | search box: the combine toggle's label while it asks for any keyword | OR | OR |
|
|
4347
|
+
| `searchCombineAndTitle` | search box: the combine toggle's tooltip (`title`) and description in the AND mode | Show the reports that contain every keyword | すべてのキーワードを含む日報を出す |
|
|
4348
|
+
| `searchCombineOrTitle` | search box: the same in the OR mode | Show the reports that contain any keyword | どれかのキーワードを含む日報を出す |
|
|
4349
|
+
| `searchClear` | search box: the clear button's name (`aria-label`) and tooltip | Clear the search | 検索を消す |
|
|
4350
|
+
| `searching` | search box: the status while a search request runs | Searching... | 検索中... |
|
|
4351
|
+
| `searchNoHits` | search box: the status of an answer without a hit | No report matches | 当たる日報はありません |
|
|
4352
|
+
| `searchSyntaxHelp` | search box: the text field's description and tooltip (`title`) | Separate keywords with spaces. Words in double quotes form one keyword that needs every word. A word in Latin letters also finds its kana reading (denpun finds でんぷん); text found through a reading is underlined with dots. Enter searches, Esc clears. | キーワードは空白で区切ります。二重引用符で囲んだ語は、すべての語を含む 1 つのキーワードになります。英字の語はローマ字の読みでも仮名を探します (denpun → でんぷん)。読みで当たった所は点線の下線で示します。Enter で検索、Esc で消去します。 |
|
|
4353
|
+
| `searchMatchOutsideView` | match indicator: the text of a row that matched in fields the view does not draw | Hidden match | 表示外で一致 |
|
|
4354
|
+
| `searchMatchOutsideViewDescription` | match indicator: that text's tooltip (`title`) and its sentence in the row's hidden match description | Matched in fields this view does not show, such as labels or details | この画面に出していない項目 (ラベル・明細など) で一致しました |
|
|
4355
|
+
| `searchReadingMarkDescription` | marked text: the description of a mark found only through a romaji reading | Matched by its romaji reading | ローマ字の読みで一致 |
|
|
4308
4356
|
| `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 件 |
|
|
4309
4357
|
| `debounce` | header annotation | `(300)` → Debounce: 300 ms | `(300)` → デバウンス: 300 ms |
|
|
4310
4358
|
| `streamProgressCount` | stream badge before the total is known | `(1234)` → Loading 1,234 | `(1234)` → 読み込み中 1,234 件 |
|
|
@@ -4322,6 +4370,11 @@ first), `DAILY_REPORT_LABEL_CATALOGS` (the frozen catalogs), `resolveDailyReport
|
|
|
4322
4370
|
| `searchFieldMatched` | match sentences of a row (the DetailList row's and the side pane's hidden indicator sentences, the List card's hidden match description): a field heading the row's leaves marked with a typed word's mark (`field` is the heading's resolved label) | `("Author")` → Author matched | `("作成者")` → 作成者で一致 |
|
|
4323
4371
|
| `searchFieldMatchedByReading` | the same, for a heading whose marks matched only through a romaji reading | `("Author")` → Author matched by its romaji reading | `("作成者")` → 作成者がローマ字の読みで一致 |
|
|
4324
4372
|
| `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) |
|
|
4373
|
+
| `searchHitCount` | header annotation: the number of the answer's hits, and the search box's status of an answer with hits (digit grouping) | `(1)` → 1 hit; `(1234)` → 1,234 hits | `(1234)` → 1,234 件ヒット |
|
|
4374
|
+
| `searchFailed` | search box: the status of a failed search (`failure` is `invalid` / `busy` / `unavailable` / `failed`) | `("busy")` → The server is busy. Please search again in a moment | `("busy")` → 混み合っています。少し待ってからもう一度検索してください |
|
|
4375
|
+
| `searchMatchedKeywords` | match indicator: how many of an OR answer's keywords the row matched | `(2, 3)` → Match 2/3 | `(2, 3)` → 一致 2/3 |
|
|
4376
|
+
| `searchMatchedKeywordsDescription` | match indicator: the hidden sentence of the same count | `(2, 3)` → Matches 2 of 3 keywords | `(2, 3)` → キーワード 3 個のうち 2 個に一致 |
|
|
4377
|
+
| `searchFiltersFailed` | search filter row: the status line when the candidates could not be loaded (`failure` is `busy` / `unavailable` / `failed`) | `("unavailable")` → Filter options are not available | `("unavailable")` → 絞り込みの候補を使えません |
|
|
4325
4378
|
|
|
4326
4379
|
### External Source Badge Configuration (`sourceTypeConfigs`)
|
|
4327
4380
|
|
|
@@ -4550,8 +4603,9 @@ Documenting a name here is no reason to export it, and the values that tune the
|
|
|
4550
4603
|
`DAILY_REPORT_SHARP_THUMBNAIL_DECODE_BUDGET_BYTES` / `DAILY_REPORT_SHARP_THUMBNAIL_DECODE_WORK_MODEL` (the render deadline `renderMs` and the
|
|
4551
4604
|
decode-work and walk bounds, for a host's conformance checks) / `DAILY_REPORT_ATTACHMENT_THUMBNAIL_OUTPUT_MEDIA_TYPES` /
|
|
4552
4605
|
`DAILY_REPORT_ATTACHMENT_THUMBNAIL_MAX_OUTPUT_BYTES`.
|
|
4553
|
-
- SSE and caching: `
|
|
4554
|
-
with the wire version
|
|
4606
|
+
- SSE and caching: `buildDailyReportRedisKeys(redisKeyPrefix)` (`DailyReportRedisKeys`: the keys the service builds under the prefix, the SSE
|
|
4607
|
+
stream `sseStream` with the wire version, `<prefix>:sse-stream:v4`, and the epoch keys `idsEpoch`, `dateEpochPrefix` and `detailEpochPrefix`,
|
|
4608
|
+
for a host that reads or writes the stream or the epochs itself) / `DailyReportSseReader` /
|
|
4555
4609
|
`readDailyReportSseStreamEntry(raw, logger)` (reads one Redis stream entry, `{ id, message }` as the client returns it, once: `{ entry, json }`,
|
|
4556
4610
|
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
|
|
4557
4611
|
contract) /
|