feat: Nutzungsanleitung auf /anleitung (#18)
- 6 Abschnitte: Einführung, Verfahrensstränge, Ampel, KI-Bewertung, Szenarien, Fristen - Ampel-Beispiele inline mit Ampel.svelte - Inhaltsverzeichnis mit Sprungmarken - Strang-Labels dynamisch von /api/ampel/definition - Navigation: Anleitung-Link in Desktop + Mobile Closes #18
This commit is contained in:
parent
f5862406f3
commit
b601c0a366
@ -44,6 +44,7 @@
|
||||
<a href="/karte" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Karte</a>
|
||||
<a href="/fristen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Fristen</a>
|
||||
<a href="/fraktionen" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Fraktionen</a>
|
||||
<a href="/anleitung" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">Anleitung</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Hamburger Button -->
|
||||
@ -80,6 +81,7 @@
|
||||
<a href="/karte" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Karte</a>
|
||||
<a href="/fristen" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Fristen</a>
|
||||
<a href="/fraktionen" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Fraktionen</a>
|
||||
<a href="/anleitung" onclick={() => menuOpen = false} class="block text-gray-600 hover:text-gray-900 hover:bg-gray-50 px-3 py-3 rounded-md text-base font-medium">Anleitung</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
381
frontend/src/routes/anleitung/+page.svelte
Normal file
381
frontend/src/routes/anleitung/+page.svelte
Normal file
@ -0,0 +1,381 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Ampel from '$lib/components/Ampel.svelte';
|
||||
import { fetchAmpelDefinition, type AmpelDefinition } from '$lib/api';
|
||||
|
||||
let definition = $state<AmpelDefinition | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Mock-Ampeldaten pro Strang für die Beispiele
|
||||
const MOCK_AMPELN: Record<string, {
|
||||
schritte: { id: string; label: string; aktiv: boolean; erreicht: boolean; farbe: string }[];
|
||||
abzweigung: { id: string; label: string; farbe: string } | null;
|
||||
kontrollfrage: string | null;
|
||||
beschreibung: string;
|
||||
ablauf: string;
|
||||
}> = {
|
||||
antrag: {
|
||||
schritte: [
|
||||
{ id: 'eingereicht', label: 'Eingereicht', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'beraten', label: 'Beraten', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'beschlossen', label: 'Beschlossen', aktiv: true, erreicht: true, farbe: 'gelb' },
|
||||
{ id: 'umgesetzt', label: 'Umgesetzt', aktiv: false, erreicht: false, farbe: 'grau' },
|
||||
],
|
||||
abzweigung: null,
|
||||
kontrollfrage: 'Hat die Verwaltung umgesetzt, was beschlossen wurde?',
|
||||
beschreibung: 'Politik will etwas verändern — die Verwaltung soll es umsetzen.',
|
||||
ablauf: 'Antrag wird eingereicht → in Ausschüssen beraten → vom Rat beschlossen → von der Verwaltung umgesetzt (oder auch nicht).',
|
||||
},
|
||||
anfrage: {
|
||||
schritte: [
|
||||
{ id: 'gestellt', label: 'Gestellt', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'beantwortet', label: 'Beantwortet', aktiv: true, erreicht: true, farbe: 'gruen' },
|
||||
],
|
||||
abzweigung: null,
|
||||
kontrollfrage: 'Wurde die Anfrage befriedigend beantwortet?',
|
||||
beschreibung: 'Politik fragt — die Verwaltung antwortet.',
|
||||
ablauf: 'Anfrage wird gestellt → Verwaltung antwortet schriftlich → KI bewertet, ob die Antwort die Frage tatsächlich beantwortet.',
|
||||
},
|
||||
beschlussvorlage: {
|
||||
schritte: [
|
||||
{ id: 'eingebracht', label: 'Eingebracht', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'beraten', label: 'Beraten', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'beschlossen', label: 'Beschlossen', aktiv: true, erreicht: true, farbe: 'gelb' },
|
||||
{ id: 'umgesetzt', label: 'Umgesetzt', aktiv: false, erreicht: false, farbe: 'grau' },
|
||||
],
|
||||
abzweigung: null,
|
||||
kontrollfrage: 'Wurde so umgesetzt wie beschlossen?',
|
||||
beschreibung: 'Die Verwaltung legt einen Vorschlag vor — der Rat stimmt zu.',
|
||||
ablauf: 'Verwaltung bringt Vorlage ein → Beratung in Ausschüssen → Ratsbeschluss → Umsetzung durch die Verwaltung.',
|
||||
},
|
||||
mitteilung: {
|
||||
schritte: [
|
||||
{ id: 'vorgelegt', label: 'Vorgelegt', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'kenntnisnahme', label: 'Kenntnisnahme', aktiv: true, erreicht: true, farbe: 'gruen' },
|
||||
],
|
||||
abzweigung: null,
|
||||
kontrollfrage: null,
|
||||
beschreibung: 'Die Verwaltung informiert den Rat — keine Abstimmung, nur Kenntnisnahme.',
|
||||
ablauf: 'Verwaltung legt Mitteilung vor → Rat nimmt zur Kenntnis.',
|
||||
},
|
||||
};
|
||||
|
||||
// Beispiel-Ampel mit Abzweigung (versandet)
|
||||
const MOCK_ABZWEIGUNG = {
|
||||
strang: 'antrag',
|
||||
strang_label: 'Antrag (versandet)',
|
||||
kontrollfrage: 'Hat die Verwaltung umgesetzt?',
|
||||
schritte: [
|
||||
{ id: 'eingereicht', label: 'Eingereicht', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'beraten', label: 'Beraten', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'beschlossen', label: 'Beschlossen', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'umgesetzt', label: 'Umgesetzt', aktiv: false, erreicht: false, farbe: 'grau' },
|
||||
],
|
||||
abzweigung: { id: 'versandet', label: 'Versandet', farbe: 'rot' },
|
||||
};
|
||||
|
||||
const MOCK_ABGELEHNT = {
|
||||
strang: 'antrag',
|
||||
strang_label: 'Antrag (abgelehnt)',
|
||||
kontrollfrage: null,
|
||||
schritte: [
|
||||
{ id: 'eingereicht', label: 'Eingereicht', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'beraten', label: 'Beraten', aktiv: false, erreicht: true, farbe: 'gruen' },
|
||||
{ id: 'beschlossen', label: 'Beschlossen', aktiv: false, erreicht: false, farbe: 'grau' },
|
||||
{ id: 'umgesetzt', label: 'Umgesetzt', aktiv: false, erreicht: false, farbe: 'grau' },
|
||||
],
|
||||
abzweigung: { id: 'abgelehnt', label: 'Abgelehnt', farbe: 'rot' },
|
||||
};
|
||||
|
||||
const STRANG_FARBEN: Record<string, { bg: string; border: string; text: string }> = {
|
||||
antrag: { bg: 'bg-green-50', border: 'border-green-200', text: 'text-green-800' },
|
||||
anfrage: { bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-800' },
|
||||
beschlussvorlage: { bg: 'bg-amber-50', border: 'border-amber-200', text: 'text-amber-800' },
|
||||
mitteilung: { bg: 'bg-gray-50', border: 'border-gray-200', text: 'text-gray-700' },
|
||||
};
|
||||
|
||||
const STRANG_ORDER = ['antrag', 'anfrage', 'beschlussvorlage', 'mitteilung'];
|
||||
|
||||
const TOC = [
|
||||
{ id: 'was-ist', label: 'Was ist der Antragstracker?' },
|
||||
{ id: 'straenge', label: 'Die vier Verfahrensstränge' },
|
||||
{ id: 'ampel', label: 'Die Ampel verstehen' },
|
||||
{ id: 'ki-bewertung', label: 'Die KI-Bewertung' },
|
||||
{ id: 'szenarien', label: 'Konkrete Szenarien' },
|
||||
{ id: 'fristen', label: 'Fristen-Tracking' },
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
definition = await fetchAmpelDefinition();
|
||||
} catch (e) {
|
||||
error = 'Strang-Definitionen konnten nicht geladen werden.';
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
function strangLabel(key: string): string {
|
||||
if (definition?.straenge[key]) return definition.straenge[key].label;
|
||||
const fallback: Record<string, string> = {
|
||||
antrag: 'Anträge',
|
||||
anfrage: 'Anfragen',
|
||||
beschlussvorlage: 'Beschlussvorlagen',
|
||||
mitteilung: 'Mitteilungen',
|
||||
};
|
||||
return fallback[key] || key;
|
||||
}
|
||||
|
||||
function buildMockAmpel(key: string) {
|
||||
const mock = MOCK_AMPELN[key];
|
||||
if (!mock) return null;
|
||||
return {
|
||||
strang: key,
|
||||
strang_label: strangLabel(key),
|
||||
kontrollfrage: mock.kontrollfrage,
|
||||
schritte: mock.schritte,
|
||||
abzweigung: mock.abzweigung,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Anleitung — Antragstracker Hagen</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Anleitung</h1>
|
||||
<p class="text-gray-500 mb-8">Wie der Antragstracker funktioniert — und wie du ihn nutzt.</p>
|
||||
|
||||
<!-- Inhaltsverzeichnis -->
|
||||
<nav class="bg-white rounded-lg border border-gray-200 p-4 mb-10">
|
||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-2">Inhalt</p>
|
||||
<ol class="space-y-1">
|
||||
{#each TOC as item}
|
||||
<li>
|
||||
<a href="#{item.id}" class="text-sm text-green-700 hover:text-green-900 hover:underline">
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- 1. Was ist der Antragstracker? -->
|
||||
<section id="was-ist" class="mb-12 scroll-mt-24">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">Was ist der Antragstracker?</h2>
|
||||
<div class="prose prose-gray max-w-none">
|
||||
<p>
|
||||
Der Antragstracker ist ein kommunalpolitisches Kontroll-Instrument für den Rat der Stadt Hagen.
|
||||
Er verfolgt den Weg von Anträgen, Anfragen und Beschlüssen durch die Gremien — und macht sichtbar,
|
||||
was beschlossen wurde und was davon tatsächlich umgesetzt wird.
|
||||
</p>
|
||||
<p>
|
||||
Daten werden aus dem Ratsinformationssystem (Allris) importiert. Eine KI analysiert den aktuellen
|
||||
Stand jedes Vorgangs und bewertet, ob beschlossene Maßnahmen umgesetzt wurden.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Die vier Verfahrensstränge -->
|
||||
<section id="straenge" class="mb-12 scroll-mt-24">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">Die vier Verfahrensstränge</h2>
|
||||
<p class="text-gray-600 mb-6">
|
||||
Jeder Vorgang im Rat folgt einem von vier Verfahrenssträngen. Jeder Strang hat eigene Schritte,
|
||||
eine eigene Ampel — und eine eigene Kontrollfrage.
|
||||
</p>
|
||||
|
||||
{#if error}
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700 mb-4">{error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-6">
|
||||
{#each STRANG_ORDER as key}
|
||||
{@const farben = STRANG_FARBEN[key]}
|
||||
{@const mock = MOCK_AMPELN[key]}
|
||||
{@const ampelData = buildMockAmpel(key)}
|
||||
<div class="{farben.bg} border {farben.border} rounded-lg p-5">
|
||||
<h3 class="text-lg font-semibold {farben.text} mb-1">{strangLabel(key)}</h3>
|
||||
<p class="text-sm text-gray-700 mb-3">{mock.beschreibung}</p>
|
||||
|
||||
{#if ampelData}
|
||||
<div class="bg-white/60 rounded-md p-3 mb-3">
|
||||
<Ampel ampel={ampelData} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if mock.kontrollfrage}
|
||||
<p class="text-sm font-medium text-gray-800 mb-1">
|
||||
Kontrollfrage: <span class="italic">{mock.kontrollfrage}</span>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<p class="text-sm text-gray-600">
|
||||
<span class="font-medium">Ablauf:</span> {mock.ablauf}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Die Ampel verstehen -->
|
||||
<section id="ampel" class="mb-12 scroll-mt-24">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">Die Ampel verstehen</h2>
|
||||
<div class="prose prose-gray max-w-none mb-6">
|
||||
<p>
|
||||
Jeder Vorgang hat eine Ampel, die seinen Fortschritt zeigt. Die Schritte werden
|
||||
von links nach rechts durchlaufen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 mb-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-5 h-5 rounded-full border-2 border-gray-300 bg-white shrink-0 mt-0.5"></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Offen (weißer Kreis)</p>
|
||||
<p class="text-sm text-gray-600">Dieser Schritt wurde noch nicht erreicht.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-5 h-5 rounded-full bg-gray-300 shrink-0 mt-0.5"></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Durchlaufen (grau)</p>
|
||||
<p class="text-sm text-gray-600">Dieser Schritt liegt in der Vergangenheit — er wurde bereits passiert.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-5 h-5 rounded-full bg-yellow-500 shrink-0 mt-0.5" style="box-shadow: 0 0 0 3px rgba(234, 179, 8, 0.19)"></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Aktuell — wartet (gelb)</p>
|
||||
<p class="text-sm text-gray-600">Der Vorgang befindet sich gerade in diesem Schritt. Es passiert etwas — oder es wird darauf gewartet.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-5 h-5 rounded-full bg-green-500 shrink-0 mt-0.5" style="box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.19)"></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Erledigt (grün)</p>
|
||||
<p class="text-sm text-gray-600">Dieser Schritt ist abgeschlossen — der aktive Schritt leuchtet grün, wenn er positiv abgeschlossen ist.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-5 h-5 rounded-full bg-red-500 shrink-0 mt-0.5" style="box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.19)"></div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Gescheitert (rot)</p>
|
||||
<p class="text-sm text-gray-600">Hier ist etwas schiefgegangen — abgelehnt, gescheitert oder nicht umgesetzt.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Abzweigungen</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Manchmal nimmt ein Vorgang nicht den normalen Weg. In diesen Fällen zeigt die Ampel eine Abzweigung:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<p class="text-sm font-medium text-red-700 mb-2">Versandet</p>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Ein Antrag wurde beschlossen, aber nie umgesetzt. Die Verwaltung hat ihn schlicht nicht weiterverfolgt.
|
||||
</p>
|
||||
<Ampel ampel={MOCK_ABZWEIGUNG} />
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<p class="text-sm font-medium text-red-700 mb-2">Abgelehnt</p>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Der Vorgang wurde in der Beratung oder Abstimmung abgelehnt und nicht weiterverfolgt.
|
||||
</p>
|
||||
<Ampel ampel={MOCK_ABGELEHNT} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Die KI-Bewertung -->
|
||||
<section id="ki-bewertung" class="mb-12 scroll-mt-24">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">Die KI-Bewertung</h2>
|
||||
<div class="prose prose-gray max-w-none">
|
||||
<p>
|
||||
Jeder Vorgang wird von einer KI analysiert. Sie liest die zugehörigen Dokumente aus dem
|
||||
Ratsinformationssystem und erstellt:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Eine Zusammenfassung</strong> — worum geht es, was wurde beschlossen?</li>
|
||||
<li><strong>Einen Umsetzungs-Score</strong> — wie weit ist die Umsetzung fortgeschritten (0–100%)?</li>
|
||||
</ul>
|
||||
|
||||
<h3>Was bedeutet der Prozentwert?</h3>
|
||||
<ul>
|
||||
<li><strong>0%</strong> — Keine erkennbare Umsetzung</li>
|
||||
<li><strong>1–49%</strong> — Teilweise umgesetzt, wesentliche Teile fehlen</li>
|
||||
<li><strong>50–79%</strong> — Überwiegend umgesetzt, aber nicht vollständig</li>
|
||||
<li><strong>80–100%</strong> — Weitgehend bis vollständig umgesetzt</li>
|
||||
</ul>
|
||||
|
||||
<h3>Neubewertung</h3>
|
||||
<p>
|
||||
Bewertungen können neu angefordert werden — etwa wenn neue Informationen vorliegen oder eine
|
||||
Einschätzung fragwürdig erscheint. Dabei kann eine Anmerkung mitgegeben werden, die der KI
|
||||
als zusätzlichen Kontext dient.
|
||||
</p>
|
||||
|
||||
<h3>Versionierung</h3>
|
||||
<p>
|
||||
Jede Bewertung wird versioniert. Alte Bewertungen bleiben erhalten und sind nachvollziehbar.
|
||||
So lässt sich verfolgen, wie sich die Einschätzung über die Zeit verändert hat.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Konkrete Szenarien -->
|
||||
<section id="szenarien" class="mb-12 scroll-mt-24">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">Konkrete Szenarien</h2>
|
||||
<div class="space-y-3">
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<p class="text-sm font-semibold text-gray-900 mb-1">„Welche beschlossenen Anträge wurden nie umgesetzt?"</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
→ <a href="/explorer" class="text-green-700 hover:underline">Explorer</a> öffnen, nach Status „versandet" filtern.
|
||||
Zeigt alle Vorgänge, bei denen die Verwaltung beschlossene Maßnahmen nicht weiterverfolgt hat.
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<p class="text-sm font-semibold text-gray-900 mb-1">„Hat die Verwaltung meine Anfrage wirklich beantwortet?"</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
→ Anfrage im Explorer suchen, KI-Bewertung lesen. Falls unbefriedigend: Neubewertung mit
|
||||
eigener Anmerkung anfordern.
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<p class="text-sm font-semibold text-gray-900 mb-1">„Welche Fristen laufen demnächst ab?"</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
→ <a href="/fristen" class="text-green-700 hover:underline">Fristen</a> öffnen.
|
||||
Zeigt alle anstehenden und überfälligen Termine aus Beschlusstexten.
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<p class="text-sm font-semibold text-gray-900 mb-1">„Wie stimmt meine Fraktion im Vergleich zu anderen ab?"</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
→ <a href="/abstimmungen" class="text-green-700 hover:underline">Abstimmungen</a> öffnen.
|
||||
Vergleicht das Abstimmungsverhalten aller Fraktionen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Fristen-Tracking -->
|
||||
<section id="fristen" class="mb-12 scroll-mt-24">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">Fristen-Tracking</h2>
|
||||
<div class="prose prose-gray max-w-none">
|
||||
<p>
|
||||
In vielen Beschlüssen stecken konkrete Termine: „bis Ende Q2 umsetzen", „Bericht bis März vorlegen".
|
||||
Der Antragstracker erkennt solche Fristen automatisch per KI und macht sie sichtbar.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Automatische Erkennung</strong> — Die KI extrahiert Termine aus Beschlusstexten</li>
|
||||
<li><strong>Manuelle Ergänzung</strong> — Fristen können auch von Hand hinzugefügt werden</li>
|
||||
<li><strong>Farbkodierung</strong> — Überfällige Fristen werden rot markiert, anstehende gelb</li>
|
||||
</ul>
|
||||
<p>
|
||||
Alle Fristen sind unter <a href="/fristen">/fristen</a> einsehbar, gefiltert nach Status und Zeitraum.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
83
scripts/check-background-jobs.sh
Executable file
83
scripts/check-background-jobs.sh
Executable file
@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
# Prüft Background-Jobs anhand ihrer Heartbeat-Dateien.
|
||||
# Per Cron alle 5 Minuten aufrufen:
|
||||
# */5 * * * * /path/to/antragstracker/scripts/check-background-jobs.sh >> /path/to/antragstracker/data/job-checker.log 2>&1
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
DATA_DIR="data"
|
||||
MAX_STALE_SECONDS=1800 # 30 Minuten ohne Update = stale
|
||||
|
||||
now=$(date +%s)
|
||||
|
||||
check_job() {
|
||||
local name="$1"
|
||||
local heartbeat_file="$2"
|
||||
local restart_cmd="$3"
|
||||
|
||||
if [ ! -f "$heartbeat_file" ]; then
|
||||
return 0 # Kein Heartbeat = Job nicht aktiv, OK
|
||||
fi
|
||||
|
||||
local status=$(python3 -c "import json; d=json.load(open('$heartbeat_file')); print(d.get('status','unknown'))" 2>/dev/null || echo "error")
|
||||
|
||||
if [ "$status" = "completed" ]; then
|
||||
echo "[$(date)] ✅ $name: fertig"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# PID prüfen
|
||||
local pid=$(python3 -c "import json; d=json.load(open('$heartbeat_file')); print(d.get('pid',''))" 2>/dev/null || echo "")
|
||||
if [ -n "$pid" ] && [ "$pid" != "null" ] && [ "$pid" != "None" ]; then
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
echo "[$(date)] ❌ $name: Prozess $pid tot → Neustart"
|
||||
eval "$restart_cmd"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Timestamp prüfen
|
||||
local last_batch=$(python3 -c "
|
||||
import json, datetime
|
||||
d = json.load(open('$heartbeat_file'))
|
||||
ts = d.get('last_batch_at') or d.get('started_at') or ''
|
||||
if ts:
|
||||
dt = datetime.datetime.fromisoformat(ts.replace('Z', '+00:00'))
|
||||
print(int(dt.timestamp()))
|
||||
else:
|
||||
print(0)
|
||||
" 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$last_batch" != "0" ]; then
|
||||
local age=$((now - last_batch))
|
||||
if [ "$age" -gt "$MAX_STALE_SECONDS" ]; then
|
||||
echo "[$(date)] ⚠️ $name: Heartbeat ${age}s alt (max ${MAX_STALE_SECONDS}s) → Neustart"
|
||||
# Kill alten Prozess
|
||||
[ -n "$pid" ] && [ "$pid" != "null" ] && kill "$pid" 2>/dev/null || true
|
||||
sleep 2
|
||||
eval "$restart_cmd"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Alles gut
|
||||
local progress=$(python3 -c "
|
||||
import json
|
||||
d = json.load(open('$heartbeat_file'))
|
||||
done = d.get('total_geocoded', d.get('total_done', '?'))
|
||||
pending = d.get('total_pending', '?')
|
||||
print(f'{done} done, {pending} pending')
|
||||
" 2>/dev/null || echo "?")
|
||||
echo "[$(date)] ✓ $name: läuft (PID $pid, $progress)"
|
||||
}
|
||||
|
||||
# === Jobs registrieren ===
|
||||
|
||||
PROJ_DIR="$(pwd)"
|
||||
check_job "Geocoding" \
|
||||
"$DATA_DIR/geocode-heartbeat.json" \
|
||||
"cd '$PROJ_DIR' && nohup bash scripts/geocode_background.sh >> data/geocode.log 2>&1 &"
|
||||
|
||||
# Weitere Jobs hier eintragen:
|
||||
# check_job "OParl-Sync" "$DATA_DIR/sync-heartbeat.json" "..."
|
||||
68
scripts/geocode_background.sh
Executable file
68
scripts/geocode_background.sh
Executable file
@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# Background Geocoding mit Heartbeat alle 20 Minuten
|
||||
# Schreibt Status nach data/geocode-heartbeat.json
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
HEARTBEAT_FILE="data/geocode-heartbeat.json"
|
||||
BATCH_SIZE=1200 # ~20 Min bei 1/s
|
||||
|
||||
while true; do
|
||||
START=$(date +%s)
|
||||
START_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Run batch
|
||||
OUTPUT=$(python3 scripts/geocode_pending.py --limit $BATCH_SIZE 2>&1) || true
|
||||
|
||||
END=$(date +%s)
|
||||
DURATION=$((END - START))
|
||||
|
||||
# Parse output for stats
|
||||
GEOCODED=$(echo "$OUTPUT" | grep -oP 'geocoded: \K\d+' 2>/dev/null || echo "0")
|
||||
FAILED=$(echo "$OUTPUT" | grep -oP 'failed: \K\d+' 2>/dev/null || echo "0")
|
||||
REMAINING=$(python3 -c "
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('data/tracker.db')
|
||||
r = conn.execute('SELECT COUNT(*) FROM orte WHERE lat IS NULL').fetchone()[0]
|
||||
t = conn.execute('SELECT COUNT(*) FROM orte WHERE lat IS NOT NULL').fetchone()[0]
|
||||
print(f'{r}|{t}')
|
||||
conn.close()
|
||||
" 2>/dev/null || echo "0|0")
|
||||
|
||||
PENDING=$(echo "$REMAINING" | cut -d'|' -f1)
|
||||
DONE=$(echo "$REMAINING" | cut -d'|' -f2)
|
||||
|
||||
# Write heartbeat
|
||||
cat > "$HEARTBEAT_FILE" << EOF
|
||||
{
|
||||
"status": "running",
|
||||
"last_batch_at": "$START_ISO",
|
||||
"duration_seconds": $DURATION,
|
||||
"batch_geocoded": $GEOCODED,
|
||||
"batch_failed": $FAILED,
|
||||
"total_geocoded": $DONE,
|
||||
"total_pending": $PENDING,
|
||||
"pid": $$
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "[$(date)] Batch done: +$GEOCODED geocoded, $FAILED failed, $PENDING remaining"
|
||||
|
||||
# Done?
|
||||
if [ "$PENDING" = "0" ]; then
|
||||
cat > "$HEARTBEAT_FILE" << EOF
|
||||
{
|
||||
"status": "completed",
|
||||
"completed_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||
"total_geocoded": $DONE,
|
||||
"total_pending": 0,
|
||||
"pid": null
|
||||
}
|
||||
EOF
|
||||
echo "✅ Geocoding fertig!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Kleine Pause zwischen Batches
|
||||
sleep 5
|
||||
done
|
||||
Loading…
Reference in New Issue
Block a user