Entwicklerhandbuch
| Ordner | Was |
|---|---|
app/ | Android-App (Kotlin, Jetpack Compose, minSdk 26, Credential Manager) |
web/ | Symfony-8-Web-App (PostgreSQL): API für Agentenstatistiken, Statistikseiten, Benutzerverwaltung |
docs/ | Diese Dokumentation (VitePress), auf GitHub Pages veröffentlicht |
So funktioniert es
Die App meldet den Benutzer mit Sign in with Google an und bekommt einen Google-ID-Token.
In Ingress teilt der Agent seine All-Time-Statistiken mit der App. Die App wandelt sie in eine
agent_stat-Zeile um (die Spaltennamen der 4e-website) und speichert sie sofort mitPOST /api/agent-statsundAuthorization: Bearer <id token>:json{ "datetime": "2026-10-19 15:35:56", "nickname": "Agent", "faction": "Enlightened", "ap": 243517130, … }webprüft den Token gegen seine Google-Web-Client-ID (OAUTH_GOOGLE_ID), findet den Benutzer (über die Google-ID, sonst über die E-Mail) und speichert die Statistiken (einmal prodatetime). Ein unbekanntes Konto wird als Benutzer registriert und speichert sofort Statistiken. Ohne Agentennamen legen die ersten Statistiken ihn fest (außer ein anderer Benutzer hat ihn schon).Die App zeigt die neuesten Statistiken mit Medaillen (
GET /api/agent-stats). Im Web sehen angemeldete Benutzer ihre Statistiken unter/agent-stats.Screenshots des Ingress-Medaillenrasters, die mit der App geteilt werden, werden auf dem Gerät nach den Sondermedaillen der 4e-website (Anomalie-, Jahres- und Event-Medaillen) durchsucht. Der Agent prüft sie; sie werden in
webgespeichert.
Die App verwendet die Web-Client-ID als serverClientId; der Android-OAuth-Client registriert nur den Paketnamen und den SHA-1. Siehe Google-Anmeldung einrichten.
Schnellstart
# Web-App (Docker, Migrationen, Fixtures)
cd web && bin/install && bin/start
# Android-App (braucht ein JDK 17–25, z. B. das JBR von Android Studio)
cd app
cp local.properties.example local.properties # google.serverClientId und Server-URLs setzen
./gradlew installDebugUm alles ohne Google-Zugangsdaten auszuprobieren, nimm in dev Fake-Tokens: app/README.md → Without Google.
Mehr Details: app/README.md, web/CLAUDE.md, Architektur, API.
Tests
cd web && make tests # PHPUnit, PHPStan, Rector, PHP-CS-Fixer
cd app && ./gradlew testDebugUnitTest lintDebugDie CI führt beides auf GitHub Actions aus; die Testabdeckung von Symfony wird mit dieser Dokumentation veröffentlicht (Downloads).
Dokumentation
Diese Seite wird mit VitePress aus docs/ gebaut, auf Englisch im Hauptordner und auf Deutsch und Spanisch in docs/de/ und docs/es/:
cd docs
npm install
npm run dev # http://localhost:5173/AgentTools/
npm run build # docs/.vitepress/distDer GitHub-Pages-Workflow (.github/workflows/symfony-pages.yml) baut sie bei jedem Push auf master, der docs/, web/ oder app/ ändert, und fügt die Dev-APK (/agenttools-dev.apk) und die Testabdeckung des Webs (/coverage/) hinzu. Die Screenshots in docs/public/screenshots/ zeigen die Demo-Agenten (symfony console app:demo-agents; die App mit einem Fake-Token als DemoFrog angemeldet), nie echte Benutzer. Jede Sprache hat ihre eigenen: Englisch in app/ und web/, Deutsch und Spanisch in deren Ordnern de/ und es/, die die deutschen und spanischen Seiten zeigen.
Die Screenshots der Website nimmt ein Skript auf, während die Web-App in dev läuft und die Demo-Agenten angelegt sind: headless Chromium im dunklen Design und in jeder Sprache, als DemoFrog angemeldet, ohne die Teile, die es nur in dev gibt. Es braucht ein Chromium oder Chrome (CHROME, Standard chromium; nicht Brave, das die Sprache des Browsers vor Websites verbirgt):
cd docs
CHROME=/path/to/chrome npm run screenshots:web # optional: -- <URL der Web-App> [en de es]Die Screenshots der App entstehen auf einem Handy über adb, in jeder Sprache. Das Skript braucht die Web-App auf 127.0.0.1:8001 mit Fake-Tokens und die Debug-App, damit als DemoFrog angemeldet; wie man das einrichtet, steht oben in docs/scripts/app-screenshots.sh. Echte Benutzer sind privat, solange es läuft, und ihre Einstellungen und die Sprache der App werden am Ende wiederhergestellt. Ein Handy oder der Pixel-6a-Emulator (Pixel_6a_API_35, derselbe Bildschirm) geht; sind beide verbunden, wählt ANDROID_SERIAL eines, und oben im Skript steht der Befehl für den Emulator (mit 2 GB statt seiner 6):
cd docs
npm run screenshots:app # optional: -- en de esDie Linkliste
Die Ingress-Linkliste der Website (/links), der App (☰ → Links) und dieser Seite (Links) ist eine Markdown-Datei, docs/links.md, die per Pull-Request geändert wird. Zwischen <!-- #region list --> und <!-- #endregion list --> hat sie nur ## -Abschnitte (auf Englisch) mit Einträgen - [Titel](https://…) — Beschreibung (die Beschreibung ist optional, nur https); die deutsche und die spanische Seite binden diesen Teil ein.
- Die Web-App liest die Datei aus ihrem Checkout (das Deployment hat das ganze Repository), eine gemergte Änderung ist also nach dem nächsten Deployment live.
LinkList/LinkListParserliest sie mit league/commonmark (gecacht, bis sich die Datei ändert) für/linksundGET /api/links. - Die App lädt
GET /api/linksund behält die letzte Liste in ihren Dateien (links.json) für offline. LinkListFileTestschlägt bei einem Eintrag in einem anderen Format fehl; die Symfony-Tests laufen bei Pull-Requests, die die Datei ändern..github/workflows/links.ymlprüft jeden Link einmal am Tag mit lychee (Konfiguration in.github/lychee.toml: Wiederholungen, 403 und 429 zählen als erreichbar). Ein Link, der zwei Läufe hintereinander fehlschlägt, kommt in ein Issue mit dem Labelbroken-links, das täglich aktualisiert und geschlossen wird, wenn alle Links wieder gehen. Ein Pull-Request, der die Liste ändert, schlägt bei einem kaputten Link fehl. Telegram (t.me) antwortet auch für einen Kanal, den es nicht mehr gibt: Solche findet die Prüfung nicht.
Dev-App
Der GitHub-Pages-Workflow baut außerdem den Build-Typ dev des neuesten master und stellt ihn auf der Seite Downloads als agenttools-dev.apk bereit. Es ist ein Release-Build mit eigenem Paket (com.elkuku.agenttools.dev, „AgentTools Dev“, wird neben der Release-App installiert), spricht mit dem Live-Server und hat wie die Debug-Builds den Serverwechsel in den Einstellungen (nur HTTPS). Sein Versionsname ist <versionName>-dev+<run number>.<commit>.
Er ist mit dem Dev-Schlüssel signiert (~/agenttools-dev.jks, Alias agenttools-dev), der in den Repository-Secrets DEV_KEYSTORE_BASE64 (base64 -w0 ~/agenttools-dev.jks) und DEV_KEYSTORE_PASSWORD (Keystore- und Schlüsselpasswort) liegt. Sein SHA-1 hat einen eigenen Android-OAuth-Client für das Paket .dev. Lokal baust du ihn mit den Properties dev.* (siehe local.properties.example): cd app && ./gradlew assembleDev.
Die App veröffentlichen
- Erhöhe
versionCodeundversionNameinapp/app/build.gradle.kts(eine APK mit gleichem oder niedrigeremversionCodelässt sich nicht über die vorherige installieren). - Baue die signierte APK:
cd app && ./gradlew assembleRelease. Sie wird mit dem Keystore auslocal.propertiessigniert (release.*, siehelocal.properties.example) und verwendetserver.baseUrl.release. Der SHA-1 des Release-Schlüssels hat einen eigenen Android-OAuth-Client. - Veröffentliche sie als GitHub-Release, z. B.
gh release create v0.2.0 agenttools-0.2.0.apk --title "AgentTools app 0.2.0". Das README verlinkt das neueste Release.
Die Web-App deployen
Auf dem Server installiert web/bin/deploy die Composer-Abhängigkeiten, erzeugt .env.local.php, kompiliert die Assets und führt die Migrationen mit dem PHP von Plesk aus. Der SSH-Benutzer braucht eine Shell ohne chroot (/bin/bash), sonst ist das PHP von Plesk nicht verfügbar. Die Produktionslogs liegen in web/var/log/prod-*.log.
Slack-Benachrichtigungen
Die Web-App postet nach Slack: neue Benutzer, Stats-Uploads (mit dem AP-Zuwachs) und CSV-Importe in einen Aktivitätskanal (SLACK_CHANNEL) und Fehler (derselbe höchstens alle 10 Minuten; nicht 400, 403, 404, 405, 422) und Deprecations (jede einmal am Tag) in einen Fehlerkanal (SLACK_ERRORS_CHANNEL). Die Produktion nutzt #agenttools-uploads und #agenttools-errors, dev #agenttools-uploads-dev und #agenttools-errors-dev (web/.env.dev); die Tests posten nichts. Dieselben Nachrichten gehen auch an Telegram.
- Auf api.slack.com/apps eine App im Workspace anlegen, ihrem Bot den Scope
chat:writegeben (OAuth & Permissions) und sie installieren. Das Bot-Token (xoxb-…) kopieren. - Die vier Kanäle anlegen und den Bot in jeden einladen (
/invite @AgentTools). - Das Token in
web/.env.localeintragen, auf dem Server und im Dev-Checkout:und deployen (SLACK_DSN=slack://xoxb-…@defaultweb/bin/deployschreibt es in.env.local.php). Andere Kanäle: dort auchSLACK_CHANNELundSLACK_ERRORS_CHANNELsetzen. - Mit
bin/console app:slack:testprüfen (-vzeigt die Antworten von Slack): Es prüft das Token, postet in beide Kanäle der Umgebung und sagt, was bei Fehlern von Slack zu tun ist, z. B.not_in_channel.
Ohne SLACK_DSN (null://null, der Standard in dev und in den Tests) wird nichts gepostet. Ein Slack-Fehler bricht nie eine Anfrage ab: Aktivitäten werden nach der Antwort gesendet, Fehler landen nur im Log.
Telegram-Benachrichtigungen
Dieselben Nachrichten gehen auch an Telegram, über den Bot @IngressAgentToolsBot: Aktivitäten in einen Kanal (TELEGRAM_CHAT_ID), Fehler und Deprecations in einen anderen (TELEGRAM_ERRORS_CHAT_ID). Die Produktion nutzt die Kanäle „AgentTools Uploads“ und „AgentTools Errors“ (web/.env), dev „AgentTools Uploads DEV“ und „AgentTools Errors DEV“ (web/.env.dev); die Bilder liegen in images/telegram/. Eine leere Chat-ID lässt Telegram aus, so wie ein leerer Slack-Kanal Slack auslässt.
- In Telegram bei @BotFather einen neuen Bot anlegen (
/newbot) und sein Token (123456789:AA…) kopieren. - Die vier Kanäle anlegen und den Bot in jedem als Administrator hinzufügen, der Nachrichten posten darf.
- Das Token in
web/.env.localeintragen, auf dem Server und im Dev-Checkout:und deployen.TELEGRAM_DSN=telegram://123456789:AA…@default - Mit
bin/console app:telegram:testprüfen (-vzeigt die Antworten von Telegram): Es prüft das Token, postet in beide Chats der Umgebung und sagt, was bei Fehlern von Telegram zu tun ist, z. B.chat not found.
Andere Chats: app:telegram:test --chats listet die Chats, aus denen der Bot Nachrichten gesehen hat (Telegram behält sie einen Tag, also zuerst eine Nachricht in einem neuen Chat senden); ihre IDs als TELEGRAM_CHAT_ID und TELEGRAM_ERRORS_CHAT_ID in web/.env.local setzen. Die ID einer Gruppe beginnt mit -, die eines Kanals oder einer Supergruppe mit -100.