Compare commits

..
2 Commits
Author SHA1 Message Date
irrlichtandClaude Sonnet 5 8a1ab8a146 Poll-Takt von 15 Minuten auf 2 Stunden senken
Reduziert den Traffic gegen die Quellen. slot_von() rundet jetzt generisch
auf TAKT-Grenzen statt fest auf 15-Minuten-Slots, die Stillstands-Schwelle
in quellen_stillstand steigt von 6h auf 24h, damit der Watchdog nicht schon
nach drei verpassten Slots anschlaegt. GDELT-Vergleich aus den Kommentaren
entfernt, da die Slots nicht mehr mit dessen 15-Minuten-Paketen zusammenfallen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013vHZCwcCJnz4LKNgvT4bxv
2026-09-04 20:34:22 +02:00
irrlichtandClaude Sonnet 5 c43d8496bf Branding auf Wurzelwerk (SWP) umstellen, Deployment systemagnostisch machen
Ersetzt "Lagebild" durch den Projektnamen Wurzelwerk (Stinkwurzpresse) in
allen Units, Configs und Docs. Entfernt konkrete Hostnamen (Bebop, Pi 5,
Debby) aus README/NOTES zugunsten generischer Rollenbezeichnungen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013vHZCwcCJnz4LKNgvT4bxv
2026-09-04 20:28:57 +02:00
12 changed files with 85 additions and 77 deletions
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+1 -1
View File
@@ -9,6 +9,6 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY poller.py sources.toml ./
USER ingest
# Ohne Argumente: Dauerbetrieb im 15-Minuten-Takt, Senke aus $DATABASE_URL.
# Ohne Argumente: Dauerbetrieb im 2-Stunden-Takt, Senke aus $DATABASE_URL.
# -u, damit die Logs unmittelbar im journal landen und nicht im Puffer haengen.
ENTRYPOINT ["python", "-u", "/app/poller.py"]
+22 -22
View File
@@ -1,4 +1,4 @@
# Ingest
# Wurzelwerk — Ingest
Stufe 0 und 1 der Pipeline: Quellenregister und Roh-Erfassung. Keine
Kodierung — was hier landet, soll sich beliebig oft neu kodieren lassen.
@@ -10,54 +10,54 @@ Kodierung — was hier landet, soll sich beliebig oft neu kodieren lassen.
| `sources.toml` | Quellenregister, einzige Wahrheit ueber die Quellen |
| `schema.sql` | Postgres-Schema (`quellen`, `raw_items`, `codings`, `poll_laeufe`) |
| `poller.py` | Poller, zwei Senken: Postgres und gzip-JSONL |
| `Containerfile` | Image fuer Bebop |
| `lagebild-ingest.container`, `lagebild.network` | Quadlet-Units, rootless |
| `Containerfile` | Image fuer den Produktivbetrieb |
| `wurzelwerk-ingest.container`, `wurzelwerk.network` | Quadlet-Units, rootless |
| `ingest.env.example` | Vorlage fuer die Zugangsdaten |
## Bebop
## Deploy mit Postgres + Podman/Quadlet (rootless)
```sh
# 1. Schema einspielen
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f schema.sql
# 2. Image bauen
podman build -t localhost/lagebild-ingest:latest .
podman build -t localhost/wurzelwerk-ingest:latest .
# 3. Konfiguration ablegen
mkdir -p ~/.config/lagebild
cp ingest.env.example ~/.config/lagebild/ingest.env
cp sources.toml ~/.config/lagebild/sources.toml
chmod 600 ~/.config/lagebild/ingest.env
$EDITOR ~/.config/lagebild/ingest.env
mkdir -p ~/.config/wurzelwerk
cp ingest.env.example ~/.config/wurzelwerk/ingest.env
cp sources.toml ~/.config/wurzelwerk/sources.toml
chmod 600 ~/.config/wurzelwerk/ingest.env
$EDITOR ~/.config/wurzelwerk/ingest.env
# 4. Quadlet installieren
mkdir -p ~/.config/containers/systemd
cp lagebild-ingest.container lagebild.network ~/.config/containers/systemd/
cp wurzelwerk-ingest.container wurzelwerk.network ~/.config/containers/systemd/
systemctl --user daemon-reload
systemctl --user start lagebild-ingest
journalctl --user -u lagebild-ingest -f
systemctl --user start wurzelwerk-ingest
journalctl --user -u wurzelwerk-ingest -f
# Damit der Dienst ohne offene Sitzung weiterlaeuft:
loginctl enable-linger "$USER"
```
`sources.toml` ist eingehaengt, nicht ins Image gebacken: eine neue Quelle ist
ein `systemctl --user restart lagebild-ingest`, kein Neubau.
ein `systemctl --user restart wurzelwerk-ingest`, kein Neubau.
## Pi 5 — Schatten-Ingest
## Schatten-Ingest auf einem Zweitgeraet
Ohne Postgres, ohne Container:
```sh
python3 poller.py --sink jsonl:/var/lib/lagebild/schatten
python3 poller.py --sink jsonl:/pfad/zu/deinem/ablageordner
```
Legt `<dir>/YYYYMMDD/YYYYMMDDHHMMSS.jsonl.gz` an, dedupliziert ueber eine
SQLite daneben. Als `systemd --user`-Unit mit `Restart=always` einrichten.
Der Sinn ist nicht Redundanz um ihrer selbst willen: RSS-Feeds haben kein
Archiv. Steht Bebops Poller einen Tag still, ist dieser Tag unwiederbringlich
weg. Kodierungen lassen sich wiederholen, Rohdaten nie.
Archiv. Steht der Produktiv-Poller einen Tag still, ist dieser Tag
unwiederbringlich weg. Kodierungen lassen sich wiederholen, Rohdaten nie.
## Betrieb
@@ -101,8 +101,8 @@ ueber die gesamte Historie, ohne dass ein Feed erneut abgerufen wird.
## Verhalten
- **15-Minuten-Takt**, an den Slotgrenzen ausgerichtet (+30 s Versatz),
identisch zu GDELTs Paketgrenzen — damit bleiben die Slots vergleichbar.
- **2-Stunden-Takt**, an den Slotgrenzen ausgerichtet (+30 s Versatz), um
unnoetigen Traffic bei den Quellen zu vermeiden.
- **Bedingter GET** ueber ETag/Last-Modified, im Register gespeichert. Im
Test antworteten 4 von 12 Quellen im zweiten Durchlauf mit 304.
- **Dedup** ueber `unique (quelle, item_key)`; `item_key` ist guid, ersatzweise
@@ -130,5 +130,5 @@ spiegel | derstandard | 0.92 | Niederlage fuer Donald Trump - Gericht in Missou
- Telegram-Fetcher (Telethon/MTProto). Register ist vorbereitet, Eintraege
stehen auf `aktiv = false`. Die Bot-API kann fremde Kanaele nicht lesen.
- Ersatz-URL fuer den BR24-Feed (aktuell 404).
- Quellenpool von 12 auf ~40 erweitern; Regionalzeitungen ueber SearXNG auf
Debby suchen.
- Quellenpool von 12 auf ~40 erweitern; Regionalzeitungen ueber SearXNG
suchen.
+22 -18
View File
@@ -1,44 +1,48 @@
# swp-01-ingest
# swp-01-ingest — Wurzelwerk
Stufe 0/1 der Lagebild-Pipeline: Quellenregister und Roh-Erfassung
(RSS-Poller, Senken Postgres oder gzip-JSONL). Details zu Betrieb,
Verhalten und offenen Punkten stehen in [`NOTES.md`](NOTES.md).
Stufe 0/1 der Wurzelwerk-Pipeline (Stinkwurzpresse, SWP): Quellenregister
und Roh-Erfassung (RSS-Poller, Senken Postgres oder gzip-JSONL). Details zu
Betrieb, Verhalten und offenen Punkten stehen in [`NOTES.md`](NOTES.md).
## Deploy (Bebop, Postgres + Podman/Quadlet)
## Deploy (Server mit Postgres + Podman/Quadlet)
```sh
# 1. Schema einspielen
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f schema.sql
# 2. Image bauen
podman build -t localhost/lagebild-ingest:latest .
podman build -t localhost/wurzelwerk-ingest:latest .
# 3. Konfiguration ablegen
mkdir -p ~/.config/lagebild
cp ingest.env.example ~/.config/lagebild/ingest.env
cp sources.toml ~/.config/lagebild/sources.toml
chmod 600 ~/.config/lagebild/ingest.env
$EDITOR ~/.config/lagebild/ingest.env
mkdir -p ~/.config/wurzelwerk
cp ingest.env.example ~/.config/wurzelwerk/ingest.env
cp sources.toml ~/.config/wurzelwerk/sources.toml
chmod 600 ~/.config/wurzelwerk/ingest.env
$EDITOR ~/.config/wurzelwerk/ingest.env
# 4. Quadlet installieren
mkdir -p ~/.config/containers/systemd
cp lagebild-ingest.container lagebild.network ~/.config/containers/systemd/
cp wurzelwerk-ingest.container wurzelwerk.network ~/.config/containers/systemd/
systemctl --user daemon-reload
systemctl --user start lagebild-ingest
journalctl --user -u lagebild-ingest -f
systemctl --user start wurzelwerk-ingest
journalctl --user -u wurzelwerk-ingest -f
# Damit der Dienst ohne offene Sitzung weiterlaeuft:
loginctl enable-linger "$USER"
```
`sources.toml` ist eingehaengt, nicht ins Image gebacken: eine neue Quelle
ist ein `systemctl --user restart lagebild-ingest`, kein Neubau.
ist ein `systemctl --user restart wurzelwerk-ingest`, kein Neubau.
## Deploy (Pi 5, ohne Postgres/Container)
## Deploy (Zweitgeraet ohne Postgres/Container, z. B. als Schatten-Ingest)
Fuer ein schlankes Zweitsystem ohne Postgres und ohne Container genuegt
Python 3.11+ mit den Paketen aus `requirements.txt`:
```sh
python3 poller.py --sink jsonl:/var/lib/lagebild/schatten
python3 poller.py --sink jsonl:/pfad/zu/deinem/ablageordner
```
Als `systemd --user`-Unit mit `Restart=always` einrichten (siehe
`lagebild-robots.service` / `.timer` fuer den woechentlichen Robots-Check).
`wurzelwerk-robots.service` / `.timer` fuer den woechentlichen
Robots-Check).
+3 -3
View File
@@ -1,10 +1,10 @@
# Nach ~/.config/lagebild/ingest.env kopieren und ausfuellen (chmod 600).
# Nach ~/.config/wurzelwerk/ingest.env kopieren und ausfuellen (chmod 600).
#
# Bei rootless Podman erreichen sich Container ueber ein gemeinsames Netz per
# Containername. Liegt Postgres bereits in einem Netz, dort denselben Namen
# eintragen wie in der Zeile Network= der Quadlet-Datei.
DATABASE_URL=postgresql://lagebild:GEHEIM@lagebild-pg:5432/lagebild
DATABASE_URL=postgresql://wurzelwerk:GEHEIM@wurzelwerk-pg:5432/wurzelwerk
# Kontaktadresse gehoert in den User-Agent: wer unsere Abrufe sieht, soll
# wissen, wen er anschreiben kann.
INGEST_USER_AGENT=lagebild-ingest/0.1 (+https://DEINE-DOMAIN/lagebild; kontakt@DEINE-DOMAIN)
INGEST_USER_AGENT=wurzelwerk-ingest/0.1 (+https://stinkwurzpresse.de/wurzelwerk; kontakt@stinkwurzpresse.de)
-6
View File
@@ -1,6 +0,0 @@
# Ablegen unter ~/.config/containers/systemd/lagebild.network
[Unit]
Description=Lagebild internes Netz
[Network]
NetworkName=lagebild
+11 -10
View File
@@ -1,16 +1,16 @@
#!/usr/bin/env python3
"""Ingest-Poller fuer den deutschen Ereignisdatensatz.
Liest die Quellen aus sources.toml, holt alle 15 Minuten die Feeds und legt
Liest die Quellen aus sources.toml, holt alle 2 Stunden die Feeds und legt
jedes neue Item roh ab. Bewusst ohne Kodierung: was hier landet, soll sich
beliebig oft neu kodieren lassen.
Zwei Senken:
--sink postgres://... Bebop, die eigentliche Datenbank
--sink jsonl:/pfad/dir Pi 5, Schatten-Ingest als gzip-JSONL pro Slot
--sink postgres://... Produktivbetrieb, die eigentliche Datenbank
--sink jsonl:/pfad/dir Schatten-Ingest als gzip-JSONL pro Slot
Aufrufe:
poller.py --sink "$DATABASE_URL" # Dauerbetrieb, 15-Minuten-Takt
poller.py --sink "$DATABASE_URL" # Dauerbetrieb, 2-Stunden-Takt
poller.py --sink "$DATABASE_URL" --once # ein Durchlauf
poller.py --check-robots # Lizenzstatus nachpruefen
"""
@@ -38,9 +38,9 @@ from email.utils import parsedate_to_datetime
HIER = pathlib.Path(__file__).parent
USER_AGENT = os.environ.get(
"INGEST_USER_AGENT",
"lagebild-ingest/0.1 (+https://example.org/lagebild; kontakt@example.org)",
"wurzelwerk-ingest/0.1 (+https://stinkwurzpresse.de/wurzelwerk; kontakt@stinkwurzpresse.de)",
)
TAKT = 15 * 60 # Slotlaenge in Sekunden, wie bei GDELT
TAKT = 2 * 60 * 60 # Slotlaenge in Sekunden
VERSATZ = 30 # Sekunden nach der Slotgrenze, nicht exakt darauf
NS = {
"a": "http://www.w3.org/2005/Atom",
@@ -66,9 +66,10 @@ def _jetzt():
def slot_von(ts):
"""Auf 15 Minuten abrunden, wie GDELTs Paketgrenzen."""
return ts.replace(second=0, microsecond=0,
minute=(ts.minute // 15) * 15)
"""Auf TAKT-Sekunden seit Epoch abrunden (bei 2 h also volle gerade Stunden)."""
sekunden = int(ts.timestamp())
return dt.datetime.fromtimestamp(
(sekunden // TAKT) * TAKT, tz=dt.timezone.utc)
def norm_titel(t):
@@ -184,7 +185,7 @@ class JsonlSenke:
"""Schatten-Ingest fuer den Pi: gzip-JSONL, ein Verzeichnis pro Tag.
Kein Schema, keine Abfragen - nur die Garantie, dass die Rohdaten noch
da sind, wenn Bebop einen Tag lang stillstand. Die SQLite daneben haelt
da sind, wenn der Produktiv-Poller einen Tag lang stillstand. Die SQLite daneben haelt
nur die gesehenen Schluessel, damit nicht bei jedem Slot der komplette
Feed erneut geschrieben wird.
"""
+4 -3
View File
@@ -48,7 +48,7 @@ create table if not exists raw_items (
autor text,
pubdate timestamptz, -- Angabe der Quelle, kann fehlen/luegen
abgerufen_am timestamptz not null default now(),
slot timestamptz not null, -- auf 15 min abgerundet, GDELT-kompatibel
slot timestamptz not null, -- auf den Poll-Takt abgerundet
sprache text not null default 'de',
roh jsonb not null, -- alle geparsten Feldwerte, unveraendert
@@ -113,7 +113,8 @@ create table if not exists poll_laeufe (
create index if not exists poll_laeufe_slot_idx on poll_laeufe (slot desc);
create index if not exists poll_laeufe_quelle_idx on poll_laeufe (quelle, begonnen_am desc);
-- Welche aktive Quelle hat seit ueber sechs Stunden nichts Neues geliefert?
-- Welche aktive Quelle hat seit ueber 24 Stunden (12 verpasste Slots bei
-- 2-Stunden-Takt) nichts Neues geliefert?
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
@@ -122,5 +123,5 @@ 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'
or now() - max(r.abgerufen_am) > interval '24 hours'
order by stille desc nulls first;
@@ -1,29 +1,29 @@
# Quadlet-Unit fuer rootless Podman.
# Ablegen unter ~/.config/containers/systemd/lagebild-ingest.container,
# danach: systemctl --user daemon-reload && systemctl --user start lagebild-ingest
# Ablegen unter ~/.config/containers/systemd/wurzelwerk-ingest.container,
# danach: systemctl --user daemon-reload && systemctl --user start wurzelwerk-ingest
#
# Der Poller laeuft dauerhaft und taktet sich selbst auf die 15-Minuten-Grenzen.
# Der Poller laeuft dauerhaft und taktet sich selbst auf die 2-Stunden-Grenzen.
# Deshalb kein systemd-Timer: ein Timer wuerde den bedingten GET-Zustand
# (ETag/Last-Modified) bei jedem Start neu aufbauen muessen.
[Unit]
Description=Lagebild Ingest-Poller
Description=Wurzelwerk Ingest-Poller
After=network-online.target
Wants=network-online.target
[Container]
Image=localhost/lagebild-ingest:latest
ContainerName=lagebild-ingest
Image=localhost/wurzelwerk-ingest:latest
ContainerName=wurzelwerk-ingest
AutoUpdate=local
EnvironmentFile=%h/.config/lagebild/ingest.env
EnvironmentFile=%h/.config/wurzelwerk/ingest.env
# Quellenregister von aussen einhaengen: eine neue Quelle ist dann ein
# Neustart, kein Neubau des Images.
Volume=%h/.config/lagebild/sources.toml:/app/sources.toml:ro,Z
Volume=%h/.config/wurzelwerk/sources.toml:/app/sources.toml:ro,Z
# Dasselbe Netz wie der Postgres-Container.
Network=lagebild.network
Network=wurzelwerk.network
NoNewPrivileges=true
ReadOnly=true
@@ -3,11 +3,11 @@
# damit systemd den Fehlerzustand sichtbar haelt.
[Unit]
Description=Lagebild - robots.txt der Quellen pruefen
Description=Wurzelwerk - robots.txt der Quellen pruefen
[Service]
Type=oneshot
ExecStart=/usr/bin/podman run --rm \
--network=host \
-v %h/.config/lagebild/sources.toml:/app/sources.toml:ro \
localhost/lagebild-ingest:latest --check-robots
-v %h/.config/wurzelwerk/sources.toml:/app/sources.toml:ro \
localhost/wurzelwerk-ingest:latest --check-robots
@@ -1,8 +1,8 @@
# Nach ~/.config/systemd/user/ ablegen, dann:
# systemctl --user enable --now lagebild-robots.timer
# systemctl --user enable --now wurzelwerk-robots.timer
[Unit]
Description=Lagebild - woechentliche robots.txt-Pruefung
Description=Wurzelwerk - woechentliche robots.txt-Pruefung
[Timer]
OnCalendar=Mon 05:30
+6
View File
@@ -0,0 +1,6 @@
# Ablegen unter ~/.config/containers/systemd/wurzelwerk.network
[Unit]
Description=Wurzelwerk internes Netz
[Network]
NetworkName=wurzelwerk