# 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](backend/src/db/migrate.js) ```sql -- 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): ```sql 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](backend/src/constants.js) ```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](backend/src/services/config.service.js) Add: - `getSensitiveFlipConfirmationEnabled()` → bool (default `true`) - `getSensitiveFlagOneWayLatch()` → bool (default `false`) - `setSensitiveConfig({ flipConfirmationEnabled, oneWayLatch })` — used by control center ### 1.4 Pairing Service — Accept Topic Flag From Customer **File:** [backend/src/services/pairing.service.js](backend/src/services/pairing.service.js) - `createPairingRequest({ customerId, durationMinutes, isFreeTrial, topicSensitivity })` — new param, validated against `TopicSensitivity` enum; defaults to `regular` if omitted. - Insert `topic_sensitivity` column when creating the `chat_sessions` row. - When broadcasting the `chat_request` WS message to candidate mitras, include `topic_sensitivity` in the payload (alongside `duration_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 `ACTIVE` or `EXTENDING` (not flippable if `CLOSING`, `COMPLETED`, etc.). - Read `sensitive_flag_one_way_latch` from config. If `true` and current value is `sensitive`, reject with 409. - No-op if `fromValue === toValue`. - Transaction: UPDATE `chat_sessions.topic_sensitivity`, INSERT `session_sensitivity_log` row. - Broadcast WS message `session_topic_updated` to 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](backend/src/services/extension.service.js) - `requestExtension` does not change the flag; flag carries over unchanged. - When broadcasting the extension request WS message to the mitra, include `topic_sensitivity` from the session row. ### 1.7 New Routes **File:** [backend/src/routes/public/shared.chat.routes.js](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](backend/src/routes/public/client.chat.routes.js) — `POST /api/client/chat/request` Extend request body schema: ```js { 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](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](backend/src/routes/internal/session.routes.js) - Extend session list query to accept `topic_sensitivity` filter param (`all` / `regular` / `sensitive`). - Extend session detail response to include `topic_sensitivity` and recent `session_sensitivity_log` entries. **File:** [backend/src/routes/internal/dashboard.routes.js](backend/src/routes/internal/dashboard.routes.js) (or wherever dashboard stats live) - Add `sensitive_session_stats` to 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](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` — null if cancelled, otherwise the selected enum. ### 2.2 Topic Sensitivity Enum **New file:** `client_app/lib/core/constants/topic_sensitivity.dart` ```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](client_app/lib/features/home/home_screen.dart) Current "Mulai Curhat" → opens pricing sheet directly. New flow: 1. Tap "Mulai Curhat" 2. Show `TopicSelectionBottomSheet` → await result 3. If result is null → no-op 4. 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](client_app/lib/features/chat/widgets/pricing_bottom_sheet.dart) - Accept `TopicSensitivity topicSensitivity` constructor param. - When firing the pairing request (via `PairingNotifier`), pass through `topic_sensitivity` to the request body. ### 2.5 Pairing Notifier — Pass Topic to Backend **File:** [client_app/lib/core/pairing/pairing_notifier.dart](client_app/lib/core/pairing/pairing_notifier.dart) - Extend `startPairing({ int durationMinutes, bool isFreeTrial, required TopicSensitivity topicSensitivity })` to include `topic_sensitivity` in 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_updated` is wired but **silently no-ops** (so the server-side event doesn't cause errors). **File:** [client_app/lib/core/chat/chat_notifier.dart](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](mitra_app/lib/core/chat/widgets/chat_request_overlay.dart) - Extend `ChatRequestIncomingData` (in [mitra_app/lib/core/chat/chat_request_notifier.dart](mitra_app/lib/core/chat/chat_request_notifier.dart)) with `TopicSensitivity topicSensitivity` field. - Parse from WS `chat_request` message payload (`topic_sensitivity`). - When sensitive: show yellow accent (e.g., left-border or top-strip) + `SensitivityBadge` near session metadata. ### 3.3 Chat Request Notifier — Parse Topic **File:** [mitra_app/lib/core/chat/chat_request_notifier.dart](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](mitra_app/lib/core/chat/mitra_chat_notifier.dart) - Add `TopicSensitivity topicSensitivity` to 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_enabled` from app_config provider (fetched on app start or via dedicated provider) - Show confirmation dialog if enabled - Call `PATCH /api/shared/chat/sessions/:sessionId/topic` via existing `ApiClient` - Show toast on success; show error dialog on failure (e.g., one-way latch violation → "Sesi sudah ditandai sensitif dan tidak bisa diubah kembali.") ### 3.5 Mitra Chat Screen — Yellow Doodle + Toggle **File:** [mitra_app/lib/features/chat/screens/mitra_chat_screen.dart](mitra_app/lib/features/chat/screens/mitra_chat_screen.dart) - Conditional background: watch `topicSensitivity` from `mitraChatNotifier`; 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 to `flipTopic()`. - If `one_way_latch` is enabled and current is `sensitive`, disable the icon and show tooltip "Sesi sudah terkunci sensitif." ### 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.yaml` assets section. ### 3.7 Extension UI — Current Flag **File:** [mitra_app/lib/features/chat/widgets/extension_request_card.dart](mitra_app/lib/features/chat/widgets/extension_request_card.dart) (or wherever the extension request UI lives) - Read `topicSensitivity` from the extension WS payload / session state. - If sensitive: yellow accent + `SensitivityBadge` on the extension card. ### 3.8 Chat History List + Transcript **File:** [mitra_app/lib/features/chat/screens/chat_history_screen.dart](mitra_app/lib/features/chat/screens/chat_history_screen.dart) (confirm name) - List row: add `SensitivityBadge` when 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](mitra_app/lib/core/network/api_client.dart) (confirm name) Add: ```dart Future 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](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](control_center/src/pages/sessions/SessionsPage.jsx) - Add dropdown filter: "Semua / Umum / Sensitif". - Pass `topic_sensitivity` query 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_log` entries: "Mitra X menandai topik sebagai sensitif pada HH:MM". ### 4.4 Dashboard — Sensitive Stats Panel **File:** [control_center/src/pages/dashboard/DashboardPage.jsx](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](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 had `topic_sensitivity = 'sensitive'` - `sensitive_accepted` — count of those with `response = 'accepted'` - `sensitive_rejected` — count with `response = 'rejected'` - `sensitive_acceptance_rate` — `sensitive_accepted / sensitive_total` (null if `sensitive_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](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](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_updated` WS 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:** - [ ] `flipSessionSensitivity` logs every call in `session_sensitivity_log` - [ ] Ownership check: mitra A cannot flip mitra B's session - [ ] Status check: flip on `CLOSING`/`COMPLETED` session → 409 - [ ] Latch violation returns 409 with clear error code - [ ] No-op flip (same value) does not create log entry