Phase 3.3 — Session Topic Sensitivity (complete): - Backend: topic_sensitivity column + session_sensitivity_log, sensitivity service (flip with one-way-latch + audit), PATCH /api/shared/chat/sessions/:id/topic, topic carried in pairing + extension WS payloads, CC filter + sensitive stats + per-mitra sensitive columns on activity page - client_app: TopicSelectionBottomSheet before pricing, topic flows through pairing request, silent WS handler for session_topic_updated - mitra_app: SensitivityBadge + SensitivityTheme + sensitivityConfigProvider, overlay badge + yellow accent, chat screen app-bar toggle with configurable confirmation + latch, extension card shows current flag, history + transcript yellow theme - control_center: Sensitivitas Topik settings section, topic filter + column with inline audit log, sensitive stats dashboard card, mitra activity sensitive columns with QC flag Phase 3.4 — Self-Managed Auth (foundation only): - Migration: auth_sessions + otp_requests tables, social identity columns on customers, password_hash + lockout on control_center_users, OTP + CC lockout app_config keys - New services: password (bcrypt + complexity), token (JWT HS256 + refresh rotation, session_id claim pre-wires future Valkey revocation), social-identity (Google + Apple JWKS), OTP (Fazpass stub — real API TBD) - Constants: AuthProvider + OtpChannel - Middleware, auth route rewrites, WS auth update, Firebase → FCM isolation still pending (next chunk); Fazpass docs + Apple Developer setup still required before E2E testing Docs: - requirement/phase3.3.md, phase3.3-plan.md, phase3.3-testing.md - requirement/phase3.4.md, phase3.4-plan.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
26 KiB
Phase 3.3 Implementation Plan: Session Topic Sensitivity
Summary of Clarified Requirements
| Topic | Decision |
|---|---|
| Customer selection UX | Required bottom sheet after "Mulai Curhat", before pricing; two buttons (not toggle) |
| Customer flag mutability | Locked for session duration — customer cannot change |
| Mitra flag mutability | Can flip mid-session via app-bar toggle |
| Flip confirmation dialog | Default true, disableable via app_config key sensitive_flip_confirmation_enabled |
| One-way latch | Default false, enableable via app_config key sensitive_flag_one_way_latch |
| Visual cue for mitra | Label "Topik sensitif" + warning-yellow doodle background |
| Customer visual cue | None — customer UI stays pink always |
| Storage | chat_sessions.topic_sensitivity enum + mirrored on chat_requests-like flow (actual rows live in chat_sessions before acceptance; see Section 1.1) |
| Audit trail | New session_sensitivity_log table — logs every mitra flip |
| WS event to customer | Sent but silently ignored client-side (future-proofing) |
| Extension | Shows current flag; flag carries over unchanged on acceptance |
| Control center | Sensitive stats panel + filter on sessions page + 2 new config keys |
| Pricing | Unchanged — schema leaves room for future differentiation |
| Push notification content | Stays generic — label only surfaces in overlay |
| Multi-device mitra sync | Out of scope |
| Auto-moderation | Out of scope |
Data Model Note
After reviewing the codebase, chat_requests is NOT a separate table — pairing requests live as rows in chat_sessions with status transitions (SEARCHING → PENDING_ACCEPTANCE → ACTIVE → ...). Mitra notifications are tracked in chat_request_notifications. Therefore, one column on chat_sessions is sufficient — no mirroring needed.
Work Stream 1: Backend — Schema, Services, Routes
1.1 DB Migration
File: backend/src/db/migrate.js
-- New column on chat_sessions
ALTER TABLE chat_sessions
ADD COLUMN IF NOT EXISTS topic_sensitivity VARCHAR(16) NOT NULL DEFAULT 'regular';
-- Optional index if CC filter needs it
CREATE INDEX IF NOT EXISTS idx_chat_sessions_topic_sensitivity
ON chat_sessions (topic_sensitivity);
-- New audit table
CREATE TABLE IF NOT EXISTS session_sensitivity_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
changed_by_mitra_id UUID NOT NULL REFERENCES mitras(id),
from_value VARCHAR(16) NOT NULL,
to_value VARCHAR(16) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_session_sensitivity_log_session
ON session_sensitivity_log (session_id);
Seed new app_config keys (in the same migration or the seed script):
INSERT INTO app_config (key, value) VALUES
('sensitive_flip_confirmation_enabled', 'true'::jsonb),
('sensitive_flag_one_way_latch', 'false'::jsonb)
ON CONFLICT (key) DO NOTHING;
1.2 Constants
File: backend/src/constants.js
const TopicSensitivity = Object.freeze({
REGULAR: 'regular',
SENSITIVE: 'sensitive',
});
const WsMessage = Object.freeze({
// ... existing
SESSION_TOPIC_UPDATED: 'session_topic_updated',
});
1.3 Config Service
File: backend/src/services/config.service.js
Add:
getSensitiveFlipConfirmationEnabled()→ bool (defaulttrue)getSensitiveFlagOneWayLatch()→ bool (defaultfalse)setSensitiveConfig({ flipConfirmationEnabled, oneWayLatch })— used by control center
1.4 Pairing Service — Accept Topic Flag From Customer
File: backend/src/services/pairing.service.js
createPairingRequest({ customerId, durationMinutes, isFreeTrial, topicSensitivity })— new param, validated againstTopicSensitivityenum; defaults toregularif omitted.- Insert
topic_sensitivitycolumn when creating thechat_sessionsrow. - When broadcasting the
chat_requestWS message to candidate mitras, includetopic_sensitivityin the payload (alongsideduration_minutes,is_free_trial).
1.5 New Service: Sensitivity Service
New file: backend/src/services/sensitivity.service.js
Exports:
flipSessionSensitivity({ sessionId, mitraId, toValue })- Load session; verify ownership (mitra_id matches).
- Verify session status is
ACTIVEorEXTENDING(not flippable ifCLOSING,COMPLETED, etc.). - Read
sensitive_flag_one_way_latchfrom config. Iftrueand current value issensitive, reject with 409. - No-op if
fromValue === toValue. - Transaction: UPDATE
chat_sessions.topic_sensitivity, INSERTsession_sensitivity_logrow. - Broadcast WS message
session_topic_updatedto both participants:{ session_id, topic_sensitivity, changed_at }. - Return updated session.
getSessionSensitivityLog(sessionId)— used by control center session detail (future).
1.6 Extension Service — Expose Current Flag
File: backend/src/services/extension.service.js
requestExtensiondoes not change the flag; flag carries over unchanged.- When broadcasting the extension request WS message to the mitra, include
topic_sensitivityfrom the session row.
1.7 New Routes
File: backend/src/routes/public/shared.chat.routes.js (or the existing shared chat routes file — confirm name at implementation)
| Method | Path | Purpose | Auth |
|---|---|---|---|
PATCH |
/api/shared/chat/sessions/:sessionId/topic |
Mitra flips flag | Firebase (mitra) |
Body: { topic_sensitivity: 'regular' \| 'sensitive' }
Response: { session_id, topic_sensitivity, changed_at }
1.8 Client Chat Request Route — Accept Topic Param
File: backend/src/routes/public/client.chat.routes.js — POST /api/client/chat/request
Extend request body schema:
{
duration_minutes: Joi/ajv,
is_free_trial: bool,
topic_sensitivity: enum('regular', 'sensitive') // required
}
Pass through to createPairingRequest.
1.9 Internal Routes — Config + Stats
File: backend/src/routes/internal/config.routes.js
Add:
GET /internal/config/sensitivity→{ flip_confirmation_enabled, one_way_latch }PATCH /internal/config/sensitivity→ accept same shape
File: backend/src/routes/internal/session.routes.js
- Extend session list query to accept
topic_sensitivityfilter param (all/regular/sensitive). - Extend session detail response to include
topic_sensitivityand recentsession_sensitivity_logentries.
File: backend/src/routes/internal/dashboard.routes.js (or wherever dashboard stats live)
- Add
sensitive_session_statsto the dashboard payload:{ total_sessions, sensitive_count, sensitive_percent }over the last N days (match existing stat windows).
1.10 WebSocket — New Message Type
File: backend/src/plugins/websocket.js
No plugin change needed if broadcast is done via existing sendToSessionParticipant helper — just a new WsMessage.SESSION_TOPIC_UPDATED constant.
Work Stream 2: client_app — Topic Selection
2.1 New Widget: Topic Selection Bottom Sheet
New file: client_app/lib/features/chat/widgets/topic_selection_bottom_sheet.dart
A non-dismissible DraggableScrollableSheet (or showModalBottomSheet with isDismissible: false and enableDrag: false) containing:
- Title, body, sub-question (copy as in PRD Section 1)
- Two equal-width buttons: "Topik umum" (primary) + "Topik sensitif" (secondary style, equal height)
- Helper line below buttons
- System back / back button cancels and returns to home
Return: Future<TopicSensitivity?> — null if cancelled, otherwise the selected enum.
2.2 Topic Sensitivity Enum
New file: client_app/lib/core/constants/topic_sensitivity.dart
enum TopicSensitivity {
regular('regular'),
sensitive('sensitive');
final String value;
const TopicSensitivity(this.value);
static TopicSensitivity fromString(String v) =>
values.firstWhere((e) => e.value == v, orElse: () => TopicSensitivity.regular);
}
2.3 Home Screen — Wire Bottom Sheet Into Flow
File: client_app/lib/features/home/home_screen.dart
Current "Mulai Curhat" → opens pricing sheet directly.
New flow:
- Tap "Mulai Curhat"
- Show
TopicSelectionBottomSheet→ await result - If result is null → no-op
- If result is non-null → open existing
PricingBottomSheet, passing selected topic as param
2.4 Pricing Bottom Sheet — Carry Topic
File: client_app/lib/features/chat/widgets/pricing_bottom_sheet.dart
- Accept
TopicSensitivity topicSensitivityconstructor param. - When firing the pairing request (via
PairingNotifier), pass throughtopic_sensitivityto the request body.
2.5 Pairing Notifier — Pass Topic to Backend
File: client_app/lib/core/pairing/pairing_notifier.dart
- Extend
startPairing({ int durationMinutes, bool isFreeTrial, required TopicSensitivity topicSensitivity })to includetopic_sensitivityin the API call body.
2.6 No Customer UI Changes Beyond Selection
- Chat screen remains pink doodle — no conditional coloring.
- WS listener for
session_topic_updatedis wired but silently no-ops (so the server-side event doesn't cause errors).
File: client_app/lib/core/chat/chat_notifier.dart (or wherever WS messages are dispatched)
Add a switch case for session_topic_updated that logs and returns — no state update.
Work Stream 3: mitra_app — Flag Display + Mid-Session Flip
3.1 Shared Enum + Widget: Sensitivity Badge
New file: mitra_app/lib/core/constants/topic_sensitivity.dart — same shape as client_app version.
New file: mitra_app/lib/core/chat/widgets/sensitivity_badge.dart
A small pill widget rendering "Topik sensitif" on yellow background. Configurable size. Reused across overlay, chat header, extension card, history list.
3.2 Chat Request Overlay — Label + Color
File: mitra_app/lib/core/chat/widgets/chat_request_overlay.dart
- Extend
ChatRequestIncomingData(in mitra_app/lib/core/chat/chat_request_notifier.dart) withTopicSensitivity topicSensitivityfield. - Parse from WS
chat_requestmessage payload (topic_sensitivity). - When sensitive: show yellow accent (e.g., left-border or top-strip) +
SensitivityBadgenear session metadata.
3.3 Chat Request Notifier — Parse Topic
File: mitra_app/lib/core/chat/chat_request_notifier.dart
Extend ChatRequestIncomingData constructor + WS handler to populate topicSensitivity.
3.4 Mitra Chat Notifier — Track + Flip Topic
File: mitra_app/lib/core/chat/mitra_chat_notifier.dart
- Add
TopicSensitivity topicSensitivityto the chat state (populated from session fetch on connect). - Handle incoming WS
session_topic_updated→ update state (authoritative from backend — handles the case where mitra flipped on another device in the future). - New method
flipTopic():- Read
sensitive_flip_confirmation_enabledfrom app_config provider (fetched on app start or via dedicated provider) - Show confirmation dialog if enabled
- Call
PATCH /api/shared/chat/sessions/:sessionId/topicvia existingApiClient - Show toast on success; show error dialog on failure (e.g., one-way latch violation → "Sesi sudah ditandai sensitif dan tidak bisa diubah kembali.")
- Read
3.5 Mitra Chat Screen — Yellow Doodle + Toggle
File: mitra_app/lib/features/chat/screens/mitra_chat_screen.dart
- Conditional background: watch
topicSensitivityfrommitraChatNotifier; swap doodle asset (pink ↔ yellow). - Header banner: persistent small strip showing "Topik sensitif" label when sensitive.
- App-bar toggle icon (e.g.,
Icons.flag/Icons.flag_outlined) wired toflipTopic().- If
one_way_latchis enabled and current issensitive, disable the icon and show tooltip "Sesi sudah terkunci sensitif."
- If
3.6 Doodle Assets
New files: mitra_app/assets/doodle_yellow.png (or SVG) — warning-yellow version of existing pink doodle pattern.
- Exact yellow hex: proposed
#FFC107(Material amber) or#F7B500— confirm with designer at implementation. Choose whichever contrasts best with white text/dark badges. - Update
mitra_app/pubspec.yamlassets section.
3.7 Extension UI — Current Flag
File: mitra_app/lib/features/chat/widgets/extension_request_card.dart (or wherever the extension request UI lives)
- Read
topicSensitivityfrom the extension WS payload / session state. - If sensitive: yellow accent +
SensitivityBadgeon the extension card.
3.8 Chat History List + Transcript
File: mitra_app/lib/features/chat/screens/chat_history_screen.dart (confirm name)
- List row: add
SensitivityBadgewhen session is sensitive. - Transcript detail: yellow doodle background for sensitive sessions (pink otherwise).
3.9 Mitra API Client
File: mitra_app/lib/core/network/api_client.dart (confirm name)
Add:
Future<Session> flipSessionTopic(String sessionId, TopicSensitivity to);
Work Stream 4: control_center — Config + Stats + Filter
4.1 Settings Page — New Sensitivity Section
File: control_center/src/pages/settings/SettingsPage.jsx
Add new section "Sensitivitas Topik":
- Checkbox: "Aktifkan dialog konfirmasi saat Mitra menandai topik sensitif" (
sensitive_flip_confirmation_enabled) - Checkbox: "Kunci searah — Mitra tidak bisa membatalkan tanda topik sensitif" (
sensitive_flag_one_way_latch)
Wire to new endpoints: GET/PATCH /internal/config/sensitivity.
4.2 Sessions Page — Filter
File: control_center/src/pages/sessions/SessionsPage.jsx
- Add dropdown filter: "Semua / Umum / Sensitif".
- Pass
topic_sensitivityquery param to session list endpoint. - Add column "Topik" with badge (green "Umum" / yellow "Sensitif").
4.3 Session Detail — Show Audit Trail
File: control_center/src/pages/sessions/SessionDetailPage.jsx (confirm name)
- Show current
topic_sensitivity. - Show compact timeline of
session_sensitivity_logentries: "Mitra X menandai topik sebagai sensitif pada HH:MM".
4.4 Dashboard — Sensitive Stats Panel
File: control_center/src/pages/dashboard/DashboardPage.jsx
- New card: "Sesi Sensitif" with count + percentage over selected date range.
4.5 Mitra Activity — Per-Mitra Sensitive Breakdown
Backend file: backend/src/services/mitra-activity.service.js
Extend getMitraActivitySummary SQL to join chat_request_notifications → chat_sessions on session_id and group per-mitra, returning additional columns:
sensitive_total— count of notifications where joined session hadtopic_sensitivity = 'sensitive'sensitive_accepted— count of those withresponse = 'accepted'sensitive_rejected— count withresponse = 'rejected'sensitive_acceptance_rate—sensitive_accepted / sensitive_total(null ifsensitive_total = 0)
This is a pure join — no new column needed on chat_request_notifications because the flag is derivable from the parent session row.
Backend file: backend/src/routes/internal/mitra-activity.routes.js
GET /internal/mitra-activity/summary response includes the new fields above.
Control center file: control_center/src/pages/mitra-activity/MitraActivityPage.jsx
Extend summary table with additional columns (may need a sub-grouping to keep it readable):
| Mitra | Total | Accepted | Rejected | Missed | Ignored | Rate (%) | Avg Response (s) | Sensitive Total | Sensitive Accepted | Sensitive Rate (%) |
Optionally: color the "Sensitive Rate" cell red when significantly lower than the overall rate — useful for spotting mitras who accept regular requests but consistently skip sensitive ones (potential QC signal).
Optional detail view: on the detail log table, add a "Topik" column with the session's sensitivity badge.
5. Implementation Order
| Step | What | Apps Affected | Dependencies |
|---|---|---|---|
| Work Stream 1: Backend | |||
| 1 | DB migration: topic_sensitivity column, session_sensitivity_log table, 2 new app_config keys |
Backend | None |
| 2 | Constants: TopicSensitivity, WsMessage.SESSION_TOPIC_UPDATED |
Backend | None |
| 3 | Config service: getters + setter for sensitivity flags | Backend | Step 1 |
| 4 | Pairing service: accept + persist + broadcast topic_sensitivity |
Backend | Steps 1–2 |
| 5 | New sensitivity service: flipSessionSensitivity, getSessionSensitivityLog |
Backend | Steps 1–4 |
| 6 | Extension service: include topic_sensitivity in WS payload |
Backend | Step 4 |
| 7 | New route: PATCH /api/shared/chat/sessions/:sessionId/topic |
Backend | Step 5 |
| 8 | Extend POST /api/client/chat/request schema |
Backend | Step 4 |
| 9 | Internal routes: /internal/config/sensitivity, filter on sessions, dashboard sensitive stats |
Backend | Step 3, 5 |
| Work Stream 2: client_app | |||
| 10 | TopicSensitivity enum |
client_app | None |
| 11 | TopicSelectionBottomSheet widget |
client_app | Step 10 |
| 12 | Home screen: new flow (topic sheet → pricing) | client_app | Step 11 |
| 13 | Pricing sheet + PairingNotifier: accept + forward flag | client_app | Steps 10, 12 |
| 14 | ChatNotifier: silently handle session_topic_updated WS |
client_app | None |
| Work Stream 3: mitra_app | |||
| 15 | TopicSensitivity enum + SensitivityBadge widget + yellow doodle asset |
mitra_app | None |
| 16 | ChatRequestNotifier + overlay: parse topic, show badge + yellow accent | mitra_app | Step 15 |
| 17 | MitraChatNotifier: track topic, handle session_topic_updated, flipTopic() |
mitra_app | Step 15, backend step 7 |
| 18 | Mitra chat screen: yellow doodle + header + app-bar toggle + config-driven dialog/latch | mitra_app | Step 17 |
| 19 | Extension card: show current flag | mitra_app | Step 15, backend step 6 |
| 20 | History list + transcript: badge + yellow doodle | mitra_app | Step 15 |
| Work Stream 4: control_center | |||
| 21 | Settings page: sensitivity config section | control_center | Backend step 9 |
| 22 | Sessions list: filter + column | control_center | Backend step 9 |
| 23 | Session detail: audit trail timeline | control_center | Backend step 9 |
| 24 | Dashboard: sensitive stats card | control_center | Backend step 9 |
| 25 | Mitra Activity: extend summary SQL + API + table with sensitive columns | Backend + control_center | Backend step 1 |
| Testing | |||
| 26 | E2E: customer selects sensitive → mitra sees label + yellow → mitra flips → audit + stats reflect | All | All above |
6. New Files
| File | Purpose |
|---|---|
backend/src/services/sensitivity.service.js |
Flip + audit + one-way-latch enforcement |
client_app/lib/core/constants/topic_sensitivity.dart |
Enum |
client_app/lib/features/chat/widgets/topic_selection_bottom_sheet.dart |
Customer topic-selection UI |
mitra_app/lib/core/constants/topic_sensitivity.dart |
Enum |
mitra_app/lib/core/chat/widgets/sensitivity_badge.dart |
Reusable badge |
mitra_app/assets/doodle_yellow.png |
Yellow doodle for sensitive sessions |
7. Modified Files (Primary)
| File | Change |
|---|---|
backend/src/db/migrate.js |
Migration + seed |
backend/src/constants.js |
TopicSensitivity + WsMessage.SESSION_TOPIC_UPDATED |
backend/src/services/config.service.js |
Getters/setter |
backend/src/services/pairing.service.js |
Accept + persist + broadcast topic |
backend/src/services/extension.service.js |
Include topic in WS payload |
backend/src/routes/public/client.chat.routes.js |
Request body schema |
backend/src/routes/public/shared.chat.routes.js |
New PATCH topic route |
backend/src/routes/internal/config.routes.js |
GET/PATCH sensitivity config |
backend/src/routes/internal/session.routes.js |
Filter + audit in detail |
backend/src/routes/internal/dashboard.routes.js |
Sensitive stats |
client_app/lib/features/home/home_screen.dart |
New pre-pricing step |
client_app/lib/features/chat/widgets/pricing_bottom_sheet.dart |
Accept + forward topic |
client_app/lib/core/pairing/pairing_notifier.dart |
API body |
client_app/lib/core/chat/chat_notifier.dart |
No-op WS handler |
mitra_app/lib/core/chat/chat_request_notifier.dart |
Parse topic |
mitra_app/lib/core/chat/widgets/chat_request_overlay.dart |
Badge + yellow accent |
mitra_app/lib/core/chat/mitra_chat_notifier.dart |
State + flip method + WS handler |
mitra_app/lib/features/chat/screens/mitra_chat_screen.dart |
Yellow doodle + app-bar toggle |
mitra_app/lib/features/chat/widgets/extension_request_card.dart |
Badge + accent |
mitra_app/lib/features/chat/screens/chat_history_screen.dart |
Badge on row + yellow transcript |
mitra_app/pubspec.yaml |
Register new asset |
control_center/src/pages/settings/SettingsPage.jsx |
Sensitivity config section |
control_center/src/pages/sessions/SessionsPage.jsx |
Filter + column |
control_center/src/pages/dashboard/DashboardPage.jsx |
Sensitive stats card |
8. Risks & Mitigations
| Risk | Mitigation |
|---|---|
Existing sessions have no topic_sensitivity value after migration |
Column has DEFAULT 'regular' + NOT NULL — backfill automatic |
| Mitra flips topic but WS delivery fails to customer | Customer silently ignores anyway; backend state is source of truth, no user-facing consequence |
Race: mitra flips to sensitive, then to regular while latch config toggles mid-session |
flipSessionSensitivity reads latch config at flip-time; flip is atomic in a DB transaction |
| Customer cancels topic-selection sheet → home screen flashes | Confirm sheet result is null and return cleanly to home — no pairing request created |
| Yellow color clashes with existing pink UI elements | Restrict yellow to chat screen background + extension card; test contrast with message bubbles and badges |
Mitra flips flag after session is CLOSING |
Backend rejects with 409; mitra UI gracefully shows toast "Sesi sudah berakhir" |
| Old sessions in history have no audit log entries | No migration needed — history just shows current value with no log entries |
9. Testing Checklist
Customer flow (client_app):
- Tap "Mulai Curhat" → topic sheet appears, cannot dismiss by tap-outside
- System back cancels entire flow
- Topik umum → pricing sheet shows, pairing request sent with
topic_sensitivity: regular - Topik sensitif → pricing sheet shows, pairing request sent with
topic_sensitivity: sensitive - Chat screen stays pink regardless of flag
- Customer receives
session_topic_updatedWS message after mitra flip → no UI change, no error
Mitra flow (mitra_app):
- Sensitive incoming request → yellow accent + "Topik sensitif" badge on overlay
- Regular incoming request → no badge, no color change
- Sensitive active session → yellow doodle background + header label
- Flip toggle (confirmation enabled): dialog shows, Batal cancels, Tandai flips
- Flip toggle (confirmation disabled via CC): flips instantly with toast
- One-way latch enabled + session already sensitive: toggle disabled with tooltip
- One-way latch enabled + session regular: can flip to sensitive, then toggle locks
- Mitra requests extension on sensitive session → extension card shows badge + yellow
- Mitra requests extension after mid-session flip to sensitive → extension card reflects current flag
- History list: sensitive sessions show badge
- Transcript: sensitive sessions show yellow doodle
Control center:
- Settings page: toggle
sensitive_flip_confirmation_enabled→ mitra app behavior changes on next flip - Settings page: toggle
sensitive_flag_one_way_latch→ mitra app behavior changes - Sessions page: filter "Sensitif" only shows sensitive sessions
- Session detail: audit trail shows all flips with mitra name + timestamp
- Dashboard: sensitive session count + % matches manual query
- Mitra Activity: per-mitra sensitive total / accepted / rate columns populate correctly
- Mitra with 0 sensitive requests shows
—(not 0%) for sensitive rate
Backend:
flipSessionSensitivitylogs every call insession_sensitivity_log- Ownership check: mitra A cannot flip mitra B's session
- Status check: flip on
CLOSING/COMPLETEDsession → 409 - Latch violation returns 409 with clear error code
- No-op flip (same value) does not create log entry