Files
hotline-planner/dev/backend/web app webhook claude/HotlinePlannerWebhook/DEVLOG.md
2026-02-23 14:02:44 +01:00

6.7 KiB

HotlinePlanner Sync — Dev Log

Stack

  • Runtime: ASP.NET Core 10.0 (net10.0)
  • Auth: Microsoft.Identity.Web 4.3.0 — Confidential Client / OIDC
  • Graph: Microsoft.Graph 5.103.0 (SDK v5)
  • Database: SQLite + EF Core 10.0.3
  • Real-time: ASP.NET Core SignalR (built-in)
  • Token cache: In-memory (prototype) — lost on restart

Setup Checklist

1. Azure App Registration

  1. Go to https://portal.azure.com → App registrations → New registration
  2. Name: HotlinePlannerSync | Supported account types: Single tenant (or multitenant as needed)
  3. Redirect URI: https://YOUR-NGROK-URL.ngrok-free.app/signin-oidc (Web)
  4. After creation, note: Application (client) ID and Directory (tenant) ID
  5. Certificates & secrets → New client secret → copy the Value (not the ID)
  6. API permissions → Add: Calendars.Read (Delegated), offline_access (Delegated) → Grant admin consent

2. ngrok (local webhook exposure)

# Install ngrok from https://ngrok.com
ngrok http 5001 --scheme https
# Copy the HTTPS URL, e.g. https://abc123.ngrok-free.app

Update appsettings.Development.json:

"Webhook": {
  "BaseUrl": "https://abc123.ngrok-free.app",
  "ClientState": "your-random-secret-string"
}

Important: ngrok URL changes on each restart unless you have a paid plan with a static domain.

3. Fill appsettings

In appsettings.Development.json (or use dotnet user-secrets):

"AzureAd": {
  "TenantId":     "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "ClientId":     "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "ClientSecret": "your~client~secret~value"
}

Prefer user-secrets to keep credentials out of source control:

dotnet user-secrets set "AzureAd:TenantId"     "xxx"
dotnet user-secrets set "AzureAd:ClientId"     "xxx"
dotnet user-secrets set "AzureAd:ClientSecret" "xxx"
dotnet user-secrets set "Webhook:BaseUrl"       "https://xxx.ngrok-free.app"
dotnet user-secrets set "Webhook:ClientState"   "my-secret-state"

4. Run

dotnet run
# App starts on https://localhost:5001 (or http://localhost:5000)

Architecture

Browser ──── OIDC login ──────────────────► Microsoft Entra ID
             │                                     │
             │ ◄─── redirect + tokens ─────────────┘
             │
             ▼
      HomeController.Onboard()
         ├─ Upsert UserConfig (SQLite)
         ├─ SubscriptionService.CreateSubscription()  ──► Graph POST /subscriptions
         └─ DeltaSyncService.Sync() [initial]          ──► Graph GET /me/calendarView/delta

Microsoft Graph ──── POST /api/notifications/listen ──────► NotificationsController
                          │
                          ├─ Handshake? → return validationToken as text/plain
                          ├─ Log to WebhookLogs (FIRST)
                          ├─ Verify ClientState
                          ├─ Return 202 Accepted
                          └─ Fire & forget: DeltaSyncService.Sync(userId)
                                                │
                                                └─► Graph GET /me/calendarView/delta?$deltaToken=...
                                                    ├─ Page through results
                                                    ├─ Deduplicate (SHA-256 of Subject+Start+End)
                                                    └─ Save new DeltaLink to UserConfig

SubscriptionRenewalBackgroundService (every 12h)
  └─ Find subscriptions expiring < 24h
  └─ PATCH /subscriptions/{id} with new ExpirationDateTime

DashboardHub (SignalR)
  └─ All events flow through ActivityLogger → SignalR push → live feed in browser

Database Tables

UserConfigs

Column Type Notes
UserId TEXT PK Azure AD Object ID
Email TEXT preferred_username claim
DisplayName TEXT name claim
SubscriptionId TEXT Graph subscription ID
SubscriptionExpiration DATETIME When subscription expires
SubscriptionStatus TEXT None / Active / Expiring / Expired
LastDeltaLink TEXT Full delta URL for incremental sync
CreatedAt DATETIME First login
LastSyncAt DATETIME Last successful delta sync
TotalEventsSynced INT Running count
TenantId TEXT Azure AD Tenant ID

WebhookLogs

Column Type Notes
Id INT PK Auto-increment
ReceivedAt DATETIME UTC timestamp
RawPayload TEXT Full JSON body
EndpointType TEXT Listen / Lifecycle
IsHandshake BOOL Validation token request
ClientState TEXT Received client state
SubscriptionId TEXT From notification
UserId TEXT Extracted from resource path
ChangeType TEXT created / updated / deleted
LifecycleEvent TEXT reauthorizationRequired etc.
ProcessingStatus TEXT Received / Processing / Completed / Failed / Skipped
ErrorMessage TEXT If failed
EventsProcessed INT New/updated from delta
EventsDeduplicated INT Filtered by hash

Key Endpoints

Endpoint Auth Description
GET / Public Landing page
GET /Home/Login Public Triggers OIDC flow
GET /Home/Onboard Authenticated Post-login onboarding
GET /Sync Authenticated Live dashboard
GET /sync/data Authenticated JSON data for dashboard AJAX
POST /sync/manual Authenticated Trigger manual delta sync
GET /sync/log/{id} Authenticated Raw payload for a log entry
POST /api/notifications/listen Anonymous Graph change notifications
POST /api/notifications/lifecycle Anonymous Graph lifecycle notifications

Delta Query Notes

  • Initial sync: GET /me/calendarView/delta?startDateTime=now-30d&endDateTime=now+90d
  • Incremental sync: GET {stored_delta_link} (URL already contains all params)
  • Delta link stored per-user in UserConfigs.LastDeltaLink
  • If delta link is gone (410 Gone), the code will log an error; manually clear LastDeltaLink to force a full re-sync

Token Cache Notes

  • AddInMemoryTokenCaches() stores tokens in RAM — lost on app restart
  • Users must re-login after restart for tokens to repopulate
  • For production: replace with AddDistributedTokenCaches() + Redis/SQL

Changes from Original Spec

  • Target: .NET 10.0 (not 8.0 — SDK 10.0.102 is installed)
  • Excel/OneDrive output: removed (per user request)
  • Real-time dashboard: SignalR push (not polling)
  • Dashboard shows everything: auth events, subscription creation, webhook handshakes, delta syncs, renewals