mirror of
https://github.com/Jeuners/logbuch.git
synced 2026-09-09 15:02:32 +02:00
Struktur, erster Artikel, Publish-Workflow
This commit is contained in:
commit
fe9cc8f492
5 changed files with 312 additions and 0 deletions
90
.github/workflows/publish.yml
vendored
Normal file
90
.github/workflows/publish.yml
vendored
Normal file
|
|
@ -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}"
|
||||
64
README.md
Normal file
64
README.md
Normal file
|
|
@ -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
|
||||
|
||||
<!-- Neue Eintraege oben anfuegen, Format: - [Titel](artikel/DATEI.md) ... ein Satz -->
|
||||
|
||||
- [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/<slug>` ... 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.
|
||||
|
|
@ -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.*
|
||||
27
artikel/_vorlage.md
Normal file
27
artikel/_vorlage.md
Normal file
|
|
@ -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.*
|
||||
67
preview.sh
Executable file
67
preview.sh
Executable file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue