Commit Graph

33 Commits

Author SHA1 Message Date
Salastil 9068a02542 Merge pull request #10 from Salastil/claude/nitter-rss-tweets
Add Nitter (Twitter) source support with dedicated tweet rendering
2026-07-22 22:28:43 -04:00
Claude 7c44552ae0 Rework tweet card click targets: frame->tweet, photo->new tab, video->play
The whole card previously linked to our own /article/[id] page. Per
request, clicking the card frame now opens the original tweet (in a
new tab) instead — there's no separate "full article" view for a
tweet anyway. Clicking a photo opens that image by itself in a new
tab (still resolved through the configured media mode, so proxy mode
doesn't leak the browser's IP to Twitter when viewing the full image
either). Clicking a video's native controls still just plays/pauses
it rather than navigating anywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c1L8ghNBFjfiH64UMViP8
2026-07-23 00:39:30 +00:00
Claude 058f6bbca8 Specify video/mp4 MIME type on tweet video source
Firefox needs an explicit type on <source> to pick a decoder,
especially when the URL's extension is followed by a query string
(?tag=29) rather than ending cleanly in .mp4. fxtwitter's own
response confirms these are always video/mp4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c1L8ghNBFjfiH64UMViP8
2026-07-23 00:30:53 +00:00
Claude 78f59287dc Fix crash rendering tweets published before tweet.media existed
article.tweet?.media.slice(...) only guarded against a missing tweet
object, not a missing media array — pre-existing published tweets in
the DB predate that field and threw "Cannot read properties of
undefined (reading 'slice')" on every category page containing one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c1L8ghNBFjfiH64UMViP8
2026-07-23 00:16:34 +00:00
Claude d4ab690061 Render actual tweet video/multi-image media instead of a single thumbnail
Tweets can carry up to 4 photos/videos/gifs; fxtwitter's media.all
preserves their original order and, for videos, gives a real playable
.mp4 plus a poster thumbnail. TweetCard now renders these as a
1/2/3/4-item grid (Twitter's own layout shapes) with fixed cell
heights so a tall portrait image no longer dictates the whole card's
height in the column view, and video/gif items play back with native
controls instead of showing a static frame. Each item's url (and a
video's thumbnail) still resolves through the configured Nitter media
mode (self-host/proxy/direct) individually.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c1L8ghNBFjfiH64UMViP8
2026-07-23 00:11:20 +00:00
Claude c2b34623d1 Add configurable tweet media hosting mode and fxtwitter base URL
Adds nitterMediaMode (self-host/proxy/direct, default proxy) and
fxtwitterBaseUrl to global settings with a new Retention tab panel.
Tweet images and avatars now resolve through the chosen mode instead
of always being downloaded — proxy mode streams media through a new
SSRF-hardened /media/proxy route (hostname allowlist + DNS-rebinding
defense) so the origin server's IP is never exposed to Twitter's CDN,
direct hotlinks the original URL, and self-host keeps the prior
always-download behavior. fxtwitterBaseUrl lets the enrichment call
target a self-hosted FixTweet mirror instead of the public instance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c1L8ghNBFjfiH64UMViP8
2026-07-22 23:43:52 +00:00
Claude c0d90fc33b Fix multi-word category pages never matching their own articles
/category/x-news filtered by the raw URL slug ("x-news") instead of the
real category name ("X News"), so it never matched merged_articles.category
values for any multi-word category — only worked for the seeded defaults
because they're all single words where the slug and name happen to be
identical once lowercased. Now resolves the slug back to the actual
category name via the site's own category list before filtering.
2026-07-22 22:58:03 +00:00
Claude 8cc256f27d Don't fall back to a favicon for image-less tweets
resolveHeroImage's favicon fallback exists so regular articles never look
entirely bare, but for a tweet it meant an image-less tweet showed the
Nitter instance's own favicon slapped on as if it were the tweet's photo.
publishDirect now skips that fallback specifically for tweet items —
TweetCard.svelte already renders cleanly with no image at all.
2026-07-22 22:17:44 +00:00
Claude bc6e75c124 Fix fxtwitter endpoint URL and confirm response shape against a real call
Was calling /2/status/<id> (no username) based on the originally-given
example; the actual working endpoint is /<handle>/status/<id> (no version
prefix), confirmed via a real curl response. text, created_timestamp, and
author.name/avatar_url all match the assumed shape exactly — only
media.photos[].url remains unverified (that test tweet had no photo), still
guarded by the existing RSS-image fallback either way.
2026-07-22 21:57:48 +00:00
Claude 5cb9e6e4cd Add "Nitter" source type: tweets rendered as a distinct embed card
Nitter list/user RSS feeds are ingested as their own source type, enriched
via fxtwitter (author name/handle/avatar, cleaner text, attached photo) with
a graceful RSS-only fallback when that enrichment fails. Tweets always
publish directly, one per article, and never enter the LLM
clustering/synthesis pipeline — the same bypass already used for YouTube,
since merging unrelated tweets together makes no sense.

Rendering: a new distinct embed-card component (avatar, name + @handle,
full untruncated text, optional attached image, published-date-only
timestamp, no like/retweet stats) replaces the plain article row wherever a
tweet appears, on both the category-page list and the article detail page.

Verified end-to-end against the real sample Nitter RSS feed (served
locally): ingestion (all 100 items, tweet metadata correctly extracted,
retweet/quote-tweet blockquotes correctly excluded from own-content text),
publishing (bypasses clustering, tweet field threaded through to the
published article), and rendering (embed card appears on the homepage feed
and the article detail page, no duplicate title).

Known follow-up: fxtwitter's JSON field names are based on public
documentation, not a verified live response (that API is unreachable from
this sandbox) — worth a real curl check before relying on the enrichment
path in production; the RSS-only fallback path is what's actually been
exercised here.
2026-07-22 21:54:37 +00:00
Salastil 30e3576206 Merge pull request #9 from Salastil/claude/source-management-content-1w2yno
Fix Local category page never showing Local-tagged sources
2026-07-21 21:02:10 -04:00
Claude 41e474653f Fix Local category page never showing Local-tagged sources
/category/local secretly filtered by geo:'philadelphia' instead of the
Local category, a leftover from an old "Local: <region>" colon-syntax
convention that has no admin UI behind it anymore — the Sources tab assigns
plain category names via checkboxes, so a source tagged "Local" never got a
matching geo value and could never show up here, even though it correctly
appeared on Top Stories (which only checks pushToTopStories + the category
array, not geo). Local now filters by category like every other category
page.

Verified: reproduced the exact bug (geo: null despite category: ['Local']),
confirmed /api/feed?geo=philadelphia returns nothing while the new
/api/feed?category=local correctly returns the article.
2026-07-22 01:01:08 +00:00
Salastil 9595cfa33e Merge pull request #8 from Salastil/claude/source-management-content-1w2yno
Stop showing source URLs in the Sources list — they were breaking the…
2026-07-21 20:28:43 -04:00
Claude 8d894c8562 Stop showing source URLs in the Sources list — they were breaking the layout
A long RSS/Google News URL with no natural wrap points would overflow its
grid column and break the whole row layout. The URL is still visible (and
editable) via the edit form — the list row now only shows a status line
when there's something to say (poll result, error, or a just-cleared note).
2026-07-22 00:27:07 +00:00
Salastil 35b04e3fcd Merge pull request #7 from Salastil/claude/source-management-content-1w2yno
Call out the FRONTEND_ORIGIN/ORIGIN match requirement more sharply in…
2026-07-21 20:24:42 -04:00
Claude 5d210c961c Call out the FRONTEND_ORIGIN/ORIGIN match requirement more sharply in the deploy docs
Confirmed by reproduction: a mismatch between backend FRONTEND_ORIGIN and
frontend ORIGIN throws "CORS error: Incorrect 'Access-Control-Allow-Origin'
header is present on the requested resource" on every page, since
SvelteKit's server-side fetch enforces real CORS during SSR. Easy to trip on
since the two values live in separate .env files edited at different times.
2026-07-21 22:46:59 +00:00
Salastil 0a98dc48a5 Merge pull request #6 from Salastil/claude/source-management-content-1w2yno
Switch to adapter-node and document deploying behind Nginx Proxy Manager
2026-07-21 18:36:13 -04:00
Claude 0512428601 Switch to adapter-node and document deploying behind Nginx Proxy Manager
adapter-auto doesn't produce a runnable standalone server when it can't
detect a supported hosting platform (Vercel/Netlify/Cloudflare/etc.) — this
is a self-hosted app with no such platform, so builds were silently missing
a real server output. Swapped to adapter-node, which builds to
build/index.js: a persistent Node server that reads PORT/HOST/ORIGIN at
runtime, exactly what a reverse proxy needs to point a domain at.

Added a README section covering the full path: building/running both apps
as plain Node processes, the env vars each needs, and the Nginx Proxy
Manager side (proxy host + Custom Locations for /api and /media under a
single-domain, path-routed setup, or a simpler two-domain alternative).

Verified live: the adapter-node build actually serves pages and correctly
picks up ADMIN_PANEL_ENABLED via `node --env-file=.env build/index.js`
(SvelteKit's $env/dynamic/private reads process.env directly in production,
unlike the vite-dev-time gap from the previous fix).
2026-07-21 22:33:44 +00:00
Salastil f7d4851ad5 Merge pull request #5 from Salastil/claude/source-management-content-1w2yno
Claude/source management content 1w2yno
2026-07-21 16:24:37 -04:00
Claude 56bb2e6b1b Add a redirect for bare /admin to /admin/settings
Only /admin/login and /admin/settings existed as actual pages, so navigating
to /admin itself 404'd regardless of ADMIN_PANEL_ENABLED. Settings already
redirects to login on a 401, so that's the sensible default landing spot.
2026-07-21 20:23:26 +00:00
Claude fd12f11cd7 Fix ADMIN_PANEL_ENABLED never being read from frontend/.env
process.env.ADMIN_PANEL_ENABLED was always undefined in the running
SvelteKit server process — Vite only injects VITE_-prefixed vars into
process.env for server-side code; plain vars in frontend/.env never reached
it, so the admin panel stayed disabled (cog hidden, /admin/* 404s) no matter
what the .env file said. Switched to SvelteKit's own $env/dynamic/private,
which reads it correctly in dev, preview, and adapter-based deployments.

Reproduced and verified the fix against a real frontend/.env file (not an
inline shell var, which is what masked this the first time).
2026-07-21 20:22:09 +00:00
Salastil 4ab56da5c5 Merge pull request #4 from Salastil/claude/source-management-content-1w2yno
Replace admin username/password with a per-launch API key, and disabl…
2026-07-21 16:06:17 -04:00
Claude 619515db96 Replace admin username/password with a per-launch API key, and disable the admin panel by default
Two hardening changes beyond just a password:

- The admin panel no longer uses stored credentials at all. The backend
  generates a random API key on every startup and prints it to its own
  console (never through the DB-backed logger, since that's only reachable
  from inside the panel this key protects). Every /api/admin/* request must
  carry it as an X-Api-Key header, checked with a timing-safe comparison on
  every call — there's no session to create or steal, and restarting the
  backend invalidates the previous key immediately. The old admin_users and
  sessions tables, scrypt password hashing, and cookie-based session plumbing
  are removed entirely (dropped via migration for existing installs, not
  left behind unused). The login page keeps its existing layout but now asks
  for this key and explains where to find it, storing it in the browser's
  localStorage rather than relying on a server session.

- The admin panel (the masthead's cog icon and the /admin/* pages
  themselves) is now disabled by default on every deployment, gated by a new
  frontend-only ADMIN_PANEL_ENABLED env var. This is a separate, UI-only
  visibility control — the API key above is what actually protects the
  backend regardless of this flag.
2026-07-21 20:03:48 +00:00
Salastil aee1f2467e Merge pull request #3 from Salastil/claude/source-management-content-1w2yno
Add a per-source "Push to Top Stories?" opt-in, off by default
2026-07-21 15:26:52 -04:00
Claude 64ef671c5e Add a per-source "Push to Top Stories?" opt-in, off by default
Every ingested article used to show up on the homepage regardless of its
source, which meant a handful of high-volume feeds could flood "Top Stories."
Sources now default to not appearing there; a source has to explicitly opt
in via a new checkbox (also toggleable inline with a star icon) for its
articles to show up on the homepage feed. An article shows there if any of
its contributing sources opted in — merged/clustered stories aren't held to
requiring all sources to agree. Category pages, Local, tags, and events are
unaffected; this only gates the bare, no-filter homepage query.

Schema: sources.push_to_top_stories and merged_articles.top_stories, both
backfilled for existing databases via ALTER TABLE.
2026-07-21 19:13:36 +00:00
Salastil 0f0aa7037a Merge pull request #2 from Salastil/claude/source-management-content-1w2yno
Fix source deletion, add content clearing, editable multi-category sources, News category, wider layout, and a YouTube module
2026-07-21 14:57:37 -04:00
Claude d469f00292 Resolve YouTube @handles and vanity URLs to a channel ID automatically
YouTube's public Atom feed only accepts a channel_id (or the legacy user
param) — it has no equivalent for the newer @handle format, so pasting a
handle URL straight into the source's url field wouldn't have worked. The
adapter now accepts a bare channel ID, a /channel/UC... URL, an @handle URL,
or a bare handle/username, resolving whichever was given to the actual
channel ID by reading it off the channel page when needed.
2026-07-21 18:54:43 +00:00
Salastil ab2584f51a Merge pull request #1 from Salastil/claude/source-management-content-1w2yno
Fix source deletion, add content clearing, multi-category/editable so…
2026-07-21 14:28:46 -04:00
Claude e204c70e00 Fix source deletion, add content clearing, multi-category/editable sources, News category, wider layout, and a YouTube source module
- Fix "Body cannot be empty" error on DELETE by making the JSON content-type
  parser tolerate empty bodies, and by only sending Content-Type from the
  frontend when a request actually has one.
- Deleting a source now cascades: raw content items and any article composed
  entirely from that source are removed too, plus their media.
- Add admin endpoints/UI to clear all articles, all media, or a single
  source's content without deleting the source, so things can be repopulated
  fresh.
- Sources can now be assigned multiple categories via checkboxes (instead of
  free text) and edited in place, not just added/deleted.
- Add a "News" default category (seeded fresh, backfilled on existing DBs) so
  general news sources have a real home instead of the pseudo-category "Top
  stories", which is just the homepage's all-categories chronological view.
- Widen the site's content column 15% (1080px -> 1242px).
- Add YouTube as its own source type/ingestion module: pulls a channel's
  public Atom feed, and each video always publishes directly as its own
  article (title, embedded video, publish date, description) rather than
  going through the cross-source clustering/synthesis pipeline.
2026-07-21 18:09:20 +00:00
Salastil b742320108 Many Improvements 2026-07-21 12:51:31 -04:00
Salastil d16cd508b5 Log pending but not eligible items 2026-07-17 10:49:09 -04:00
Salastil a5b108d9fa Initial Commit 2026-07-17 10:33:20 -04:00
Salastil a5d35515f2 Initial commit 2026-07-17 10:15:17 -04:00