Files
adminandClaude Opus 5 4983662514 Poll-Takt auf 15 Minuten und Revisionshistorie fuer Items
Der 2-Stunden-Takt verlor ganze Artikel. Ein RSS-Feed ist ein Fenster,
kein Archiv: ist das Fenster kuerzer als der Poll-Abstand, fallen
Eintraege zwischen zwei Abrufen heraus und sind nicht nachholbar.
Gemessen am 2026-09-04 haelt zeit.de 0,6 h vor, spiegel und welt rund
5 h; von der Zeit sah man etwa jeden dritten Artikel. Der bedingte GET
macht haeufiges Pollen billig, ein unveraenderter Abruf ist ein 304
ohne Body.

Zweitens schrieb der Upsert Aenderungen nicht fort. Redaktionen aendern
Ueberschriften nach der Veroeffentlichung; bei gleicher GUID lief das in
ein `on conflict do nothing` und blieb unsichtbar. raw_items haelt jetzt
den aktuellen Stand und zaehlt Revisionen, raw_item_versionen haelt jede
Fassung einschliesslich der ersten. Erkannt wird das ueber einen
Inhalts-Hash aus Titel, Teaser und URL.

Die Migration zieht Bestandsdaten nach. Die SQL-Hashformel ist
zeichengleich zu poller.inhalt_hash(), geprueft an 814 migrierten Items:
null Scheinrevisionen beim ersten Lauf danach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZAdZb3EifGb9nvQ5Zp2TE
2026-09-05 18:17:07 +02:00

81 lines
3.0 KiB
PL/PgSQL

-- Migration 001: Revisionshistorie fuer Items
--
-- Fuer bestehende Installationen. Bei einer Neuinstallation deckt schema.sql
-- das bereits ab; diese Datei ist dann ein No-op.
--
-- psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f migrations/001-revisionen.sql
begin;
alter table raw_items
add column if not exists inhalt_hash text,
add column if not exists revisionen integer not null default 0,
add column if not exists zuletzt_geaendert timestamptz;
-- Bestandsdaten nachziehen. Die Formel muss zeichengleich zu
-- poller.inhalt_hash() sein, sonst gilt beim naechsten Lauf jedes Altitem
-- als geaendert. Trennzeichen ist U+001F (unit separator).
update raw_items
set inhalt_hash = md5(coalesce(titel, '') || E'\x1f' ||
coalesce(teaser, '') || E'\x1f' ||
coalesce(url, ''))
where inhalt_hash is null;
alter table raw_items alter column inhalt_hash set not null;
create index if not exists raw_items_revidiert_idx
on raw_items (zuletzt_geaendert desc) where revisionen > 0;
create table if not exists raw_item_versionen (
id bigserial primary key,
raw_item_id bigint not null references raw_items(id) on delete cascade,
gesehen_am timestamptz not null default now(),
slot timestamptz not null,
titel text not null,
teaser text,
url text,
inhalt_hash text not null,
unique (raw_item_id, inhalt_hash)
);
create index if not exists raw_item_versionen_item_idx
on raw_item_versionen (raw_item_id, gesehen_am);
-- Erstfassung der Bestandsitems eintragen, damit die Historie vollstaendig ist.
-- gesehen_am auf abgerufen_am setzen, nicht auf now(): die Fassung ist alt.
insert into raw_item_versionen (raw_item_id, gesehen_am, slot, titel, teaser,
url, inhalt_hash)
select id, abgerufen_am, slot, titel, teaser, url, inhalt_hash
from raw_items
on conflict (raw_item_id, inhalt_hash) do nothing;
alter table poll_laeufe
add column if not exists items_geaendert integer not null default 0;
drop view if exists titel_revisionen;
create view titel_revisionen as
select r.id, r.quelle, r.url, r.revisionen,
count(*) as fassungen,
(array_agg(v.titel order by v.gesehen_am))[1] as erste_fassung,
(array_agg(v.titel order by v.gesehen_am desc))[1] as letzte_fassung,
min(v.gesehen_am) as erstmals,
max(v.gesehen_am) as zuletzt
from raw_items r
join raw_item_versionen v on v.raw_item_id = r.id
where r.revisionen > 0
group by r.id, r.quelle, r.url, r.revisionen;
create or replace view quellen_stillstand as
select q.id, q.name, max(r.abgerufen_am) as letztes_item,
now() - max(r.abgerufen_am) as stille
from quellen q
left join raw_items r on r.quelle = q.id
where q.aktiv
group by q.id, q.name
having max(r.abgerufen_am) is null
or now() - max(r.abgerufen_am) > interval '6 hours'
order by stille desc nulls first;
commit;