Fix Advoware timestamp parsing bug causing sync loops
- Change .replace(tzinfo=BERLIN_TZ) to BERLIN_TZ.localize() for correct pytz TZ handling - Update Phase 4 to use proper TZ localization for adv_ts - Add documentation section on correct Advoware timestamp handling - Add .gitignore, GOOGLE_SETUP_README.md, and check_db.py utility
This commit is contained in:
255
.gitignore
vendored
Normal file
255
.gitignore
vendored
Normal file
@@ -0,0 +1,255 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the executable, and should not be committed.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# Google Service Account credentials
|
||||
service-account.json
|
||||
token.pickle
|
||||
credentials.json
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Invoke
|
||||
.inv
|
||||
|
||||
# NPM cache
|
||||
.npm-cache
|
||||
92
GOOGLE_SETUP_README.md
Normal file
92
GOOGLE_SETUP_README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Google Service Account Setup für Advoware Calendar Sync
|
||||
|
||||
## Übersicht
|
||||
Dieser Calendar Sync verwendet **ausschließlich Google Service Accounts** für die Authentifizierung. Kein OAuth, kein Browser-Interaktion - perfekt für Server-Umgebungen!
|
||||
|
||||
## Voraussetzungen
|
||||
- Google Cloud Console Zugang
|
||||
- Berechtigung zum Erstellen von Service Accounts
|
||||
- (Optional) Google Workspace Admin Zugang für Domain-wide Delegation
|
||||
|
||||
## Schritt 1: Google Cloud Console aufrufen
|
||||
1. Gehen Sie zu: https://console.cloud.google.com/
|
||||
2. Melden Sie sich mit Ihrem Google-Konto an
|
||||
3. Wählen Sie ein bestehendes Projekt aus oder erstellen Sie ein neues
|
||||
|
||||
## Schritt 2: Google Calendar API aktivieren
|
||||
1. Klicken Sie auf "APIs & Dienste" → "Bibliothek"
|
||||
2. Suchen Sie nach "Google Calendar API"
|
||||
3. Klicken Sie auf "Google Calendar API" → "Aktivieren"
|
||||
|
||||
## Schritt 3: Service Account erstellen
|
||||
1. Gehen Sie zu "IAM & Verwaltung" → "Service-Konten"
|
||||
2. Klicken Sie auf "+ Service-Konto erstellen"
|
||||
3. Grundlegende Informationen:
|
||||
- **Service-Kontoname**: `advoware-calendar-sync`
|
||||
- **Beschreibung**: `Service Account für Advoware-Google Calendar Synchronisation`
|
||||
- **E-Mail**: wird automatisch generiert
|
||||
4. Klicken Sie auf "Erstellen und fortfahren"
|
||||
|
||||
## Schritt 4: Berechtigungen zuweisen
|
||||
1. **Rolle zuweisen**: Wählen Sie eine der folgenden Optionen:
|
||||
- Für volle Zugriffe: `Editor`
|
||||
- Für eingeschränkte Zugriffe: `Calendar API Admin` (falls verfügbar)
|
||||
2. Klicken Sie auf "Fertig"
|
||||
|
||||
## Schritt 5: JSON-Schlüssel erstellen und installieren
|
||||
1. Klicken Sie auf das neu erstellte Service-Konto
|
||||
2. Gehen Sie zum Tab "Schlüssel"
|
||||
3. Klicken Sie auf "Schlüssel hinzufügen" → "Neuen Schlüssel erstellen"
|
||||
4. Wählen Sie "JSON" als Schlüsseltyp
|
||||
5. Klicken Sie auf "Erstellen"
|
||||
6. Die JSON-Datei wird automatisch heruntergeladen
|
||||
7. **Benennen Sie die Datei um zu: `service-account.json`**
|
||||
8. **Kopieren Sie die Datei nach: `/opt/motia-app/service-account.json`**
|
||||
9. **Stellen Sie sichere Berechtigungen ein:**
|
||||
```bash
|
||||
chmod 600 /opt/motia-app/service-account.json
|
||||
```
|
||||
|
||||
## Schritt 6: Domain-wide Delegation (nur für Google Workspace)
|
||||
Falls Sie Google Workspace verwenden und auf Kalender anderer Benutzer zugreifen möchten:
|
||||
|
||||
1. Gehen Sie zurück zum Service-Konto
|
||||
2. Aktivieren Sie "Google Workspace Domain-wide Delegation"
|
||||
3. Notieren Sie sich die "Unique ID" des Service-Kontos
|
||||
4. Gehen Sie zu Google Admin Console: https://admin.google.com/
|
||||
5. "Sicherheit" → "API-Berechtigungen"
|
||||
6. "Domain-wide Delegation" → "API-Clienten verwalten"
|
||||
7. Fügen Sie die Unique ID hinzu
|
||||
8. Berechtigungen: `https://www.googleapis.com/auth/calendar`
|
||||
|
||||
## Schritt 7: Testen
|
||||
Nach dem Setup können Sie den Calendar Sync testen:
|
||||
|
||||
```bash
|
||||
# Vollständige Termindetails synchronisieren
|
||||
curl -X POST http://localhost:3000/api/flows/advoware_cal_sync \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"full_content": true}'
|
||||
|
||||
# Nur "blocked" Termine synchronisieren (weniger Details)
|
||||
curl -X POST http://localhost:3000/api/flows/advoware_cal_sync \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"full_content": false}'
|
||||
```
|
||||
|
||||
## Wichtige Hinweise
|
||||
- ✅ **Kein Browser nötig** - läuft komplett server-seitig
|
||||
- ✅ **Automatisch** - einmal setup, läuft für immer
|
||||
- ✅ **Sicher** - Service Accounts haben granulare Berechtigungen
|
||||
- ✅ **Skalierbar** - perfekt für Produktionsumgebungen
|
||||
|
||||
## Fehlerbehebung
|
||||
- **"service-account.json nicht gefunden"**: Überprüfen Sie den Pfad `/opt/motia-app/service-account.json`
|
||||
- **"Access denied"**: Überprüfen Sie die Berechtigungen des Service Accounts
|
||||
- **"API not enabled"**: Stellen Sie sicher, dass Calendar API aktiviert ist
|
||||
- **"Invalid credentials"**: Überprüfen Sie die service-account.json Datei
|
||||
|
||||
## Sicherheit
|
||||
- Halten Sie die `service-account.json` Datei sicher und versionieren Sie sie nicht
|
||||
- Verwenden Sie IAM-Rollen in GCP-Umgebungen statt JSON-Keys
|
||||
- Rotiere Service Account Keys regelmäßig
|
||||
19
bitbylaw/check_db.py
Normal file
19
bitbylaw/check_db.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import asyncio
|
||||
import asyncpg
|
||||
from config import Config
|
||||
|
||||
async def check_db():
|
||||
conn = await asyncpg.connect(
|
||||
host=Config.POSTGRES_HOST or 'localhost',
|
||||
user=Config.POSTGRES_USER,
|
||||
password=Config.POSTGRES_PASSWORD,
|
||||
database=Config.POSTGRES_DB_NAME,
|
||||
timeout=10
|
||||
)
|
||||
try:
|
||||
row = await conn.fetchrow('SELECT * FROM calendar_sync WHERE sync_id = $1', '1329fa1f-9de5-49dc-95c6-a13525f315c5')
|
||||
print('DB Row:', dict(row) if row else 'No row found')
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
asyncio.run(check_db())
|
||||
@@ -256,5 +256,33 @@ Cron-Step für regelmäßige Ausführung.
|
||||
- Timestamps: Fehlende in Google können zu Fallback führen.
|
||||
- Performance: Bei vielen Terminen könnte Paginierung helfen.
|
||||
|
||||
## Korrekter Umgang mit Advoware-Timestamps
|
||||
|
||||
### Problemstellung
|
||||
Advoware-Timestamps (z.B. `'zuletztGeaendertAm'`) werden in Berlin-Zeit geliefert, aber das Parsing mit `datetime.datetime.fromisoformat(...).replace(tzinfo=BERLIN_TZ)` führte zu falschen Offsets (z.B. 53 Minuten Unterschied), da `replace(tzinfo=...)` auf naive datetime nicht korrekt mit pytz-TZ-Objekten funktioniert. Dies verursachte Endlosschleifen in Phase 4, da `adv_ts` falsch hochgesetzt wurde.
|
||||
|
||||
### Lösung
|
||||
Verwende `BERLIN_TZ.localize(naive_datetime)` statt `.replace(tzinfo=BERLIN_TZ)`:
|
||||
- `localize()` setzt die TZ korrekt auf pytz-TZ-Objekte.
|
||||
- Beispiel:
|
||||
```python
|
||||
naive = datetime.datetime.fromisoformat('2025-10-23T14:18:36.245')
|
||||
adv_ts = BERLIN_TZ.localize(naive) # Ergebnis: 2025-10-23 14:18:36.245+02:00
|
||||
```
|
||||
- Dies stellt sicher, dass Timestamps korrekt in UTC konvertiert werden (z.B. 12:18 UTC) und Vergleiche in Phase 4 funktionieren.
|
||||
|
||||
### Implementierung
|
||||
- In `calendar_sync_event_step.py`, Phase 4:
|
||||
```python
|
||||
adv_ts = BERLIN_TZ.localize(datetime.datetime.fromisoformat(adv_data['zuletztGeaendertAm']))
|
||||
```
|
||||
- Für Google-Timestamps: `.astimezone(BERLIN_TZ)` bleibt korrekt.
|
||||
- Alle Timestamps werden zu UTC normalisiert für DB-Speicherung und Vergleiche.
|
||||
|
||||
### Vermeidung von Fehlern
|
||||
- Niemals `.replace(tzinfo=pytz_tz)` verwenden – immer `tz.localize(naive)`.
|
||||
- Teste Parsing: `BERLIN_TZ.localize(datetime.datetime.fromisoformat(ts)).astimezone(pytz.utc)` sollte korrekte UTC ergeben.
|
||||
- Bei anderen TZ: Gleiche Regel anwenden.
|
||||
|
||||
## Erweiterungen
|
||||
|
||||
|
||||
@@ -500,7 +500,7 @@ async def handler(event, context):
|
||||
new_frnr = await safe_create_advoware_appointment(advoware, standardize_appointment_data(google_map[event_id], 'google'), employee_kuerzel, row['advoware_write_allowed'])
|
||||
if new_frnr and str(new_frnr) != 'None':
|
||||
async with conn.transaction():
|
||||
await conn.execute("UPDATE calendar_sync SET advoware_frnr = $1, sync_status = 'synced' WHERE sync_id = $2;", int(new_frnr), row['sync_id'])
|
||||
await conn.execute("UPDATE calendar_sync SET advoware_frnr = $1, sync_status = 'synced', last_sync = $3 WHERE sync_id = $2;", int(new_frnr), row['sync_id'], datetime.datetime.now(BERLIN_TZ))
|
||||
logger.info(f"Phase 3: Recreated Advoware appointment {new_frnr} for sync_id {row['sync_id']}")
|
||||
else:
|
||||
logger.warning(f"Phase 3: Failed to recreate Advoware for sync_id {row['sync_id']}, frNr is None")
|
||||
@@ -557,7 +557,7 @@ async def handler(event, context):
|
||||
try:
|
||||
new_event_id = await create_google_event(service, calendar_id, standardize_appointment_data(adv_map[frnr], 'advoware'))
|
||||
async with conn.transaction():
|
||||
await conn.execute("UPDATE calendar_sync SET google_event_id = $1, sync_status = 'synced' WHERE sync_id = $2;", new_event_id, row['sync_id'])
|
||||
await conn.execute("UPDATE calendar_sync SET google_event_id = $1, sync_status = 'synced', last_sync = $3 WHERE sync_id = $2;", new_event_id, row['sync_id'], datetime.datetime.now(BERLIN_TZ))
|
||||
logger.info(f"Phase 3: Recreated Google event {new_event_id} for sync_id {row['sync_id']}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Phase 3: Failed to recreate Google for sync_id {row['sync_id']}: {e}")
|
||||
@@ -591,7 +591,7 @@ async def handler(event, context):
|
||||
if strategy == 'source_system_wins':
|
||||
if row['source_system'] == 'advoware':
|
||||
# Check for changes in source (Advoware) or unauthorized changes in target (Google)
|
||||
adv_ts = datetime.datetime.fromisoformat(adv_data['zuletztGeaendertAm']).astimezone(BERLIN_TZ)
|
||||
adv_ts = BERLIN_TZ.localize(datetime.datetime.fromisoformat(adv_data['zuletztGeaendertAm']))
|
||||
google_ts_str = google_data.get('updated', '')
|
||||
google_ts = datetime.datetime.fromisoformat(google_ts_str.rstrip('Z')).astimezone(BERLIN_TZ) if google_ts_str else None
|
||||
if adv_ts > row['last_sync']:
|
||||
@@ -609,7 +609,8 @@ async def handler(event, context):
|
||||
# Check for changes in source (Google) or unauthorized changes in target (Advoware)
|
||||
google_ts_str = google_data.get('updated', '')
|
||||
google_ts = datetime.datetime.fromisoformat(google_ts_str.rstrip('Z')).astimezone(BERLIN_TZ) if google_ts_str else None
|
||||
adv_ts = datetime.datetime.fromisoformat(adv_data['zuletztGeaendertAm']).astimezone(BERLIN_TZ)
|
||||
adv_ts = BERLIN_TZ.localize(datetime.datetime.fromisoformat(adv_data['zuletztGeaendertAm']))
|
||||
logger.debug(f"Phase 4: Checking sync_id {row['sync_id']}: adv_ts={adv_ts}, google_ts={google_ts}, last_sync={row['last_sync']}")
|
||||
if google_ts and google_ts > row['last_sync']:
|
||||
await safe_update_advoware_appointment(advoware, frnr, google_std, row['advoware_write_allowed'], row['employee_kuerzel'])
|
||||
async with conn.transaction():
|
||||
@@ -619,7 +620,7 @@ async def handler(event, context):
|
||||
logger.warning(f"Phase 4: Unauthorized change in Advoware frNr {frnr}, resetting to Google event {event_id}")
|
||||
await safe_update_advoware_appointment(advoware, frnr, google_std, row['advoware_write_allowed'], row['employee_kuerzel'])
|
||||
async with conn.transaction():
|
||||
await conn.execute("UPDATE calendar_sync SET sync_status = 'synced', last_sync = $2 WHERE sync_id = $1;", row['sync_id'], google_ts)
|
||||
await conn.execute("UPDATE calendar_sync SET sync_status = 'synced', last_sync = $2 WHERE sync_id = $1;", row['sync_id'], datetime.datetime.now(BERLIN_TZ))
|
||||
logger.info(f"Phase 4: Reset Advoware frNr {frnr} to Google event {event_id}")
|
||||
elif strategy == 'last_change_wins':
|
||||
adv_ts = await get_advoware_timestamp(advoware, frnr)
|
||||
|
||||
Reference in New Issue
Block a user