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.
|
- Timestamps: Fehlende in Google können zu Fallback führen.
|
||||||
- Performance: Bei vielen Terminen könnte Paginierung helfen.
|
- 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
|
## 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'])
|
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':
|
if new_frnr and str(new_frnr) != 'None':
|
||||||
async with conn.transaction():
|
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']}")
|
logger.info(f"Phase 3: Recreated Advoware appointment {new_frnr} for sync_id {row['sync_id']}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Phase 3: Failed to recreate Advoware for sync_id {row['sync_id']}, frNr is None")
|
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:
|
try:
|
||||||
new_event_id = await create_google_event(service, calendar_id, standardize_appointment_data(adv_map[frnr], 'advoware'))
|
new_event_id = await create_google_event(service, calendar_id, standardize_appointment_data(adv_map[frnr], 'advoware'))
|
||||||
async with conn.transaction():
|
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']}")
|
logger.info(f"Phase 3: Recreated Google event {new_event_id} for sync_id {row['sync_id']}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Phase 3: Failed to recreate Google for sync_id {row['sync_id']}: {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 strategy == 'source_system_wins':
|
||||||
if row['source_system'] == 'advoware':
|
if row['source_system'] == 'advoware':
|
||||||
# Check for changes in source (Advoware) or unauthorized changes in target (Google)
|
# 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_str = google_data.get('updated', '')
|
||||||
google_ts = datetime.datetime.fromisoformat(google_ts_str.rstrip('Z')).astimezone(BERLIN_TZ) if google_ts_str else None
|
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']:
|
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)
|
# Check for changes in source (Google) or unauthorized changes in target (Advoware)
|
||||||
google_ts_str = google_data.get('updated', '')
|
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
|
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']:
|
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'])
|
await safe_update_advoware_appointment(advoware, frnr, google_std, row['advoware_write_allowed'], row['employee_kuerzel'])
|
||||||
async with conn.transaction():
|
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}")
|
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'])
|
await safe_update_advoware_appointment(advoware, frnr, google_std, row['advoware_write_allowed'], row['employee_kuerzel'])
|
||||||
async with conn.transaction():
|
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}")
|
logger.info(f"Phase 4: Reset Advoware frNr {frnr} to Google event {event_id}")
|
||||||
elif strategy == 'last_change_wins':
|
elif strategy == 'last_change_wins':
|
||||||
adv_ts = await get_advoware_timestamp(advoware, frnr)
|
adv_ts = await get_advoware_timestamp(advoware, frnr)
|
||||||
|
|||||||
Reference in New Issue
Block a user