Skip to content

MD Analytics (in-app dashboard)

The Astradial iOS app doubles as a Managing Director monitoring tool: an Analytics tab (replacing Favourites) showing company-wide call activity and reports. UI follows the Apple Health design language (Summary cards + charts), per Hari's requirement — pure SwiftUI + Swift Charts, no custom chart libraries.

Tab layout (June 2026 v1, implemented)

The phone tab bar is: Analytics · Recents · Contacts · Keypad · Tickets (Favourites → Analytics and Voicemail → Tickets, both per Hari June 2026; favourites + voicemail code remains dormant in NativePhoneUI.swift).

Tickets tab (implemented)

Mirror of the editor tickets page (architecture), native iOS list UI, in Linphone/UI/Main/TicketsUI.swift:

  • GET /api/v1/tickets — list + status_counts strip (Open / In Progress / Closed pills), Open/In Progress/Closed/All segmented filter, actionable-first order preserved from the API.
  • Rows: caller avatar + name/number, URGENT/HIGH priority badges, "N missed calls" summary fallback, callback-detection line ("Answered call 49s at 7:05 PM", green) from callback_* columns, closed-time line, relative last-call timestamp.
  • Swipe actions: Close / In Progress (PATCH /tickets/:id).
  • Detail: Call Back (dials via liblinphone), status & priority pickers (PATCH), source/missed-attempts/last-call details, call timeline (GET /tickets/:id/events — Missed/Bot Dropped/Outbound badges + duration), callback banner.
  • + manual ticket sheet (POST /tickets, source=manual).
  • Sample-data fallback with connect banner; pull-to-refresh. SSE (/tickets/stream) not consumed yet — candidate follow-up.

Analytics tab = "Pulse" — Health-style page, design approved by Hari 2026-06-10. Decision-first: hero + action list + discipline metric, then exactly three graphs, each with a computed insight line (e.g. "Worst window: 11 AM — 5 of today's 27 missed calls. Add one operator there."). Locked parameters: answer-rate target 95% (orange ≥85, red <85), revenue-at-risk = unrecovered missed callers × ₹150 default (Settings stepper, keychain-free pref).

# Element What it drives
1 Today's Pulse — answer-rate ring + calls/missed/₹ at risk "Are we leaking patients right now?"
2 Recovery Discipline — % recovered (7d) + median time-to-callback vs 15 min + still-unreached count The metric to manage staff on. (Call Back Now card removed 2026-06-10: MDs oversee, they don't dial — the actionable list is the Tickets tab.)
G1 Today by Hour (missed overlay) + worst-window insight Add operator at peak
G2 30-day Volume Trend + week-over-week % insight Demand growth = revenue
G3 New Patients & Follow-Ups (new-caller bars + outbound line) + quota insight Marketing effect + follow-up discipline
All Calls & Recordings list (playback via recording_url) Spot-check conversations

Earlier draft card set (superseded):

Card Metric Chart Data source
Total Calls count, D/W/M range Bar by hour/day GET /api/v1/calls aggregated client-side
Missed Calls inbound non-ANSWERED count Bar (red) same
Avg Pickup Time mean wait_time of ANSWERED Line (green) same — wait_time = duration - billsec (ring time)
User Sessions login/logout per user blocked: needs server API (see gaps)
Recent Calls last 25 org-wide calls list + ▶ recording playback same; recording_url streamed with X-API-Key header

Implementation: Linphone/UI/Main/AstradialMD.swiftAstradialAPI (actor, paginated fetch), AnalyticsViewModel (bucketing into hour/day series), HealthCard (reusable Health-style card), MDSession (FirebaseAuth gate), AstradialSettingsView. When no API key is configured the dashboard renders deterministic sample data with a "Connect" banner — so the UI is demoable offline.

Data access

2026-07-11: Pulse analytics moved server-side

The Pulse tab no longer pulls raw /api/v1/calls rows (~15 serial pages of up to 3000 rows) and aggregates on-device — it fetches one pre-aggregated payload from GET /api/v1/analytics/pulse (PSTN-only, session-deduped in SQL, ~2 KB, ~9 ms) and just renders + derives toggles locally. This is the single source of truth for iOS and Android Pulse; contract + parity in iOS ↔ Android Contract. The web dashboard keeps its own GET /api/v1/calls/stats (different needs; now parallelized). Recents/call-log still use the raw /api/v1/calls list. Shipped: platform PR #529.

  • API: the app never touches MariaDB directly. Everything goes through the AstraPBX Call Logs API (/api/v1/calls + recording_url), authenticated with an ak_ API key (created in dashboard → API & Webhooks; see Authentication). Key is stored in the iOS keychain; base URL + key entered in Settings.
  • Login (no API keys — June 2026): MD signs in with Firebase Auth (same project as the webapp). The app exchanges the Firebase ID token via the platform's existing POST /api/v1/auth/user-login for a 24h org-scoped, role-enriched JWT (also stamps org_users.last_login); all /calls + /tickets requests carry Authorization: Bearer <jwt>. Role from the exchange drives owner gating. ⚠️ The app still bundles Linphone's GoogleService-Info.plist — swap it for an iOS app registered in the Astradial Firebase project or the exchange 401s (task #17).

Settings page (implemented)

iOS-Settings-style Form (sheet from the Analytics profile button, like Health's profile):

  • MD account — Firebase email, sign out.
  • SIP Account (Linphone) — registration status + editable username/password/domain/transport, "Apply & Register" (reuses Linphone's AccountLoginViewModel, i.e. this is also the manual provisioning path until QR scanning lands).
  • Astradial Server — base URL only (auth is the signed-in account).
  • About — app + SDK version.

MD feature list (requested ▸ status)

  1. Missed call notification ▸ server gap — extend ticket-alerts pipeline to push FCM/APNs per missed call (task #16).
  2. User login/logoff time ▸ server gap — needs /api/v1/reports/user-sessions (task #15). Decide source: editor auth sessions vs SIP REGISTER events.
  3. Call trends by day/week/month (graph) ▸ ✅ implemented (D/W/M segmented control).
  4. Average call pickup time by day (graph) ▸ ✅ implemented (line chart from CDR wait_time).
  5. Call recording playback from call logs ▸ ✅ in the Recent Calls card via recording_url (org-wide CDR list, which is the MD view; the device's own Recents tab stays local SIP history).

Server-side gaps (tracked as tasks)

Gap Needed by Proposal
User sessions endpoint User Sessions card GET /api/v1/reports/user-sessions?date= from editor session log or Asterisk REGISTER events
Missed-call push Notifications Auto-ticket pipeline → FCM topic per org MD group
Aggregated reports endpoint (optional) Perf at scale GET /api/v1/reports/call-stats?from&to&bucket=day server-side aggregation; v1 client-side aggregation caps at 1000 CDRs per load
Firebase user → API key exchange Clean onboarding Endpoint exchanging Firebase ID token for scoped ak_ key, so MDs never paste keys

Android parity (private overlay)

On Android the same Pulse Analytics + Tickets tabs ship as the private overlay of astradial-android (the public OSS dialer has neither — see clean split). They consume the identical platform APIs (/api/v1/calls, /tickets, auth/user-login) with the same Firebase-login → org-JWT model. Implementation differences from iOS: native Android (Kotlin + Material/One UI views, not SwiftUI); charts use a lightweight Android approach (Vico / hand-drawn — no heavyweight chart lib), the design parity target being the same Health-style decision-first layout. Tracked under Android milestone M4.