Skip to content

Commit 3fa4e51

Browse files
feat: add PCD entity history (#27)
* feat: add PCD entity history * docs: describe the History section by its final design * style: widen the content column on PCD routes * feat: present entity history changes as readable summaries * style: list related entities as bullets in history * style: show entity type icons for related history links * style: render entity names in history summaries as inline code * feat: show rewritten text fields side by side in history * docs: describe text change presentation in history * feat: diff history text by word and render markdown descriptions * style: colour added and removed tag pills in history * test: assert tag pill tones * style: drop added and removed words from tag pill summaries * fix: reset table row expansion when data changes * feat: add ISO date format and use it in history * feat: locale-aware numeric date format for history * docs: note locale-dependent text under pre-rendering * feat: toggle expandable table rows by clicking the row * feat: show history on quality profile pages * fix: type the aria-label on history type icons
1 parent a9b98ec commit 3fa4e51

50 files changed

Lines changed: 3631 additions & 138 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/backend/content.md‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ Custom format pages also list the quality profiles that score them. References r
2828
application-specific scoring into effective Radarr and Sonarr scores and appear in both HTML and
2929
Markdown representations.
3030

31+
Every detail page ends with a History section: one row per commit that touched the entity (commit
32+
link, change title, date), expandable to the field-level diff and links to the other entities
33+
changed in the same commit. History is compiled from the PCD repo's op log by the pipeline (see
34+
[tooling/pcd.md](../tooling/pcd.md#history)) and appears in both HTML and Markdown representations.
35+
36+
The expanded diff is a list of one-line summaries, not raw fields.
37+
`src/lib/shared/utils/pcd/history-view.ts` has a presenter per change shape that matters (profile
38+
scoring, custom format conditions, regex patterns, profile qualities, tags, quality definition
39+
tiers, and scalar fields), each writing a sentence like "Release Group coffee added" with entity
40+
names as inline code, linked to their pages when they still exist. Plain text fields (regex
41+
patterns, naming formats) carry a word-level inline diff when the edit is small, and a side-by-side
42+
before and after when more than half the text changed, since a rewrite has nothing readable to diff.
43+
Markdown fields (descriptions) are diffed block by block and rendered as markdown: unchanged
44+
paragraphs render as they are, added and removed blocks are marked whole, and a paragraph edited in
45+
place gets word-level highlights. Shapes without a presenter fall back to a line diff of the changed
46+
subtree rendered as YAML, the same YAML the entity export view uses. Changes that display
47+
identically before and after (a tier max size moving between two unlimited values) are hidden.
48+
3149
Seven entity types are browsable:
3250

3351
| Entity Type | Route segment | Arr-specific |
@@ -82,15 +100,19 @@ The PCD pipeline is a pre-build step (`pnpm compile:pcd`) that fetches PCD repos
82100
their SQL operations into SQLite, and extracts entity state as JSON. For implementation details, see
83101
[tooling/pcd.md](../tooling/pcd.md).
84102

85-
The pipeline outputs two things:
103+
The pipeline outputs three things:
86104

87105
1. **Per-database JSON** (`src/lib/data/pcd/{id}.json`) containing full entity data, typed as
88106
`CompiledDatabase` from `src/lib/types/pcd.ts`. Consumed by `+page.server.ts` load functions.
89107

90108
2. **Nav index** (`src/lib/data/pcd/index.json`) containing entity names per database. Consumed by
91109
`+layout.server.ts` to populate the sidebar navigation.
92110

93-
Both outputs are gitignored. The build command is `pnpm compile:pcd && pnpm build`.
111+
3. **Per-database history** (`src/lib/data/pcd/history/{id}.json`) containing each entity's change
112+
log. Consumed by `src/lib/shared/utils/pcd/history-data.ts` for the detail pages and markdown
113+
artifacts. Skipped with `pnpm compile:pcd -- --no-history`.
114+
115+
All outputs are gitignored. The build command is `pnpm compile:pcd && pnpm build`.
94116

95117
## Database Selection
96118

‎docs/backend/llm.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ entity pages and the serializers, so page and artifact cannot drift apart.
200200
sizes in megabytes per minute (the native arr unit; the HTML page's unit dropdown is
201201
display-only). A max of 0, or at or above the arr's slider cap (2000 for Radarr, 1000 for Sonarr),
202202
renders as `Unlimited`.
203+
- **History** (every type): `## History` with a `| Commit | Change | Date |` table, newest first,
204+
the commit linked to GitHub and the change title suffixed with the kind when it is not a plain
205+
update (`Created`, `Renamed`). Field-level diffs stay on the page. Omitted when the entity has no
206+
compiled history. Kind labels and links come from `src/lib/shared/utils/pcd/history.ts`, shared
207+
with the page's History section.
203208

204209
Only detail pages have mirrors. The entity list pages do not, so `/pcd/*` stays in the lint rule's
205210
`pending` list and the detail artifacts are guaranteed by their own build instead: entries derive

‎docs/frontend/seo.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ required for crawlers to see the content.
1818
The rule: if a crawler needs to see it, it goes in the server load function. Anything in `onMount`
1919
or client-side fetch is invisible to crawlers.
2020

21+
### Locale-dependent text
22+
23+
Pre-rendered HTML carries whatever the build machine produced. Text that depends on the visitor's
24+
locale, such as `DateTime` in its `numeric` format, is rendered once at build time with the build
25+
machine's locale and then patched by Svelte during hydration to match the visitor. Crawlers see the
26+
build machine's version, which is why any such element must also expose a locale-independent value:
27+
`DateTime` always sets an ISO `datetime` attribute. Keep locale-dependent formatting out of titles,
28+
descriptions, and other metadata, which are never re-rendered.
29+
2130
## Dynamic Routes
2231

2332
Routes with parameters (e.g. `/wiki/[slug]`) need SvelteKit to know which pages to generate. Two

‎docs/frontend/ui.md‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ pages and the API reference.
158158
`src/lib/client/ui/table/types.ts`: key, header, icon, width, align, sortable). `icon` is an
159159
optional `{ src, alt }` image rendered before the header text. Without `cell`, table cells render
160160
`row[col.key]` directly. `card` renders each row's card-view content; when `href` returns a URL for
161-
a row, both the table row and the card become links.
161+
a row, both the table row and the card become links. `expanded` only reaches the table: cards never
162+
expand, so a card should carry its own summary of whatever the expanded row shows. A row that has
163+
expanded content and no `href` toggles open on click anywhere in the row, not only on its chevron.
162164

163165
```svelte
164166
<script lang="ts">
@@ -473,9 +475,11 @@ headings (`h1` to `h3`) with ids become entries, and the first `h1` (with or wit
473475
a title link back to `#top`. Renders nothing on pages without id'd headings. Headings carrying a
474476
`data-method` attribute get a color-coded HTTP method label (used by the API reference).
475477

476-
No props. Positioning is owned by the root layout, not the component: hidden below 1280px, floated
477-
to the right of the content column, pinned to the viewport (`position: fixed`) with an internal
478-
scrollbar when taller than the viewport.
478+
No props. Positioning is owned by the root layout, not the component: floated to the right of the
479+
content column, pinned to the viewport (`position: fixed`) with an internal scrollbar when taller
480+
than the viewport. The content column is `max-w-3xl` for prose and `max-w-5xl` on PCD routes (tables
481+
and diffs); the panel is hidden below 1280px for the prose column and below 1600px for the wide one,
482+
where the pair would not fit beside the sidebar.
479483

480484
### Dropdown
481485

@@ -554,13 +558,16 @@ Inline label for tags, statuses, and counts.
554558

555559
Renders a formatted `<time>` element with a `datetime` attribute for SEO.
556560

557-
| Prop | Type | Required | Default |
558-
| -------- | ------------------- | -------- | -------- |
559-
| `date` | `string` | yes | |
560-
| `format` | `'short' \| 'long'` | no | `'long'` |
561+
| Prop | Type | Required | Default |
562+
| -------- | -------------------------------- | -------- | -------- |
563+
| `date` | `string` | yes | |
564+
| `format` | `'short' \| 'long' \| 'numeric'` | no | `'long'` |
561565

562-
Short format: "May 17". Long format: "May 17, 2026". Accepts ISO date strings and full ISO
563-
timestamps (as produced by YAML date parsing).
566+
Short format: "May 17". Long format: "May 17, 2026". Both are fixed to en-US. Numeric format is all
567+
digits in the visitor's locale ("5/17/2026" or "17/05/2026"), for dense tables; the prerendered text
568+
uses the build machine's locale and is patched on hydration (see the locale note in
569+
[seo.md](./seo.md#locale-dependent-text)). The `datetime` attribute is always ISO. Accepts ISO date
570+
strings and full ISO timestamps (as produced by YAML date parsing).
564571

565572
### Author
566573

‎docs/tooling/pcd.md‎

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
# PCD Pipeline
22

33
Build-time pipeline that fetches PCD repositories, compiles their SQL operations, and outputs
4-
structured JSON for the website to consume.
4+
structured JSON for the website to consume: the current state of every entity, plus the change
5+
history of every entity derived from the same replay.
56

67
## Source
78

89
```
910
tooling/pcd/
10-
├── index.ts # Entry point, orchestrates fetch -> compile -> extract
11+
├── index.ts # Entry point, orchestrates fetch -> replay -> extract
1112
├── config.json # Database registry
12-
├── fetch.ts # GitHub tarball download and extraction
13-
├── build.ts # In-memory SQLite compilation
13+
├── fetch.ts # git clone and op file to commit mapping
14+
├── build.ts # In-memory SQLite creation and op execution
15+
├── ops.ts # Op file parser (batch header, per-op markers)
16+
├── diff.ts # Structural diff between two extracted entities
17+
├── history.ts # History replay fold (pure)
18+
├── replay.ts # SQLite adapter for the history replay
1419
├── extract.ts # Entity extraction via SQL queries
1520
└── types.ts # Pipeline-internal types
1621
```
@@ -46,28 +51,37 @@ Each entry has:
4651
## Pipeline Flow
4752

4853
```
49-
pnpm compile:pcd
54+
pnpm compile:pcd [-- --no-history]
5055
1. Read config.json
5156
2. For each database:
52-
a. Fetch tarball from GitHub API
53-
b. Read pcd.json manifest from extracted files
57+
a. Clone the repo at its branch (blobless clone, full commit history)
58+
b. Read pcd.json manifest from the checkout
5459
c. Resolve schema version from manifest dependencies
55-
d. Fetch schema tarball (cached if same version as previous database)
60+
d. Clone the schema at that version tag (cached if same version as previous database)
5661
e. Create in-memory SQLite with foreign keys enabled
5762
f. Execute schema ops in numeric filename order
58-
g. Execute base ops in numeric filename order
59-
h. Extract all entity data via SQL queries
60-
i. Write {id}.json to src/lib/data/pcd/
63+
g. Map each base op file to the commit that added it (one git log)
64+
h. Replay base ops one file at a time, recording per-entity history
65+
i. Extract all entity data via SQL queries and assert it matches the replay
66+
j. Write {id}.json and history/{id}.json to src/lib/data/pcd/
6167
3. Write index.json (nav-only data for sidebar)
6268
4. Clean up temp directories
6369
```
6470

71+
With `--no-history`, step g and the per-file bookkeeping in h are skipped: base ops execute in one
72+
pass and only `{id}.json` is written. Pages and artifacts then show no History section.
73+
6574
## Fetching
6675

67-
Repos are fetched as tarballs via the GitHub API
68-
(`https://api.github.com/repos/{owner}/{repo}/tarball/{ref}`). No git required at build time. Schema
69-
tarballs are cached within a pipeline run since multiple databases typically pin the same schema
70-
version.
76+
Repos are cloned with `git clone --filter=blob:none --single-branch --branch {ref}` from
77+
`https://github.com/{owner}/{repo}.git`. A blobless clone downloads the full commit history but only
78+
the checked-out tree's file contents, so one clone serves both the op files and the commit lookup.
79+
Git is required at build time. Schema clones are cached within a pipeline run since multiple
80+
databases typically pin the same schema version.
81+
82+
Commit metadata comes from one `git log --name-only --diff-filter=A -- ops` per repo, mapping each
83+
op file to the commit that added it (hash, author date, subject). An op file with no matching commit
84+
falls back to its `@exportedAt` header and no commit link.
7185

7286
## Schema Resolution
7387

@@ -87,6 +101,41 @@ numeric filename prefix (`0.schema.sql` before `1.languages.sql` before `10.some
87101

88102
No custom SQLite functions are needed. Exported PCD ops use plain SQL with name-based WHERE clauses.
89103

104+
## History
105+
106+
A database repo's `ops/` folder is an append-only log. The first file is a bulk import with no
107+
markers. Every later file is one Profilarr export batch (in practice one commit) with a header
108+
(`-- @name:`, `-- @exportedAt:`, `-- @opIds:`) and each op wrapped in markers naming the entity it
109+
touches:
110+
111+
```sql
112+
-- --- BEGIN op 3587 ( update regular_expression "Special Edition" )
113+
update "regular_expressions" set "pattern" = '...' where "name" = 'Special Edition';
114+
-- --- END op 3587
115+
```
116+
117+
`ops.ts` parses a file into its header, ops (verb, entity type, name, SQL) and, per op, the other
118+
same-type names the SQL mentions (the old name in a rename's WHERE clause). Test entities
119+
(`test_entity`, `test_release`) are ignored: the site does not extract them.
120+
121+
`history.ts` replays the files in order. After each file it re-reads only the entities the markers
122+
touched, using the per-type extractors in `extract.ts` with a name filter, and diffs each against
123+
its previous state (`diff.ts`, which matches array items by name so a changed condition reads as one
124+
change). The kind of each entry comes from the state transition, not the marker verb: absent then
125+
present is `created`, present then absent is `deleted`, both present is `updated`. An entity that
126+
appeared while exactly one name its ops mention disappeared is a `renamed` entry, and the old name's
127+
history moves under the new name; chains through temporary names inside one file resolve to the
128+
original. When a regex or custom format disappears, the custom formats or profiles that referenced
129+
it are re-read too, so cascades are attributed to the file that caused them. A file with no markers
130+
(the bulk import) or an unlabeled op is diffed in full instead, with no related links.
131+
132+
After the last file, a full extraction must deep-equal the replayed state. A mismatch fails the
133+
build naming the differing entities. This is what guarantees a page's History section can never
134+
disagree with the entity it sits under. Entities that no longer exist are dropped from the output,
135+
and related links only point at entities that still exist.
136+
137+
Per database the replay costs about one second on top of the normal compile.
138+
90139
## Extraction
91140

92141
After compilation, the pipeline queries the SQLite database for each entity type with appropriate
@@ -105,13 +154,21 @@ database. These are consumed by `+page.server.ts` load functions for entity deta
105154
`+layout.server.ts` to populate the sidebar. Kept separate to avoid shipping full entity data to
106155
every page.
107156

157+
**Per-database history** (`history/{id}.json`): `EntityHistory` from `src/lib/types/pcd.ts`, keyed
158+
by `{entityType}:{name}` (entity types as they appear in op markers, e.g. `custom_format`,
159+
`radarr_naming`), each a list of `HistoryEntry` in replay order. Lives in its own folder so the
160+
routes' `pcd/*.json` database globs never see it. Loaded by
161+
`src/lib/shared/utils/pcd/history-data.ts`, which tolerates the folder being absent.
162+
108163
## Shared Types
109164

110165
`src/lib/types/pcd.ts` defines the compiled data shape, used by both the pipeline and the SvelteKit
111166
app. `CompiledDatabase` retains both the database manifest version and its pinned schema dependency
112167
version. Key interfaces:
113168

114-
- `CompiledDatabase` - top-level container with metadata and all entity collections
169+
- `CompiledDatabase` - top-level container with metadata (including the source `repo` and `branch`,
170+
used for commit links) and all entity collections
171+
- `EntityHistory`, `HistoryEntry`, `EntityChange` - the per-entity change log
115172
- `CustomFormat` - name, description, tags, conditions (with discriminated union for condition data)
116173
- `QualityProfile` - name, scoring, quality list with groups, languages
117174
- `RegularExpression` - name, pattern, description, tags

0 commit comments

Comments
 (0)