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>
514 lines
26 KiB
Markdown
514 lines
26 KiB
Markdown
# 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<TopicSensitivity?>` — 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<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](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
|