- Kotlin 91.1%
- Shell 8.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- ci.yml neu: Unit-Tests + Lint bei jedem Push/PR (Runner-Label docker, arm64-aapt2-Override, Speicher-Limits wie im App-Schwesterprojekt) - release.yml lauffähig gemacht: altes ubuntu-latest/checkout@v4-Setup wurde vom Runner nie angezogen; jetzt Tarball/Clone-Checkout per Job-Token, Release-Publishing über GITHUB_TOKEN statt FORGEJO_TOKEN-Secret, Debug-Signing-Fallback wenn Keystore-Secrets fehlen - README: Release-Abschnitt auf Tag-Push-Ablauf umgeschrieben |
||
| .forgejo/workflows | ||
| app | ||
| gradle/wrapper | ||
| scripts | ||
| server | ||
| .gitignore | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| README.md | ||
| settings.gradle.kts | ||
| test-auf-emulator.sh | ||
Dienstplan-ICS
Kleine Android-App (Kotlin + Jetpack Compose), um Schichtdienste schnell im
Monatskalender einzutragen. Die Termine bleiben dauerhaft in der App gespeichert
und werden als abonnierbarer Kalender-Feed veröffentlicht – Google Kalender
aktualisiert sich dann von selbst, ganz ohne manuellen Export und Import. Als
Fallback kann die .ics weiterhin als Datei gespeichert oder geteilt werden.
Workflow
- Dienst auswählen: Unter dem Kalender liegen die Dienstarten als Chips (Standard: Tag 06:00–14:00, Nacht 22:00–06:00, Doppelschicht 06:00–22:00). Ein Tippen aktiviert den Dienst (hervorgehoben).
- Tag antippen: Der aktive Dienst wird an dem Tag eingetragen (farbiger Punkt in der Tagzelle). Derselbe Dienst + derselbe Tag nochmal antippen → wieder entfernt. Verschiedene Dienste dürfen am selben Tag koexistieren.
- Dienste verwalten: ✎ am Chip öffnet den Dialog — Name, Start-/Endzeit (TimePicker) oder der Haken „Ganztägig". Ende vor Start = Dienst geht über Mitternacht (wird in der ICS korrekt als Folgetag geschrieben). Im Dialog gibt es auch Löschen.
- Veröffentlichen (Abo-Feed): „ICS erstellen" → „Veröffentlichen – Feed aktualisieren" schickt den kompletten Kalender per HTTP-PUT an den eigenen Feed-Server. Google Kalender hat die Abo-URL einmalig hinterlegt („Weitere Kalender" → „+" → „Aus URL") und holt jede Änderung selbst ab (üblich innerhalb von ~12–24 h, Rhythmus steuert Google).
- Fallback-Export: „Speichern" (Android-Dateidialog) und „Teilen"
(Android-Teilen-Menü) geben die
.icswie bisher als Datei aus.
Die Termine bleiben dauerhaft gespeichert (SharedPreferences) – die App ist die Datenquelle des Feeds. „Kalender leeren" (neben der Dienste-Zähler) entfernt alle Termine auf Wunsch; auch das ändert den Feed erst beim nächsten Veröffentlichen.
Stabile Termine statt Duplikate
Jeder Termin bekommt eine feste UID aus (Datum, Dienstart). Beim Abo-Refresh
aktualisiert Google so bestehende Termine, statt sie zu duplizieren; Termine,
die aus dem Feed verschwinden, werden im Abo entfernt.
Server einrichten
Der Feed braucht einen Ort, den Google erreichen kann – ein kleines
nginx-Setup auf dem eigenen VPS genügt (keine eigene Server-Software): siehe
server/README.md. Die URLs trägt man in der App unter dem
Zahnrad oben rechts ein (Upload-URL + Abo-URL, Abo-URL wird zusätzlich als
webcal:// angezeigt).
Mehrere Personen: Jede Person bekommt auf dem Server ein eigenes Token-Paar
(per server/person.sh, ein Befehl, kein nginx-Reload) und damit eine eigene
Upload-URL und einen eigenen Abo-Feed. Damit veröffentlicht jede App nur den
eigenen Kalender — ein „Veröffentlichen“ überschreibt nie den Kalender einer
anderen Person. In der App selbst ist nichts Besonderes nötig: Jedes Gerät
speichert einfach seine eigenen URLs.
Installation über Obtainium
Releases werden auf git.weick.io veröffentlicht und lassen sich mit Obtainium direkt installieren und automatisch aktuell halten (Voraussetzung: das Repo ist öffentlich und ein Release enthält die APK).
In Obtainium: App hinzufügen → Quelle „HTML“ → dann:
| Einstellung | Wert |
|---|---|
| App-URL | https://git.weick.io/dennis/dienstplan-ics/releases |
| Regex zum Filtern von APK-Links | dienstplan-v[0-9.]+\.apk |
| Nach Linktext filtern | an (der Dateiname steckt im Linktext, die URL ist eine UUID) |
| Ersten Link verwenden | an (Forgejo listet neueste Releases zuerst) |
| Version aus ganzer Seite extrahieren | an |
| Version-Extraktions-Regex + Match-Gruppe | dienstplan-v([0-9.]+)\.apk, Gruppe 1 |
Obtainium prüft die Releases-Seite dann bei jedem Update-Lauf: Erscheint ein
neues dienstplan-vX.Y.Z.apk, bietet es das Update an.
Umstieg von der Debug-APK: Die Release-APK ist anders signiert, daher muss die bisherige Installation einmal deinstalliert werden – dabei gehen die in der App gespeicherten Termine verloren. Vorher also einmal „Veröffentlichen“ drücken (Google hat die Termine dann im Abo-Feed) und die Dienstarten notieren.
Bauen & installieren
Benötigt: JDK 17–21 und Android-SDK (auf diesem Rechner vorhanden unter
~/.local/share/jdks/ und ~/Android/Sdk; die nutzerlokale
~/.gradle/gradle.properties verweist per org.gradle.java.home auf das
Temurin-JDK 21, da Gradle 8.9 mit dem systemweiten Java 26 nicht läuft —
der Pfad ist maschinenspezifisch und liegt bewusst außerhalb des Repos).
./gradlew assembleDebug # Debug-APK bauen
./gradlew testDebugUnitTest # Unit-Tests (39 Stück)
Die APK liegt danach unter app/build/outputs/apk/debug/app-debug.apk.
Installation auf dem Handy — entweder per USB mit
adb install app/build/outputs/apk/debug/app-debug.apk
oder die APK aufs Handy kopieren (Mail/Cloud/USB) und dort direkt öffnen
(Unbekannte Quellen/Installation aus dieser Quelle zulassen bestätigen).
Für den täglichen Gebrauch auf dem Handy siehe oben
Installation über Obtainium.
Release veröffentlichen
Ein Release ist ein Git-Tag vX.Y.Z plus signierte APK als Forgejo-Release;
versionName wird aus dem Tag abgeleitet, versionCode zählt die Commits —
Versionszahlen werden nie manuell gepflegt.
Einmalig eingerichtet:
- Keystore (
keystore/dienstplan-release.jks+keystore.properties, beide gitignored) — ⚠️ unbedingt extern sichern (Passwort-Manager/USB-Stick): Geht der Keystore verloren, kann Android die APK nicht mehr als „dieselbe App“ aktualisieren und alle Installationen müssten mit Datenverlust neu aufgesetzt werden. - Repo-Secrets für die Release-Pipeline (Einstellungen → Actions → Secrets):
RELEASE_KEYSTORE_BASE64(base64 -w0 keystore/dienstplan-release.jks),RELEASE_STORE_PASSWORD,RELEASE_KEY_ALIAS,RELEASE_KEY_PASSWORD(Werte wie inkeystore.properties). Fehlen sie, baut die Pipeline zwar, signiert aber debug — der Release-Body weist dann deutlich darauf hin. - Forgejo-Token in
.release-env(gitignored):FORGEJO_TOKEN=…(Profil → Einstellungen → Anwendungen → Token, Recht: Repository Lesen+Schreiben) — nur fürscripts/release.sh, die Pipelines nutzen den automatischen Job-Token. - Das Repo muss öffentlich sein, sonst sieht Obtainium die Releases nicht.
Automatisch (Forgejo Actions): Bei jedem Push läuft die CI
(.forgejo/workflows/ci.yml: Unit-Tests + Lint). Ein Release genügt:
git tag v1.2.3 && git push origin v1.2.3
Die Release-Pipeline (.forgejo/workflows/release.yml) testet, baut die
signierte APK und veröffentlicht sie als Forgejo-Release. Getriggert wird sie
durch den Tag-Push — erst taggen, dann pushen (Commits mit offengebliebenen
Fehlern scheitern dort sichtbar). Die APK heißt dabei dienstplan-v1.2.3.apk
(das Namensmuster erwartet die Obtainium-Konfiguration oben — beim Upgrade der
Version also nichts anpassen).
Notfall (ohne Runner/ohne Actions): scripts/release.sh macht dasselbe
von der Entwicklermaschine aus:
scripts/release.sh 1.2.3 # Tests → Tag v1.2.3 → signierter Build → Forgejo-Release
Struktur
app/src/main/java/de/dennis/dienstplanics/
├── MainActivity.kt # Einstieg, Compose-Setup, ViewModel-Factory
├── PlanViewModel.kt # UI-State (Dienstarten, Termine, Monat, Veröffentlichen)
├── model/Model.kt # ShiftType, Entry, PlanState, MonthGrid (pure Kotlin)
├── model/PublishState.kt # PublishConfig + PublishState (pure Kotlin)
├── ics/IcsGenerator.kt # RFC-5545-ICS mit VTIMEZONE Europe/Berlin
├── ics/StableUid.kt # deterministische UID pro (Datum, Dienstart)
├── data/ShiftTypeStore.kt # Store-Interface + Standard-Dienste
├── data/SharedPrefsShiftTypeStore.kt
├── data/PlanStore.kt + SharedPrefsPlanStore.kt # dauerhafte Termine
├── data/PublishConfigStore.kt # Server-Konfiguration
├── net/FeedUploader.kt # HTTP-PUT (java.net, ohne Extra-Abhängigkeit)
└── ui/ # PlanScreen, CalendarGrid, ShiftChips, ShiftEditDialog
server/ # nginx-Config, person.sh (Personen anlegen/entfernen) + Anleitung
scripts/release.sh # Tests → Tag → signierter Build → Forgejo-Release
.forgejo/workflows/release.yml # Optional: CI-Build bei Tag-Push (braucht Actions-Runner)
Die Geschäftslogik (Toggle-Regeln, Monatsraster, ICS-Generierung inkl. Übernacht-, Ganztägig- und Escaping-Fällen, stabile UIDs, Publish-Statusübergänge) ist pure Kotlin und durch Unit-Tests abgedeckt.