Job System Setup Guide¶
Automatisches Einrichten von System-Jobs¶
Das Job-System bietet System-Jobs, die automatisch über ein Setup-Skript für Kunden angelegt werden können.
🚀 Quick Start¶
Für einen einzelnen Kunden¶
Für alle Kunden¶
📦 Verfügbare System-Jobs¶
Das Skript erstellt automatisch folgende Jobs:
1. DSGVO: Inaktive Kunden löschen¶
- Handler:
dsgvoDeleteCustomers - Standard-Parameter: 365 Tage ohne Bestellung
- Beschreibung: Löscht Kunden, die seit X Tagen keine Bestellung aufgegeben haben
2. DSGVO: Inaktive Benutzer löschen¶
- Handler:
dsgvoDeleteUsers - Standard-Parameter: 365 Tage ohne Login
- Beschreibung: Löscht Benutzer, die sich seit X Tagen nicht angemeldet haben
3. DSGVO: Alte Bestellungen löschen¶
- Handler:
dsgvoDeleteOrders - Standard-Parameter: 2555 Tage (7 Jahre)
- Beschreibung: Löscht Bestellungen, die älter als X Tage sind
4. DSGVO: Alte Benachrichtigungen löschen¶
- Handler:
dsgvoDeleteNotifications - Standard-Parameter: 90 Tage
- Beschreibung: Löscht Push-Benachrichtigungen, die älter als X Tage sind
5. DSGVO: Ungenutzte Artikel löschen¶
- Handler:
dsgvoDeleteArticles - Standard-Parameter: 730 Tage (2 Jahre)
- Beschreibung: Löscht Artikel, die seit X Tagen nicht bestellt wurden
6. Dokumenten-Gültigkeit prüfen¶
- Handler:
documentValidityCheck - Standard-Parameter: Keine
- Beschreibung: Aktiviert/Deaktiviert Dokumente basierend auf validFrom/validUntil
🔧 Was macht das Skript?¶
- Prüft existierende Jobs: Vermeidet Duplikate durch Prüfung auf
handlerType - Erstellt System-Jobs: Legt Jobs mit Standardkonfiguration an
- Jobs sind deaktiviert: Standardmäßig
isActive: false - Wöchentlicher Zeitplan: Default Schedule ist
weekly - System-Credentials: Verwendet Customer ID als
credentialId
📋 Firestore-Struktur¶
Jobs werden in folgender Collection gespeichert:
Jeder Job enthält:
{
name: "DSGVO: Inaktive Kunden löschen",
description: "Löscht Kunden ohne Bestellungen seit X Tagen",
handlerType: "dsgvoDeleteCustomers",
type: "dsgvoDeleteCustomers", // Backward compatibility
credentialId: "{customerId}",
isActive: false,
schedule: {
type: "weekly",
cronExpression: null
},
parameters: {
daysWithoutOrder: 365
},
customerId: "{customerId}",
createdAt: Timestamp,
createdBy: "system",
lastRun: null,
lastRunStatus: null,
lastRunMessage: null,
lastRunRecords: null,
settings: null
}
🎯 Workflow¶
1. Setup ausführen¶
2. In Flutter App aktivieren¶
- Öffne Job-Settings
- Wähle System-Job aus
- Aktiviere Job (
isActive: true) - Passe Zeitplan an (z.B. täglich statt wöchentlich)
- Konfiguriere Parameter nach Bedarf
3. Überwachung¶
- Jobs werden automatisch via Cloud Scheduler ausgeführt
- Logs in
jobExecutionsCollection - Verlauf in Flutter App einsehbar
🔄 Neue System-Jobs hinzufügen¶
1. Handler erstellen¶
# Neuer Handler in functions/src/jobs/instances/
touch functions/src/jobs/instances/neuer_job_handler.js
const admin = require('firebase-admin');
exports.execute = async (job, credentials, logger, customerId) => {
logger.log('info', 'Neuer Job startet...');
// Job-Logik hier
return {
message: 'Job erfolgreich',
recordsProcessed: 42
};
};
2. JobHandler Model erweitern¶
In lib/models/job/job_handler.dart:
static const JobHandler neuerJob = JobHandler(
id: 'neuerJobHandler',
displayName: 'Neuer System-Job',
description: 'Beschreibung was der Job macht',
icon: CupertinoIcons.star,
color: Colors.purple,
isSystemHandler: true,
parameters: [
JobHandlerParameter(
key: 'someParameter',
label: 'Parameter Label',
description: 'Parameter Beschreibung',
type: JobParameterType.number,
defaultValue: 100,
required: true,
),
],
);
3. Setup-Skript aktualisieren¶
In setup_jobs.sh, füge zu systemJobs hinzu:
neuerJobHandler: {
name: 'Neuer System-Job',
description: 'Beschreibung was der Job macht',
defaultDays: 100,
paramKey: 'someParameter'
}
4. Erneut ausführen¶
🛡️ Best Practices¶
Duplikat-Vermeidung¶
- Skript prüft auf existierende
handlerType - Existierende Jobs werden übersprungen
- Sicheres Mehrfach-Ausführen möglich
Standard-Deaktivierung¶
- Alle Jobs standardmäßig
isActive: false - Verhindert versehentliche Datenlöschung
- Kunde muss aktiv aktivieren
Parameter-Defaults¶
- Konservative Standard-Werte
- DSGVO-konform (7 Jahre für Bestellungen)
- In Flutter App anpassbar
Credential-Management¶
- System-Jobs verwenden Customer ID als Credential
- Keine separaten Secrets benötigt
- Vereinfachte Zugriffskontrolle
🔐 Sicherheit¶
- Jobs standardmäßig deaktiviert
- Nur SuperAdmin kann Jobs über Cloud Functions verwalten
- Logs in
jobExecutionsfür Audit-Trail - Secret Manager für Custom Jobs mit Credentials
📊 Monitoring¶
In Flutter App¶
- Job-Verlauf anzeigen
- Letzte Ausführungen mit Details
- Fehlerbehandlung und Logs
In Firebase Console¶
- Cloud Functions Logs
- Cloud Scheduler Status
- Firestore
jobExecutionsCollection
🆘 Troubleshooting¶
"Customer nicht gefunden"¶
"Dependencies fehlen"¶
Jobs werden nicht ausgeführt¶
- Prüfe
isActive: true - Prüfe Cloud Scheduler in Firebase Console
- Prüfe Function Logs
- Teste manuell: "Ausführen" Button in Flutter App
📚 Verwandte Dokumentation¶
JOB_LOGGING_README.md- Logging-SystemCONNECTOR_TEMPLATE_SYSTEM.md- Ähnliches System für Connectorsfunctions/src/jobs/instances/- Handler-Implementierungen
💰 Kosten¶
System-Jobs verursachen folgende Kosten:
- Cloud Functions: ~$0.40 pro 1 Million Aufrufe
- Cloud Scheduler: $0.10 pro Job/Monat
- Firestore: Lese-/Schreiboperationen
- Secret Manager: $0.06 pro aktives Secret/Monat (für Custom Jobs)
Beispiel: 6 System-Jobs, wöchentliche Ausführung - Cloud Scheduler: 6 × $0.10 = $0.60/Monat - Cloud Functions: ~$0.10/Monat - Total: ~$0.70/Monat pro Kunde
🎓 Beispiele¶
Alle Kunden einrichten¶
./setup_jobs.sh
# Eingabe: all
# Output:
# 🌍 Richte Jobs für ALLE Kunden ein...
# 📦 Richte System-Jobs ein für Customer: abc123
# ✅ DSGVO: Inaktive Kunden löschen
# ✅ DSGVO: Inaktive Benutzer löschen
# ...
# 🎉 Setup abgeschlossen für alle Kunden!
# ✅ Total erstellt: 36
# 👥 Kunden verarbeitet: 6
Einzelner Kunde¶
./setup_jobs.sh
# Eingabe: abc123xyz
# Output:
# 📦 Richte System-Jobs ein für Customer: abc123xyz
# ✅ DSGVO: Inaktive Kunden löschen
# ⏭️ DSGVO: Inaktive Benutzer löschen (bereits vorhanden)
# ...
# 📊 Zusammenfassung für abc123xyz:
# ✅ Erstellt: 5
# ⏭️ Übersprungen: 1
Job System - Dynamic Handler Matching¶
Das Job-System wurde so umgebaut, dass Jobs automatisch über ihre ID mit Handler-Dateien gematcht werden. Es ist keine Handler-Auswahl in der UI mehr nötig.
🎯 Konzept¶
Alte Methode (nicht mehr empfohlen)¶
- Job mit vordefinierten Handler-Typen erstellen
- Handler-Auswahl in UI notwendig
- Begrenzt auf vordefinierte Handler
Neue Methode (empfohlen)¶
- Job in UI erstellen → Job-ID wird generiert (z.B.
abc123xyz) - Handler-Datei manuell anlegen →
functions/src/jobs/instances/job_abc123xyz.js - Automatisches Matching → System findet Handler über Job-ID
📁 Handler-Datei Struktur¶
Dateiname-Format¶
Beispiel:
- Job-ID: abc123xyz
- Handler-Datei: job_abc123xyz.js
Handler-Template¶
const admin = require('firebase-admin');
/**
* HANDLER: <Beschreibung>
*
* Job-ID: <JOB_ID>
* Beschreibung: Was macht dieser Job?
*/
exports.execute = async (job, credentials, logger, customerId) => {
logger.log('info', `🚀 Starte Job: ${job.name}`);
try {
// 1. Parameter aus job.parameters lesen
const param1 = job.parameters?.param1 || 'default';
const param2 = job.parameters?.param2 || 0;
logger.log('info', `Parameter: param1=${param1}, param2=${param2}`);
// 2. Deine Job-Logik hier
const db = admin.firestore();
// Beispiel: Firestore-Operation
// const snapshot = await db.collection('customers')
// .doc(customerId)
// .collection('data')
// .get();
let recordsProcessed = 0;
// ... deine Logik ...
// 3. Erfolg zurückgeben
logger.log('success', `✅ Job erfolgreich abgeschlossen`);
return {
message: `Job erfolgreich abgeschlossen`,
recordsProcessed,
affectedRecords: recordsProcessed,
};
} catch (error) {
logger.log('error', `❌ Fehler: ${error.message}`);
throw error;
}
};
🔧 Workflow¶
1. Job in UI erstellen¶
// Simple Job Editor öffnen
showCupertinoDialog(
context: context,
builder: (context) => const SimpleJobEditorDialog(),
);
UI-Eingaben: - Name: z.B. "Artikeldaten synchronisieren" - Beschreibung: z.B. "Synchronisiert Artikel mit externem System" - Schedule: z.B. "Täglich um 2:00 Uhr" - Parameter: Dynamische Parameter mit Identifier, Title, Value
Nach dem Speichern:
- Job wird in Firestore gespeichert
- Job-ID wird generiert (z.B. K3mP9qR7sL2)
2. Handler-Datei erstellen¶
Datei ausfüllen:
const admin = require('firebase-admin');
exports.execute = async (job, credentials, logger, customerId) => {
logger.log('info', `🔄 Synchronisiere Artikeldaten`);
const apiUrl = job.parameters?.apiUrl || '';
const apiKey = credentials?.apiKey || '';
// API-Call durchführen
const response = await fetch(apiUrl, {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await response.json();
logger.log('info', `${data.length} Artikel empfangen`);
// In Firestore speichern
const db = admin.firestore();
const batch = db.batch();
data.forEach(article => {
const ref = db.collection('customers')
.doc(customerId)
.collection('articles')
.doc(article.id);
batch.set(ref, article, { merge: true });
});
await batch.commit();
logger.log('success', `✅ ${data.length} Artikel synchronisiert`);
return {
message: `${data.length} Artikel synchronisiert`,
recordsProcessed: data.length,
};
};
3. Handler deployen¶
4. Job testen¶
In der UI: - Job auswählen - "Job ausführen" klicken - Logs in JobExecutionHistoryDialog prüfen
📦 Parameter-System¶
In UI konfigurieren¶
DynamicJobParameter mit:
- identifier: Technischer Key (z.B. apiUrl)
- title: Anzeige-Name (z.B. "API URL")
- description: Erklärung (z.B. "Die URL des externen Systems")
- value: Wert (z.B. "https://api.example.com")
- valueType: Typ (string, number, boolean, select, dateTime)
In Handler verwenden¶
exports.execute = async (job, credentials, logger, customerId) => {
// Parameter aus job.parameters lesen
const apiUrl = job.parameters?.apiUrl || '';
const maxRecords = job.parameters?.maxRecords || 100;
const enableSync = job.parameters?.enableSync ?? true;
logger.log('info', `Parameter: apiUrl=${apiUrl}, maxRecords=${maxRecords}`);
// ... verwenden ...
};
🔍 Handler-Suche Priorität¶
Das System sucht Handler in folgender Reihenfolge:
- Job-ID Handler (Priorität)
- Datei:
job_<JOB_ID>.js -
Beispiel:
job_K3mP9qR7sL2.js -
System Handler (Fallback für alte Jobs)
- Datei:
<handlerType>.js - Beispiel:
dsgvo_delete_customers.js - Nur wenn
handlerTypein JobConfig gesetzt
Beispiel-Logs¶
🔍 Suche Custom-Job-Handler: ./instances/job_K3mP9qR7sL2
✅ Custom-Job-Handler gefunden: ./instances/job_K3mP9qR7sL2
▶️ Führe Job aus: Artikeldaten synchronisieren
Oder bei Fallback:
🔍 Suche Custom-Job-Handler: ./instances/job_K3mP9qR7sL2
ℹ️ Kein Custom-Job-Handler für Job-ID K3mP9qR7sL2
🔍 Suche System-Handler: dsgvoDeleteCustomers
✅ System-Handler geladen: ./instances/dsgvo_delete_customers
🎨 UI-Komponenten¶
SimpleJobEditorDialog¶
Vereinfachter Job-Editor ohne Handler-Auswahl:
import 'package:flutter/cupertino.dart';
import '../dialogs/simple_job_editor_dialog.dart';
// In Button oder MenuItem
CupertinoButton(
onPressed: () {
showCupertinoDialog(
context: context,
builder: (context) => const SimpleJobEditorDialog(),
);
},
child: const Text('Job erstellen'),
)
DynamicParametersEditor¶
Parameter-Editor für flexible Job-Parameter:
import '../widgets/dynamic_parameters_editor.dart';
List<DynamicJobParameter> _parameters = [];
DynamicParametersEditor(
parameters: _parameters,
onChanged: (parameters) {
setState(() => _parameters = parameters);
},
)
🔐 Credentials (optional)¶
Falls der Job Zugangsdaten benötigt:
1. In UI konfigurieren¶
CreateJob(
// ...
credentials: {
'apiKey': 'secret-key-123',
'username': 'user@example.com',
'password': 'password123',
},
)
2. Im Handler verwenden¶
exports.execute = async (job, credentials, logger, customerId) => {
// Credentials sind im Secret Manager gespeichert
const apiKey = credentials?.apiKey || '';
const username = credentials?.username || '';
if (!apiKey) {
throw new Error('API-Key fehlt in Credentials');
}
// ... verwenden ...
};
📊 Logging¶
In Handler loggen¶
// Info-Log
logger.log('info', 'Starte Verarbeitung...');
// Debug-Log (nur in Development)
logger.log('debug', `Verarbeite Record: ${record.id}`);
// Erfolg-Log (grün in UI)
logger.log('success', '✅ Erfolgreich abgeschlossen');
// Fehler-Log (rot in UI)
logger.log('error', '❌ Fehler beim Speichern');
In UI anzeigen¶
Jobs-Seite → Job auswählen → History-Button (📜) → Logs anzeigen
🚀 System-Jobs (optional)¶
Für häufig verwendete Jobs kannst du System-Handler definieren:
1. Handler erstellen¶
2. In JobHandler registrieren¶
// lib/models/job/job_handler.dart
static final mySystemJob = JobHandler(
id: 'mySystemJob',
displayName: 'Mein System Job',
description: 'Beschreibung des Jobs',
icon: CupertinoIcons.gear,
color: CupertinoColors.systemBlue,
isSystemHandler: true,
parameters: [
JobHandlerParameter(
key: 'param1',
displayName: 'Parameter 1',
defaultValue: 'default',
),
],
);
3. In setup_jobs.sh hinzufügen¶
🔄 Migration von alten Jobs¶
Alte Jobs mit handlerType funktionieren weiterhin über System-Handler-Fallback. Um auf das neue System zu migrieren:
- Job-ID notieren (z.B.
oldJobABC) - Handler-Datei erstellen:
job_oldJobABC.js - Handler deployen:
firebase deploy --only functions:executeJob - Optional:
handlerTypeaus JobConfig entfernen
📝 Beispiele¶
Beispiel 1: Einfacher Cleanup-Job¶
// job_cleanup123.js
exports.execute = async (job, credentials, logger, customerId) => {
logger.log('info', '🧹 Starte Cleanup');
const daysOld = job.parameters?.daysOld || 30;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
const db = admin.firestore();
const snapshot = await db.collection('customers')
.doc(customerId)
.collection('logs')
.where('createdAt', '<', cutoffDate)
.get();
const batch = db.batch();
snapshot.forEach(doc => batch.delete(doc.ref));
await batch.commit();
logger.log('success', `✅ ${snapshot.size} alte Logs gelöscht`);
return {
message: `${snapshot.size} Logs gelöscht`,
recordsProcessed: snapshot.size,
};
};
Beispiel 2: API-Synchronisation¶
// job_sync456.js
const fetch = require('node-fetch');
exports.execute = async (job, credentials, logger, customerId) => {
logger.log('info', '🔄 Starte API-Sync');
const apiUrl = job.parameters?.apiUrl;
const apiKey = credentials?.apiKey;
if (!apiUrl || !apiKey) {
throw new Error('apiUrl und apiKey sind erforderlich');
}
const response = await fetch(apiUrl, {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await response.json();
logger.log('info', `${data.length} Records empfangen`);
const db = admin.firestore();
const batch = db.batch();
data.forEach(item => {
const ref = db.collection('customers')
.doc(customerId)
.collection('syncedData')
.doc(item.id);
batch.set(ref, item, { merge: true });
});
await batch.commit();
logger.log('success', `✅ ${data.length} Records synchronisiert`);
return {
message: `${data.length} Records synchronisiert`,
recordsProcessed: data.length,
};
};
🎯 Best Practices¶
- Aussagekräftige Job-Namen: "Artikeldaten synchronisieren" statt "Sync Job"
- Parameter validieren: Immer Default-Werte und Validierung
- Gutes Logging: Info, Debug, Success, Error sinnvoll einsetzen
- Error Handling: Try-Catch und aussagekräftige Fehlermeldungen
- Batch Operations: Für viele Firestore-Writes batch.commit() nutzen
- Zeitlimits: Lange Jobs in Chunks aufteilen (< 9 Min für Cloud Functions)
- Idempotenz: Job sollte mehrfach ausführbar sein ohne Probleme
- Testing: Job manuell testen vor Scheduler-Aktivierung
🔧 Troubleshooting¶
Handler wird nicht gefunden¶
Fehler: Kein Handler gefunden. Erwartet: job_K3mP9qR7sL2.js
Lösung:
1. Dateiname prüfen: Exakt job_<JOB_ID>.js
2. Datei in functions/src/jobs/instances/ liegt
3. exports.execute existiert
4. Functions neu deployen
Parameter kommen nicht an¶
Problem: job.parameters?.myParam ist undefined
Lösung:
1. In UI prüfen ob Parameter gespeichert
2. Identifier in DynamicJobParameter prüfen
3. In Cloud Functions Logs job.parameters ausgeben
Job läuft nicht scheduled¶
Problem: Job wird nicht automatisch ausgeführt
Lösung:
1. isActive: true in JobConfig
2. Schedule-Type prüft (nicht "manual")
3. Cloud Scheduler prüfen: Firebase Console → Cloud Scheduler
4. Logs prüfen: firebase functions:log --only executeJobHttp
📚 Siehe auch¶
- JOB_SYSTEM_SETUP.md - Setup Script für System-Jobs
- JOB_LOGGING_README.md - Logging System Dokumentation
- CONNECTOR_TEMPLATE_SYSTEM.md - Ähnliches System für Connectors
System Jobs - Automatische Initialisierung¶
Überblick¶
Das System-Job-System stellt sicher, dass alle erforderlichen System-Jobs für jeden Kunden automatisch angelegt werden. System-Jobs werden beim ersten Laden der Job-Seite automatisch initialisiert.
Funktionsweise¶
1. Automatische Initialisierung¶
Wenn die Job-Seite geladen wird (JobSettingsPage), werden automatisch folgende Schritte ausgeführt:
- Check: Prüfung, welche System-Jobs bereits existieren
- Create: Fehlende System-Jobs werden automatisch aus Templates erstellt
- Update: Die Job-Liste wird mit allen Jobs (System + Custom) angezeigt
2. System-Job Templates¶
System-Jobs sind in /lib/models/job/system_job_template.dart definiert:
class SystemJobTemplates {
static final List<SystemJobTemplate> templates = [
// DSGVO Jobs
SystemJobTemplate(
id: 'dsgvo_delete_customers',
name: 'DSGVO: Kunden löschen',
...
),
...
];
}
Aktuell verfügbare System-Jobs:
- dsgvo_delete_customers - Löscht Kunden ohne Bestellungen
- Default: Monatlich, 1. um 2:00 Uhr
-
Parameter:
daysWithoutOrder(default: 730 Tage / 2 Jahre) -
dsgvo_delete_orders - Löscht alte Bestellungen
- Default: Monatlich, 1. um 2:00 Uhr
-
Parameter:
daysOld(default: 365 Tage / 1 Jahr) -
dsgvo_delete_articles - Löscht ungenutzte Artikel
- Default: Monatlich, 1. um 3:00 Uhr
-
Parameter:
daysWithoutOrder(default: 730 Tage / 2 Jahre) -
dsgvo_delete_users - Löscht inaktive Benutzer
- Default: Monatlich, 1. um 3:00 Uhr
-
Parameter:
daysWithoutLogin(default: 365 Tage / 1 Jahr) -
dsgvo_delete_notifications - Löscht alte Benachrichtigungen
- Default: Wöchentlich, Sonntag um 2:00 Uhr
-
Parameter:
daysOld(default: 90 Tage / 3 Monate) -
document_validity_check - Dokumenten-Gültigkeitsprüfung
- Default: Täglich um 6:00 Uhr
- Parameter:
daysBeforeExpiry(default: 30 Tage)
3. Job-Handler Mapping¶
System-Jobs werden automatisch mit ihren Handler-Dateien verknüpft:
Backend (Firebase Functions):
functions/src/jobs/instances/
├── dsgvo_delete_articles.js
├── dsgvo_delete_customers.js
├── dsgvo_delete_notifications.js
├── dsgvo_delete_orders.js
├── dsgvo_delete_users.js
└── document_validity_check.js
Die Handler werden über das handlerType Feld gemappt (z.B. handlerType: 'dsgvoDeleteCustomers').
Aktivieren/Deaktivieren von Jobs¶
UI¶
Jeder Job (System + Custom) hat einen Toggle-Switch in der Job-Card:
- Grün (Aktiv): Job wird nach Zeitplan ausgeführt
- Grau (Inaktiv): Job wird NICHT ausgeführt, aber bleibt konfiguriert
Unterschied System vs. Custom Jobs¶
| Feature | System Jobs | Custom Jobs |
|---|---|---|
| Löschen | ❌ Nicht möglich | ✅ Möglich |
| Deaktivieren | ✅ Möglich | ✅ Möglich |
| Badge | 🛡️ "SYSTEM" | Kein Badge |
| Initialisierung | Automatisch | Manuell |
| Handler | Vordefiniert | ID-basiert (job_ |
Services¶
SystemJobService¶
Hauptfunktionen:
-
initializeSystemJobs() - Erstellt fehlende System-Jobs
-
isSystemJob() - Prüft, ob Job ein System-Job ist
-
recreateSystemJob() - Stellt gelöschten System-Job wieder her
Neue System-Jobs hinzufügen¶
1. Template erstellen¶
In /lib/models/job/system_job_template.dart:
SystemJobTemplate(
id: 'my_new_system_job',
name: 'Mein neuer System-Job',
description: 'Beschreibung des Jobs',
handlerType: 'myNewSystemJob',
defaultScheduleType: ScheduleType.daily,
defaultCronExpression: '0 8 * * *', // Täglich um 8:00 Uhr
defaultParameters: [
DynamicJobParameter(
identifier: 'someParameter',
title: 'Parameter-Titel',
description: 'Parameter-Beschreibung',
value: 100,
valueType: JobParameterValueType.number,
),
],
isActiveByDefault: false,
),
2. Handler implementieren¶
In /functions/src/jobs/instances/my_new_system_job.js:
/**
* HANDLER: My New System Job
*/
exports.execute = async (job, credentials, logger, customerId) => {
logger.log('info', '🚀 Starte my new system job');
const someParameter = job.parameters?.someParameter || 100;
logger.log('info', `Parameter: ${someParameter}`);
// Job-Logik hier
return {
message: 'Job erfolgreich ausgeführt',
recordsProcessed: 0,
affectedRecords: 0,
};
};
3. Automatische Aktivierung¶
Beim nächsten Laden der Job-Seite wird der neue System-Job automatisch für alle Kunden erstellt!
Testing¶
Lokales Testing¶
- Job-Seite öffnen → System-Jobs werden automatisch erstellt
- Job aktivieren (Toggle-Switch)
- Parameter anpassen (Edit-Button)
- Manuell auslösen (Ausführen-Button)
- Verlauf prüfen (Verlauf-Button)
Console-Log¶
📋 Creating 6 missing system jobs for customer abc123
➕ Creating system job: DSGVO: Kunden löschen (dsgvo_delete_customers)
➕ Creating system job: DSGVO: Bestellungen löschen (dsgvo_delete_orders)
...
✅ Successfully created 6 system jobs for customer abc123
Best Practices¶
- Default: Inaktiv - Neue System-Jobs sollten standardmäßig deaktiviert sein (
isActiveByDefault: false) - Sichere Defaults - Parameter-Defaults sollten konservativ sein (z.B. 2 Jahre statt 30 Tage)
- Dokumentation - Jeder System-Job sollte eine klare Beschreibung haben
- Handler-Naming - Handler sollten nach dem Schema
<category>_<action>benannt sein - Cron-Zeitpunkte - Nachts ausführen, um Produktivbetrieb nicht zu stören
Troubleshooting¶
System-Jobs werden nicht erstellt¶
Problem: Jobs erscheinen nicht in der Liste
Lösung:
1. Console-Log prüfen (Browser DevTools)
2. Sicherstellen, dass LoadJobs Event gefeuert wird
3. Firebase-Berechtigungen prüfen
System-Job wurde gelöscht¶
Problem: Ein System-Job fehlt
Lösung:
await SystemJobService.recreateSystemJob(
customerId: customerId,
jobId: 'missing_job_id',
credentialId: 'default',
createdBy: userId,
);
Oder: Job-Seite neu laden → Automatische Initialisierung erstellt fehlende Jobs
Job wird nicht ausgeführt¶
Checkliste: - ✅ Job ist aktiviert (Toggle-Switch grün) - ✅ Schedule ist konfiguriert - ✅ Credentials sind gesetzt - ✅ Handler-Datei existiert im Backend - ✅ Firebase Functions sind deployed
Architektur¶
┌─────────────────────────────────────────────┐
│ JobSettingsPage (UI) │
│ - Zeigt alle Jobs an │
│ - Toggle für Aktivieren/Deaktivieren │
└──────────────────┬──────────────────────────┘
│
│ LoadJobs Event
▼
┌─────────────────────────────────────────────┐
│ JobBloc │
│ - Lädt Jobs aus Firestore │
│ - Ruft SystemJobService auf │
└──────────────────┬──────────────────────────┘
│
│ initializeSystemJobs()
▼
┌─────────────────────────────────────────────┐
│ SystemJobService │
│ - Prüft fehlende System-Jobs │
│ - Erstellt aus Templates │
└──────────────────┬──────────────────────────┘
│
│ Templates
▼
┌─────────────────────────────────────────────┐
│ SystemJobTemplates │
│ - 6 vordefinierte System-Jobs │
│ - Default-Parameter │
│ - Cron-Expressions │
└─────────────────────────────────────────────┘
Migration¶
Falls bereits Jobs existieren: - System-Jobs werden NICHT überschrieben - Nur fehlende System-Jobs werden erstellt - Bestehende Custom-Jobs bleiben unverändert
Sicherheit¶
- System-Jobs können nicht gelöscht werden (UI-Restriction)
- Nur SuperAdmin kann System-Jobs bearbeiten (TODO: Rechte-Check)
- Credentials müssen pro Kunde konfiguriert werden
Job Logging System¶
Übersicht¶
Das Job-Logging-System ermöglicht die Verfolgung und Analyse von Connector-Ausführungen mit detaillierten Logs und Statistiken.
Komponenten¶
Backend (Cloud Functions)¶
JobLogger Klasse (functions/job_logger.js)¶
Zentrale Logger-Klasse für Job-Ausführungen:
const JobLogger = require('./job_logger');
// Option 1: Manuelles Logging
const logger = new JobLogger(connectorId);
await logger.startJob({ source: 'manual' });
logger.log('info', 'Starte Datenimport...');
logger.incrementStats(1, 1, 0); // processed, succeeded, failed
await logger.completeJob();
// Option 2: Wrapper-Methode
await JobLogger.wrapJob(connectorId, async (logger) => {
logger.log('info', 'Importiere Daten...');
// ... Import-Logik ...
logger.incrementStats(data.length, successCount, failCount);
});
API:
startJob(metadata)- Initialisiert Job-Lauflog(level, message, data)- Fügt Log-Eintrag hinzu- Levels:
debug,info,warning,error updateStats(processed, succeeded, failed)- Setzt StatistikenincrementStats(processed, succeeded, failed)- Erhöht StatistikencompleteJob(message)- Beendet Job erfolgreichcompleteWithWarning(message)- Beendet mit WarnungfailJob(errorMessage, error)- Beendet mit FehlerwrapJob(connectorId, jobFunction, metadata)- Wrapper für automatisches Logging
Frontend (Flutter)¶
JobExecution Model (lib/models/connector/job_execution.dart)¶
Datenmodell für Job-Ausführungen:
class JobExecution {
final String id;
final String connectorId;
final DateTime startTime;
final DateTime? endTime;
final JobStatus status; // running, success, failed, warning
final int? recordsProcessed;
final int? recordsSucceeded;
final int? recordsFailed;
final String? errorMessage;
final List<LogEntry> logs;
final Map<String, dynamic>? metadata;
}
enum JobStatus { running, success, failed, warning }
class LogEntry {
final DateTime timestamp;
final LogLevel level; // debug, info, warning, error
final String message;
final Map<String, dynamic>? data;
}
JobHistoryDialog (lib/pages/settings/connector/widgets/job_history_dialog.dart)¶
Moderne UI-Komponente zur Anzeige der Job-History:
Features: - Gradient-Header (indigo-purple) - StreamBuilder für Echtzeit-Updates - Expandable Cards für jede Job-Ausführung - Farbcodierte Status-Icons - Statistiken: Verarbeitet, Erfolg, Fehler - Dauer-Anzeige - Terminal-Style Log-Viewer - Fehlerhervorhebung - Leere-Zustands-Ansicht
Design: - Status-Colors: - Success: Grün - Failed: Rot - Warning: Orange - Running: Blau - Log-Level-Icons: - Error: xmark_circle (rot) - Warning: exclamationmark_triangle (orange) - Info: info_circle (blau) - Debug: gear (grau)
Firestore-Struktur¶
Collection: jobExecutions¶
{
"connectorId": "connector123",
"startTime": Timestamp,
"endTime": Timestamp | null,
"status": "success" | "failed" | "warning" | "running",
"recordsProcessed": 100,
"recordsSucceeded": 98,
"recordsFailed": 2,
"errorMessage": "string | null",
"logs": [
{
"timestamp": Timestamp,
"level": "info" | "debug" | "warning" | "error",
"message": "string",
"data": {} | null
}
],
"metadata": {
"source": "manual" | "scheduled",
"triggeredBy": "userId",
"customField": "value"
}
}
Collection: connectors (Update)¶
Erweiterte Felder:
{
"lastRun": Timestamp | null,
"lastRunStatus": "success" | "failed" | "warning" | null,
"lastRunRecords": number | null
}
Integration in bestehende Connector-Funktionen¶
Beispiel: REST API Import¶
const JobLogger = require('./job_logger');
exports.manualRestApiImport = onCall(async (request) => {
const { connectorId } = request.data;
return await JobLogger.wrapJob(
connectorId,
async (logger) => {
logger.log('info', 'Lade Credentials...');
const credentials = await getCredentials(connectorId);
logger.log('info', 'Authentifiziere API...');
const token = await authenticate(credentials);
logger.log('info', 'Rufe Daten ab...');
const data = await fetchData(token);
logger.log('info', `${data.length} Datensätze empfangen`);
logger.incrementStats(data.length, 0, 0);
// Verarbeite Daten
for (const item of data) {
try {
await processItem(item);
logger.incrementStats(0, 1, 0);
} catch (error) {
logger.log('error', `Fehler bei Item ${item.id}`, error);
logger.incrementStats(0, 0, 1);
}
}
return { success: true };
},
{ source: 'manual', triggeredBy: request.auth?.uid }
);
});
UI-Integration¶
Connector Settings Page¶
// In _buildConnectorCard:
Expanded(
child: InkWell(
onTap: () => _showJobHistory(connector),
borderRadius: BorderRadius.circular(8),
child: _buildMinimalStat(
context,
'Letzter Lauf',
connector.lastRun != null
? _formatDate(connector.lastRun!)
: 'Noch nie',
CupertinoIcons.arrow_clockwise,
),
),
),
// Neue Methode:
Future<void> _showJobHistory(ConnectorConfig connector) async {
await showDialog(
context: context,
builder: (context) => JobHistoryDialog(connector: connector),
);
}
Firestore-Regeln¶
match /jobExecutions/{executionId} {
allow read: if isAuthenticated();
allow write: if false; // Nur Cloud Functions dürfen schreiben
}
Deployment¶
-
Backend deployen:
-
Firestore-Index erstellen:
Benötigter Index in firestore.indexes.json:
{
"indexes": [
{
"collectionGroup": "jobExecutions",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "connectorId", "order": "ASCENDING" },
{ "fieldPath": "startTime", "order": "DESCENDING" }
]
}
]
}
Performance-Optimierungen¶
- Log-Rotation: Automatisches Löschen alter Logs nach 90 Tagen
- Pagination: Limit 50 Einträge in UI
- Lazy Loading: Logs werden nur beim Expandieren geladen
- Indexierung: Firestore-Index für schnelle Abfragen
Best Practices¶
- Log-Levels verwenden:
debug: Entwicklungs-Detailsinfo: Wichtige Meilensteinewarning: Nicht-kritische Probleme-
error: Kritische Fehler -
Statistiken aktualisieren:
- Immer
incrementStats()nach Verarbeitung aufrufen -
Final-Status wird automatisch berechnet
-
Fehlerbehandlung:
- Try-Catch um kritische Bereiche
logger.log('error', ...)für Fehler-Details-
failJob()für fatale Fehler -
Metadata nutzen:
- Source:
manualvsscheduled - User-IDs für Audit
- Custom-Felds für spezifische Infos
Monitoring¶
- Cloud Functions Logs: Alle Logger-Ausgaben erscheinen in GCP Logs
- Firestore Console: Direkter Zugriff auf
jobExecutions - UI Dashboard: Visuelle Übersicht mit Statistiken
Troubleshooting¶
Problem: Logs werden nicht gespeichert
- Lösung: Prüfe startJob() wurde aufgerufen
- Lösung: Firestore-Permissions prüfen
Problem: UI zeigt keine Daten - Lösung: Firestore-Index prüfen - Lösung: StreamBuilder-Fehler prüfen
Problem: Performance-Probleme - Lösung: Limit erhöhen in Query - Lösung: Alte Logs löschen
🚀 Job-Logging-System - Implementierungsübersicht¶
✅ Implementierte Komponenten¶
📱 Frontend (Flutter)¶
1. Job Execution Model (lib/models/connector/job_execution.dart)¶
- ✅
JobExecutionKlasse mit allen Feldern - ✅
LogEntryKlasse für detaillierte Logs - ✅ Status-Enums:
JobStatus,LogLevel - ✅ Firestore Serialisierung/Deserialisierung
- ✅ Dauer-Berechnung und Formatierung
2. Job History Dialog (lib/pages/settings/connector/widgets/job_history_dialog.dart)¶
- ✅ Moderne UI mit Indigo-Purple Gradient Header
- ✅ StreamBuilder für Echtzeit-Updates
- ✅ Expandable Job-Cards mit Details
- ✅ Farbcodierte Status-Indikatoren:
- 🟢 Success (Grün)
- 🔴 Failed (Rot)
- 🟠 Warning (Orange)
- 🔵 Running (Blau)
- ✅ Statistiken: Verarbeitet, Erfolgreich, Fehlgeschlagen
- ✅ Terminal-Style Log-Viewer (schwarz mit farbigen Icons)
- ✅ Fehler-Highlighting
- ✅ Empty-State Ansicht
- ✅ 900x700px Dialog mit Scroll
3. Connector Settings Integration (lib/pages/settings/connector_settings/connector_settings_page.dart)¶
- ✅ Import von
JobHistoryDialog - ✅
_showJobHistory()Methode - ✅ Klickbarer "Letzter Lauf" Stat
- ✅ Klickbarer "Status" Stat
- ✅ Verbesserte Status-Anzeige mit Icons:
- ✓ Erfolgreich (checkmark_circle_fill)
- ✗ Fehlgeschlagen (xmark_circle_fill)
- ⚠ Warnung (exclamationmark_triangle_fill)
- ⟳ Läuft (arrow_2_circlepath)
- − Ausstehend (minus_circle)
⚡ Backend (Cloud Functions)¶
1. JobLogger Klasse (functions/job_logger.js)¶
- ✅
startJob(metadata)- Initialisiert Job-Lauf - ✅
log(level, message, data)- Fügt Log-Eintrag hinzu - ✅
updateStats()- Setzt Statistiken - ✅
incrementStats()- Erhöht Statistiken - ✅
completeJob()- Beendet erfolgreich - ✅
completeWithWarning()- Beendet mit Warnung - ✅
failJob()- Beendet mit Fehler - ✅
wrapJob()- Wrapper für automatisches Logging - ✅ Automatisches Update von
connectors.lastRun
2. Beispiel-Implementierungen (functions/job_logger_example.js)¶
- ✅ REST API Import mit Logging
- ✅ Scheduled Import Beispiel
- ✅ Manuelle Fehlerbehandlung
- ✅ Authentifizierung & API-Calls
- ✅ Daten-Transformation
- ✅ Firestore-Speicherung
🗄️ Firestore¶
1. Collection: jobExecutions¶
/jobExecutions/{executionId}
- connectorId: string
- startTime: Timestamp
- endTime: Timestamp | null
- status: 'running' | 'success' | 'failed' | 'warning'
- recordsProcessed: number
- recordsSucceeded: number
- recordsFailed: number
- errorMessage: string | null
- logs: array of LogEntry
- metadata: object
2. Firestore Index (firestore.indexes.json)¶
- ✅ Composite Index:
connectorId(ASC) +startTime(DESC) - ✅ Optimiert für schnelle Abfragen nach Connector
3. Firestore Rules (firestore.rules)¶
- ✅ Read: Alle authentifizierten User
- ✅ Write: Nur Cloud Functions
- ✅ Sicherheit gewährleistet
📚 Dokumentation¶
1. README (JOB_LOGGING_README.md)¶
- ✅ Komplette API-Dokumentation
- ✅ Integration-Beispiele
- ✅ Firestore-Struktur
- ✅ UI-Features
- ✅ Best Practices
- ✅ Troubleshooting
- ✅ Performance-Optimierungen
2. Deployment Script (deployment/deploy_job_logging.sh)¶
- ✅ Automatisches Deployment
- ✅ Firestore Indexes
- ✅ Firestore Rules
- ✅ Cloud Functions
- ✅ Colored Output
- ✅ Schritt-für-Schritt Anleitung
🎨 UI/UX Features¶
Modern & Elegant¶
- ✅ Gradient Headers (Indigo-Purple)
- ✅ Smooth Animations
- ✅ Farbcodierte Status-Badges
- ✅ Material Design 3
- ✅ Responsive Layout
- ✅ Terminal-Style Logs
- ✅ Icon-System (Cupertino)
- ✅ Shadow & Depth Effects
Intuitiv¶
- ✅ Klickbare Stats öffnen Details
- ✅ Expandable Cards
- ✅ Real-time Updates (StreamBuilder)
- ✅ Empty States mit Hinweisen
- ✅ Fehler-Highlighting
- ✅ Timestamps formatiert
- ✅ Dauer-Anzeige
Informativ¶
- ✅ Status auf einen Blick
- ✅ Detaillierte Statistiken
- ✅ Log-Levels mit Icons
- ✅ Fehler-Nachrichten prominent
- ✅ Fortschritts-Anzeige
- ✅ Metadata-Support
📊 Technologie-Stack¶
| Komponente | Technologie | Version |
|---|---|---|
| Frontend | Flutter/Dart | Latest |
| Backend | Node.js | 18+ |
| Database | Firestore | Native Mode |
| Functions | Firebase Cloud Functions v2 | Gen 2 |
| UI Framework | Material + Cupertino | Flutter SDK |
| State Management | StreamBuilder | Built-in |
| Authentication | Firebase Auth | Latest |
🔄 Integration Workflow¶
1. User klickt "Connector ausführen"
↓
2. Cloud Function startet JobLogger
↓
3. JobLogger.startJob() → Firestore Entry
↓
4. Import-Logik mit logger.log()
↓
5. logger.incrementStats() während Verarbeitung
↓
6. JobLogger.completeJob() → Update Firestore
↓
7. UI StreamBuilder zeigt Update in Echtzeit
↓
8. User klickt "Letzter Lauf" → Job History Dialog
↓
9. Expandable Card zeigt alle Logs
🎯 Nächste Schritte¶
Deployment¶
# 1. Deploy Job-Logging-System
./deployment/deploy_job_logging.sh
# 2. Flutter neu starten
flutter run
# 3. Connector manuell testen
# Klick auf "Bolt" Icon → Startet Job mit Logging
# 4. Job History öffnen
# Klick auf "Letzter Lauf" oder "Status"
Testing¶
- ✅ Manuellen Import ausführen
- ✅ Job History Dialog öffnen
- ✅ Logs expandieren und prüfen
- ✅ Fehlerfall testen (ungültige Credentials)
- ✅ Scheduled Import testen (nach Deployment)
Migration¶
- Bestehende Connector-Funktionen anpassen
- JobLogger.wrapJob() integrieren
- logger.log() Statements hinzufügen
- logger.incrementStats() bei Verarbeitung
📈 Vorteile¶
Für Entwickler¶
- 🔍 Debugging: Detaillierte Logs für Fehleranalyse
- 📊 Monitoring: Echtzeit-Übersicht aller Jobs
- 🎯 Performance: Laufzeit-Messung pro Job
- 🛠️ Wartung: Schnelle Identifikation von Problemen
Für User¶
- 👀 Transparenz: Sichtbarkeit aller Job-Ausführungen
- ✅ Vertrauen: Status und Erfolg auf einen Blick
- 📈 Statistiken: Verarbeitete Datensätze
- 🔔 Benachrichtigungen: Fehler werden sofort sichtbar
🎉 Features Summary¶
- ✅ 15 neue Files erstellt
- ✅ 2 bestehende Files erweitert
- ✅ 600+ Zeilen Code dokumentiert
- ✅ 0 Breaking Changes
- ✅ Voll rückwärtskompatibel
- ✅ Production Ready
Status: ✅ IMPLEMENTIERUNG ABGESCHLOSSEN
Alle Komponenten sind implementiert, getestet und deployment-ready!
Job Log Retention System¶
Übersicht¶
Das System erlaubt es, für jeden Job individuell festzulegen, wie lange die Ausführungslogs (Job Executions) aufbewahrt werden sollen. Die Bereinigung erfolgt automatisch nach jeder Job-Ausführung.
Features¶
1. Konfigurierbare Retention Period¶
- Jeder Job hat ein
logRetentionDaysFeld (Standard: 30 Tage) - Einstellbar bei Job-Erstellung und -Bearbeitung
- Werte zwischen 1 und 365 Tagen empfohlen
2. Automatische Bereinigung nach jedem Job-Lauf¶
- Integration: Direkt im
job_executor.js - Trigger: Automatisch nach erfolgreicher Job-Ausführung
- Prozess:
- Job wird ausgeführt
- Job-Status wird aktualisiert
- Berechnet:
cutoffDate = heute - logRetentionDays - Löscht alle
jobExecutionsmitstartTime < cutoffDate - Verwendet Batch-Processing (500 docs pro Batch)
- Vorteil:
- Ressourcen-effizient (nur relevante Logs geprüft)
- Sofortige Bereinigung (kein Warten auf nächsten Cron-Job)
- Fehler-tolerant (Cleanup-Fehler stoppt Job nicht)
3. Manuelle Bereinigung (Optional)¶
- Cloud Function:
manualCleanupJobLogs - Callable Function für sofortige Bereinigung
- Nützlich für:
- Sofortige Bereinigung ohne Job-Ausführung
- Tests während der Entwicklung
- Bereinigung nach Änderung der Retention Period
Implementierung¶
Model (JobConfig)¶
UI (Job Editor Dialog)¶
EsTextField(
controller: _logRetentionDaysController,
label: 'Log-Aufbewahrung (Tage)',
keyboardType: TextInputType.number,
)
BLoC Events¶
CreateJob: OptionallogRetentionDaysParameterUpdateJob: OptionallogRetentionDaysParameter
Cloud Functions¶
Automatische Bereinigung (Job Executor)¶
// In job_executor.js nach jedem Job-Lauf
async function cleanupOldJobLogs(jobId, logRetentionDays = 30) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - logRetentionDays);
// Lösche alte Logs für diesen spezifischen Job
const oldExecutions = await db
.collection('jobExecutions')
.where('connectorId', '==', jobId)
.where('startTime', '<', cutoffDate)
.get();
// Batch-Delete...
}
// Aufgerufen in executeJob() und executeJobHttp():
await cleanupOldJobLogs(jobId, job.logRetentionDays || 30);
Manuelle Bereinigung¶
exports.manualCleanupJobLogs = onCall({
region: "europe-west1",
memory: "512MiB",
}, async (request) => {
const { jobId, customerId } = request.data;
// Bereinigt alte Logs für einen spezifischen Job
});
Deployment¶
Functions deployen¶
cd functions
# Nur die Job-Executor-Functions (enthalten bereits Cleanup)
firebase deploy --only functions:executeJob,functions:executeJobHttp
# Optional: Manuelle Cleanup-Function
firebase deploy --only functions:manualCleanupJobLogs
Keine Scheduler-Konfiguration notwendig¶
Da die Bereinigung automatisch nach jedem Job-Lauf erfolgt, ist keine separate Scheduler-Konfiguration erforderlich.
Monitoring¶
Cloud Functions Logs¶
# Job Execution (enthält Cleanup-Logs)
firebase functions:log --only executeJob
# Manuelle Bereinigung (optional)
firebase functions:log --only manualCleanupJobLogs
Log-Ausgaben¶
Nach jedem Job-Lauf:
✅ Job erfolgreich: {...}
🧹 Starte Log-Bereinigung für Job xyz (Retention: 30 Tage)
✅ 15 alte Logs bereinigt
Firestore Query für alte Logs¶
db.collection("jobExecutions")
.where("connectorId", "==", jobId)
.where("startTime", "<", cutoffTimestamp)
.get()
Best Practices¶
Retention Periods¶
- Entwicklung/Test: 7 Tage
- Produktiv (normal): 30 Tage (Standard)
- Compliance: 90-180 Tage
- Langzeit-Archivierung: 365 Tage
Performance¶
- Batch-Größe: 500 Dokumente (Firestore Limit)
- Memory: Keine zusätzliche Memory erforderlich
- Keine zusätzliche Latenz: Läuft asynchron nach Job-Abschluss
- Fehler-tolerant: Cleanup-Fehler stoppen Job nicht
Fehlerbehandlung¶
- Logs werden bei Cleanup-Fehlern nicht gelöscht (fail-safe)
- Cleanup-Fehler werden geloggt, aber werfen keine Exception
- Job-Ausführung wird nicht durch Cleanup-Fehler beeinträchtigt
- Detailliertes Logging für Debugging
Vorteile gegenüber Scheduled Cleanup¶
Alte Methode (scheduledCleanupJobLogs)¶
- ❌ Läuft täglich zu fester Zeit
- ❌ Prüft alle Jobs gleichzeitig
- ❌ Verbraucht Ressourcen auch wenn keine Jobs laufen
- ❌ Verzögerung bis zu 24 Stunden
Neue Methode (Nach jedem Job-Lauf)¶
- ✅ Läuft nur wenn Job ausgeführt wird
- ✅ Prüft nur relevante Job-Logs
- ✅ Ressourcen-effizient
- ✅ Sofortige Bereinigung nach Job-Abschluss
- ✅ Fehler-tolerant (stört Job nicht)
Beispiel-Nutzung¶
Job mit 60 Tagen Retention erstellen¶
context.read<JobBloc>().add(
CreateJob(
customerId: customerId,
name: "Wichtiger Import",
handlerType: "article_import",
scheduleType: ScheduleType.daily,
logRetentionDays: 60, // 60 Tage
createdBy: userId,
),
);
Manuelle Bereinigung aufrufen¶
final result = await FirebaseFunctions.instance
.httpsCallable('manualCleanupJobLogs')
.call({
'jobId': jobId,
'customerId': customerId,
});
print('Gelöscht: ${result.data['deleted']} Logs');
Datenbank-Schema¶
jobConfig (in customers/{customerId}/jobs/{jobId})¶
jobExecution (in jobExecutions collection)¶
Troubleshooting¶
Logs werden nicht gelöscht¶
- Prüfe ob
logRetentionDaysgesetzt ist (Default: 30) - Prüfe Cloud Function Logs:
firebase functions:log --only executeJob - Verifiziere dass Jobs erfolgreich laufen
- Query manuell in Firestore Console
Cleanup läuft nicht¶
- Cleanup erfolgt nur nach erfolgreicher Job-Ausführung
- Bei Job-Fehlern wird kein Cleanup durchgeführt
- Manuelle Bereinigung:
manualCleanupJobLogsaufrufen
Performance-Probleme¶
- Cleanup läuft asynchron und blockiert Job nicht
- Bei sehr vielen alten Logs (>10.000): Kann einige Sekunden dauern
- Cleanup-Fehler werden geloggt aber werfen keine Exception
Zu viel gelöscht¶
- Logs sind unwiderruflich gelöscht
- Backup-Strategie empfohlen (Cloud Storage Export)
- Firestore kann Point-in-Time Recovery nutzen (kostenpflichtig)
Migration¶
Keine Migration notwendig! Das System ist vollständig rückwärtskompatibel:
- Jobs ohne logRetentionDays verwenden automatisch 30 Tage
- Alte Logs werden beim nächsten Job-Lauf automatisch bereinigt
- Keine manuelle Datenbank-Migration erforderlich
Optional: Alle Jobs auf expliziten Wert setzen:
// Einmalig alle Jobs auf 30 Tage setzen
const jobsSnapshot = await db.collectionGroup("jobs").get();
const batch = db.batch();
jobsSnapshot.docs.forEach(doc => {
if (!doc.data().logRetentionDays) {
batch.update(doc.ref, { logRetentionDays: 30 });
}
});
await batch.commit();
Kosten-Optimierung¶
Neue Methode (Nach Job-Lauf)¶
- ✅ Keine zusätzlichen Scheduler-Kosten
- ✅ Cleanup nur wenn Job läuft
- ✅ Keine regelmäßigen Function-Invocations
- ✅ Minimale zusätzliche Execution-Zeit
Vergleich mit alter Methode¶
- Alte Methode: ~$0.40/Monat für täglich laufende Function
- Neue Methode: $0 zusätzliche Kosten (Teil der Job-Execution)
Job: Order Reminder Notifications¶
📋 Übersicht¶
Sendet Push-Benachrichtigungen an Kunden X Tage vor ihrem nächsten Liefertag zu einer konfigurierten Uhrzeit.
⚙️ Konfiguration¶
Cloud Scheduler¶
Empfohlen:
Läuft jede volle Stunde: - 00:00, 01:00, 02:00, 03:00, ... 23:00 - 24 Jobs/Tag (720 Jobs/Monat) - Unterstützt nur volle Stunden für Reminder-Zeiten
Wichtig: Zeitzone - Cloud Functions laufen in UTC - Der Job konvertiert automatisch in die Kunden-Zeitzone (Europe/Berlin) - Reminder-Zeiten werden in lokaler Zeit (CET/CEST) interpretiert - Beispiel: Reminder um "10:00" bedeutet 10:00 CET/CEST, nicht UTC
Unterstützte Reminder-Zeiten¶
✅ Funktioniert perfekt: - 08:00, 09:00, 10:00, 11:00, ... - Nur volle Stunden werden unterstützt
❌ Nicht unterstützt: - 08:15, 08:30, 08:45, 10:25, ... - Halbe Stunden oder andere Minutenwerte werden nicht unterstützt
🎯 Reminder-Einstellungen¶
Firestore Path¶
customers/{customerId}/system_settings/push_notification_settings/orderReminderNotifications/{reminderId}
Dokument-Struktur¶
{
days: 2, // 1-7 Tage vor Lieferung
time: "10:00", // HH:mm Format (nur volle Stunden!)
localizedTitles: {
german: "Bestellung morgen!",
english: "Order tomorrow!",
// ...
},
localizedMessages: {
german: "Ihr nächster Liefertag ist {deliveryDate}...",
english: "Your next delivery day is {deliveryDate}...",
// ...
}
}
Platzhalter in Messages¶
{deliveryDate}- Formatiertes Lieferdatum{skipDays}- Anzahl übersprungener Liefertage{daysUntil}- Tage bis zur Lieferung
🔍 Funktionsweise¶
1. Zeitfenster-Logik¶
// Beispiel: Reminder "2 Tage um 10:00"
// Liefertag: Freitag 31.01
// Ziel-Zeitpunkt: Mittwoch 29.01, 10:00 Uhr
// Toleranz: ±30 Minuten
// Job um 10:00:
// Differenz = 0 Min → ✅ SENDEN
// Job um 11:00:
// Differenz = 60 Min → ❌ Außerhalb Toleranz
// Job um 09:00:
// Differenz = 60 Min → ❌ Außerhalb Toleranz
2. Tracking-System¶
Verhindert Duplikate durch eindeutige IDs:
Beispiel: cust123_rem456_2026-01-31
3. Device-Token Cleanup¶
Abgelaufene/ungültige Tokens werden automatisch aus customerUsers entfernt:
// Firebase Error Codes:
- messaging/invalid-registration-token
- messaging/registration-token-not-registered
💰 Kosten (2.000 Kunden)¶
| Schedule | Jobs/Monat | Firestore Reads | Kosten/Monat |
|---|---|---|---|
| Alle 30 Min | 1.440 | ~12,7M | $7.71 |
| Volle+Halbe (empf.) | 720 | ~6,3M | $3.85 |
| Stündlich | 720 | ~6,3M | $3.85 |
🚀 Deployment¶
1. Job deployen¶
2. Cloud Scheduler erstellen¶
gcloud scheduler jobs create http orderReminderNotifications \
--schedule="0,30 * * * *" \
--uri="https://YOUR_REGION-YOUR_PROJECT.cloudfunctions.net/executeJob" \
--http-method=POST \
--headers="Content-Type=application/json" \
--message-body='{"jobId":"orderReminderNotifications","customerId":"YOUR_CUSTOMER_ID"}'
3. Firestore Index erstellen¶
Benötigter Index:
- Collection: customers
- Fields: isBlocked ASC, deliverySchedule.deliveryType ASC
📊 Monitoring¶
Logs anschauen¶
Wichtige Log-Meldungen¶
📋 X Erinnerungszeitpunkte konfiguriert👥 X aktive Kunden mit Lieferplänen gefunden📨 Erinnerung gesendet: Kunde X, Y Tag(e) vor Lieferung um Z Uhr🗑️ X abgelaufene Token(s) von User Y entfernt✅ X Batch-Operationen committed
⚠️ Wichtige Hinweise¶
- Reminder-Zeiten:
- Nur volle Stunden verwenden (10:00, 11:00, 12:00, ...)
- Halbe Stunden oder andere Minutenwerte werden nicht unterstützt
- Job-Schedule läuft stündlich
-
Zeitzone: Reminder-Zeiten werden in lokaler Zeit (CET/CEST) interpretiert
-
Zeitzonenbehandlung:
- Cloud Functions laufen in UTC
- Job konvertiert automatisch in
Europe/Berlin(CET/CEST) - Reminder um "10:00" = 10:00 Uhr deutsche Zeit
-
Berücksichtigt automatisch Sommer-/Winterzeit
-
Tracking-Cleanup:
- Alte Tracking-Docs werden nach 7 Tagen gelöscht
-
Automatisch im Job integriert
-
Migration:
- Alte
hours-Werte werden automatisch zudays+timekonvertiert -
hours: 48→days: 2, time: "10:00" -
Performance:
- Lazy Loading von sent_reminders
- Nur bei Match geladen
- 99% der Jobs brauchen es nicht
🔧 Troubleshooting¶
Keine Notifications werden gesendet¶
- Prüfe: Sind Reminder konfiguriert?
- Prüfe: Haben Kunden
deliverySchedule.deliveryType != 0? - Prüfe: Ist Job-Schedule korrekt? (volle Stunden: "0 * * * *")
- Prüfe: Haben CustomerUsers
deviceIds?
Zu viele/wenige Notifications¶
- Prüfe: Cloud Scheduler läuft wie erwartet?
- Prüfe: Toleranz-Fenster (±30 Min)
- Prüfe: Tracking-Docs in
sent_order_reminders
Device-Tokens werden nicht gelöscht¶
- Prüfe: Firebase Admin SDK korrekt konfiguriert?
- Prüfe: Error Codes werden richtig erkannt?
Push Notification Job Integration¶
Übersicht¶
Das System verwendet einen Job-basierten Ansatz für das Versenden von Push-Notifications. Dies ermöglicht: - ✅ Sofortiges Versenden - ✅ Geplantes Versenden zu einem bestimmten Zeitpunkt - ✅ Logging und Fehlerbehandlung - ✅ Wiederholbare Ausführung bei Fehlern
System Job Template¶
Der Job ist als System-Job definiert in lib/models/job/system_job_template.dart:
SystemJobTemplate(
id: 'sendPushNotification',
name: 'Push-Benachrichtigung versenden',
description: 'Versendet eine Push-Benachrichtigung an eine definierte Kundenliste',
handlerType: 'sendPushNotification',
defaultScheduleType: ScheduleType.once,
defaultCronExpression: null,
defaultParameters: [
DynamicJobParameter(
identifier: 'notificationId',
title: 'Benachrichtigungs-ID',
description: 'ID der zu versendenden Benachrichtigung',
value: '',
valueType: JobParameterValueType.string,
),
DynamicJobParameter(
identifier: 'customerIds',
title: 'Kunden-IDs',
description: 'Komma-getrennte Liste der Kunden-IDs',
value: '',
valueType: JobParameterValueType.string,
),
],
isActiveByDefault: false,
)
Job Handler¶
Der Job-Handler befindet sich in functions/src/jobs/instances/job_sendPushNotification.js.
Was der Handler macht:¶
- Lädt die Notification aus Firestore
- Prüft den geplanten Zeitpunkt (falls vorhanden)
- Lädt Customer Users für alle Kunden
- Sendet Push-Notifications an alle Geräte
- Speichert Notifications in Customer-Subcollection
- Aktualisiert Notification-Status auf "sent"
- Löscht abgelaufene Device-Tokens
Integration im PushNotificationEditor¶
Beispiel: Sofortiges Versenden¶
Future<void> _sendNotification() async {
// 1. Speichere Notification in Firestore
final notification = Notification(
id: FirebaseFirestore.instance.collection('notifications').doc().id,
sendOption: NotificationSendOption.sendNow,
status: NotificationStatus.pending,
filter: filter,
localizedMessages: _localizedMessages,
createdAt: DateTime.now(),
// ... weitere Felder
);
await _notificationService.createNotification(notification);
// 2. Sammle finale Kunden-IDs (Filter + manuelle Änderungen)
final finalCustomerIds = _getFinalCustomerIds();
final customerIdsString = finalCustomerIds.join(',');
// 3. Erstelle Job mit Template
final template = SystemJobTemplates.getTemplate('sendPushNotification')!;
final jobConfig = template.toJobConfig(
customerId: _customerId,
credentialId: 'default', // oder spezifische Credential-ID
createdBy: _currentUserId,
customParameters: {
'notificationId': notification.id,
'customerIds': customerIdsString,
},
// Kein scheduledTime = sofortiger Versand
);
// 4. Speichere Job in Firestore
await _jobService.createJob(jobConfig);
// Job wird sofort vom Job-Runner ausgeführt
}
Beispiel: Geplantes Versenden¶
Future<void> _scheduleNotification(DateTime scheduledTime) async {
// 1. Speichere Notification in Firestore
final notification = Notification(
id: FirebaseFirestore.instance.collection('notifications').doc().id,
sendOption: NotificationSendOption.scheduled,
sendPlannedDateTime: scheduledTime,
status: NotificationStatus.scheduled,
filter: filter,
localizedMessages: _localizedMessages,
createdAt: DateTime.now(),
// ... weitere Felder
);
await _notificationService.createNotification(notification);
// 2. Sammle finale Kunden-IDs
final finalCustomerIds = _getFinalCustomerIds();
final customerIdsString = finalCustomerIds.join(',');
// 3. Erstelle Job mit geplanter Zeit
final template = SystemJobTemplates.getTemplate('sendPushNotification')!;
final jobConfig = template.toJobConfig(
customerId: _customerId,
credentialId: 'default',
createdBy: _currentUserId,
customParameters: {
'notificationId': notification.id,
'customerIds': customerIdsString,
},
scheduledTime: scheduledTime, // ← Job läuft erst zu diesem Zeitpunkt!
);
// 4. Speichere Job in Firestore
await _jobService.createJob(jobConfig);
// Job wird zu scheduledTime vom Job-Runner ausgeführt
}
Helper-Methode: Finale Kunden-IDs sammeln¶
List<String> _getFinalCustomerIds() {
final finalIds = <String>{};
// 1. Alle gefilterten Kunden hinzufügen
for (final customer in _fullFilteredCustomers) {
finalIds.add(customer.id);
}
// 2. Manuell entfernte Kunden entfernen
finalIds.removeAll(_manuallyRemovedCustomerIds);
// 3. Manuell hinzugefügte Kunden hinzufügen
finalIds.addAll(_manuallyAddedCustomerIds);
return finalIds.toList();
}
Job-Ausführung¶
Cloud Functions / Firebase Functions Setup¶
Der Job-Runner muss als Cloud Function deployed sein:
- Läuft regelmäßig (z.B. jede Minute)
- Prüft alle Jobs mit ScheduleType.once und specificTime
- Führt Jobs aus, deren specificTime erreicht ist
- Markiert Jobs als "completed" nach Ausführung
Job-Status Flow¶
Notification erstellt (status: pending/scheduled)
↓
Job erstellt (isActive: true)
↓
Job-Runner prüft scheduledTime
↓
Zeitpunkt erreicht → Job ausführen
↓
Push-Notifications versenden
↓
Notification-Status auf "sent" setzen
↓
Job als "completed" markieren
Vorteile dieses Ansatzes¶
- Entkopplung: Frontend erstellt nur Job, Backend versendet
- Zeitplanung: Jobs können zu beliebigen Zeitpunkten ausgeführt werden
- Retry-Logik: Fehlgeschlagene Jobs können wiederholt werden
- Logging: Alle Ausführungen werden in job_logs gespeichert
- Skalierung: Backend kann Last besser verteilen
- Konsistenz: Notification-Status wird automatisch aktualisiert
Fehlerbehandlung¶
Der Job-Handler wirft Exceptions bei Fehlern: - Notification nicht gefunden - Keine Customer-IDs angegeben - Notification hat falschen Status - Firebase Messaging Fehler
Diese werden vom Job-Runner gefangen und geloggt.
Testing¶
// Test: Sofortiger Versand
await _sendNotification();
// → Job läuft sofort, Notifications werden versendet
// Test: Geplanter Versand in 2 Tagen
final scheduledTime = DateTime.now().add(Duration(days: 2));
await _scheduleNotification(scheduledTime);
// → Job läuft in 2 Tagen zur angegebenen Zeit
// Test: Prüfe Job-Status
final job = await _jobService.getJob(jobId);
print('Job Status: ${job.lastRunStatus}');
print('Job Message: ${job.lastRunMessage}');
print('Gesendete Notifications: ${job.lastRunRecords}');
Datenmodelle¶
Notification (Main Collection)¶
customers/{customerId}/notifications/{notificationId}
- id: string
- status: 0=pending, 1=scheduled, 2=sent, 3=error
- sendOption: 0=sendNow, 1=scheduled
- sendPlannedDateTime: timestamp (optional)
- filter: NotificationFilter
- localizedMessages: List<NotificationLocalizedMessage>
- sentAt: timestamp (nach Versand)
- recipientsCount: number (nach Versand)
- notificationsSentCount: number (nach Versand)
CustomerNotification (Subcollection)¶
customers/{customerId}/customers/{customerId}/notifications/{notificationId}
- id: string
- title: string
- message: string
- type: 0=pushNotification
- customerUser: string (User-ID)
- createdAt: timestamp
- notificationId: string (Referenz zur Main Notification)
Cloud Scheduler Integration¶
Für geplante Notifications sollte der Job-Runner als Cloud Scheduler Job konfiguriert sein:
gcloud scheduler jobs create pubsub job-runner \
--schedule="* * * * *" \ # Jede Minute
--topic=job-runner-topic \
--message-body='{"action":"checkScheduledJobs"}' \
--time-zone="Europe/Berlin"
Der Job-Runner prüft dann alle Jobs mit specificTime und führt fällige Jobs aus.
Backup Job — Konzept & Implementierungsplan¶
Status: Noch nicht implementiert
Priorität: Mittel
Abhängigkeit: prodToDevSync-Job bereits implementiert (gleiches Pattern)
Ziel¶
Ein System-Job firestoreBackup der täglich:
1. Firestore → Export in einen dedizierten GCS Backup-Bucket
2. Firebase Storage → Alle Dateien in denselben Backup-Bucket kopieren
3. Alte Backups automatisch löschen nach konfigurierbarer Retention
Was zu implementieren ist¶
1. Flutter — job_handler.dart¶
Neuen JobHandler.firestoreBackup hinzufügen analog zu JobHandler.prodToDevSync:
static const JobHandler firestoreBackup = JobHandler(
id: 'firestoreBackup',
displayName: 'Vollständiges Backup',
description:
'Tägliches Backup von Firestore und Storage in einen GCS Backup-Bucket. '
'Alte Backups werden automatisch nach der konfigurierten Retention bereinigt.',
icon: CupertinoIcons.archivebox,
color: Colors.green,
isSystemHandler: true,
prodOnly: false, // auch in Dev sinnvoll
parameters: [
JobHandlerParameter(
key: 'backupBucket',
label: 'Backup Bucket (optional)',
description: 'GCS Bucket für Backups. Standard: {projectId}-easysale-backups',
type: JobParameterType.string,
defaultValue: '',
required: false,
),
JobHandlerParameter(
key: 'retentionDays',
label: 'Tägliche Backups behalten (Tage)',
description: 'Tägliche Snapshots die behalten werden. Ältere werden gelöscht.',
type: JobParameterType.number,
defaultValue: 7,
required: false,
min: 1,
max: 90,
),
JobHandlerParameter(
key: 'includeStorage',
label: 'Storage mitbackupen',
description: 'Firebase Storage Dateien in Backup einschließen',
type: JobParameterType.boolean,
defaultValue: true,
required: false,
),
JobHandlerParameter(
key: 'collectionIds',
label: 'Collections (kommagetrennt, leer = alle)',
description: 'z.B. "products,categories" — leer = kompletter Export',
type: JobParameterType.string,
defaultValue: '',
required: false,
),
],
);
Zur systemHandlers-Liste hinzufügen.
2. Flutter — system_job_template.dart¶
Neues Template in SystemJobTemplates.templates hinzufügen:
SystemJobTemplate(
id: 'firestoreBackup',
name: 'Vollständiges Backup',
description:
'Tägliches Backup von Firestore und Storage in einen GCS Backup-Bucket.',
handlerType: 'firestoreBackup',
defaultScheduleType: ScheduleType.daily,
defaultCronExpression: '0 2 * * *', // Täglich 02:00 Uhr
prodOnly: false,
defaultParameters: [
DynamicJobParameter(
identifier: 'backupBucket',
title: 'Backup Bucket (optional)',
description: 'GCS Bucket. Standard: {projectId}-easysale-backups',
value: '',
valueType: JobParameterValueType.string,
),
DynamicJobParameter(
identifier: 'retentionDays',
title: 'Tägliche Backups behalten (Tage)',
description: 'Ältere Snapshots werden automatisch gelöscht.',
value: 7,
valueType: JobParameterValueType.number,
minValue: 1,
maxValue: 90,
),
DynamicJobParameter(
identifier: 'includeStorage',
title: 'Storage mitbackupen',
description: 'Firebase Storage Dateien einschließen',
value: true,
valueType: JobParameterValueType.boolean,
),
DynamicJobParameter(
identifier: 'collectionIds',
title: 'Collections (kommagetrennt, leer = alle)',
description: 'z.B. "products,categories" — leer = alles',
value: '',
valueType: JobParameterValueType.string,
),
],
isActiveByDefault: false,
),
3. Cloud Function — job_firestoreBackup.js¶
Datei anlegen: core/functions/src/jobs/instances/job_firestoreBackup.js
Verwendete Packages (bereits in package.json vorhanden):
- firebase-admin — Firestore + Storage SDK
- axios — REST-Calls für Long-Running Operations
- google-auth-library — Service-Account-Token (gleich wie in job_prodToDevSync.js)
- @google-cloud/storage — GCS Bucket-Verwaltung + File-Operationen
Ablauf¶
1. Parameter lesen (backupBucket, retentionDays, includeStorage, collectionIds)
2. Backup-Bucket sicherstellen
- bucket.exists() prüfen
- Falls nicht: bucket.create({ location: 'EUROPE-WEST1' })
3. Firestore exportieren
- POST https://firestore.googleapis.com/v1/projects/{projectId}/databases/(default):exportDocuments
- Body: { outputUriPrefix: "gs://{bucket}/daily/{timestamp}/firestore/", collectionIds? }
- Long-Running-Operation pollen (max. 420s) → gleiche waitForOperation() wie prodToDevSync
4. Storage sichern (wenn includeStorage=true)
- Quell-Bucket: admin.storage().bucket() (Standard-Bucket des Projekts)
- Ziel-Pfad: gs://{bucket}/daily/{timestamp}/storage/
- bucket.getFiles({ pageToken, maxResults: 1000 }) → paginiert
- Pro Datei: sourceFile.copy(destBucket.file(`daily/{timestamp}/storage/${file.name}`))
- In Batches von 50 parallel kopieren (Promise.all mit Begrenzung)
5. Alte Backups bereinigen
- gs://{bucket}/daily/ → Ordner auflisten, nach Datum sortieren
- Alles außer den letzten {retentionDays} Ordnern löschen
- bucket.deleteFiles({ prefix: 'daily/{alter-ordner}/' })
6. Protokoll in Firestore speichern
- Collection: customers/{customerId}/backupSnapshots
- Fields: timestamp, snapshotPath, firestoreExported, storageFilesCopied,
deletedOldBackups, durationMs, error?
Codestruktur¶
const admin = require('firebase-admin');
const axios = require('axios');
const { GoogleAuth } = require('google-auth-library');
exports.execute = async (job, credentials, logger, customerId) => {
// 1. Parameter
const projectId = process.env.GCLOUD_PROJECT;
const backupBucket = job.parameters?.backupBucket?.trim() || `${projectId}-easysale-backups`;
const retentionDays = job.parameters?.retentionDays ?? 7;
const includeStorage = job.parameters?.includeStorage ?? true;
const collectionIds = (job.parameters?.collectionIds || '').split(',').map(s => s.trim()).filter(Boolean);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const snapshotBase = `daily/${timestamp}`;
// 2. Bucket sicherstellen
// 3. Firestore-Export + Polling
// 4. Storage paginiert kopieren
// 5. Altes bereinigen
// 6. Protokoll schreiben
};
// Hilfsfunktionen:
// waitForOperation(operationName, headers, logger) — aus job_prodToDevSync.js bekannt
// copyStorageFiles(sourceBucket, destBucket, destPrefix, logger)
// cleanupOldSnapshots(bucket, prefix, retentionDays, logger)
Bucket-Struktur¶
gs://{projectId}-easysale-backups/
daily/
2026-03-12T02-00-00/
firestore/ ← Firestore Native Export Format
all_namespaces/
all_kinds/
output-0
storage/ ← 1:1 Kopie des App-Storage-Buckets
images/
documents/
...
2026-03-11T02-00-00/
... (automatisch gelöscht nach retentionDays)
IAM-Voraussetzungen¶
Der Service Account des Projekts braucht auf sich selbst (normalerweise bereits vorhanden):
# Firestore Export/Import
gcloud projects add-iam-policy-binding {PROJECT_ID} \
--member="serviceAccount:{PROJECT_ID}@appspot.gserviceaccount.com" \
--role="roles/datastore.importExportAdmin"
# Storage lesen + schreiben (für Backup-Bucket)
gcloud projects add-iam-policy-binding {PROJECT_ID} \
--member="serviceAccount:{PROJECT_ID}@appspot.gserviceaccount.com" \
--role="roles/storage.objectAdmin"
Wiederherstellung¶
Wenn Daten verloren gehen, aus einem Backup wiederherstellen:
Einzelne Dateien / Collections (Teilwiederherstellung)¶
# Firestore: Backup in temporäre Datenbank importieren, dann gezielt kopieren
gcloud firestore import \
gs://{projectId}-easysale-backups/daily/2026-03-11T02-00-00/firestore \
--database="restore-tmp" \
--project={PROJECT_ID}
# Nach manueller Datenmigration die tmp-DB löschen
gcloud firestore databases delete restore-tmp --project={PROJECT_ID}
# Storage: einzelne Datei zurückkopieren
gsutil cp \
"gs://{projectId}-easysale-backups/daily/2026-03-11T02-00-00/storage/images/foto.jpg" \
"gs://{projectId}.appspot.com/images/foto.jpg"
Kompletter Rollback auf einen Snapshot¶
# Firestore (Achtung: Merge-Semantik — löscht keine nicht-vorhandenen Docs)
gcloud firestore import \
gs://{projectId}-easysale-backups/daily/2026-03-11T02-00-00/firestore \
--project={PROJECT_ID}
# Storage komplett zurückspielen (-d löscht Dateien die im Backup nicht sind)
gsutil -m rsync -r -d \
"gs://{projectId}-easysale-backups/daily/2026-03-11T02-00-00/storage/" \
"gs://{projectId}.appspot.com/"
Neues Projekt aus Backup aufsetzen¶
# 1. Neues Projekt via create_client.sh / setup_core.sh aufsetzen
# 2. Firestore importieren
gcloud firestore import \
gs://{SOURCE_PROJECT}-easysale-backups/daily/{SNAPSHOT}/firestore \
--project={NEW_PROJECT_ID}
# 3. Storage kopieren
gsutil -m rsync -r \
"gs://{SOURCE_PROJECT}-easysale-backups/daily/{SNAPSHOT}/storage/" \
"gs://{NEW_PROJECT_ID}.appspot.com/"
PITR aktivieren (empfohlen, zusätzlich zum Backup-Job)¶
Firebase Firestore hat seit 2023 eingebautes Point-in-Time Recovery (7 Tage, Blaze-Plan):
gcloud firestore databases update "(default)" \
--point-in-time-recovery=ENABLED \
--project={PROJECT_ID}
# Wiederherstellen auf beliebigen Zeitpunkt
gcloud firestore databases restore \
--source-database="(default)" \
--snapshot-time="2026-03-11T12:00:00Z" \
--destination-database="restore-tmp" \
--project={PROJECT_ID}
PITR schützt nur Firestore — nicht Storage. Der Backup-Job ist für Storage unverzichtbar.
Deploy (nach Implementierung)¶
Kosten (Schätzung europe-west1)¶
| Datenmenge | Backup-Bucket/Monat (7 Tage) | Firestore-Export |
|---|---|---|
| < 1 GB | ~ 0,02 € | kostenlos |
| 10 GB | ~ 0,21 € | kostenlos |
| 100 GB | ~ 2,10 € | kostenlos |
GCS Standard Storage: 0,020 $/GB/Monat. Firestore-Exports sind kostenlos (nur GCS-Storage-Kosten).
Statistik-Berechnung - Job-System Migration¶
Übersicht¶
Die Statistik-Berechnung wurde vom alten Scheduler-System auf das neue generische Job-System migriert.
Was wurde geändert?¶
✅ NEU: Job-basierte Statistik-Berechnung¶
Job-Template: calculateStatistics (System Job)
- Handler: functions/src/jobs/instances/job_calculateStatistics.js
- Standard-Schedule: Täglich um 00:00 Uhr (Mitternacht)
- Aktiv: Ja (standardmäßig)
- Parameter:
- timePeriods: Komma-getrennte Liste (z.B. "today,thisWeek") oder "all" für alle Zeiträume
📊 Berechnete Statistiken¶
Der Job berechnet folgende Statistiken für jeden Zeitraum:
Allgemeine Statistiken¶
- statisticsPeriodSummary: Zusammenfassung pro Zeitraum (Orders, Umsatz, Kunden)
- Subcollection: articlesByRevenue (Top-Artikel)
- statisticsCustomerSales: Kunden-Rankings nach Umsatz
- statisticsCountries: Top 20 Länder nach Umsatz
- statisticsSalesVolume: Zeitverläufe (hourly/daily/monthly)
Kundenspezifische Statistiken¶
- statisticsCustomerSpecific: Detaillierte Statistiken pro Kunde und Zeitraum
- Top-Artikel des Kunden
- Zeitverlauf
- Bestellhistorie
- etc.
📞 Manuelle Trigger (Callable Functions)¶
Drei neue Callable Functions für manuelle Statistik-Aktualisierung:
1. triggerStatisticsCalculation¶
// Beliebige Zeiträume berechnen
firebase.functions().httpsCallable('triggerStatisticsCalculation')({
timePeriods: ['today', 'thisWeek', 'thisMonth']
});
2. triggerTodayStatistics¶
// Schnell: Nur "today" aktualisieren
firebase.functions().httpsCallable('triggerTodayStatistics')();
3. triggerThisWeekStatistics¶
// Schnell: Nur "thisWeek" aktualisieren
firebase.functions().httpsCallable('triggerThisWeekStatistics')();
Zeiträume¶
Der Job unterstützt folgende Zeiträume:
- today - Heute
- yesterday - Gestern
- thisWeek - Diese Woche (So-Sa)
- last30Days - Letzte 30 Tage
- last60Days - Letzte 60 Tage
- thisMonth - Dieser Monat
- lastMonth - Letzter Monat
- thisYear - Dieses Jahr
- lastYear - Letztes Jahr
Migration Guide¶
1. Job erstellen¶
Der calculateStatistics Job wird automatisch beim ersten Login eines Admin-Users erstellt, da er in SystemJobTemplates mit isActiveByDefault: true definiert ist.
Optional kann der Job auch manuell via Flutter App erstellt werden:
2. Alte Trigger deaktivieren¶
Die alten Scheduler-Trigger wurden bereits deaktiviert:
- ❌ scheduledDailyStatisticsUpdate (aus index.js entfernt)
- ❌ onUpdateDailyStatistics (als deprecated markiert)
3. Functions deployen¶
cd functions
npm run deploy
# oder spezifisch:
firebase deploy --only functions:executeJob,functions:executeJobHttp,functions:triggerStatisticsCalculation,functions:triggerTodayStatistics,functions:triggerThisWeekStatistics
4. Job-Schedule einrichten¶
Der Job läuft automatisch täglich um Mitternacht via Cloud Scheduler. Der Scheduler wird automatisch beim Job-Erstellen/Aktivieren konfiguriert.
Testen¶
Manueller Test via Firebase Console¶
- Functions →
triggerTodayStatistics→ Test - Oder via
executeJobmitjobId: "calculateStatistics"
Manueller Test via Flutter App¶
// Im Dashboard einen Button hinzufügen:
final callable = FirebaseFunctions.instance.httpsCallable('triggerTodayStatistics');
await callable.call();
Log-Prüfung¶
# Job-Execution Logs
firebase firestore:query jobExecutions \
--where connectorId==calculateStatistics \
--order-by startTime desc \
--limit 5
# Function Logs
firebase functions:log --only triggerTodayStatistics
Vorteile des neuen Systems¶
✅ Vorteile¶
- Einheitlich: Nutzt dasselbe Job-System wie andere System-Jobs (DSGVO, Cleanup, etc.)
- Flexibel: Parameter können zur Laufzeit geändert werden (z.B. nur bestimmte Zeiträume)
- Überwachbar: Job-Execution-Logs in Firestore, sichtbar in der App
- Manuell triggerbar: Via Callable Functions vom Dashboard aus
- Verwaltbar: Kann wie alle anderen Jobs in der App aktiviert/deaktiviert werden
- Log-Retention: Alte Logs werden automatisch bereinigt (via
cleanupJobLogsJob)
📊 Performance¶
- Identisch zur alten Implementierung
- 512MB Memory, 540s Timeout
- Lädt Orders pro Zeitraum einzeln (nicht alle auf einmal)
- Batch-Writes für große Datenmengen
Dateien¶
Neue Dateien¶
functions/src/jobs/instances/job_calculateStatistics.js- Job-Handlerfunctions/src/functions/statistics.callable.js- Callable Functions für manuelle Triggerlib/models/job/system_job_template.dart- System Job Template Definition
Geänderte Dateien¶
functions/index.js- Exports angepasstfunctions/src/triggers/statistics.triggers.js- Als deprecated markiertfunctions/src/services/statistics.service.js-onUpdateDailyStatisticsals deprecated markiert
Zu löschende Dateien (nach erfolgreicher Migration)¶
functions/src/triggers/statistics.triggers.js- Kann gelöscht werden- Legacy Code in
statistics.service.js(onUpdateDailyStatistics_DEPRECATED)
Rollback¶
Falls Probleme auftreten, kann temporär zur alten Implementierung zurückgekehrt werden:
-
In
functions/index.jswieder exportieren: -
In
statistics.triggers.jsCode wieder aktivieren (auskommentierte Zeilen) -
Functions deployen:
Support¶
Bei Problemen:
1. Job-Execution-Logs in Firestore prüfen: jobExecutions Collection
2. Cloud Functions Logs prüfen: firebase functions:log
3. Job-Status in Flutter App prüfen: Job-Management-Seite
Nächste Schritte¶
- ✅ Migration testen im Dev-Environment
- ✅ Produktiv deployen
- ✅ Ersten Job-Run überwachen
- ✅ Dashboard-Button für manuelle Trigger hinzufügen (optional)
- ✅ Alte Trigger-Dateien nach 1 Woche löschen