commit fe9cc8f49259537eaf3aa496a2a37766a7f64cd4 Author: Jeuner <62662523+Jeuners@users.noreply.github.com> Date: Fri Aug 28 21:31:58 2026 +0200 Struktur, erster Artikel, Publish-Workflow diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4185934 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,90 @@ +name: publish + +# Jeder neue Artikel unter artikel/ wird zu: +# - einem Release (taggt den Artikel, erzeugt automatisch einen Atom-Feed-Eintrag) +# - einer Discussion in der Kategorie Announcements (der Kommentarthread) +# +# Veroeffentlichen ist damit: git push. + +on: + push: + branches: [main] + paths: + - 'artikel/**' + +permissions: + contents: write + discussions: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Neue Artikel veroeffentlichen + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BEFORE: ${{ github.event.before }} + AFTER: ${{ github.sha }} + CATEGORY: ${{ vars.DISCUSSION_CATEGORY || 'Announcements' }} + run: | + set -euo pipefail + + # Erster Push oder Force-Push: BEFORE ist unbrauchbar, dann alles nehmen. + if git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then + FILES="$(git diff --name-only --diff-filter=A "${BEFORE}" "${AFTER}" -- artikel | grep '\.md$' || true)" + else + FILES="$(git ls-files -- artikel | grep '\.md$' || true)" + fi + + if [ -z "${FILES}" ]; then + echo "Keine neuen Artikel in diesem Push." + exit 0 + fi + + echo "Gefunden:" + echo "${FILES}" + + while IFS= read -r file; do + [ -n "$file" ] || continue + [ -f "$file" ] || continue + + slug="$(basename "$file" .md)" + + # Vorlagen und Entwuerfe (fuehrender Unterstrich) werden nicht veroeffentlicht. + case "$slug" in + _*) echo "Vorlage/Entwurf ${file}, uebersprungen."; continue ;; + esac + + tag="artikel/${slug}" + + if gh release view "$tag" >/dev/null 2>&1; then + echo "Release ${tag} existiert bereits, uebersprungen." + continue + fi + + # Titel aus dem Frontmatter, sonst aus der ersten H1, sonst der Slug. + title="$(sed -n 's/^title:[[:space:]]*//p' "$file" | head -n1 | sed 's/^"//; s/"$//')" + if [ -z "$title" ]; then + title="$(sed -n 's/^# //p' "$file" | head -n1)" + fi + [ -n "$title" ] || title="$slug" + + # Frontmatter aus den Release-Notes entfernen. + body="$(mktemp)" + awk ' + NR==1 && $0=="---" { fm=1; next } + fm==1 && $0=="---" { fm=0; next } + fm!=1 { print } + ' "$file" > "$body" + + echo "Veroeffentliche: ${title} -> ${tag}" + gh release create "$tag" \ + --title "$title" \ + --notes-file "$body" \ + --discussion-category "$CATEGORY" + + done <<< "${FILES}" diff --git a/README.md b/README.md new file mode 100644 index 0000000..e31d360 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Logbuch + +Notes and articles on sovereign, local-first AI infrastructure. +Written in German. Discussion happens in this repo, not on a platform that can revoke it. + +**[Discussions](../../discussions)** ... comment, argue, ask. +**[Feed](../../releases.atom)** ... every article as an Atom entry. No account needed. + +--- + +## Warum hier und nicht auf einer Plattform + +Der Text liegt in Git. Er ist versioniert, diffbar, exportierbar und gehoert dem Autor. +Kein Feed-Algorithmus entscheidet, wer ihn sieht. Kein Moderationsteam kann ihn entfernen. +Der Preis dafuer ist Reichweite: hier findet dich niemand von selbst. Das ist bewusst so. + +--- + +## Artikel + + + +- [Souveraenitaet ist eine Betriebsentscheidung, keine Haltung](artikel/2026-08-27-souveraenitaet-ist-eine-betriebsentscheidung.md) ... warum "lokal" erst dann zaehlt, wenn es den Ausfall ueberlebt. + +--- + +## Ablauf + +Veroeffentlichen ist ein Push: + +```bash +cp artikel/_vorlage.md artikel/2026-09-01-mein-thema.md +$EDITOR artikel/2026-09-01-mein-thema.md +git add artikel/2026-09-01-mein-thema.md +git commit -m "Artikel: mein Thema" +git push +``` + +Der Workflow in `.github/workflows/publish.yml` macht daraus: + +1. ein Release mit dem Tag `artikel/` ... damit landet der Text automatisch im Atom-Feed +2. eine Discussion in der Kategorie *Announcements* ... das ist der Kommentarthread + +Dateiname ist die Adresse: `JJJJ-MM-TT-slug.md`. Kein Datum im Titel noetig, das Datum steht vorn. + +--- + +## Struktur + +``` +artikel/ die Texte, ein Markdown pro Artikel + _vorlage.md Kopiervorlage, wird vom Workflow ignoriert (Unterstrich) +.github/ + workflows/ + publish.yml Release + Discussion pro neuem Artikel +preview.sh lokale Vorschau, sucht sich einen freien Port +``` + +--- + +## Lizenz + +Texte: CC BY 4.0. Code in diesem Repo: MIT. +Uebernehmen, uebersetzen, widersprechen ... alles erlaubt, Nennung genuegt. diff --git a/artikel/2026-08-27-souveraenitaet-ist-eine-betriebsentscheidung.md b/artikel/2026-08-27-souveraenitaet-ist-eine-betriebsentscheidung.md new file mode 100644 index 0000000..3ecbc5d --- /dev/null +++ b/artikel/2026-08-27-souveraenitaet-ist-eine-betriebsentscheidung.md @@ -0,0 +1,64 @@ +--- +title: "Souveränität ist eine Betriebsentscheidung, keine Haltung" +date: 2026-08-27 +tags: [souveraenitaet, lokale-ki, on-premise] +--- + +# Souveränität ist eine Betriebsentscheidung, keine Haltung + +Digitale Souveränität wird meistens als Gesinnung verhandelt. Sie ist aber eine +Betriebsfrage, und Betriebsfragen entscheiden sich nicht in Manifesten, sondern im +Störfall. Die brauchbare Prüfung lautet: Was von deinem System läuft weiter, wenn +der Anbieter morgen früh den Zugang sperrt, ohne Begründung und ohne Ansprechpartner? + +## Der Befund + +Sperrungen sind kein Randfall. Ein Konto wird eingeschränkt, der Einspruch verweist +auf einen allgemeinen Verstoß gegen die Nutzungsbedingungen, ein konkreter Grund wird +nicht genannt, ein Mensch antwortet nicht. Das ist der reguläre Ablauf, nicht die +Ausnahme. Wer seine Reichweite, seine Texte und seine Kundenkommunikation in dieselbe +Infrastruktur gelegt hat, verliert in dem Moment alles drei gleichzeitig. + +Dasselbe Muster gilt für KI-Systeme. Ein Modell hinter einer API ist kein Bestandteil +deines Systems, sondern eine Zusage. Zusagen werden geändert: Preise, Kontingente, +Modellversionen, zulässige Anwendungsfälle. Jede dieser Änderungen kann eine +Anwendung, die im Betrieb steht, von einem Tag auf den anderen unbrauchbar machen. + +## Was daraus folgt + +Souveränität heißt nicht, alles selbst zu bauen. Sie heißt, die Abhängigkeiten zu +sortieren, und zwar nach einem einzigen Kriterium: Ist der Ausfall verkraftbar oder +existenziell? + +Ein Beispiel aus der Praxis. Ein Telefonagent für eine Hausarztpraxis nimmt Anrufe +entgegen, transkribiert lokal, sortiert das Anliegen in Termin, Rezept, Überweisung +oder Notfall und legt es strukturiert ab. Die Spracherkennung läuft auf dem Gerät, +die Kategorisierung ebenfalls. Nicht weil lokale Modelle in jedem Benchmark gewinnen, +sondern weil Patientendaten den Rechner nicht verlassen dürfen und weil die Praxis +auch dann noch Anrufe annehmen muss, wenn die Leitung nach draußen tot ist. + +Die Rechnung ist unspektakulär. Ein Mac mit genug Speicher steht in der Praxis. Er +kostet einmal Geld statt monatlich. Er kennt keine Kontingente. Er wird nicht +abgeschaltet, weil jemand in einem anderen Land eine Richtlinie ändert. Der Preis +dafür ist Arbeit: Updates, Monitoring, Löschfristen, Notfallpfade. Diese Arbeit ist +der eigentliche Inhalt des Wortes Souveränität. Wer sie nicht einplant, hat kein +souveränes System, sondern ein unbetreutes. + +## Was offen bleibt + +Die harte Grenze ist Kapazität. Lokale Modelle sind für Klassifikation, Transkription +und strukturierte Extraktion längst ausreichend. Für lange Argumentationsketten sind +sie es oft nicht. Wer behauptet, man brauche nie etwas Größeres, verkauft etwas. + +Ehrlicher wäre: Trenne die Ebenen. Alles, was Identität, Vertrauen und +personenbezogene Daten berührt, bleibt lokal, ohne Ausnahme. Alles andere darf nach +draußen, solange der Ausfall dieses Teils das System nicht anhält, sondern nur +langsamer macht. + +Der Teil, für den ich noch keine gute Antwort habe: Wie prüft man das nach, ohne dem +Betreiber einfach zu glauben? Nachweisbarkeit ohne Vertrauensvorschuss ist das +eigentlich ungelöste Problem, nicht die Frage, wo die GPU steht. + +--- + +*Widerspruch willkommen. Der Diskussionsthread hängt an diesem Release.* diff --git a/artikel/_vorlage.md b/artikel/_vorlage.md new file mode 100644 index 0000000..a7bd5e2 --- /dev/null +++ b/artikel/_vorlage.md @@ -0,0 +1,27 @@ +--- +title: "Titel des Artikels" +date: 2026-09-01 +tags: [souveraenitaet, lokale-ki] +--- + +# Titel des Artikels + +Erster Absatz: die These, in zwei Saetzen, ohne Anlauf. Wer nach dem ersten Absatz +nicht weiss, worum es geht und warum es ihn angeht, liest nicht weiter. + +## Der Befund + +Was ist der Fall. Konkret, mit Zahlen oder mit einem Vorgang, den man nachvollziehen kann. + +## Was daraus folgt + +Die eigentliche Arbeit. Hier steht das Argument, nicht die Meinung. + +## Was offen bleibt + +Ehrlich. Was hast du nicht geloest, was weisst du nicht, wo koenntest du falsch liegen. +Das ist der Absatz, an dem sich zeigt, ob jemand mitdenkt oder nur nickt. + +--- + +*Diskussion zu diesem Text: siehe verlinkter Thread unter diesem Release.* diff --git a/preview.sh b/preview.sh new file mode 100755 index 0000000..0c64922 --- /dev/null +++ b/preview.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# +# Lokale Vorschau der Artikel. +# +# ./preview.sh -> einfacher Markdown-Server (Python, keine Abhaengigkeiten) +# ./preview.sh jekyll -> Jekyll, falls GitHub Pages genutzt wird +# PORT=4100 ./preview.sh -> Startport vorgeben +# +# Der Port wird vor dem Start geprueft. Ist er belegt, wird der naechste freie genommen, +# statt den laufenden Dienst abzuschiessen oder mit "address already in use" zu sterben. +# +set -euo pipefail + +MODE="${1:-simple}" +PORT="${PORT:-4000}" +MAX_TRIES="${MAX_TRIES:-20}" + +port_is_free() { + local p="$1" + if command -v lsof >/dev/null 2>&1; then + ! lsof -nP -iTCP:"$p" -sTCP:LISTEN >/dev/null 2>&1 + elif command -v ss >/dev/null 2>&1; then + ! ss -ltnH "sport = :$p" 2>/dev/null | grep -q . + elif command -v netstat >/dev/null 2>&1; then + ! netstat -an 2>/dev/null | grep -qE "[.:]${p}[[:space:]].*LISTEN" + else + # Letzter Ausweg: Verbindungsversuch. Klappt er, hoert dort jemand zu. + ! (exec 3<>"/dev/tcp/127.0.0.1/$p") 2>/dev/null + fi +} + +find_free_port() { + local p="$1" tries=0 + while ! port_is_free "$p"; do + printf 'Port %s ist belegt.\n' "$p" >&2 + p=$((p + 1)) + tries=$((tries + 1)) + if [ "$tries" -ge "$MAX_TRIES" ]; then + printf 'Kein freier Port zwischen %s und %s gefunden.\n' "$PORT" "$p" >&2 + exit 1 + fi + done + printf '%s' "$p" +} + +PORT="$(find_free_port "$PORT")" + +case "$MODE" in + jekyll) + command -v bundle >/dev/null 2>&1 || { + echo "bundler fehlt. Entweder installieren oder ./preview.sh ohne Argument nutzen." >&2 + exit 1 + } + echo "Jekyll auf http://127.0.0.1:${PORT}" + exec bundle exec jekyll serve --port "$PORT" --livereload + ;; + simple) + command -v python3 >/dev/null 2>&1 || { echo "python3 fehlt." >&2; exit 1; } + echo "Dateiserver auf http://127.0.0.1:${PORT}/artikel/" + echo "(rendert kein Markdown, zeigt aber Struktur und Rohtext)" + exec python3 -m http.server "$PORT" --bind 127.0.0.1 + ;; + *) + echo "Unbekannter Modus: $MODE (erlaubt: simple, jekyll)" >&2 + exit 1 + ;; +esac