Commit Graph
168 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