Commit Graph
100 Commits
Author SHA1 Message Date
thalladaandClaude Opus 5.5 ddc98788c6 Require 80% shared title words for same-site related duplicates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeJmv3WED4w8S3GV7mMjS7
2026-10-12 19:26:37 +00:00
thalladaandClaude Opus 5.5 682fc4a6fb Drop re-published copies of one story from related lists
The ingest dedupe only merges within one run, so a story an outlet
re-publishes on another day under a new URL and a reworded headline lands
in the archive twice and both copies showed up as related links. Links now
also skip an article from the same site as the pick or a closer link when
60% of their title words match, besides exact title matches anywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeJmv3WED4w8S3GV7mMjS7
2026-10-12 18:50:24 +00:00
thalladaandClaude Opus 5.5 08b15c42b4 Simplify related articles: shared article header, ids only, no job
- Both article pages render one `_article_head.html` partial from an
  `ArticleHead` built by a single constructor instead of two copies.
- `related::for_article` returns ids; the cosine stays stored for tuning.
- Title keys travel with each candidate, dropping the side map.
- Drop the `related-refresh` dashboard job: the backfill is a one-off and
  `features related` covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeJmv3WED4w8S3GV7mMjS7
2026-10-12 16:11:43 +00:00
thalladaandClaude Opus 5.5 03f0a454be Add related articles under web issue articles and keep embeddings forever
Each pick's web page now lists up to five related articles: the nearest
embedded articles across the whole archive (cosine >= 0.55), excluding the
rest of the issue, excerpt-only articles and same-title copies. They are
computed once per issue after publishing and stored in issue_related
(migration 0014), since the full embedding scan is too slow per page view;
`features related [--date]` and the related-refresh job backfill older issues.

Related articles that ran in an issue link to that issue page; the rest link
to a new signed-in /articles/{id} page rendering the stored body with no
summary. /articles/{id} for a published article redirects to its issue page.

embedding_retention_days = 0 now means keep every vector (previously it would
have deleted every unrated, unpublished embedding) and is the new default, so
related lists can reach the whole archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeJmv3WED4w8S3GV7mMjS7
2026-10-11 21:34:31 +00:00
thalladaandClaude Opus 5.5 bf6a8d0129 Flatten X4 tables that hold images or nested tables
CrossPoint and CrossInk print an image's alt text instead of drawing it
inside a table cell, and CrossInk drops all text of a nested table.
Layout tables (The Space Review's figures) lost every image on the X4:
59 images across 8 articles in the published issues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:13:21 +00:00
thalladaandClaude Opus 5.5 7851d8e231 Add an X4 preview tool that renders pages with the real XTC converter
Builds both editions around one article (a URL through real extraction, or
an HTML file), runs the XTC converter on the X4 edition and decodes every
page to PNG. --convert re-renders an existing EPUB, for iterating on a
stylesheet. The decoder follows the converter's encoder, which writes the
level's low bit to the first plane, rather than the spec's table.

The device itself reads the X4 EPUB through its firmware, which lays out
differently. Check those layouts with the firmware's own host tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:53:06 +00:00
thalladaandClaude Opus 5.5 5abf062c24 Add rebuild: republish an issue's EPUBs from its stored content
`daily-epub rebuild --date D` (and the rebuild-YYYY-MM-DD job) takes the
issues.issue_json snapshot and restores each article's body from its row.
It then builds both editions and the XTC, publishes them under the same
names and records the issue as a run would. Nothing is curated again or
refreshed from live rows, so headlines, summaries, social lines and rating
links stay as published; only images are downloaded again. It takes the
run lock, opens the database without migrating, and the job is marked
dangerous like generate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:53:06 +00:00
thalladaandClaude Opus 5.5 9e521ddaef Lay out X4 code blocks as one paragraph per line
The X4 firmwares (CrossPoint and forks such as CrossInk) ignore
white-space and treat <pre> as inline. Its margins never applied, so the
paragraph after a listing started on the next line. CrossInk also glued
the first code line onto the paragraph before it. The simplifier now makes
each listing line a <p class="code-line">, the one path both firmwares lay
out identically. The first and last lines carry the block's margins.
Inline tags open across a line break are closed and reopened on the next
line.

A blank source line used to be a paragraph of its own. With the firmware's
default paragraph spacing that read as a double newline, so blank lines
now become a half-line gap (p.code-gap) on the line before.

The X4 theme matches the standard one but uses only the device's exact
levels (#000, #555, #aaa), so glyphs are not dithered. Inline code is
display: inline for the XTC converter's CREngine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:53:06 +00:00
thalladaandClaude Opus 5.5 50551114cd Richer monochrome syntax theme and code-block layout in the standard EPUB
Following zenbones and lackluster, the theme keeps what the code names in
ink (functions bold, types italic) and moves the language's own syntax to
grey (keywords bold grey, literals and punctuation grey). Comments are the
lightest. Weight and slant carry the distinctions, so nothing depends on
colour.

Code blocks stay left-aligned inside a <figure>, which centres for image
captions, and have explicit margins. Inline code is display: inline because
CREngine readers (KOReader, PocketBook) make it a block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:53:06 +00:00
thalladaandClaude Opus 5.5 13c81440ef Seed a hinted and an unhinted code block
The dev server now shows both highlighting paths: a Rust block marked by
its source, and a Python block left to detection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:27:23 +00:00
thalladaandClaude Opus 5.5 248038ec97 Style syntax tokens: print colours on the web, monochrome in the EPUBs
The web uses ink with a few print colours: oxblood keywords, bottle-green
strings, umber literals, slate types, muted italic comments. Each is
defined as an --hl-* token, and all reach at least 4.5:1 on paper-2 in
both themes. The EPUBs rely on weight and slant (bold keywords, grey
italic comments), using flat selectors for the X4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:27:23 +00:00
thalladaandClaude Opus 5.5 78fc38e99d Highlight code blocks in article bodies
arborium (tree-sitter) renders each token as a flat <span class="hl-…">,
which the X4's no-descendant-selector stylesheet can still target.

The sanitizer strips class attributes, so language hints never reached
the database. annotate_hints now copies them onto <pre data-lang> before
readability and sanitizing: pre/code classes, Rouge and GitHub wrapper
divs, SyntaxHighlighter's brush:, and data-lang style attributes.
ammonia keeps only canonical values.

Highlighting runs at render time for the web article page and both EPUB
editions, so stored articles benefit too. Unhinted blocks are guessed
from content (JSON that parses, shebangs, leading <script>/<style>, then
betlang at >= 0.5). A borderline block also takes the article's prevailing
language when that language is among its top candidates. Anything else
stays plain, since wrong colours are worse than none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:27:23 +00:00
thalladaandClaude Fable 5.1 8d0516bedc Show table-centric dashboard pages at full height
Only the profile page's version history keeps the 70vh scrolling island;
it has three sections below it. Everything else is paginated or last on
its page, so the page itself scrolls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 22:16:07 +00:00
thalladaandClaude Fable 5.1 e5ca78ba41 Size the wide table wrapper to its table
The wrapper spanned the whole wide track while the table sat centred inside
it, so the empty flanks were part of the scroll container and caught the
wheel. A centred grid item is sized fit-content, so the wrapper now hugs the
table; the page-column floor and viewport cap move onto the wrapper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 22:02:36 +00:00
thalladaandClaude Fable 5.1 3c4f8de3a4 Keep the dashboard's vertical rhythm under the grid
Grid items never collapse margins, so the row-filter input and the rating
form pushed the next child down by their bottom margins.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 21:43:42 +00:00
thalladaandClaude Fable 5.1 dd91b83c27 Badge lists wrap on their own; note table floors in the doc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 21:29:03 +00:00
thalladaandClaude Fable 5.1 815e7307d1 Rebuild app.css for the merged templates; keep the ratings timestamp on one line
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 21:19:07 +00:00
thallada 9af6fedbe0 Merge branch 'tables-tpl' into dashboard-tables 2026-09-20 21:13:01 +00:00
thalladaandClaude Fable 5.1 6016745297 Let dashboard tables wrap and use the whole viewport
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 21:12:22 +00:00
thalladaandClaude Fable 5.1 8a355b637f Give every dashboard table cell a content class
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 21:08:32 +00:00
thalladaandClaude Fable 5.1 8889a4e351 Add the dashboard table framework design and Codex briefs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 20:54:12 +00:00
thallada 0f17480be7 Tweak feed.json structuring 2026-09-19 18:53:50 +00:00
thallada 525ddfd88d Add feed.json alternate to feed.xml
Mainly so my glance web dashboard can read the feed.
2026-09-19 18:22:16 +00:00
thallada f1b1e7fd98 Tweak styling of interest tags on web and in epubs 2026-09-17 18:50:01 +00:00
thalladaandClaude Fable 5.1 ce9b629115 Show matched interests as a tag row instead of a callout
With the why line gone the accent-bar callout held nothing but "Matches: a · b",
which no longer earned its quoting. Every surface now puts one row directly
under the rubric: a small-caps "Matches" label and one pill chip per interest.
On the web that is one `.interest-chip` component (admins still get links to
the interest's article filter); the EPUB gets `.matches`/`.interest` rules in
both stylesheets, and `Understanding::interests_line` goes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESJbbzQ1b8fs5AA65tKw2q
2026-09-15 01:09:14 +00:00
thalladaandClaude Fable 5.1 36424784af Drop the editor's "Why it's here" line
After a rewrite of the prompt rule and a few more live issues the line still
read as a second abstract, so it goes entirely: the rule and the "why" key in
the editor's JSON, the parsing and 14-word cap, the Pick field, the
issue_articles and candidate_runs writes, the Brief prompt's per-pick why,
the explain output, the dashboard "editor" columns, and the rendering on the
web index, article and public pages, the Atom feed and both EPUB pages.

The "Matches: …" interests line stays (restyled in the next commit). The
issue_articles.why and candidate_runs.editor_why columns remain in the
schema unused; stored issue snapshots that still carry "why" deserialize
because Pick ignores unknown fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESJbbzQ1b8fs5AA65tKw2q
2026-09-15 00:59:13 +00:00
thalladaandClaude Fable 5.1 cd2bbe8840 justfile: list interests-categorize among the jobs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 17:57:49 +00:00
thalladaandClaude Fable 5.1 8b56a65bad Rebuild app.css for the Interests page classes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 17:57:49 +00:00
thalladaandClaude Fable 5.1 1089c29179 Merge branch 'interests'
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:55:07 +00:00
thalladaandClaude Fable 5.1 7063b835db interests backfill: use cached vectors when the Voyage key is unset
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:55:06 +00:00
thallada 57ba47a5a0 Merge branch 'interests-web' into interests 2026-09-13 05:49:25 +00:00
thalladaandClaude Fable 5.1 861ae6cb5e Interests dashboard page, Articles interest filter, and interest links (steps 5-7)
Every interest name in the web UI links admins to the Articles page
filtered and sorted by that interest's stored matches; the Interests page
lists weights derived from current ratings and manages categories.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:49:25 +00:00
thalladaandClaude Fable 5.1 af70da1493 Reuse an existing category's spelling in the categorizer; document the Interests page (step 9)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:45:53 +00:00
thalladaandClaude Fable 5.1 055b1df769 Write match rows per run, categorize interests, and add the interests CLI (steps 4 and 8)
The signals stage upserts each eligible article's matched interests; an
LLM batch files uncategorized interests before the prompt is built (and on
demand as the interests-categorize job); `interests import` and
`interests backfill` load the OPML and seed match rows once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:44:49 +00:00
thalladaandClaude Fable 5.1 22cfb34788 Add the first-class interests plan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:22:25 +00:00
thallada 154731a003 Merge branch 'interests-step3' into interests 2026-09-13 05:20:16 +00:00
thalladaandClaude Fable 5.1 a78e44f56e Make the interests table the only source of standing interests (step 3)
The OPML file and the profile's ## Interests section become one-time
import inputs; the prompt groups by the stored category and the OPML
config key is gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:20:16 +00:00
thalladaandClaude Fable 5.1 2d857e3e10 Cut weak interest matches and add the rating-driven affinity signal (step 2)
An interest matches an article only when it is in the top three by z and
z >= 1.0, so the Matches line, the stored rows and the weights agree. The
new bounded affinity signal blends each matched interest's rating-derived
weight, gated on attributable ratings like feed affinity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:20:05 +00:00
thalladaandClaude Fable 5.1 f0c0927ab8 Add the interests table and module (first-class interests, step 1)
Standing interests get a table of their own plus article_interests, the
per-run top-3 matches, and a pure rates() that derives each interest's
Beta-smoothed weight from current ratings the way feed affinity does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:00:56 +00:00
thallada e82f7b0d13 Merge branch 'layout-align' 2026-09-12 04:50:48 +00:00
thalladaandClaude Fable 5.1 4338dc1a99 One article header everywhere: rubric, why block, then the summary
The web index, the article page and both EPUB pages each ordered the same
metadata differently, and the "understanding" line was one wall of text
with the why line above it on one page and below it on another. Every
surface now reads title, meta, rubric, why block, summary:

- the rubric is a newspaper kicker (topic group and format in small caps)
  followed by the specific topics; depth and technicality stay on the
  dashboard article page only
- the accent-bar why block holds the editor's line and "Matches: …" (the
  matched interests) beneath it
- the social line moves after the summary on the article page and chapter,
  matching the index pages, and the EPUB chapter footer no longer carries
  the understanding line

`understanding_line` becomes an `Understanding { kicker, topics, interests }`
struct shared by the web view models and the EPUB, and the three web
templates include one `_understanding.html` partial.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxctvrEPQZ9pX58LRgGYNZ
2026-09-12 04:50:48 +00:00
thalladaandClaude Fable 5.1 37ddddf867 Ask the editor for a reader-specific "why" instead of a second abstract
Across 96 picks in four live issues the "Why it's here" line was a 12-word
restatement of the summary printed next to it: one used the second person,
five mentioned the reader at all. Rule 8 now says what the line is for, what
it must not do (describe the piece, list the interests shown beneath it), and
gives a bad/good pair.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxctvrEPQZ9pX58LRgGYNZ
2026-09-12 04:45:09 +00:00
thalladaandClaude Fable 5.1 71079a93f5 Stop summaries ending in "(Excerpt.)"
The summary prompt told the model "if the text is a truncated excerpt,
summarize only what is there and say it is an excerpt". Any article cut
to the 3k-token budget tripped that clause, and the model complied with
a terse "(Excerpt.)" label on the abstract. Bodies the extractor could
only excerpt also carried the reader-facing "(excerpt only — read
online)" note into the prompt, which the model mirrored.

Separate the two cases: text truncated for length gets no caveat, and
genuine feed-teaser bodies (the article's excerpt_only flag, now passed
in as SummarySource) are labelled "opening excerpt only" with a request
for one plain closing clause. Strip the extractor's note from the prompt
text, forbid labelling the abstract outright, and scrub a short trailing
"(Excerpt…)" parenthetical from the response as a guard.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnYokPekNmU7LNSf6rdYAk
2026-09-10 19:11:45 +00:00
thallada 31d2690701 Merge branch 'index-author' 2026-09-09 18:12:38 +00:00
thalladaandClaude Fable 5.1 1a009a2113 Name the author ahead of the feed on issue index lines
Every list of articles now reads "Author · Feed · Publication · N min":
the signed-in issue page, the public issue page, the RSS feed and the
EPUB's In This Issue page. `author_label` drops an author that is empty
or only repeats the feed's title, as a dozen personal blogs do. Article
pages keep their own byline.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXYGBPoHZSDSfE5WcJ9bvj
2026-09-09 18:12:34 +00:00
thalladaandClaude Fable 5.1 c538afa540 Unwrap Scour's /r/rss/ redirector when canonicalizing URLs
Scour interest feeds link every entry through
https://scour.ing/r/rss/<percent-encoded article URL>, so the wrapper
became the article's canonical URL: the same post arriving via Hacker
News and Scour never merged, and the publication fallback showed
"scour.ing" instead of the site's own domain. Resolve the wrapper like
the Google News redirector. `publication_label` re-canonicalizes the
stored URL so the 477 articles saved with the wrapper show the real
domain too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXYGBPoHZSDSfE5WcJ9bvj
2026-09-09 18:12:30 +00:00
thalladaandClaude Fable 5.1 f2a44507d4 Show the publication after the feed name, and link feeds to Miniflux for admins
Readers see "Feed · Publication" on the issue, article, public and RSS
pages and in the EPUB index and chapter headers; the publication is the
page's site name, else its domain, and is dropped when it would repeat
the feed's title. Admins get the feed name linked to its Miniflux page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWmCpUojfHXhSZ2129Z7Nv
2026-09-09 06:01:29 +00:00
thallada 87f3a5dbf1 Merge branch 'miniflux-feed-link' into publication 2026-09-09 05:48:46 +00:00
thalladaandClaude Fable 5.1 f71a4c2799 Keep the publication an article's page declares
Readability already reads og:site_name and JSON-LD publisher.name; carry
that through Page and Extracted and store it in articles.publication, the
same way the page byline is kept. `publication_label` is what the readers
will see after the feed name: the site name, else the domain, and nothing
when it would only repeat the feed's own title.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWmCpUojfHXhSZ2129Z7Nv
2026-09-09 05:48:46 +00:00
thalladaandClaude Fable 5.1 8a8d5d1f16 Add a browser-facing Miniflux URL and link feeds to their entries page
The pipeline talks to Miniflux on loopback, so links meant for a person
need their own base. `[miniflux].public_url` defaults to base_url, and
`feed_url` builds the web UI's `/feed/{id}/entries` page, which the feeds
dashboard now uses instead of the non-existent `/feeds/{id}` route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWmCpUojfHXhSZ2129Z7Nv
2026-09-09 05:45:14 +00:00
thalladaandClaude Fable 5.1 c8df55e91a Keep the contents sidebar where it was across page loads
Every navigation is a full document load, so the sidebar came back at
scrollTop 0 and revealCurrent() smooth-scrolled the current chapter into
view: a jump on every click even though the link was already in view.

app.js saves the panel's scrollTop to sessionStorage (large screens only,
keyed by the issue) on link clicks and pagehide; theme.js, which already
runs before first paint, restores it as soon as the parser has closed the
nav. The load-time reveals (initial and the first scroll-tracking paint on
the full issue page) are now instant; opening the mobile panel and
tracking while scrolling stay smooth.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXYGBPoHZSDSfE5WcJ9bvj
2026-09-09 05:27:02 +00:00
thalladaandClaude Fable 5.1 c707f432f2 Render the web site at 110% by default
The operator reads the site at 110% browser zoom; make that the default
scale. Tailwind v4 utilities are rem-based, so `html { font-size:110% }`
scales type, spacing, columns and controls together the way zoom does,
while media queries keep firing at the same viewport widths.

Things that did not scale with the root size and had to move:
- the `lg` reader grid is now 15rem + gap-x-8 (was 16rem + gap-x-10) so
  the TOC sidebar and the 68ch article still fit at 1024-1100px;
- the TOC reveal margin in app.js is 1.5rem of the computed root size
  instead of a fixed 24px, and the run funnel SVG height is `h-3`;
- long unbreakable tokens on the phone-width dashboard (settings group
  ids and headings, config warnings, run warnings) now wrap with
  `overflow-wrap:anywhere`, and the feed candidates table sits in the
  same `.scroll-x` wrapper as the other dashboard tables. Overview and
  feeds already overflowed a 390px viewport before this change; settings
  started to. All three fit now.

Verified with before/after screenshots of 27 pages at 390-1440px on the
seeded dev database: no page body scrolls horizontally, the reader
layout, TOC sidebar and dashboard match the old 110%-zoom rendering.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXYGBPoHZSDSfE5WcJ9bvj
2026-09-09 04:50:08 +00:00
thalladaandClaude Fable 5.1 b02e1c7b8c Add an AI slop verdict that downrates the author's future articles
A fourth explicit verdict, "AI slop" (Vote::Slop, label `slop`), joins the
rating widget on the web, the EPUB footer links, the /r/ confirmation page,
the CLI, imports and the dashboard filters. It counts as a full negative
(curation.feedback.slop_value, -1.0) in the neighbour and affinity signals.

Beyond that, each run loads the authors whose current verdict is slop, with
no lookback, and multiplies the preliminary blend and the utility of every
candidate by that author by 1 - curation.ranking.slop_author_penalty (0.75),
so they sink before triage. The flag is recorded in signals_json, shown by
`explain` and the dashboard signals table, and the confirmation names the
author (or says no author is known, in which case only the rating applies).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXYGBPoHZSDSfE5WcJ9bvj
2026-09-09 03:36:50 +00:00
thalladaandClaude Fable 5.1 c6a9a98a0d Move the understanding line under "Why it's here" on the article page
It read as an afterthought at the bottom; next to the editor's reason it
is part of the framing before the summary and body.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-08 20:35:02 +00:00
thalladaandClaude Fable 5.1 bedbcb218f Show what the pipeline understood about each article
A muted one-line note now follows every article: the deep assessment's
topic group, format, depth and technicality, the specific topics it
extracted, and the reader interests the article matched best, e.g.

  Software engineering · analysis essay · in depth · highly technical ·
  Topics: copy-on-write, ZFS · Interests: Filesystems, Rust

It renders on the web article page footer, on the public and private
issue-page entries, in the EPUB article footer and in the EPUB "In This
Issue" index (both editions), always styled like the existing meta lines
so it stays secondary to the content.

`Pick` gains `top_interests` (serde default, no migration), filled from
the signals stage when the editor assembles the lineup; the facets were
already on the issue snapshot via `Pick.llm`, so earlier issues show the
facet and topic parts and only future issues carry interests. One shared
builder, `understanding_line` in epub::chapters, feeds every surface.

The dev seed gives four picks facets and interests so the line can be
eyeballed locally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-08 19:44:00 +00:00
thalladaandClaude Fable 5.1 7ce8c65939 Take the author from the article page, not the aggregator submitter
Readability already finds a byline (JSON-LD author, meta author tags,
byline markup); keep it on the article and let it replace an aggregator
entry's author, which for HN and friends is the submitter. A direct feed's
own author is still trusted over the page, and an aggregator name is only
used at all when no direct feed carried the story. The author is stored on
articles so it survives independently of the best entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 18:34:21 +00:00
thalladaandClaude Fable 5.1 41c144bf20 Let prose tables scroll instead of breaking words in cells
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 17:38:08 +00:00
thallada 8365b7093e Merge branch 'fix/author-affinity' 2026-09-07 17:34:59 +00:00
thallada b66a8eefa5 Merge branch 'fix/x4-pre' 2026-09-07 17:34:59 +00:00
thalladaandClaude Fable 5.1 a52f4123dd Credit ratings to the author, and less to aggregator feeds
A rating on an article that arrived only via an aggregator (HN, Lobsters,
Reddit, Scour) used to count fully against that aggregator feed. Now the
aggregator feed gets a quarter of the credit and the article's author gets
the full credit, so future articles by the same author from any feed carry
the history. The feed signal is the mean over rated direct feeds and the
rated author.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 17:34:40 +00:00
thalladaandClaude Fable 5.1 20991fedfc Keep article pages within the viewport on phones
Long unbroken strings, inline code tokens and many-column tables could push
the reader column wider than a phone screen, causing horizontal scrolling
(and a jump while the browser's URL bar collapses). Let text wrap anywhere
inside the reader column and turn prose tables into horizontal scroll
containers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 17:34:40 +00:00
thalladaandClaude Fable 5.1 0212e407e5 Make code-block whitespace explicit in the X4 edition
The X4 firmware collapses whitespace inside <pre> regardless of
white-space: pre-wrap, so code blocks came out as one run-on line. Turn
newlines into <br/>, tabs and indentation into no-break spaces, and drop the
newline right after <pre> as browsers do.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 17:34:40 +00:00
thallada 72b32dc69b Add justfile
Makes running commands easier.
2026-09-07 06:08:33 +00:00
thallada 56d2d74a97 Fix styling of Feeds dashboard overflowing container 2026-09-07 06:08:08 +00:00
thalladaandClaude Fable 5.1 81cbbb407f Add the Feeds dashboard page for reviewing feed candidates
Ranked by the shrunk mean of the linked articles' telemetry and ratings,
with Add (into a chosen Miniflux category) and Dismiss per row, a status
filter, a nav tab and an overview tile.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 04:45:37 +00:00
thallada c0f086e0da Merge branch 'fd-core' into feed-discovery 2026-09-07 04:31:24 +00:00
thalladaandClaude Fable 5.1 2bbb4809b7 Discover feeds behind aggregator-only articles
A best-effort pipeline stage after social enrichment asks Miniflux to
discover the feeds behind each aggregator-only article, validates every
result by sniffing the body, and records the survivors as feed candidates
with a per-host memo. Ranking is a shrunk mean over the linked articles'
existing telemetry and ratings. `daily-epub feeds discover` runs the same
pass over recent articles for seeding.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 04:31:13 +00:00
thalladaandClaude Fable 5.1 c971fd9317 Register the [discovery] section in settings and document it
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 04:17:29 +00:00
thalladaandClaude Fable 5.1 4a2fc6d1c4 Add the [discovery] config section for feed discovery
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 04:09:37 +00:00
thalladaandClaude Fable 5.1 e69940fac8 Fix 404 on issue downloads and make the BookOrbit link admin-only
Download hrefs were built with form encoding, which turns the spaces in
"The Daily EPUB - <date>.epub" into "+"; in a URL path that is a literal
plus, so /files/epub/<name> never matched the file. They now use the
same path-segment percent-encoder the OPDS feed already uses, and the
download-menu test follows the rendered link and expects 200.

The Read in BookOrbit button renders only for admins, and the
/issues/{date}/read redirect answers 403 to other signed-in users.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 02:39:06 +00:00
thalladaandClaude Fable 5.1 78d9ef5959 Serialize the config tests that touch the process environment
`toml_then_env_layering` sets DAILY_EPUB_* variables through figment's
Jail while other tests call `Config::load`, which reads them; under the
parallel test runner that intermittently failed
`registry_validation_rejects_bad_roles_kinds_and_efforts`. Every test on
either side of that race now holds one mutex.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 02:31:25 +00:00
thallada 5079d9c008 Merge branch 'followups': password-change redirect and requested usernames 2026-09-07 02:28:16 +00:00
thalladaandClaude Fable 5.1 e56acfb9dc Carry next through the password change and let requesters pick a username
A forced or voluntary password change now redirects to the validated
`next` target (the page the user was heading for, or the home page)
instead of landing back on the account form. Login and the
must-change-password middleware pass the destination along as
/account?change=1&next=... and the form carries it as a hidden field.

The request-access form gains a required Username field validated with
the account rules and rejected when an account or another open request
already holds it (case-insensitive). Migration 0009 stores it on the
request; the notification email and the Users dashboard show it, and
the Approve form is prefilled with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 02:28:09 +00:00
thallada a3a041dc06 Merge branch 'approve': dashboard account approval and forced password change 2026-09-07 01:43:20 +00:00
thalladaandClaude Fable 5.1 49a2de14ae Approve access requests from the dashboard with an emailed temporary password
Each open access request on /dashboard/users gains an Approve form with
a suggested username. Approving creates a user-role account with a
20-character random temporary password, emails it to the requester with
the sign-in link, and marks the request done; if the email fails the
account is deleted so the admin can retry. Approval refuses when mail is
not configured.

Migration 0008 adds users.must_change_password. A middleware on the
signed-in routers sends flagged users to /account?change=1 until they
set a new password; login honours the flag regardless of `next`, and the
CLI's `users passwd` clears it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 01:43:10 +00:00
thallada 502ba38607 Merge branch 'mail': SMTP mail, access-request notifications, request throttle 2026-09-07 01:24:39 +00:00
thalladaandClaude Fable 5.1 df76c81b59 Add SMTP mail, access-request notifications, and a shared request throttle
A new [mail] section configures an SMTP relay (SES-style STARTTLS or
implicit TLS; the password is environment-only via
DAILY_EPUB_MAIL__SMTP_PASS) and src/mail.rs wraps lettre in a small
plain-text Mailer built once at server start. Each stored access request
now emails mail.notify_to in a spawned task with the address, reason,
time, and a link to /dashboard/users. POST /request-access shares the
login endpoint's per-IP limiter (server.login_attempts per window).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-07 01:24:27 +00:00
thallada feb887a8af Merge branch 'fix-request-access': public account request flow
# Conflicts:
#	src/web/static/app.css
2026-09-06 18:04:19 +00:00
thalladaandClaude Fable 5.1 87ff3d92c8 Let public visitors request a reader account
New public page GET/POST /request-access explains what a signed-in
reader account gets (complete issues online, EPUB/XTC downloads, no
ratings or admin tools) and takes an email plus an optional reason. A
honeypot field drops bots and a partial unique index keeps one open
request per email, updating it on resubmit. Open requests show on
/dashboard/users with a "Mark done" button and as a tile on the
overview; accounts are still created with the daily-epub users CLI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-06 18:02:18 +00:00
thallada 8c821527e8 Merge branch 'fix-neighbours': any-article neighbours and wider enums 2026-09-06 17:52:47 +00:00
thalladaandClaude Fable 5.1 f0529d2d01 Show any-article nearest neighbours and widen the format/kind vocabularies
The article dashboard page gains a "Nearest articles (any)" table: the
ten closest stored embeddings by cosine, regardless of rating or run,
via a brute-force scan of article_embeddings. The deep-assessment format
facet grows from 5 to 14 values (code_repository, documentation_reference,
tool_or_product_page, discussion_thread, paper_or_report,
interview_or_transcript, video_or_podcast, fiction_or_humor, other) and
the triage kind from 10 to 16 (repo, docs, discussion, paper, media,
fiction), so a GitHub repository is no longer forced into analysis_essay.
Both prompt versions bump to 2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-06 17:52:37 +00:00
thallada af8850e948 Merge branch 'fix-import': historical rating imports 2026-09-06 17:31:47 +00:00
thalladaandClaude Fable 5.1 d94cefcbd7 Import historical ratings from arbitrary URLs as a background job
The Ratings page gains an "Import ratings" form: paste URLs (one per
line), choose a verdict and an optional note. Rows land in the new
rating_imports table and the new import-ratings job (same systemd job
template as the rest of the catalogue) canonicalizes each URL, reuses or
fetches + extracts the article, embeds it with Voyage when enabled, and
appends an explicit rating event with source "import". Per-URL status
shows on the Ratings page; the job page's journal is the live log.

Imported articles have no entry row (best_entry_id NULL, feed "Imported")
and no sources, so they act as rated neighbours without touching the
feed prior. The CLI's rating-event construction moves to rate::record_explicit
and the dashboard job start path is shared as jobs::start_job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-06 17:31:41 +00:00
thallada 7036fd347e Merge branch 'fix-ui': dashboard links and download menu 2026-09-06 17:18:31 +00:00
thalladaandClaude Fable 5.1 71665c293c Add admin dashboard links on article pages and a single download menu
Signed-in issue and article pages now carry an admin-only link to the
article's dashboard page. The separate Download EPUB / X4 EPUB / XTC
buttons become one "Download EPUB" button (Standard edition first) with a
no-JS <details> menu listing every available format and its size.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
2026-09-06 17:18:23 +00:00
thalladaandClaude Fable 5.1 142a8d9905 Merge branch 'bookorbit': Read in BookOrbit link on the issue page
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-05 05:20:34 +00:00
thalladaandClaude Fable 5.1 9aff78ebf1 Add the Read in BookOrbit button and /issues/{date}/read redirect
The signed-in issue page shows a Read in BookOrbit button (only when the
integration is active and the Standard EPUB exists) that hits
/issues/{date}/read. The route redirects to the cached BookOrbit reader
URL, or resolves the ids through OPDS on first click and caches them;
?refresh=1 re-resolves. Not indexed yet is a 503, upstream failures 502.
Includes the implementation plan.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-05 05:20:34 +00:00
thallada e370fcaeb2 Merge branch 'bookorbit-b' into bookorbit 2026-09-05 05:20:34 +00:00
thalladaandClaude Fable 5.1 0f45c34fdb Add the BookOrbit OPDS client module
Searches BookOrbit's OPDS catalog for an issue's Standard edition and
extracts the book/file ids the web reader route needs. Hand-rolled Atom
parsing, no new dependencies; unit-tested against the real feed shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-05 05:20:34 +00:00
thalladaandClaude Opus 5 a72f036bd1 Fix a speculation rule that excluded every URL
The query-string exclusion was written as the pattern string "/*\?*",
meaning "any path, then a literal ?, then anything". It never meant that.
A URL pattern string is split into components before escapes are resolved,
so the ? still ends the pathname: the pattern parses as pathname `/*` with
search `*`, and search `*` matches the empty query too. Sitting inside a
`not`, it excluded every same-origin URL, so both rules reduced to
`(matches) AND NOT (everything)` and no link was ever speculated.

Filter on the query with the component form instead, whose parts are
separate by construction. Name `pathname` alongside `search`: an object
that omits it inherits `/` from the document URL and matches nothing —
the same silent failure wearing different clothes.

Also prerender /issues, which `/issues/*` does not cover.

The MDN example this was modelled on, "/*\?*(^|&)add-to-cart=*", does
work, because what follows the ? lands in the search component and says
something there. Only the bare * collapsed.

Verified by evaluating the shipped JSON through URLPattern: /, /issues,
/issues/<date> and its chapters prerender; ?page=2, /logout, /rate and
/static/* do not; /dashboard* prefetches. Covered by a test that rejects
a ? in any href_matches string and a `search` component without its
`pathname`; it fails on the old rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164rQrMUZkf7uV1VGYuCFy4
2026-09-05 04:42:25 +00:00
thalladaandClaude Fable 5.1 0851d56459 Add [bookorbit] config, settings section and cached reader ids
New BookorbitConfig section (enabled, URLs, OPDS user; password via
DAILY_EPUB_BOOKORBIT__OPDS_PASS), its settings-dashboard group and secret
masking, migration 0005 adding bookorbit_book_id/bookorbit_file_id to
issues with Db::set_bookorbit_ids, and README rows for the new keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-05 04:36:08 +00:00
thalladaandClaude Opus 5 6c2d74c734 Stop routine excerpt fallbacks from degrading every run
Every issue since 2026-08-15 landed as `degraded`, because the extraction
stage warned on any fetch failure at all and a few always fail: the worst
of the 21 published days was 15% (33 of 220 articles), the median under
11%. A status every run carries says nothing, so warn only past a 30%
share -- roughly twice the worst day seen -- and log the rest. The exact
count was already in `counts.excerpt_only` either way.

`rebuild()` also stored the bumped profile version before reading the
interests OPML, so a rebuild that failed on a missing file would mark the
profile fresh for another week having never rewritten its text. Read the
prompt inputs first, ahead of both the model call and the writes, so a
failure stays due and costs nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164rQrMUZkf7uV1VGYuCFy4
2026-09-05 04:08:06 +00:00
thalladaandClaude Fable 5.1 17a88043ff Break Server-Timing into sess, db and tpl metrics
`app` alone said how long the origin took; it could not say where. The
header now also carries `sess` (the session/auth layers around the
routed stack), `db` (SQLite statement time, statement count in `desc`)
and `tpl` (askama rendering), each only where it was measured.

`db` needs a tracing layer: sqlx-sqlite runs statements on a worker
thread and only reports elapsed time there, inside the span the caller
handed it. The new `web::timing` module hangs the request's metrics on a
per-request span and reads them back off sqlx's `sqlx::query` event.
The layer's filter admits every span up to DEBUG, which is what lets it
see the request span past the middleware spans below it; the module docs
spell out the process-wide cost of that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-05 03:12:02 +00:00
thalladaandClaude Fable 5.1 9c4964ae27 cargo fmt
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-05 02:02:44 +00:00
thalladaandClaude Fable 5.1 1e362fee3e Batch the ratings page's per-row queries
The Current tab ran three statements per rated article: two for
`get_article` (the article and its social rows) and one for the newest
explicit `rating_events` source. That is fine on the dev seed and
linear in a production ratings table, and per-statement overhead, not
SQLite's work, was most of the page's origin time.

Collect the article ids up front, load them with the batched
`Db::get_articles` added for the issue page, and resolve every source
in one windowed query per chunk of 500 ids. Semantics are unchanged:
ties still break on `id DESC`, and an article with no explicit event
still renders an empty source.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-05 02:02:04 +00:00
thalladaandClaude Fable 5.1 f6b2d3ea9a README: HTTP/3 listeners in the reverse-proxy config
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-05 01:35:22 +00:00
thalladaandClaude Fable 5.1 e727d53b85 Cut per-request origin work: batched article loads, no session write per page
The origin was 3–8 ms per page, almost all of it per-statement overhead:
an issue page ran two statements per pick (~55 on a 25-article issue) and
every signed-in page wrote its session row back because `take_flash`
called `Session::remove`, which marks the session modified even when the
key is absent.

- `Db::get_articles` loads an issue's articles and their social rows in
  two statements; both branches of `web::issue::load` use it. The
  single-id and batch queries share one projection via a macro.
- `take_flash` reads before removing, and touches a signed-in session at
  most once a day so the inactivity expiry still slides. Anonymous
  requests never create a session.
- `Server-Timing: app;dur=<ms>` on every response, outermost layer.
- `reject_early_data`: 425 for a non-safe method that arrived as TLS 0-RTT
  data, so nginx `ssl_early_data on` is safe (RFC 8470 §5.2).
- `[profile.release]`: fat LTO, one codegen unit (binary 46 → 29 MB).

Docs: the Cloudflare proxy was retired on 2026-09-05 after measuring
+43 ms per signed-in page from Boston; README reverse-proxy section is
now the direct setup (upstream keepalive, 0-RTT lines) and the CDN
runbook carries a retired-status banner.

Dev seed, app-side: `/` 21 → 5 statements, 3.7 → 1.0 ms; `/feed.xml`
44 → 12, 9.1 → 3.0 ms; session writes per signed-in page 1 → 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-05 01:34:39 +00:00
thalladaandClaude Fable 5.1 bc40773964 Runbook: cache rules resolve last-match-wins, so bypass goes last
The first rollout put the cookie bypass rule above the cache rule, as
the runbook said to, and signed-in requests were served the cached
anonymous page. Cloudflare applies the last matching rule for each
setting. Fold the cookie exclusion into the cache rule's expression so
order stops mattering, and explain the symptom in the verify step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-04 22:32:45 +00:00
thalladaandClaude Fable 5.1 f634f06bf1 Settle on a five-minute public cache and drop the purge rationale
The edge-purge integration was reverted in the previous commit; this
finishes the job. Public pages, the feed and /issues.json now say
`public, max-age=300` for browsers and shared caches alike, the code
comments and README explain why the longer s-maxage plus purge design
was dropped, and the rollout runbook loses its purge step, marks the
HTML cache rule optional, and warns to keep the record DNS-only until
the Universal SSL certificate is active.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
2026-09-04 22:04:07 +00:00
thallada de332cd4b3 Revert "Add a [cdn] section and purge the edge after publishing"
This reverts commit 41fe61a691.
2026-09-04 22:00:06 +00:00