Phase 3.1 WS2: FCM fallback Flutter + CC, unread badges, dynamic ping

- Control center: add mitra ping config UI (require ping toggle + interval)
- Mitra app StatusNotifier: honor require_ping and ping_interval_seconds
  from API; skip heartbeat when ping not required
- Both apps: update notification services for FCM deep-linking
  - mitra_app: handle chat_request (open_accept), session_closing
  - client_app: handle session_closing, paired
- Unread badge providers:
  - mitra_app: UnreadSessions provider (polls active-with-unread, badge
    on active sessions button)
  - client_app: UnreadCount provider (polls active-with-unread, badge
    on _ActiveSessionCard)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 14:29:06 +08:00
parent ed765d230c
commit 229f889551
10 changed files with 261 additions and 21 deletions

View File

@@ -42,6 +42,17 @@ const updateExtensionTimeoutConfig = async (extension_timeout_seconds) => {
return res.data.data
}
// Phase 3.1: Mitra Ping Config
const fetchMitraPingConfig = async () => {
const res = await apiClient.get('/internal/config/mitra-ping')
return res.data.data
}
const updateMitraPingConfig = async (data) => {
const res = await apiClient.patch('/internal/config/mitra-ping', data)
return res.data.data
}
const fetchEarlyEndConfig = async () => {
const res = await apiClient.get('/internal/config/early-end')
return res.data.data
@@ -101,7 +112,17 @@ export default function SettingsPage() {
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-early-end'] }),
})
if (isLoading || maxLoading || ftLoading || etLoading || eeLoading) return <div>Loading...</div>
// Phase 3.1: Mitra Ping
const { data: mpData, isLoading: mpLoading } = useQuery({
queryKey: ['config-mitra-ping'],
queryFn: fetchMitraPingConfig,
})
const mpMutation = useMutation({
mutationFn: updateMitraPingConfig,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-mitra-ping'] }),
})
if (isLoading || maxLoading || ftLoading || etLoading || eeLoading || mpLoading) return <div>Loading...</div>
return (
<div>
@@ -215,6 +236,39 @@ export default function SettingsPage() {
</label>
{eeMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
</section>
<section style={{ marginBottom: 24 }}>
<h2>Mitra Online Status (Ping)</h2>
<p>Konfigurasi apakah mitra harus mengirim ping (heartbeat) untuk tetap online.</p>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<input
type="checkbox"
checked={mpData?.require_ping ?? true}
onChange={e => mpMutation.mutate({ require_ping: e.target.checked })}
disabled={mpMutation.isPending}
/>
Wajibkan Mitra Ping (Heartbeat)
</label>
<p style={{ fontSize: 12, color: '#666', marginBottom: 8 }}>
Jika dinonaktifkan, mitra akan tetap online tanpa perlu mengirim ping. QC bertanggung jawab atas kualitas layanan mitra.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<label>Interval Ping:</label>
<input
type="number"
min="5"
value={mpData?.ping_interval_seconds ?? 15}
onChange={e => {
const val = parseInt(e.target.value, 10)
if (val >= 5) mpMutation.mutate({ ping_interval_seconds: val })
}}
disabled={mpMutation.isPending}
style={{ width: 80 }}
/>
<span>detik</span>
</div>
{mpMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
</section>
</div>
)
}