Mitra §A: pre-home (S3a/S3b/AccountInactive) + design system + Bestie Home

- Port halo_tokens + halo_theme + HaloButton to mitra_app (rose palette,
  Bricolage display, Poppins body, JetBrainsMono).
- Build S3a Input WhatsApp (figma-bestie BestieS3 first half) with
  +62 chip, leading-zero/62 normalization, allow '+' in input.
- Build S3b OTP verification (6-digit, 60s resend timer, attempts hint,
  Focus(canRequestFocus:false) for maestro inputText compat) with full
  error branching (CODE_MISMATCH, OTP_EXPIRED, OTP_USED, ATTEMPTS_EXCEEDED,
  WRONG_FLOW, ACCOUNT_INACTIVE).
- Add AccountInactive terminal screen for is_active=false mitras.
- Typed MitraAuthError with Indonesian-first localized messages +
  retryAfterSeconds passthrough.
- Rebuild home_screen.dart to match figma BestieHome (greeting + status
  card + Ganti Status CTA + Pengingat + 2-tile dark grid).
- Backend: POST /internal/_test/seed-mitra (idempotent) and
  PATCH /internal/mitras/:id (display_name update).
- Control center: inline Edit Nama on mitras row + expandable inline log
  table under clicked mitra (vs old below-table panel).
- 5 maestro flows ts-mitra-A-01/03/04/05/06 covering invalid input, happy
  path, account inactive, phone-format normalization, and the back-to-S3a
  regression. All green.

Plan + memory documented in:
- requirement/phase4-mitra-prehome-plan.md
- requirement/flow_mitra.md / flow_mitra.mermaid.md §A

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 22:01:28 +08:00
parent ad02ee252d
commit 9696eadeaf
37 changed files with 3406 additions and 326 deletions

View File

@@ -332,6 +332,28 @@ export const internalTestRoutes = async (fastify) => {
return { ok: true, ...row }
})
// Upsert a mitra with the given phone and is_active flag. Used by the
// mitra_app pre-home Maestro flows to ensure a known mitra exists (with
// the right active state) before driving the OTP verify path. Idempotent —
// safe to run on every test pass.
fastify.post('/seed-mitra', async (request, reply) => {
const phone = request.body?.phone
const display_name = request.body?.display_name
const is_active = request.body?.is_active !== false // default true
if (!phone || !display_name) {
return reply.code(400).send({ error: 'phone and display_name required in body' })
}
const [row] = await sql`
INSERT INTO mitras (phone, display_name, is_active)
VALUES (${phone}, ${display_name}, ${is_active})
ON CONFLICT (phone) DO UPDATE
SET display_name = EXCLUDED.display_name,
is_active = EXCLUDED.is_active
RETURNING id, phone, display_name, is_active
`
return { ok: true, ...row }
})
// Mark EVERY mitra row online. Used by Maestro flows as a setup step to
// ensure a clean known-good state regardless of what previous tests did
// (e.g. force-mitra-offline leaving the dev DB with no online mitras).

View File

@@ -1,6 +1,6 @@
import { authenticate, requirePermission } from '../../plugins/auth.js'
import { getCcUserById } from '../../services/cc-user.service.js'
import { createMitra, listMitras, updateMitraStatus } from '../../services/mitra.service.js'
import { createMitra, listMitras, updateMitraStatus, updateMitraDisplayName } from '../../services/mitra.service.js'
import { getOnlineMitras, getOnlineLogs } from '../../services/mitra-status.service.js'
import { UserType } from '../../constants.js'
@@ -48,6 +48,17 @@ export const mitraManagementRoutes = async (app) => {
return reply.send({ success: true, data: mitra })
})
app.patch('/:id', {
preHandler: [authenticate, attachCcUser, requirePermission('mitra', 'update')],
}, async (request, reply) => {
const { display_name } = request.body ?? {}
if (typeof display_name !== 'string' || display_name.trim().length === 0) {
return reply.code(422).send({ success: false, error: { code: 'VALIDATION_ERROR', message: 'display_name must be a non-empty string' } })
}
const mitra = await updateMitraDisplayName(request.params.id, display_name.trim())
return reply.send({ success: true, data: mitra })
})
app.get('/online', {
preHandler: [authenticate, attachCcUser, requirePermission('mitra', 'read')],
}, async (request, reply) => {