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_countsstrip (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.swift — AstradialAPI (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 anak_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-loginfor a 24h org-scoped, role-enriched JWT (also stampsorg_users.last_login); all/calls+/ticketsrequests carryAuthorization: Bearer <jwt>. Role from the exchange drives owner gating. ⚠️ The app still bundles Linphone'sGoogleService-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)¶
- Missed call notification ▸ server gap — extend ticket-alerts pipeline to push FCM/APNs per missed call (task #16).
- User login/logoff time ▸ server gap — needs
/api/v1/reports/user-sessions(task #15). Decide source: editor auth sessions vs SIP REGISTER events. - Call trends by day/week/month (graph) ▸ ✅ implemented (D/W/M segmented control).
- Average call pickup time by day (graph) ▸ ✅ implemented (line chart from CDR
wait_time). - 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.