{"id":"cmv1vg6dy0001qf2oju9ohfuh","slug":"app-anak-android","judul":"App Anak (Android)","deskripsi":null,"parentId":"cmv05qyqx000htm4dwawyj1ll","urut":9,"kategori":"web","tag":"[]","status":"draft","versi":null,"konsep":"","createdAt":"2026-10-10T04:05:51.572Z","updatedAt":"2026-10-10T04:05:51.572Z","bagian":[{"id":"cmv3i493z0003pf2ow59ae69m","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Identitas & Konfigurasi","konten":"## alamat.json\n```json\n{\n  \"idPaket\": \"com.zomet.zxparentingkids\",\n  \"versiNama\": \"2.5.0\",\n  \"versiKode\": 32,\n  \"beranda\": \"https://zxparenting.zomet.my.id\",\n  \"api\": \"https://zxparenting.zomet.my.id/api/agent\",\n  \"repoRilis\": \"djvpri/ZX-Parenting-agent\"\n}\n```\n\n## Config build.gradle.kts\n- **namespace**: `com.zxparenting.agent`\n- **compileSdk**: 36\n- **targetSdk**: 36 (wajib untuk app baru Okt 2026)\n- **minSdk**: 24\n- **Java**: 17\n- **Compose**: BOM 2024.12.01\n- **isMinifyEnabled**: false (no R8/ProGuard)\n- **No native code** (no NDK/CMake/.so)\n\n## Dependencies (15)\n- Compose (ui, material3, material-icons-extended)\n- AppCompat 1.7.0\n- Retrofit 2.11.0 + Gson converter\n- OkHttp (logging interceptor)\n- Lifecycle (runtime-compose, viewmodel-compose)\n- buildConfig = true (BuildConfig.API, BuildConfig.VERSI_NAMA, BuildConfig.VERSI_KODE)\n\n## Rilis\n- CI: `assembleDebug` per push to main\n- Rilis: `workflow_dispatch` → AAB + APK + GitHub Release\n- Keystore: GitHub Secrets (KEYSTORE_B64, KEYSTORE_PASSWORD, KEYSTORE_ALIAS, KEYSTORE_KEY_PASSWORD)\n- Tag: `git tag -f` + `push --force` (re-rilis versi sama OK)","media":"[]","urut":0,"createdAt":"2026-10-11T07:28:12.576Z","updatedAt":"2026-10-13T11:35:50.175Z"},{"id":"cmv3i49640005pf2o86lmwlcf","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Arsitektur & Struktur Kode","konten":"## Overview\nApp Android parental control untuk HP anak. Konsumsi API `/api/agent` dari web ZX Parenting. Dual fungsi:\n1. **Agent parental control** — foreground service ping loop, blokir layar, GPS, app blocking\n2. **Edu-gamification** — login anak, kuis AI (Gemini), tugas, konten official, hadiah\n\n## File Structure (30 Kotlin files)\n```\napp/src/main/java/com/zxparenting/agent/\n  MainActivity.kt          # Entry point, nav bottom bar (Beranda/Kuis/Tugas/Hadiah/Saya)\n  PasangActivity.kt        # Layar pairing device ortu\n  StatusActivity.kt        # Status koneksi device\n  BlokirActivity.kt        # Layar blokir fullscreen (sesi habis/curfew)\n  AgentService.kt          # Foreground service, ping loop 5 menit\n  AksesService.kt          # Accessibility service, deteksi app depan\n  AdminReceiver.kt         # Device admin receiver\n  Api.kt                   # Retrofit interface (edu API: login, kuis, tugas, hadiah) — 359 lines\n  ApiAgent.kt              # HttpURLConnection client (agent API: ping, lokasi, activity) — 68 lines\n  Simpanan.kt              # SharedPreferences: token, JWT, profil anak\n  Kebijakan.kt             # Parser aturan server + keputusan blokir\n  Blokir.kt                # Status blokir aktif + daftar app diblokir\n  Lokasi.kt                # GPS on-demand, lastKnownLocation\n  Pemakaian.kt             # UsageStatsManager, laporan app usage\n  ManajerBahasa.kt         # i18n: EN default + ID, AppCompatDelegate\n  PemeriksaPembaruan.kt    # Auto-update APK dari GitHub releases\n  Pemberitahuan.kt        # Notifikasi pengingat\n  Ctx.kt                   # Global context helper\n  DataDemo.kt              # Sample data untuk mode demo (reviewer Play Console)\n  LaporError.kt            # Error reporting ke backend (POST /api/agent lapor-error)\n  LayarBeranda.kt          # Feed konten official (kuis/artikel)\n  LayarMasuk.kt            # Login anak (username + PIN) + Mode Demo\n  LayarSaya.kt             # Profil anak, bahasa, device admin status\n  LayarTugas.kt            # Daftar tugas anak\n  LayarKuisAI.kt           # Buat kuis AI (Gemini on-demand)\n  LayarKuisDetail.kt       # Detail kuis official\n  LayarKuisSession.kt      # Sesi kuis: jawab per-soal, lock, review\n  LayarHadiah.kt           # Katalog hadiah + tukar token → waktu main\n  tema/Palet.kt            # Color palette\n  tema/WarnaAktif.kt       # Dynamic color scheme\n```\n\n## Stats\n- 30 Kotlin files, ~4764 LOC\n- 143 strings (EN), 143 strings (ID)\n- 5 tab: Beranda, Kuis, Tugas, Hadiah, Saya","media":"[]","urut":1,"createdAt":"2026-10-11T07:28:12.652Z","updatedAt":"2026-10-13T11:36:01.449Z"},{"id":"cmv3i499r0007pf2olff26gad","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Manifest & Permissions","konten":"## AndroidManifest.xml\n```xml\n<!-- Permissions -->\nINTERNET\nACCESS_NETWORK_STATE\nREQUEST_INSTALL_PACKAGES      <!-- auto-update APK -->\nACCESS_FINE_LOCATION\nACCESS_COARSE_LOCATION\nFOREGROUND_SERVICE\nFOREGROUND_SERVICE_LOCATION\nPOST_NOTIFICATIONS\nPACKAGE_USAGE_STATS           <!-- tools:ignore=ProtectedPermissions -->\n\n<!-- Components -->\nMainActivity                  (LAUNCHER)\nStatusActivity                (exported=false)\nPasangActivity                (exported=false)\nBlokirActivity                (singleInstance, showOnLockScreen, excludeFromRecents)\nAgentService                  (foregroundServiceType=location)\nAksesService                  (BIND_ACCESSIBILITY_SERVICE)\nAdminReceiver                 (BIND_DEVICE_ADMIN)\nFileProvider                  (${applicationId}.fileprovider)\n```\n\n## Permission Flow\n1. User install app → login anak (username + PIN)\n2. Pairing device ortu (deviceToken dari dashboard)\n3. Request: location, usage access, device admin, accessibility\n4. AgentService start foreground → ping loop 5 menit","media":"[]","urut":2,"createdAt":"2026-10-11T07:28:12.784Z","updatedAt":"2026-10-11T07:28:12.784Z"},{"id":"cmv3i49de0009pf2ojdx6kygd","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"AgentService — Ping Loop","konten":"## AgentService.kt — Foreground service, jantung agent.\n\n`START_STICKY` (Android restart saat sistem matikan).\n\n## ✅ Dynamic Ping Interval (commit `edc2261c`, Okt 2026)\n\n### Normal: 5 menit\n`PERIODE_MS = 5 * 60_000L` — default interval, tidak boros baterai.\n\n### Mendekati curfew: 1 menit\n`hitungPeriode()` cek `Kebijakan.menitKeCurfew(jam, menit, mulai, selesai)`:\n- Jika < 10 menit sebelum curfew mulai → ping 1 menit (lag maks 1 menit)\n- Jika dalam curfew → ping 1 menit (cepat deteksi selesai curfew)\n- Jika tidak ada curfew / jauh dari curfew → ping 5 menit\n\n### Cross-midnight support\n`menitKeCurfew()` support curfew lintas tengah malam (mis 21:00-05:00):\n- 20:50 → 10 menit sebelum curfew → ping 1 menit\n- 20:55 → 5 menit → ping 1 menit\n- 21:00 → dalam curfew → ping 1 menit\n- 04:59 → masih dalam curfew → ping 1 menit\n- 05:01 → curfew selesai → ping 5 menit\n\n### Handler.postDelayed\nMenggantikan `Thread.sleep` loop — lebih responsif, tidak block thread.\n\n## Alur per ping:\n1. `ApiAgent.ping(context)` → POST `/api/agent` `{aksi: \"ping\"}` dengan header `x-device-token`\n2. Simpan timestamp ping terakhir\n3. Parse respons → `Aturan` (sessionLimit, cooldown, dailyLimit, curfew, appBlokir)\n4. Simpan aturan via `Kebijakan.simpan()`\n5. Simpan daftar app diblokir via `Blokir.simpanDaftarApp()`\n6. Catat sesi_main bila sesiTerakhir == null && !masterUnlock\n7. GPS on-demand: `Lokasi.kirimBilaDiminta()` bila server minta\n8. Evaluasi blokir: `Kebijakan.evaluasi(aturan, jam, menit)` → tampilkan/sembunyikan BlokirActivity\n9. ✅ Cek accessibility: `AksesService.instance == null` → notifikasi + log \"accessibility_off\"\n10. Tiap 12 ping (~1 jam): kirim laporan usage stats bila izin ada\n\n## Notifikasi foreground\nChannel `agent` (IMPORTANCE_LOW), ongoing notification \"ZX Agent active\" + \"Device connected to parent\".\nChannel `alert` (IMPORTANCE_HIGH) untuk notifikasi accessibility off.","media":"[]","urut":3,"createdAt":"2026-10-11T07:28:12.914Z","updatedAt":"2026-10-12T03:14:23.188Z"},{"id":"cmv3i49fp000bpf2ol2jr98s0","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"API Layer — Dual API","konten":"## Dua API client terpisah:\n\n### 1. ApiAgent.kt (68 lines) — Agent API\n- `Api.ping()` → JSONObject respons atau null\n- `Api.kirimLokasi(lat, lng, akurasi)` → Boolean\n- `Api.kirimAktivitas(tipe, detail?)` → Boolean\n- `Api.laporError(screen, message, extra?)` → Boolean\n\n### 2. Api.kt (359 lines) — Edu API (Retrofit)\nBearer JWT auth. Endpoint:\n- `POST auth/native-anak` — login (username + PIN) → JWT + AnakInfo\n- `GET anak/official` — feed konten (kuis harian, tantangan, artikel) + officialMeta nested\n- `GET anak/official/{id}` — detail kuis + soal\n- `POST anak/official/{id}/complete` — submit jawaban → skor + token reward\n- `POST anak/kuis-ai/start` — generate kuis AI (Gemini) → attemptId + soal\n- `POST anak/kuis-ai/{attemptId}/submit` — submit jawaban → hasil + explain\n- `GET anak/tugas` — daftar tugas anak\n- `POST anak/tugas/{id}/session/start` — mulai sesi kuis tugas\n- `POST anak/tugas/session/{id}/submit` — submit jawaban sesi\n- `GET anak/tugas/session/{id}/review` — review jawaban + pembahasan\n- `GET anak/saldo` — saldo token anak\n- `GET anak/streak` — streak harian + terbaik\n- `GET anak/challenge` — challenge hari ini + status selesai\n- `GET anak/hadiah` — katalog hadiah\n- `POST anak/hadiah/{id}/tukar` — tukar token → hadiah\n\n### DTO lengkap (~25 data class)\nLoginAnakReq/Res, OfficialItem/Meta/Feed, TugasAnak, Soal, OfficialDetail, CompleteReq/Res, KuisAIReq/Res, SubmitKuisReq/Res, HasilSoal, KuisSessionStartReq/Res, SoalSession, JawabanLocked, KuisSessionSubmitReq/Res, ReviewSoal, KuisSessionReviewRes, SaldoRes, StreakRes, ChallengeRes, HadiahItem, TukarReq/Res\n\n## Backend monitoring (untuk AI agent)\n- `GET /api/diagnostik?token=<CRON_TOKEN>` — health snapshot (DB, env, kuis harian, errors, tugas)\n- `GET /api/error-log?token=<CRON_TOKEN>` — error log machine-readable\n- `scripts/smoke-test.sh` — E2E smoke test (login → feed → saldo → streak → challenge → tugas → diagnostik)","media":"[]","urut":4,"createdAt":"2026-10-11T07:28:12.997Z","updatedAt":"2026-10-13T11:36:14.260Z"},{"id":"cmv3i49ld000dpf2ovkweibb5","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Parental Control: Blokir & Kebijakan","konten":"## Kebijakan.kt (88 lines)\nParser aturan server + evaluator blokir.\n\n### Aturan (dari server)\n```kotlin\ndata class Aturan(\n    sessionLimit: Int,       // menit per sesi\n    cooldown: Int,           // menit jeda antar sesi\n    dailyLimit: Int,         // menit total per hari\n    curfewMulai: String?,    // \"21:00\"\n    curfewSelesai: String?,  // \"05:00\"\n    masterUnlock: Boolean,   // ortu override\n    sesiTerpakaiHariIni: Int,\n    sesiTerakhir: Long?,     // epoch ms\n    gpsMinta: Boolean,\n    appBlokir: List<String>, // daftar paket diblokir\n)\n```\n\n### AlasanBlokir enum\n`JAM_TIDUR` | `DAILY_HABIS` | `COOLDOWN`\n\n### Kebijakan.evaluasi(aturan, jam, menit) → AlasanBlokir?\n- Cek curfew (support lintas tengah malam, mis 21:00-05:00)\n- Cek daily limit terpakai\n- Cek cooldown\n\n## Blokir.kt (46 lines)\nStatus blokir di SharedPreferences (bertahan walau service dibunuh).\n- `nyalakan(alasan)` / `matikan()` / `aktif()` / `alasan()`\n- `simpanDaftarApp(daftar)` / `daftarApp()` — JSON array\n- `overlayAppAktif` — volatile flag untuk AksesService overlay\n\n## BlokirActivity.kt (94 lines)\nFullscreen, `singleInstance`, `showOnLockScreen`, `excludeFromRecents`.\n- Tampilkan pesan blokir (bedtime/daily/cooldown)\n- Tombol \"Minta Izin Ortu\"\n- `selesaiJikaAda()` — static method dismiss","media":"[]","urut":5,"createdAt":"2026-10-11T07:28:13.202Z","updatedAt":"2026-10-11T07:28:13.202Z"},{"id":"cmv3i49pt000fpf2odq26gujv","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"GPS & Lokasi","konten":"## Lokasi.kt (48 lines)\nGPS on-demand — server minta (`gps.minta=true` di ping) → kirim posisi.\n\n### Strategi:\n1. Cek izin `ACCESS_FINE_LOCATION` / `ACCESS_COARSE_LOCATION`\n2. Ambil `lastKnownLocation` dari GPS/NETWORK/PASSIVE provider → pilih terbaru\n3. Kirim via `Api.kirimLokasi(lat, lng, akurasi)`\n\n### Data minimization:\nHanya posisi terakhir dikirim, tanpa histori. Bila lokasi terlalu tua (>= 10 menit), `requestSingleUpdate` dengan timeout singkat.\n\n### `kirimBilaDiminta(c, minta)` → \"ok\" | \"tanpa_izin\" | \"gagal\"\n","media":"[]","urut":6,"createdAt":"2026-10-11T07:28:13.361Z","updatedAt":"2026-10-11T07:28:13.361Z"},{"id":"cmv3i49v8000hpf2onfffb5dc","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Accessibility Service — App Blocking","konten":"## AksesService.kt (64 lines)\nFase 5: blokir per-app. Deteksi app yang pindah ke depan via `TYPE_WINDOW_STATE_CHANGED`.\n\n### Alur:\n1. Event `TYPE_WINDOW_STATE_CHANGED` → `e.packageName`\n2. Skip app sendiri + launcher sistem\n3. Cek `Blokir.daftarApp()` — bila paket ada di daftar → tampilkan `BlokirActivity` overlay\n4. Anak pindah ke app tidak diblokir → lepas overlay (`Blokir.overlayAppAktif = false`)\n\n### Config (`akses_service.xml`):\n```xml\naccessibilityEventTypes: typeWindowStateChanged\naccessibilityFeedbackType: feedbackGeneric\naccessibilityFlags: flagIncludeNotImportantViews\nnotificationTimeout: 100\n```","media":"[]","urut":7,"createdAt":"2026-10-11T07:28:13.557Z","updatedAt":"2026-10-11T07:28:13.557Z"},{"id":"cmv3i49xs000jpf2o40dzgs1a","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Usage Stats — Laporan Pemakaian App","konten":"## Pemakaian.kt (76 lines)\nLaporan pemakaian app via `UsageStatsManager`. Butuh izin `PACKAGE_USAGE_STATS` (user setujui di Settings).\n\n### `ringkas(c, jam=6)` → JSONObject `{paket: menit}`\n- Query events ACTIVITY_RESUMED / ACTIVITY_PAUSED\n- Hitung durasi per app (bounding antar event)\n- App masih terbuka: hitung sampai sekarang\n- Hanya app >= 1 menit\n\n### `kirim(c)` → Boolean\nKirim ke server: `Api.kirimAktivitas(c, \"app_usage\", {per_app, jam})`. Dipanggil tiap 12 ping (~1 jam).\n\n### `adaIzin(c)` → Boolean\nCek via `AppOpsManager.unsafeCheckOpNoThrow(OPSTR_GET_USAGE_STATS)`","media":"[]","urut":8,"createdAt":"2026-10-11T07:28:13.648Z","updatedAt":"2026-10-11T07:28:13.648Z"},{"id":"cmv3i4a10000lpf2o7s2qjd1j","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"UI Screens — Jetpack Compose","konten":"## UI Screens — Jetpack Compose (5 tab)\n| Screen | File | Fungsi |\n|--------|------|--------|\n| Beranda | `LayarBeranda.kt` | Feed konten official (kuis harian, tantangan, artikel) |\n| Kuis AI | `LayarKuisAI.kt` | Buat kuis custom via Gemini (topik + level + jumlah) |\n| Kuis Detail | `LayarKuisDetail.kt` | Detail kuis official + mulai sesi |\n| Kuis Session | `LayarKuisSession.kt` | Sesi kuis: jawab per-soal, lock jawaban, review |\n| Tugas | `LayarTugas.kt` | Daftar tugas anak + mulai sesi kuis tugas |\n| Hadiah | `LayarHadiah.kt` | Katalog hadiah + tukar token → waktu main |\n| Saya | `LayarSaya.kt` | Profil anak, bahasa, device admin status |\n| Login | `LayarMasuk.kt` | Login anak (username + PIN) + Mode Demo |\n| Blokir | `BlokirActivity.kt` | Layar blokir fullscreen (sesi habis/curfew) |\n| Pasang | `PasangActivity.kt` | Pairing device ortu |\n| Status | `StatusActivity.kt` | Status koneksi device |\n\n## Theme\n- `tema/Palet.kt` — color palette\n- `tema/WarnaAktif.kt` — dynamic color scheme\n- Material 3 + Compose","media":"[]","urut":9,"createdAt":"2026-10-11T07:28:13.764Z","updatedAt":"2026-10-13T11:36:15.018Z"},{"id":"cmv3i4a5a000npf2o5kpkrws5","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"i18n — Internasionalisasi","konten":"## i18n — Internasionalisasi\n- `res/values/strings.xml` — 143 strings (English default)\n- `res/values-in/strings.xml` — 143 strings (Indonesian)\n- `ManajerBahasa.kt` — AppCompatDelegate.setApplicationLocales\n- Bahasa: EN (default), ID\n- Switch bahasa di LayarSaya","media":"[]","urut":10,"createdAt":"2026-10-11T07:28:13.918Z","updatedAt":"2026-10-13T11:36:26.652Z"},{"id":"cmv3i4a84000ppf2ogxk3ju9q","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Auto-Update APK","konten":"## PemeriksaPembaruan.kt (122 lines)\nCek GitHub releases latest, banding versi (tuple angka [major, minor, patch]), unduh APK, buka installer via FileProvider.\n\n### Versi comparison\n`versiNama` dibandingkan sebagai tuple, BUKAN `versiKode`. Contoh: `1.9.0` → [1, 9, 0].\n\n### Flow:\n1. `rilisTerbaru()` — GET `api.github.com/repos/djvpri/ZX-Parenting-agent/releases/latest`\n2. Banding tag vs `BuildConfig.VERSI_NAMA`\n3. Cari asset `.apk` di release\n4. `unduh(url, ctx)` — simpan ke `getExternalFilesDir(null)/updates/zx-agent-update.apk`\n5. Buka installer via `FileProvider.getUriForFile()` + `ACTION_INSTALL_PACKAGE`\n\n### Permission: `REQUEST_INSTALL_PACKAGES`","media":"[]","urut":11,"createdAt":"2026-10-11T07:28:14.020Z","updatedAt":"2026-10-11T07:28:14.020Z"},{"id":"cmv3i4ab8000rpf2owp1zc6kx","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Device Admin & Local Storage","konten":"## AdminReceiver.kt (6 lines)\nDevice admin receiver — `force-lock` policy. Memungkinkan `lockNow()` untuk kunci layar saat sesi habis.\n\n### device_admin.xml:\n```xml\n<uses-policies>\n    <force-lock />\n</uses-policies>\n```\n\n## Simpanan.kt (64 lines)\nSharedPreferences (`zx_agent`):\n- `token` — device pairing token (x-device-token)\n- `terakhir_ping` — timestamp ping terakhir\n- `jwt_anak` — JWT bearer anak (Authorization)\n- `nama_anak`, `anak_id`, `umur_anak` — profil anak\n\nMethods: `token()`, `simpanToken()`, `hapusToken()`, `catatPing()`, `pingTerakhir()`, `jwt()`, `simpanAnak()`, `namaAnak()`, `anakId()`, `umurAnak()`, `hapusAnak()`","media":"[]","urut":12,"createdAt":"2026-10-11T07:28:14.132Z","updatedAt":"2026-10-11T07:28:14.132Z"},{"id":"cmv3i4afs000tpf2o96x899ek","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"CI/CD & Keystore","konten":"## CI/CD\n### ci.yml — per push to main\n- `assembleDebug` — build check, no artifact\n\n### rilis.yml — workflow_dispatch\n- Input: `versi` (mis. 2.5.0)\n- Auto-bump `versiKode` (+1) di alamat.json\n- Build AAB (Play Store) + APK (pasang langsung)\n- Sign dengan keystore dari GitHub Secrets\n- `git tag -f v{VERSI}` + `push --force` (re-rilis OK)\n- Create GitHub Release dengan AAB + APK\n\n### Keystore\n- **GitHub Secrets**: KEYSTORE_B64, KEYSTORE_PASSWORD, KEYSTORE_ALIAS, KEYSTORE_KEY_PASSWORD\n- Tidak committed ke repo\n- Rotation v2.4.0+\n\n### Status terakhir\n- v2.5.0 / versiKode 32 — CI run #33 success\n- AAB + APK di GitHub Release v2.5.0\n- Ready upload ke Play Console","media":"[]","urut":13,"createdAt":"2026-10-11T07:28:14.296Z","updatedAt":"2026-10-13T11:36:27.384Z"},{"id":"cmv3i4ai6000vpf2o824x6juy","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Play Console Readiness","konten":"## Play Console Readiness\n### App Access untuk Reviewer\nApp butuh pairing device ortu untuk full functionality.\nDemo mode tersedia: buka app → tombol \"Mode Demo\" di layar login.\nDemo menampilkan sample kuis, tugas, hadiah, dan UI tanpa device ortu.\n\n### Store Listing\n- **Package**: com.zomet.zxparentingkids\n- **Developer account**: Zomet Teknologi Indonesia (ID 6348446758161954408)\n- **Status**: Draf — 2 apps (ZX Parenting Ortu + ZX Parenting Kids)\n- **targetSdk**: 36 (wajib Okt 2026)\n- **AAB uploaded**: v3.4.1 ortu, v2.5.0 kids (versiKode 32)\n- **Internal testing track**: ortu committed ✅\n\n### Service account\n- zx-parenting-billing@zxparenting.iam.gserviceaccount.com\n- Diinvite via Users & permissions → Admin (all permissions) ✅\n\n### Known issues resolved\n- targetSdk 35 → 36 (Google reject \"Target SDK too low: 54\" misleading — 54=versionCode)\n- Tag conflict re-rilis → fixed dengan `git tag -f` + `push --force`","media":"[]","urut":14,"createdAt":"2026-10-11T07:28:14.382Z","updatedAt":"2026-10-13T11:36:41.638Z"},{"id":"cmv3i4ak7000xpf2oex63a65h","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Kontrak API Backend","konten":"## Kontrak API Backend — App Anak ↔ Backend\n\n### Auth\n| Method | Endpoint | Body | Response |\n|--------|----------|------|----------|\n| POST | `/api/auth/native-anak` | `{username, pin}` | `{token: JWT, anak: {id, nama, umur}}` |\n\n### Edu API (Bearer JWT)\n| Method | Endpoint | Response |\n|--------|----------|----------|\n| GET | `/api/anak/official` | `{items: [{id, tipe, judul, officialMeta: {tokenReward, levelKesulitan, views}, selesai}], total}` |\n| GET | `/api/anak/official/{id}` | detail kuis + soal |\n| POST | `/api/anak/official/{id}/complete` | skor + token reward |\n| POST | `/api/anak/kuis/start` | `{attemptId, soal: [{q, options}]}` |\n| GET | `/api/anak/tugas` | `[{id, judul, tokenReward, status, deadline}]` |\n| GET | `/api/anak/saldo` | `{saldo: int}` |\n| GET | `/api/anak/streak` | `{current, terbaik, terakhir}` |\n| GET | `/api/anak/challenge` | `{topik, tokenReward, selesai}` |\n\n### Agent API (x-device-token header)\n| Method | Endpoint | Body |\n|--------|----------|------|\n| POST | `/api/agent` | `{aksi: \"ping\", \"activity\", \"lapor-error\", \"lokasi\"}` |\n\n### Monitoring (CRON_TOKEN)\n| Method | Endpoint | Response |\n|--------|----------|----------|\n| GET | `/api/diagnostik?token=` | health snapshot JSON |\n| GET | `/api/error-log?token=` | error log JSON |\n\n### Backend fixes (Okt 2026)\n- Race condition kuis harian: cek by (topik, publishAt) — no more duplikat\n- DTO officialMeta: nested object, bukan flat fields\n- Challenge deterministic by date\n- Endpoint cleanup duplikat: one-shot, sudah dihapus","media":"[]","urut":15,"createdAt":"2026-10-11T07:28:14.455Z","updatedAt":"2026-10-13T11:36:42.310Z"},{"id":"cmv3itk5e000zpf2o3h802dja","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Gap Analysis: Web vs App Android","konten":"## Fitur yang ada di web dashboard anak (`/anak`) tapi BELUM di app Android\n\n| # | Fitur | Web File | API Endpoint | App Android |\n|---|-------|----------|--------------|-------------|\n| 1 | Tukar Token → Waktu Main | `TukarToken.tsx` | `POST /api/anak/sesi-main` | ❌ belum |\n| 2 | Katalog Hadiah (minta dari ortu) | `KatalogHadiah.tsx` | `GET/POST /api/anak/hadiah` | ❌ belum |\n| 3 | Quest Keluarga (status quest) | `QuestAnak.tsx` | `GET /api/quests-saya` | ❌ belum |\n| 4 | Challenge Harian (progress) | `ChallengeKartu.tsx` | challenge API | ❌ belum |\n| 5 | Pinjam Waktu (token darurat) | blueprint `ekonomi-token` | `GET /api/pinjam` | ❌ belum |\n| 6 | Riwayat Token (ledger mutasi) | blueprint `ekonomi-token` | `GET /api/token-ledger` | ❌ belum |\n\n## Fitur yang sudah ada di app Android\n\n| # | Fitur | App File | Status |\n|---|-------|----------|--------|\n| 1 | Login anak (PIN) | `LayarMasuk.kt` | ✅ |\n| 2 | Feed official content | `LayarBeranda.kt` | ✅ |\n| 3 | Kuis AI (Gemini) | `LayarKuisAI.kt` | ✅ |\n| 4 | Detail kuis official | `LayarKuisDetail.kt` | ✅ |\n| 5 | Sesi kuis tugas | `LayarKuisSession.kt` | ✅ |\n| 6 | Daftar tugas | `LayarTugas.kt` | ✅ |\n| 7 | Profil + bahasa | `LayarSaya.kt` | ✅ |\n| 8 | Ping loop + blokir + GPS | `AgentService.kt` | ✅ |\n| 9 | Auto-update APK | `PemeriksaPembaruan.kt` | ✅ |\n\n## Bottom nav sekarang: 4 tab\nBeranda / Kuis AI / Tugas / Saya\n\n## Saran: tambah tab ke-5 \"Hadiah\"\nGabung Tukar Token + Katalog Hadiah + Quest + Pinjam Waktu + Riwayat Token. Semua API backend sudah live.","media":"[]","urut":16,"createdAt":"2026-10-11T07:47:53.283Z","updatedAt":"2026-10-11T07:47:53.283Z"},{"id":"cmv3itk7g0011pf2o2thqxqpm","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Fitur Missing #1: Tukar Token → Waktu Main","konten":"## Konsep\nAnak tukar token (1 token = 1 menit main) → waktu main aktif. Saldo berkurang, sesi main bertambah.\n\n## Web: `TukarToken.tsx` (58 lines)\n- Dropdown pilih menit: 5, 10, 15, 30, 45, 60, 90, 120\n- Tombol \"Tukar\" → disabled jika saldo < 5\n- POST `/api/anak/sesi-main` `{menit: N}`\n- Response: `{menit, saldo}` — sukses pesan + saldo baru\n\n## API\n```\nPOST /api/anak/sesi-main\nHeader: Authorization: Bearer <jwt>\nBody: {menit: 15}\nResponse: {menit: 15, saldo: 305}\nError: {error: \"Saldo tidak cukup\"}\n```\n\n## Implementasi app Android\n```kotlin\n// Api.kt — tambah endpoint\n@POST(\"anak/sesi-main\")\nsuspend fun tukarToken(\n    @Header(\"Authorization\") bearer: String,\n    @Body req: TukarTokenReq\n): Response<TukarTokenRes>\n\ndata class TukarTokenReq(val menit: Int)\ndata class TukarTokenRes(val menit: Int, val saldo: Int)\n```\n\n## UI: di dalam tab \"Hadiah\"\n- Card \"Tukar Token → Waktu Main\"\n- Dropdown pilihan menit\n- Tombol \"Tukar\" (hijau)\n- Pesan sukses/error\n- Minimal 5 token untuk tukar","media":"[]","urut":17,"createdAt":"2026-10-11T07:47:53.357Z","updatedAt":"2026-10-11T07:47:53.357Z"},{"id":"cmv3itk9y0013pf2ofq66n7ut","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Fitur Missing #2: Katalog Hadiah","konten":"## Konsep\nOrtu buat hadiah (mainan, jajanan, dll) dengan harga coin. Anak lihat katalog → klik \"Minta\" → ortu approve → dibelikan.\n\n## Web: `KatalogHadiah.tsx` (105 lines)\n- Collapsible card \"Katalog Hadiah\"\n- GET `/api/anak/hadiah` → daftar hadiah `{id, judul, deskripsi, hargaCoin, stok, sudahMinta}`\n- Tombol \"Minta\" per item → POST `/api/anak/hadiah` `{hadiahId}`\n- Status: \"Sudah diminta\" / \"Stok habis\" / \"Minta\"\n- Response sukses: \"Permintaan terkirim ke orang tua!\"\n\n## API\n```\nGET /api/anak/hadiah\nHeader: Authorization: Bearer <jwt>\nResponse: {katalog: [{id, judul, deskripsi, hargaCoin, stok, sudahMinta}]}\n\nPOST /api/anak/hadiah\nBody: {hadiahId: \"xxx\"}\nResponse: {ok: true}\nError: {error: \"Sudah diminta\" | \"Stok habis\"}\n```\n\n## Implementasi app Android\n```kotlin\n@GET(\"anak/hadiah\")\nsuspend fun hadiahList(@Header(\"Authorization\") bearer: String): Response<HadiahListRes>\n\n@POST(\"anak/hadiah\")\nsuspend fun hadiahMinta(\n    @Header(\"Authorization\") bearer: String,\n    @Body req: HadiahMintaReq\n): Response<HadiahMintaRes>\n\ndata class HadiahItem(\n    val id: String, val judul: String,\n    val deskripsi: String?, val hargaCoin: Int,\n    val stok: Int, val sudahMinta: Boolean\n)\ndata class HadiahListRes(val katalog: List<HadiahItem>)\ndata class HadiahMintaReq(val hadiahId: String)\n```\n\n## UI: di dalam tab \"Hadiah\"\n- Section \"Katalog Hadiah\"\n- LazyColumn kartu hadiah\n- Badge harga coin + status stok\n- Tombol \"Minta\" per item","media":"[]","urut":18,"createdAt":"2026-10-11T07:47:53.446Z","updatedAt":"2026-10-11T07:47:53.446Z"},{"id":"cmv3itkd30015pf2opbcf5maf","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Fitur Missing #3: Quest Keluarga","konten":"## Konsep\nOrtu buat quest keluarga (mis: \"Bantu Mama bersih-bersih\"). Anak lihat status quest — ortu tandai selesai → anak dapat token reward.\n\n## Web: `QuestAnak.tsx` (71 lines)\n- GET `/api/quests-saya` → daftar quest\n- Quest: `{id, judul, deskripsi, tokenReward, deadline, anggota: [{anakId, selesai}]}`\n- Anak hanya lihat status (⏳ menunggu / ✅ selesai)\n- Tidak ada aksi dari anak — ortu yang tandai\n\n## API\n```\nGET /api/quests-saya\nHeader: Authorization: Bearer <jwt>\nResponse: [{id, judul, deskripsi, tokenReward, deadline, anggota: [{anakId, selesai}]}]\n```\n\n## Implementasi app Android\n```kotlin\n@GET(\"quests-saya\")\nsuspend fun questSaya(@Header(\"Authorization\") bearer: String): Response<List<QuestAnak>>\n\ndata class QuestAnak(\n    val id: String, val judul: String,\n    val deskripsi: String?, val tokenReward: Int,\n    val deadline: String?,\n    val anggota: List<QuestMember>\n)\ndata class QuestMember(val anakId: String, val selesai: Boolean)\n```\n\n## UI: di dalam tab \"Hadiah\" atau \"Beranda\"\n- Section \"Quest Keluarga\"\n- Card per quest: judul + token reward + deadline + status\n- Hijau jika selesai, putih jika menunggu\n- Pesan: \"Ortu akan menandai selesai setelah dicek\"","media":"[]","urut":19,"createdAt":"2026-10-11T07:47:53.560Z","updatedAt":"2026-10-11T07:47:53.560Z"},{"id":"cmv3itkfm0017pf2oylwemyum","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Fitur Missing #4: Challenge Harian","konten":"## Konsep\nChallenge harian per anak — target aktivitas (mis: \"Kerjakan 3 kuis hari ini\"). Progress tracking, reward token saat complete.\n\n## Web: `ChallengeKartu.tsx`\n- Menampilkan challenge aktif + progress\n- Backend: model `ChallengeProgress` — challenge harian per anak\n\n## Blueprint referensi\n`ChallengeProgress` — challenge harian per anak. Terhubung dengan `OfficialContent` (TANTANGAN_MINGGUAN).\n\n## API (estimasi — perlu cek backend)\n```\nGET /api/anak/challenge\nHeader: Authorization: Bearer <jwt>\nResponse: [{id, judul, target, progress, tokenReward, selesai}]\n```\n\n## Implementasi app Android\n```kotlin\n@GET(\"anak/challenge\")\nsuspend fun challengeAktif(@Header(\"Authorization\") bearer: String): Response<List<ChallengeItem>>\n\ndata class ChallengeItem(\n    val id: String, val judul: String,\n    val target: Int, val progress: Int,\n    val tokenReward: Int, val selesai: Boolean\n)\n```\n\n## UI: di tab \"Beranda\" (card di atas feed)\n- Progress bar: progress/target\n- Badge token reward\n- Status: aktif / selesai","media":"[]","urut":20,"createdAt":"2026-10-11T07:47:53.650Z","updatedAt":"2026-10-11T07:47:53.650Z"},{"id":"cmv3itkhp0019pf2oaauhg7wy","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Fitur Missing #5: Pinjam Waktu","konten":"## Konsep\nAnak kehabisan token → pinjam waktu darurat. Hutang token harus dibayar (dari reward berikutnya). Ortu lihat hutang anak.\n\n## Blueprint: `ekonomi-token`\n- `PINJAM` — pinjam waktu darurat\n- `BAYAR_PINJAM` — pembayaran pinjaman\n- App ortu: `LayarPinjam.kt` — anak pinjam token, bayar hutang\n\n## API\n```\nGET /api/pinjam?anakId=X\nHeader: Authorization: Bearer <jwt>\nResponse: {hutang: 10, riwayat: [{id, jumlah, status, createdAt}]}\n\nPOST /api/pinjam  (estimasi)\nBody: {jumlah: 15}\nResponse: {ok: true, hutang: 15}\n```\n\n## Implementasi app Android\n```kotlin\n@GET(\"pinjam\")\nsuspend fun pinjamList(@Header(\"Authorization\") bearer: String): Response<PinjamRes>\n\ndata class PinjamRes(val hutang: Int, val riwayat: List<PinjamItem>)\ndata class PinjamItem(val id: String, val jumlah: Int, val status: String, val createdAt: String)\n```\n\n## UI: di dalam tab \"Hadiah\"\n- Card \"Pinjam Waktu Darurat\"\n- Tampilkan hutang aktif\n- Tombol \"Pinjam\" (hanya jika tidak ada hutang aktif)\n- Riwayat pinjam/bayar","media":"[]","urut":21,"createdAt":"2026-10-11T07:47:53.726Z","updatedAt":"2026-10-11T07:47:53.726Z"},{"id":"cmv3itkjp001bpf2o26iatwyx","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Fitur Missing #6: Riwayat Token (Ledger)","konten":"## Konsep\nAnak lihat riwayat mutasi token: dari mana (kuis, tugas, challenge), untuk apa (tukar main, pinjam). Transparansi ekonomi token.\n\n## Blueprint: `ekonomi-token`\n- `TokenLedger` — mutasi: tipe, jumlah (+/-), catatan, refId\n- 10 TokenTipe: KUIS, TUGAS, CHALLENGE, FAMILY_QUEST, PINJAM, BAYAR_PINJAM, PEMAKAIAN, PENYESUAIAN, OFFICIAL_KUIS, OFFICIAL_TANTANGAN\n\n## API\n```\nGET /api/token-ledger?anakId=X\nHeader: Authorization: Bearer <jwt>\nResponse: [{id, tipe, jumlah, catatan, refId, createdAt}]\n```\n\n## Implementasi app Android\n```kotlin\n@GET(\"token-ledger\")\nsuspend fun tokenLedger(@Header(\"Authorization\") bearer: String): Response<List<LedgerItem>>\n\ndata class LedgerItem(\n    val id: String, val tipe: String,\n    val jumlah: Int, val catatan: String?,\n    val refId: String?, val createdAt: String\n)\n```\n\n## UI: di dalam tab \"Hadiah\"\n- Section \"Riwayat Token\"\n- LazyColumn: item per mutasi\n- Hijau (+) untuk masuk, merah (-) untuk keluar\n- Badge tipe (KUIS, TUGAS, PEMAKAIAN, dll)","media":"[]","urut":22,"createdAt":"2026-10-11T07:47:53.798Z","updatedAt":"2026-10-11T07:47:53.798Z"},{"id":"cmv3itkm9001dpf2ooy52z4sk","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Usulan Tab ke-5: Hadiah","konten":"## Bottom nav baru: 5 tab\n1. **Beranda** — feed official + challenge harian\n2. **Kuis AI** — buat kuis AI\n3. **Tugas** — daftar tugas\n4. **Hadiah** (BARU) — tukar token + katalog + quest + pinjam + riwayat\n5. **Saya** — profil + bahasa + device admin\n\n## Layout tab \"Hadiah\"\n```\n┌─────────────────────────────┐\n│  Saldo Token: 320           │  ← header card (hijau)\n│  = 320 menit waktu main     │\n│  Streak: 7 hari 🔥          │\n├─────────────────────────────┤\n│  Tukar Token → Waktu Main   │  ← card tukar\n│  [15 menit ▾] [Tukar]       │\n├─────────────────────────────┤\n│  Pinjam Waktu Darurat       │  ← card pinjam\n│  Hutang: 0 token            │\n│  [Pinjam 15 menit]          │\n├─────────────────────────────┤\n│  Quest Keluarga             │  ← section quest\n│  ▸ Bantu Mama bersih... ⏳  │\n│  ▸ Baca buku 30 menit ✅    │\n├─────────────────────────────┤\n│  Katalog Hadiah             │  ← section hadiah\n│  ▸ Mainan Lego (50 coin)    │\n│    [Minta]                  │\n│  ▸ Es Krim (20 coin)        │\n│    [Sudah diminta]          │\n├─────────────────────────────┤\n│  Riwayat Token              │  ← section ledger\n│  +15 KUIS  | -30 PEMAKAIAN  │\n│  +50 CHALLENGE | +25 TUGAS  │\n└─────────────────────────────┘\n```\n\n## NavItem enum update\n```kotlin\nenum class NavItem(@StringRes labelRes: Int, icon: ImageVector) {\n    Beranda(R.string.nav_home, Icons.Default.Home),\n    Kuis(R.string.quiz_ai, Icons.Default.AutoAwesome),\n    Tugas(R.string.task_title, Icons.Default.Task),\n    Hadiah(R.string.nav_reward, Icons.Default.CardGiftcard),\n    Saya(R.string.nav_profile, Icons.Default.Person),\n}\n```\n\n## File baru\n- `LayarHadiah.kt` — tab Hadiah (scrollable column: saldo + tukar + pinjam + quest + katalog + riwayat)\n- DTO di `Api.kt`: TukarTokenReq/Res, HadiahItem/ListRes/MintaReq, QuestAnak/Member, PinjamRes/Item, LedgerItem\n\n## API endpoints baru ( Retrofit)\n- `POST anak/sesi-main` — tukar token\n- `GET anak/hadiah` — katalog hadiah\n- `POST anak/hadiah` — minta hadiah\n- `GET quests-saya` — quest keluarga\n- `GET pinjam` — riwayat pinjam\n- `GET token-ledger` — riwayat token\n\n## Strings baru (EN + ID)\n- `nav_reward` = \"Hadiah\" / \"Rewards\"\n- `token_balance` = \"Saldo Token\" / \"Token Balance\"\n- `exchange_token` = \"Tukar Token\" / \"Exchange Token\"\n- `borrow_time` = \"Pinjam Waktu\" / \"Borrow Time\"\n- `family_quest` = \"Quest Keluarga\" / \"Family Quest\"\n- `gift_catalog` = \"Katalog Hadiah\" / \"Gift Catalog\"\n- `token_history` = \"Riwayat Token\" / \"Token History\"","media":"[]","urut":23,"createdAt":"2026-10-11T07:47:53.889Z","updatedAt":"2026-10-11T07:47:53.889Z"},{"id":"cmv3j0752001fpf2oha5ssndb","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Mapping Nyambung: App Ortu ↔ App Anak","konten":"## App Ortu (sisi PARENT) vs App Anak (sisi CHILD)\n\nApp ortu = 20 layar, 5 tab + 14 sub-layar, ~40 API endpoints (v2.9.7).\nApp anak = 16 layar, 4 tab (v1.9.0).\n\n### Flow end-to-end yang SUDAH jalan (ortu ↔ anak):\n\n| Fitur | App Ortu (parent side) | App Anak (child side) | API | Status |\n|-------|------------------------|----------------------|-----|--------|\n| Tugas | `LayarTugas` (tab 2) — buat tugas, validasi selesai/tolak | `LayarTugas.kt` — daftar tugas, kerjakan kuis | `GET/POST/PUT /api/tugas` | ✅ nyambung |\n| Aturan Device | `LayarAturan` (sub 1) — sessionLimit, cooldown, curfew, appBlokir | `AgentService.kt` — terima & terapkan aturan dari backend | `GET/PATCH /api/kebijakan` | ✅ nyambung |\n| Lokasi GPS | `LayarLokasi` (sub 2) — GPS toggle, geo-fence, lastSeen | `Lokasi.kt` — kirim GPS ke backend | `POST /api/lokasi` | ✅ nyambung |\n| Laporan Aktivitas | `LayarLaporan` (sub 3) — activity log per anak, ringkasan 7 hari | `Pemakaian.kt` — kirim usage stats ke backend | `POST /api/pemakaian` | ✅ nyambung |\n| Blokir App | `LayarAturan` → appBlokir list | `Blokir.kt` — enforce blokir pakai UsageStatsManager | `GET /api/blokir` | ✅ nyambung |\n| Ping/Heartbeat | (tidak ada UI — ortu lihat device online status) | `AgentService.kt` — ping loop 5 menit | `POST /api/agent/ping` | ✅ nyambung |\n| Auto-update | (tidak relevan ortu) | `PemeriksaPembaruan.kt` — cek GitHub release | GitHub Releases API | ✅ jalan |\n\n### Flow yang PUTUS (ortu bisa, anak belum):\n\n| # | Fitur | App Ortu (parent side) | App Anak (child side) | API | Status |\n|---|-------|------------------------|----------------------|-----|--------|\n| 1 | Katalog Hadiah | `LayarMarketplace` (tab 3) — CRUD hadiah, proses pesanan beli/tolak, saldo coin | BELUM — anak belum bisa lihat katalog/minta hadiah | `GET/POST /api/anak/hadiah` | ❌ putus |\n| 2 | Quest Keluarga | `LayarQuest` (sub 5) — buat quest, tandai selesai → anak dapat token | BELUM — anak belum bisa lihat status quest | `GET /api/quests-saya` | ❌ putus |\n| 3 | Challenge Harian | `LayarChallenges` (sub 13) — daily challenges + claim | BELUM — anak belum bisa lihat progress | `GET /api/anak/challenge` | ❌ putus |\n| 4 | Pinjam Waktu | `LayarPinjam` (sub 10) — lihat hutang anak | BELUM — anak belum bisa pinjam dari app | `GET/POST /api/pinjam` | ❌ putus |\n| 5 | Riwayat Token | `LayarCoin` (sub 4) — saldo ZX Coin, ledger, topup/penyesuaian | BELUM — anak belum bisa lihat mutasi token | `GET /api/token-ledger` | ❌ putus |\n| 6 | Tukar Token → Waktu Main | (ortu tidak tukar — ortu topup) | BELUM — anak belum bisa tukar token → sesi main | `POST /api/anak/sesi-main` | ❌ putus |\n\n### Flow ortu-only (tidak perlu di app anak):\n- `LayarInsights` (sub 9) — AI insights per anak (Gemini/rule-based)\n- `LayarForum` (sub 6) — forum posts, detail + komentar\n- `LayarLangganan` (sub 7) — tier, checkout Midtrans\n- `LayarPesan` (sub 8) — direct chat (anak belum butuh)\n- `LayarReferral` (sub 11) — kode referral\n- `LayarLeaderboard` (sub 12) — ranking token mingguan\n- `LayarAturan` → masterUnlock, device admin activation","media":"[]","urut":24,"createdAt":"2026-10-11T07:53:03.014Z","updatedAt":"2026-10-11T07:53:03.014Z"},{"id":"cmv3j0778001hpf2ofvx65zqa","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Kontrak API Lengkap: App Anak ↔ Backend (semua endpoint)","konten":"## Endpoint yang sudah dikonsumsi app anak ( Retrofit `Api.kt`)\n\n| Endpoint | Method | App File | Status |\n|----------|--------|----------|--------|\n| `/api/agent/ping` | POST | `AgentService.kt` | ✅ |\n| `/api/lokasi` | POST | `Lokasi.kt` | ✅ |\n| `/api/kebijakan` | GET | `Kebijakan.kt` | ✅ |\n| `/api/blokir` | GET | `Blokir.kt` | ✅ |\n| `/api/pemakaian` | POST | `Pemakaian.kt` | ✅ |\n| `/api/agent/edu` (kuis) | GET | `LayarKuisAI.kt` | ✅ |\n| `/api/agent/edu` (tugas) | GET | `LayarTugas.kt` | ✅ |\n| `/api/agent/edu/kuis/detail` | GET | `LayarKuisDetail.kt` | ✅ |\n| `/api/agent/edu/kuis/sesi` | POST | `LayarKuisSession.kt` | ✅ |\n\n## Endpoint yang HARUS ditambah ke app anak (child-side)\n\n| Endpoint | Method | Untuk Fitur | Prioritas |\n|----------|--------|-------------|-----------|\n| `/api/anak/sesi-main` | POST | Tukar Token → Waktu Main | TINGGI |\n| `/api/anak/hadiah` | GET | Katalog Hadiah (list) | TINGGI |\n| `/api/anak/hadiah` | POST | Katalog Hadiah (minta) | TINGGI |\n| `/api/quests-saya` | GET | Quest Keluarga (status) | TINGGI |\n| `/api/anak/challenge` | GET | Challenge Harian (progress) | SEDANG |\n| `/api/pinjam` | GET | Pinjam Waktu (riwayat) | SEDANG |\n| `/api/pinjam` | POST | Pinjam Waktu (ajukan) | SEDANG |\n| `/api/token-ledger` | GET | Riwayat Token (mutasi) | SEDANG |\n| `/api/anak/saldo` | GET | Saldo token real-time | TINGGI |\n\n## Endpoint ortu-only (tidak perlu di app anak)\n~30 endpoint: auth ortu, CRUD anak, CRUD tugas, device control, marketplace CRUD, quest CRUD, forum, pesan, insights, langganan, referral, leaderboard, challenges CRUD\n\n## Autentikasi\n- App ortu: JWT dari login email/password atau Google Sign-In\n- App anak: PIN-based auth (6 digit), JWT dari `/api/agent/login`\n- Header: `Authorization: Bearer <jwt>` untuk semua endpoint anak","media":"[]","urut":25,"createdAt":"2026-10-11T07:53:03.092Z","updatedAt":"2026-10-11T07:53:03.092Z"},{"id":"cmv3j079u001jpf2omx9e02xd","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Arsitektur Paritas: Ortu MVVM vs Anak Procedural","konten":"## App Ortu (v2.9.7)\n- **Pattern:** MVVM + Repository\n- **ViewModel:** `ZxVm` (757 lines), single `UiState` data class (20+ fields)\n- **State:** `MutableStateFlow<UiState>`, `collectAsState()`\n- **API:** Retrofit2, Gson, `ApiZx` interface (~40 endpoints)\n- **Nav:** manual `mutableIntStateOf` (tab + subLayar), no Navigation Compose\n- **5 tab + 14 sub-layar** dari Beranda grid tiles\n\n## App Anak (v1.9.0)\n- **Pattern:** procedural Compose (no ViewModel, no Repository)\n- **State:** `remember` + `mutableStateOf` per layar\n- **API:** dual — `ApiAgent` (HttpURLConnection) + `Api` (Retrofit)\n- **Nav:** manual `mutableIntStateOf` (tab)\n- **4 tab** (Beranda / Kuis AI / Tugas / Saya)\n\n## Gap arsitektur\nApp ortu lebih mature:\n- ViewModel → state survive config change\n- `muatSemua()` → 8 parallel fetch\n- Repository pattern → testable\n\nApp anak procedural:\n- State tidak survive config change (rotasi)\n- Fetch per layar, tidak parallel\n- Tidak ada repository\n\n## Rekomendasi: tidak rewrite arsitektur anak sekarang\n- App anak working. Rewrite = risiko.\n- Tambah fitur missing dengan pattern yang sama (procedural + Retrofit)\n- Ponytail: upgrade ke MVVM saat app anak besar (20+ layar)\n- Tambah `LayarHadiah.kt` dengan pattern yang konsisten dengan `LayarBeranda.kt`","media":"[]","urut":26,"createdAt":"2026-10-11T07:53:03.186Z","updatedAt":"2026-10-11T07:53:03.186Z"},{"id":"cmv3j07c0001lpf2ojdksezop","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Prioritas Implementasi & Roadmap","konten":"## Prioritas 1 — MVP Hadiah (quick wins, API sudah live)\n\n| Fitur | Effort | API ready | Dampak |\n|-------|--------|-----------|--------|\n| Tukar Token → Waktu Main | Kecil | ✅ `POST /api/anak/sesi-main` | Anak bisa pakai token |\n| Katalog Hadiah (lihat + minta) | Kecil | ✅ `GET/POST /api/anak/hadiah` | Loop ortu↔anak jalan |\n| Quest Keluarga (lihat status) | Kecil | ✅ `GET /api/quests-saya` | Anak tahu target |\n| Saldo token real-time | Kecil | ✅ est. `GET /api/anak/saldo` | Transparansi |\n\n## Prioritas 2 — Engagement (medium)\n\n| Fitur | Effort | API ready | Dampak |\n|-------|--------|-----------|--------|\n| Challenge Harian | Sedang | est. `GET /api/anak/challenge` | Gamifikasi |\n| Riwayat Token (ledger) | Kecil | est. `GET /api/token-ledger` | Transparansi mutasi |\n\n## Prioritas 3 — Darurat (low priority)\n\n| Fitur | Effort | API ready | Dampak |\n|-------|--------|-----------|--------|\n| Pinjam Waktu | Sedang | est. `GET/POST /api/pinjam` | Token darurat |\n\n## Estimasi total\n- Prioritas 1: ~200-300 lines Kotlin (1 file `LayarHadiah.kt` + DTO di `Api.kt`)\n- Prioritas 2: ~100-150 lines (section di layar existing)\n- Prioritas 3: ~80-100 lines (section di `LayarHadiah.kt`)\n\n## Dependency baru: TIDAK perlu\n- Retrofit sudah ada\n- Gson sudah ada\n- Compose Material3 sudah ada\n- Hanya tambah data class + API interface method + Composable\n\n## Roadmap\n1. Bump targetSdk 35→36 (wajib Play Console)\n2. Tambah `LayarHadiah.kt` (Prioritas 1)\n3. Tambah challenge + ledger (Prioritas 2)\n4. Tambah pinjam (Prioritas 3)\n5. Update bottom nav 4→5 tab\n6. Update strings.xml (EN + ID)\n7. Build AAB, release v2.0.0","media":"[]","urut":27,"createdAt":"2026-10-11T07:53:03.264Z","updatedAt":"2026-10-11T07:53:03.264Z"},{"id":"cmv3j6y1o001npf2o1awa1ynr","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Fitur Kunci HP: Status & Gap Analysis","konten":"## Yang SUDAH jalan\n\n### 1. BlokirActivity — overlay fullscreen (sesi/curfew)\nSaat `Kebijakan.evaluasi()` return `JAM_TIDUR` / `DAILY_HABIS` / `COOLDOWN`:\n- `AgentService.proses()` → `Blokir.nyalakan()` → start `BlokirActivity`\n- Fullscreen, back diblok, `FLAG_SHOW_WHEN_LOCKED` + `FLAG_TURN_SCREEN_ON` + `FLAG_KEEP_SCREEN_ON`\n- Keluar hanya saat server bilang lepas (ping berikutnya) atau `masterUnlock`\n- Tombol \"Minta izin ortu\" → kirim log `minta_buka`\n\n**File:** `BlokirActivity.kt` (90 lines), `AgentService.kt` baris 65-78, `Kebijakan.kt` `evaluasi()`\n\n### 2. AksesService — blokir per-app (overlay)\nAccessibility service deteksi `TYPE_WINDOW_STATE_CHANGED`:\n- Jika paket ada di `appBlokir` (dari server) → overlay `BlokirActivity` menutupi app\n- Anak pindah app lain → overlay lepas\n- Launcher tidak diblokir (anak bisa pilih app lain yang boleh)\n\n**File:** `AksesService.kt` (50 lines), `Blokir.kt` `simpanDaftarApp()` / `daftarApp()`\n\n### 3. Anti-uninstall — Device Admin aktif\n- `device_admin.xml`: `<force-lock />` policy dideklarasi ✅\n- `StatusActivity.kt`: `ACTION_ADD_DEVICE_ADMIN` → ortu/teknisi aktifkan sekali saat pairing\n- Device Admin aktif = Android tolak uninstall tanpa deactivate dulu\n- Disconnect button → `dpm.removeActiveAdmin()` + clear token\n\n**File:** `AdminReceiver.kt`, `StatusActivity.kt` baris 103-107, `device_admin.xml`\n\n## Yang BELUM jalan — `lockNow()` tidak pernah dipanggil\n\n```\ndevice_admin.xml:   <force-lock />          ✅ policy ada\nAdminReceiver.kt:   \"lockNow() + anti-uninstall\" ← hanya komentar\nStatusActivity.kt:  dpm.isAdminActive()     ✅ cek status\n                    dpm.removeActiveAdmin() ✅ lepas saat disconnect\n                    dpm.lockNow()           ❌ TIDAK ADA — tidak pernah dipanggil\n```\n\n## Kelemahan overlay vs lockNow\n\n| Aspek | Overlay (sekarang) | `lockNow()` (belum) |\n|-------|--------------------|---------------------|\n| Back button | Diblok di kode, bisa di-bypass (notification bar, recent apps) | HP benar-benar terkunci, butuh PIN/pattern |\n| Force stop service | Anak bisa kill app dari Settings → overlay hilang | Device Admin survive kill (system level) |\n| Uninstall app | Diblok (Device Admin aktif) | Sama — anti-uninstall jalan |\n| Notification bar | Bisa di-tarik dari atas | Tidak bisa |\n| Recent apps | Bisa switch ke app lain | Layar terkunci penuh |\n| Lock screen | Overlay di atas app, bukan lock screen system | Lock screen system Android |\n\n## Konfigurasi ortu yang trigger blokir\n\nOrtu set di `LayarAturan` (app ortu sub-layar 1):\n- `sessionLimit` — menit per sesi (default 120)\n- `cooldown` — menit jeda antar sesi (default 30)\n- `dailyLimit` — menit total per hari (default 180)\n- `curfewMulai` / `curfewSelesai` — jam tidur (mis. \"21:00\"-\"05:00\")\n- `masterUnlock` — ortu override (buka paksa)\n- `appBlokir` — daftar paket diblokir per-app\n\nBackend → `GET /api/agent/ping` response → `aturan` object → `AgentService.proses()` → `Kebijakan.evaluasi()`","media":"[]","urut":28,"createdAt":"2026-10-11T07:58:17.820Z","updatedAt":"2026-10-11T07:58:17.820Z"},{"id":"cmv3j6y3t001ppf2ozg36x3zj","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Implementasi lockNow(): 1 Baris Kode","konten":"## Solusi: tambah `dpm.lockNow()` di AgentService saat blokir\n\nDevice Admin + `<force-lock />` sudah siap. Tinggal panggil.\n\n### Patch: `AgentService.kt` → `proses()`\n\n```kotlin\n// Tambah import\nimport android.app.admin.DevicePolicyManager\nimport android.content.ComponentName\n\n// Tambah property di class AgentService\nprivate val dpm get() = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager\nprivate val adminComp get() = ComponentName(this, AdminReceiver::class.java)\nprivate val adminAktif get() = dpm.isAdminActive(adminComp)\n\n// Di proses(), setelah Blokir.nyalakan(this, blokir):\nif (blokir != null && !sedangBlokir) {\n    Blokir.nyalakan(this, blokir)\n    // Kunci layar system — tidak bisa di-bypass notification bar / recent apps\n    if (adminAktif) {\n        dpm.lockNow()\n    }\n    startActivity(\n        Intent(this, BlokirActivity::class.java)\n            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)\n    )\n}\n```\n\n### Kenapa tetap pakai BlokirActivity + lockNow?\n\n1. `lockNow()` kunci layar system → HP benar terkunci, butuh PIN/pattern\n2. `BlokirActivity` tetap tampil di lock screen (`FLAG_SHOW_WHEN_LOCKED`) → anak lihat pesan \"HP terkunci sampai jam tidur berakhir\"\n3. Saat anak buka HP (unlock PIN), `BlokirActivity` masih ada → tetap blokir\n4. Double layer: system lock + app overlay\n\n### Anti-bypass tambahan (opsional, fase lanjut)\n\n```kotlin\n// 1. Sembunyikan notification bar — BlokirActivity sudah fullscreen\n//    tambah di onCreate:\nwindow.setFlags(\n    WindowManager.LayoutParams.FLAG_FULLSCREEN,\n    WindowManager.LayoutParams.FLAG_FULLSCREEN\n)\n\n// 2. Block recent apps — override onPause\noverride fun onPause() {\n    super.onPause()\n    // bila masih blokir, restart activity\n    if (Blokir.aktif(this)) {\n        startActivity(Intent(this, BlokirActivity::class.java)\n            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP))\n    }\n}\n\n// 3. Device Admin anti-uninstall sudah jalan — tidak bisa uninstall tanpa deactivate\n```\n\n### Limitasi `lockNow()` di Android modern\n\n- Android 9+ (API 28+): `lockNow()` tetap jalan dengan Device Admin ✅\n- Android 14+ (API 34): Device Admin `force-lock` tidak deprecated ✅\n- Tidak butuh additional permission — `<force-lock />` policy sudah dideklarasi\n- Tidak konflik dengan accessibility service — keduanya independent\n\n## Tidak butuh dependency baru\n- `DevicePolicyManager` = Android framework API\n- `ComponentName` = Android framework API\n- Tidak ada library tambahan","media":"[]","urut":29,"createdAt":"2026-10-11T07:58:17.897Z","updatedAt":"2026-10-11T07:58:17.897Z"},{"id":"cmv3j6y63001rpf2o5zooayk2","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Roadmap Update: lockNow + Hadiah + v2.0.0","konten":"## Roadmap revisi (gabung lockNow + fitur hadiah)\n\n### Fase 1 — Wajib (Play Console)\n1. Bump targetSdk 35→36 (wajib new apps Okt 2026)\n2. Build AAB, release v2.0.0-beta1\n\n### Fase 2 — Kunci HP (1 baris, quick win)\n3. Tambah `dpm.lockNow()` di `AgentService.proses()` saat blokir\n4. Test: set curfew dari app ortu → HP anak harus terkunci\n5. Commit: `feat: lockNow() saat blokir sesi/curfew`\n\n### Fase 3 — Tab Hadiah MVP\n6. Buat `LayarHadiah.kt` — saldo + tukar token + katalog hadiah + quest\n7. Tambah DTO di `Api.kt`: TukarTokenReq/Res, HadiahItem, QuestAnak\n8. Tambah endpoint Retrofit: `anak/sesi-main`, `anak/hadiah`, `quests-saya`\n9. Update bottom nav 4→5 tab\n10. Update strings.xml (EN + ID): `nav_reward`, `token_balance`, `exchange_token`\n11. Commit: `feat: tab Hadiah — tukar token + katalog + quest`\n\n### Fase 4 — Engagement\n12. Tambah challenge harian (section di Beranda)\n13. Tambah riwayat token (section di Hadiah)\n14. Tambah pinjam waktu (section di Hadiah)\n15. Commit: `feat: challenge + ledger + pinjam waktu`\n\n### Fase 5 — Rilis\n16. Build AAB v2.0.0\n17. Release ke GitHub + Play Console\n18. Store listing app anak\n\n## Estimasi effort\n| Fase | Lines | File baru | Dependency |\n|------|-------|-----------|------------|\n| 1 — targetSdk 36 | ~5 (build.gradle) | 0 | 0 |\n| 2 — lockNow | ~10 | 0 | 0 |\n| 3 — Tab Hadiah | ~250-300 | 1 (`LayarHadiah.kt`) | 0 |\n| 4 — Engagement | ~150 | 0 | 0 |\n| **Total** | ~415-465 | 1 | 0 |\n\n## Versi target: v2.0.0\n- Major bump: 4→5 tab, lockNow, 6 fitur missing\n- Breaking: bottom nav structure change\n- MinSdk tetap 24, targetSdk 36","media":"[]","urut":30,"createdAt":"2026-10-11T07:58:17.980Z","updatedAt":"2026-10-11T07:58:17.980Z"},{"id":"cmv3l9oun001tpf2ot6489he2","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Verifikasi Backend Routes: 4 Endpoint Estimasi DICEK","konten":"## Hasil cek kode backend (`src/app/api/`)\n\n### ✅ Child-accessible (pakai `sesiAnak()` — Bearer JWT role=CHILD) — READY\n\n| Endpoint | Method | Auth | Response Shape | Status |\n|----------|--------|------|----------------|--------|\n| `/api/anak/sesi-main` | POST | sesiAnak ✅ | `{ok, menit, saldo}` | ✅ READY |\n| `/api/anak/hadiah` | GET | sesiAnak ✅ | `{katalog: [{id, judul, deskripsi, hargaCoin, stok, sudahMinta}]}` | ✅ READY |\n| `/api/anak/hadiah` | POST | sesiAnak ✅ | `{ok}` (201) | ✅ READY |\n| `/api/quests-saya` | GET | sesiAnak ✅ | `[{id, judul, deskripsi, tokenReward, deadline, anggota:[{anakId, selesai}]}]` | ✅ READY |\n| `/api/anak/challenge` | GET | sesiAnak ✅ | `{id, judul, topik, tokenReward, selesai}` | ✅ READY |\n| `/api/anak/streak` | GET | sesiAnak ✅ | `{current, terbaik, terakhir}` | ✅ READY (bonus) |\n\n### ❌ ORTU-ONLY (pakai `sesiOrtu()` — anak TIDAK BISA akses)\n\n| Endpoint | Method | Auth | Kenapa | Status |\n|----------|--------|------|--------|--------|\n| `/api/pinjam` | GET | sesiOrtu ❌ | Cek `ortuId` match, bukan anak login | ❌ BLOCKED |\n| `/api/pinjam` | POST | sesiOrtu ❌ | Config pinjam (limitPinjam, bunga) — ortu set | ❌ BLOCKED |\n| `/api/token-ledger` | GET | sesiOrtu ❌ | Cek `ortuId` match, butuh `anakId` param | ❌ BLOCKED |\n| `/api/coin` | GET | sesiOrtu ❌ | Saldo ZX Coin ORTU (bukan token anak) | ❌ BLOCKED |\n\n### ❌ TIDAK ADA endpoint\n\n| Endpoint | Status | Catatan |\n|----------|--------|---------|\n| `/api/anak/saldo` | ❌ TIDAK ADA | Saldo anak hanya dapat dari response `sesi-main` (setelah tukar) atau `token-ledger` (ortu-only) |\n| Child-side pinjam | ❌ TIDAK ADA | Pinjam hanya ortu-side: lihat hutang + set config. Anak belum bisa ajukan pinjam |\n| Child-side token-ledger | ❌ TIDAK ADA | Ledger hanya ortu-side. Anak belum bisa lihat riwayat sendiri |\n\n## Response Shape Correction (BP sebelumnya salah)\n\n### Challenge — BP bilang `{id, judul, target, progress, tokenReward, selesai}`\n**AKTUAL:** `{id, judul, topik, tokenReward, selesai}`\n- Tidak ada `target` / `progress` field\n- Ada `topik` (string, mis. \"Matematika perkalian\")\n- `selesai` = boolean (dari `ChallengeProgress`)\n- Challenge = 1 per hari, deterministik dari tanggal. \"Kuis Sempurna\" — selesaikan 1 kuis dengan skor penuh\n\n### DTO Kotlin correction:\n```kotlin\ndata class ChallengeItem(\n    val id: String,\n    val judul: String,\n    val topik: String,        // BUKAN target: Int\n    val tokenReward: Int,\n    val selesai: Boolean\n    // TIDAK ADA progress: Int\n)\n```\n\n### Pinjam — BP bilang anak bisa `GET/POST /api/pinjam`\n**AKTUAL:** pinjam = ortu-only. POST = config (limitPinjam, bunga), bukan ajukan pinjam.\nAnak TIDAK BISA pinjam dari app. Butuh endpoint baru.\n\n### Token-ledger — BP bilang anak bisa `GET /api/token-ledger`\n**AKTUAL:** ortu-only. Butuh `?anakId=X` + `sesiOrtu()`.\nAnak TIDAK BISA lihat ledger sendiri. Butuh endpoint baru.\n\n### Saldo — BP bilang `GET /api/anak/saldo` (estimasi)\n**AKTUAL:** endpoint TIDAK ADA. Saldo anak hanya dari response `POST /api/anak/sesi-main` (`{ok, menit, saldo}`).\nButuh endpoint baru `GET /api/anak/saldo` atau `GET /api/anak/profil` yang return saldo + streak + nama.\n\n## Yang perlu DIBUAT di backend (3 endpoint baru)\n\n### 1. `GET /api/anak/saldo` — saldo token anak real-time\n```typescript\n// src/app/api/anak/saldo/route.ts\nexport async function GET() {\n  const anak = await sesiAnak()\n  if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })\n  const saldo = await prisma.tokenBalance.findUnique({\n    where: { anakId: anak.id },\n    select: { balance: true },\n  })\n  return NextResponse.json({ saldo: saldo?.balance ?? 0 })\n}\n```\n\n### 2. `GET /api/anak/riwayat-token` — ledger anak (child-side)\n```typescript\n// src/app/api/anak/riwayat-token/route.ts\nexport async function GET() {\n  const anak = await sesiAnak()\n  if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })\n  const ledger = await prisma.tokenLedger.findMany({\n    where: { anakId: anak.id },\n    orderBy: { createdAt: 'desc' },\n    take: 50,\n    select: { id: true, tipe: true, jumlah: true, catatan: true, createdAt: true },\n  })\n  return NextResponse.json({ ledger })\n}\n```\n\n### 3. `POST /api/anak/pinjam` — anak ajukan pinjam (child-side)\n```typescript\n// src/app/api/anak/pinjam/route.ts\nexport async function POST(req: Request) {\n  const anak = await sesiAnak()\n  if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })\n  const { jumlah } = await req.json()\n  // cek hutang aktif, cek limit, catat ledger PINJAM\n  // ...\n}\n```\nCatatan: logic pinjam anak butuh cek limit config (disimpan ortu via `/api/pinjam` POST sebagai catatan di ledger). Perlu parse catatan `pinjam-config:limit=X,bunga=Y`.\n\n## Impact ke Roadmap\n\n| Fitur | Status Sebelum | Status Sekarang | Aksi |\n|-------|----------------|-----------------|------|\n| Tukar Token | API ready | ✅ CONFIRMED ready | Langsung kode |\n| Katalog Hadiah | API ready | ✅ CONFIRMED ready | Langsung kode |\n| Quest Keluarga | API ready | ✅ CONFIRMED ready | Langsung kode |\n| Challenge Harian | API \"estimasi\" | ✅ EXISTS, shape beda | Fix DTO, langsung kode |\n| Streak | (tidak ada di BP) | ✅ BONUS — ada endpoint | Tambah ke UI |\n| Saldo real-time | API \"estimasi\" | ❌ TIDAK ADA endpoint | Buat backend dulu |\n| Pinjam Waktu | API \"estimasi\" | ❌ ORTU-ONLY, no child endpoint | Buat backend dulu |\n| Riwayat Token | API \"estimasi\" | ❌ ORTU-ONLY, no child endpoint | Buat backend dulu |\n\n## Revisi prioritas\n- **Prioritas 1 (langsung kode app):** Tukar Token, Katalog Hadiah, Quest, Challenge, Streak, Saldo (dari sesi-main response)\n- **Prioritas 2 (butuh backend baru dulu):** Saldo endpoint, Riwayat Token endpoint, Pinjam endpoint\n- Backend = ~60 lines TypeScript (3 route files). App anak tetap ~465 lines.","media":"[]","urut":31,"createdAt":"2026-10-11T08:56:25.103Z","updatedAt":"2026-10-11T08:56:25.103Z"},{"id":"cmv3lm43s001vpf2oqhqsehxb","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Tech Debt: Dual API Client (ApiAgent HttpURLConnection + Api Retrofit)","konten":"## Masalah\nApp anak punya **dua API client** pattern berbeda:\n\n### 1. ApiAgent.kt (68 lines) — HttpURLConnection manual\n- Dipakai untuk: ping, lokasi, activity (agent API)\n- Header: `x-device-token`\n- Manual JSON parse dengan `JSONObject`\n- Tidak ada error handling typed, tidak ada retry, tidak ada timeout config\n\n### 2. Api.kt (256 lines) — Retrofit2 + Gson\n- Dipakai untuk: login, kuis, tugas, official content (edu API)\n- Header: `Authorization: Bearer <jwt>`\n- Typed data class, suspend function, Retrofit interface\n- Proper error handling via Response<T>\n\n## Kenapa tech debt\n- **Tidak konsisten** — developer baru harus belajar 2 pattern\n- **Susah maintain** — bug fix di satu client tidak otomatis apply ke lainnya\n- **Testability** — Retrofit mockable, HttpURLConnection tidak\n- **Duplicate logic** — base URL, timeout, logging interceptor di-maintain terpisah\n\n## Kenapa terjadi\nApiAgent dibuat duluan (fase 1 — ping loop simple). Api (Retrofit) ditambah belakangan (fase 2 — edu API). Tidak pernah di-unify.\n\n## Rekomendasi: JANGAN unify sekarang\n- App anak working. Refactor = risiko break agent service.\n- **Aturan**: endpoint baru (hadiah, quest, challenge, dll) → semua ke Retrofit `Api.kt`. JANGAN tambah ke ApiAgent.\n- Ponytail: unify ApiAgent → Retrofit saat app anak 20+ layar atau saat butuh retry/timeout proper.\n- Estimasi unify: ~1 hari kerja (rewrite ApiAgent 6 method ke Retrofit + test)\n\n## Aturan untuk roadmap\n- `LayarHadiah.kt` semua endpoint baru → Retrofit `Api.kt` (Bearer JWT, sesiAnak)\n- ApiAgent tetap untuk agent API (ping, lokasi, activity) — tidak diubah\n- Dokumentasikan di komentar ApiAgent: \"DEPRECATED — jangan tambah endpoint baru ke sini\"","media":"[]","urut":32,"createdAt":"2026-10-11T09:06:04.744Z","updatedAt":"2026-10-11T09:06:04.744Z"},{"id":"cmv3lm47l001xpf2o30wb9u86","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Risiko: Ping 5 Menit = Lag Blokir","konten":"## Masalah (ORIGINAL)\n`AgentService` ping tiap 5 menit (`PERIODE_MS = 5 * 60_000L`). Blokir dievaluasi **hanya saat ping response diterima**.\n\n## Skenario lag\n1. Ortu set curfew 21:00 dari app ortu → tersimpan di DB\n2. Ping terakhir 20:55 → response dievaluasi → belum curfew\n3. Ping berikutnya 21:00 → response dievaluasi → curfew aktif → blokir\n4. **Lag: sampai 5 menit** antara curfew mulai dan blokir aktif\n\n## ✅ FIX DONE (commit `edc2261c`, Okt 2026)\nBlueprint Opsi B implemented: **dynamic ping interval**.\n\n### Implementasi\n- `Kebijakan.menitKeCurfew(jam, menit, mulai, selesai)` — hitung menit sampai curfew mulai (support cross-midnight, mis 21:00-05:00)\n- `AgentService.hitungPeriode()` — ping 1 menit saat < 10 menit sebelum curfew, 5 menit normal\n- `Handler.postDelayed` menggantikan sleep loop — lebih responsif\n\n### Hasil\n- Normal: ping 5 menit (tidak boros baterai)\n- Mendekati curfew (<10 min): ping 1 menit → lag maks 1 menit\n- Cross-midnight curfew (21:00-05:00) didukung\n- Tidak perlu permission baru","media":"[]","urut":33,"createdAt":"2026-10-11T09:06:04.882Z","updatedAt":"2026-10-12T03:14:05.304Z"},{"id":"cmv3lm49t001zpf2og0g1cuzm","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Security: Keystore di Repo Git","konten":"## Masalah\n`keystore/rilis.keystore` + `keystore/rilis.keystore.pw` committed langsung ke git repo.\n\n## Risiko\nSiapa pun akses repo (public GitHub) = bisa:\n1. Download keystore + password\n2. Sign APK malicious dengan signature sama\n3. Replace app di HP anak (auto-update dari GitHub releases!)\n4. Akses semua data anak (agent service, lokasi, usage stats)\n\n**Attack vector nyata**: auto-update (`PemeriksaPembaruan.kt`) download APK dari GitHub releases. Attacker sign APK malicious dengan keystore curian → upload ke GitHub releases dengan tag versi lebih tinggi → app anak auto-update → install APK malicious. Signature cocok → Android terima.\n\n## ✅ FIX DONE (commit `3fcb0647`, Okt 2026)\n1. **Generate keystore BARU** — RSA 2048, alias `zxagent`, validitas 10000 hari\n2. **Simpan di GitHub Secrets**: `KEYSTORE_B64` (base64), `KEYSTORE_PASSWORD`, `KEYSTORE_ALIAS`, `KEYSTORE_KEY_PASSWORD` — semua via GitHub API\n3. **rilis.yml Secrets-first logic**: decode dari Secret, fallback ke repo file (zero-downtime transition)\n4. **Hapus keystore dari repo** — `git rm --cached` + `.gitignore` patterns\n5. **README update** — dokumentasi rotasi\n\n## Kenapa tidak rewrite git history\n- Force-push breaks public repo clones\n- Keystore lama (commit `24c114e`) tetap di history — anggap compromised permanently\n- Mitigasi: rotasi signature effective v2.4.0 (user reinstall)\n\n## Signature rotation impact\n- App anak belum di Play Store → bebas ganti signature (user uninstall + reinstall)\n- App ortu sudah di Play Store → signature locked, tidak bisa rotate tanpa Google \"Upload key reset\"\n- Effective dari release v2.4.0","media":"[]","urut":34,"createdAt":"2026-10-11T09:06:04.961Z","updatedAt":"2026-10-12T03:14:04.898Z"},{"id":"cmv3lm4cw0021pf2o3osooeo7","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Konflik: Auto-Update APK (GitHub) vs Play Store In-App Update","konten":"## Masalah (ORIGINAL)\nApp anak punya 2 mekanisme update yang akan konflik:\n1. `PemeriksaPembaruan.kt` — GitHub releases (bypass Play Store)\n2. Play Store in-app update — `AppUpdateManager` FLEXIBLE type\n\n## ✅ FIX DONE\n### Play Store installer guard\n`PemeriksaPembaruan.kt` sekarang cek installer:\n```kotlin\nprivate fun apasangDariPlayStore(c: Context): Boolean {\n    val installer = c.packageManager.getInstallerPackageName(c.packageName)\n    return installer == \"com.android.vending\"\n}\n```\n- Jika install dari Play Store → skip GitHub update check, return null\n- Jika install dari GitHub (sideload) → tetap pakai GitHub auto-update\n\n### Play Store in-app update ready\n- `AppUpdateManagerFactory.create(context)` + `AppUpdateType.FLEXIBLE`\n- Dialog \"Update tersedia\" dari Play Store\n- Update via Play Store, signature verified Google\n\n## Status roadmap\n- **Fase 1 (sekarang):** GitHub auto-update aktif untuk sideload install ✅\n- **Fase 5 (Play Store live):** installer guard + in-app update aktif untuk Play Store install ✅ ready\n- **Permission:** `REQUEST_INSTALL_PACKAGES` tetap untuk pre-Play Store, hapus setelah live","media":"[]","urut":35,"createdAt":"2026-10-11T09:06:05.072Z","updatedAt":"2026-10-12T03:14:06.891Z"},{"id":"cmv3lm4ew0023pf2oas3r8pje","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Play Console: App Access untuk Reviewer","konten":"## Masalah (ORIGINAL)\nGoogle Play reviewer harus bisa test app tanpa device ortu untuk pairing.\n\n## ✅ FIX DONE — Demo Mode\n### Implementasi\n- `LayarMasuk.kt` (Compose) — tombol \"Mode Demo\" di login screen\n- `Simpanan.setDemo(ctx)` — flag demo mode di SharedPreferences\n- `MainActivity.onCreate` — cek `Simpanan.isDemo(ctx)` → skip AgentService, tampilkan UI dengan sample data\n- `DataDemo.kt` — sample data: kuis, tugas, challenge, katalog hadiah, quest, ledger, pinjam\n\n### Reviewer flow\n1. Install APK → buka app\n2. LayarMasuk muncul → klik \"Mode Demo\"\n3. Full UI tampil dengan sample data (5 tab: Beranda, Kuis, Tugas, Hadiah, Saya)\n4. Tidak perlu pairing device ortu, tidak perlu login anak\n5. Tombol \"Hubungkan ke Ortu\" di Saya → kembali ke pairing flow\n\n### Play Console App Access form\n```\nApp butuh pairing device ortu untuk full functionality.\nDemo mode tersedia: buka app → tombol \"Mode Demo\" di layar login.\nDemo menampilkan sample kuis, tugas, hadiah, dan UI tanpa device ortu.\n```\n\n### String resources\n- EN: `demo_mode` = \"Demo Mode\", `demo_mode_desc` = \"Browse the app with sample data...\"\n- ID: `demo_mode` = \"Mode Demo\", `demo_mode_desc` = \"Jelajahi app dengan data contoh...\"\n\n### Alternatif (tidak dipakai)\nTest account di backend — lebih realistic tapi butuh data dummy di DB + cleanup. Demo mode cukup untuk reviewer.","media":"[]","urut":36,"createdAt":"2026-10-11T09:06:05.145Z","updatedAt":"2026-10-12T03:14:07.271Z"},{"id":"cmv3lm4k90025pf2osv0rmvit","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Risiko: Accessibility Service Bisa Di-kill / Disable","konten":"## Masalah (ORIGINAL)\n`AksesService` (accessibility service untuk blokir per-app) bisa:\n\n### 1. Di-kill Android (OOM, battery optimization)\n- Android kill service under memory pressure\n- `AksesService.instance` jadi null\n- `Blokir.daftarApp()` tetap di SharedPreferences (data bertahan)\n- Tapi enforcement hilang — app diblokir tidak terdeteksi lagi\n\n### 2. Disable manual oleh anak\n- Settings → Accessibility → ZX Parenting Agent → OFF\n- Anak bisa lakukan ini tanpa ortu tahu\n- App blocking total tidak berfungsi\n\n### 3. Force stop app dari Settings\n- Semua service mati (AgentService + AksesService)\n- `START_STICKY` = Android restart eventually, tapi tidak immediately\n\n## Dampak\n`appBlokir` (daftar app diblokir ortu) tidak enforced. Anak bisa buka app yang seharangnya diblokir. Ortu tidak tahu enforcement sedang tidak aktif.\n\n## ✅ FIX DONE (commit `edc2261c`, Okt 2026)\nSolusi: **deteksi + alert + log**, bukan prevent (Android tidak allow prevent).\n\n### Implementasi\n1. **AgentService ping loop** — cek `AksesService.instance == null` tiap ping\n2. **Notifikasi HIGH priority** — channel `alert` (IMPORTANCE_HIGH), \"ZX Parenting perlu perhatian\"\n3. **Log ke backend** — `Api.kirimAktivitas(\"accessibility_off\")` → ortu lihat di laporan\n4. **LayarSaya status card** — tampilkan status accessibility (Aktif/Nonaktif), tombol \"Buka Settings\" langsung ke `ACTION_ACCESSIBILITY_SETTINGS`\n\n### Tidak bisa dicegah 100%\n- Android tidak allow app mencegah user disable accessibility (by design)\n- Device Admin tidak cover accessibility (hanya cover uninstall app)\n- Solusi = deteksi + alert + log, ortu dapat notifikasi","media":"[]","urut":37,"createdAt":"2026-10-11T09:06:05.337Z","updatedAt":"2026-10-12T03:14:05.703Z"},{"id":"cmv3lzla60027pf2oe6us05ea","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Mockup Desain UI","konten":"## Mockup lengkap — 4 layar sesuai BP 38 bagian\n\n**URL:** https://zxparenting.zomet.my.id/design/beranda-mockup.html\n\n**Repo:** `zx_parenting/public/design/beranda-mockup.html`\n\n## 4 Layar\n\n### 1. Beranda\n- Greeting card: nama anak, streak, token, level\n- **Challenge harian** (BP #20, #31) — shape: `{id, judul, topik, tokenReward, selesai}`. Progress bar. Reward +5\n- **Accessibility warning** (BP #37) — banner kuning saat AksesService nonaktif\n- Feed kuis/artikel/video/tugas (sama dengan sebelumnya)\n- Bottom nav 5 tab\n\n### 2. Hadiah (tab baru)\n- **Saldo token** header (BP #31 — dari response sesi-main atau endpoint baru)\n- **Tukar token** (BP #17) — dropdown 5/15/30/60/120 menit, min 5 max 120, tombol hijau\n- **Quest keluarga** (BP #19) — card per quest, status ⏳/✅, border teal/green\n- **Katalog hadiah** (BP #18) — card per hadiah, harga coin, status Minta/Sudah diminta/Habis\n- **Riwayat token** (BP #22) — ledger list, +/- hijau/merah, badge tipe (KUIS, TUGAS, CHALLENGE, dll)\n- **Pinjam waktu** (BP #21) — card oranye, hutang aktif, tombol pinjam\n\n### 3. Blokir / Lock Screen\n- **lockNow() + overlay** double layer (BP #28, #29)\n- Fullscreen biru gelap, icon lock\n- Pesan: \"HP terkunci sampai jam tidur berakhir\"\n- Info: blokir aktif JAM_TIDUR (21:00-05:00), sistem lock Device Admin lockNow()\n- Tombol \"Minta Izin Ortu\"\n- Info anti-bypass: notification bar, recent apps, force stop\n\n### 4. Saya\n- **Status perangkat** (BP #37, #12, #28):\n  - Aksesibilitas: Aktif/Nonaktif + tombol Settings\n  - Device Admin: Aktif (anti-uninstall)\n  - Laporan Pemakaian: Aktif\n  - GPS Lokasi: Aktif\n  - Kunci Layar (lockNow): Siap\n- **Pengaturan**: bahasa, cek pembaruan (BP #35 — GitHub vs Play Store), versi app\n- **Lepas perangkat**: disconnect dari ortu + hapus token\n\n## Bottom Nav: 5 tab\n1. Beranda (house-door)\n2. Kuis AI (stars)\n3. Tugas (list-task)\n4. Hadiah (card-giftcard) — BARU\n5. Saya (person)\n\n## Tech\n- HTML/CSS murni, no framework\n- Bootstrap Icons 1.11.3\n- Inter font (Google Fonts)\n- Phone frame 390×844px (iPhone 14)\n- Scrollable feed, fixed bottom nav\n- Dark background (#0f172a) untuk showcase","media":"[]","urut":38,"createdAt":"2026-10-11T09:16:33.534Z","updatedAt":"2026-10-11T09:16:33.534Z"},{"id":"cmv3mzt540029pf2oweuqydtb","blueprintId":"cmv1vg6dy0001qf2oju9ohfuh","judul":"Desain i18n + Theme Switching","konten":"## Tujuan\nDefault bahasa Inggris. Sementara hanya opsi Indonesia. Arsitektur harus mudah tambah bahasa + ganti tema.\n\n## ✅ Status: DONE (Okt 2026)\n\n### i18n\n| Aspek | Status |\n|-------|--------|\n| strings.xml EN (values/) | ✅ 92+ strings |\n| strings.xml ID (values-in/) | ✅ 92+ strings |\n| ManajerBahasa.kt | ✅ AppCompatDelegate.setApplicationLocales, persist SharedPreferences |\n| Language picker (LayarSaya) | ✅ EN/ID/system |\n| Tambah bahasa | values-xx/strings.xml + 1 baris di picker |\n\n### Theme System — port dari ortu 1:1\n| File | Status | Desc |\n|------|--------|------|\n| `tema/Palet.kt` | ✅ | data class 20 warna, dariJson() parser, DEFAULT hardcoded |\n| `tema/WarnaAktif.kt` | ✅ | singleton mutableStateOf, gantiByKey(), inisialisasi(), TemaZX wrapper |\n| `assets/tema/default.json` | ✅ | tema Default (biru) |\n| `assets/tema/gelap.json` | ✅ | tema Malam (dark) |\n| `assets/tema/laut.json` | ✅ | tema Laut (cyan) |\n| Theme picker (LayarSaya) | ✅ | 3 pilihan, instant recompose |\n\n### View-based activities theme\n| File | Status |\n|------|--------|\n| PasangActivity.kt | ✅ WarnaAktif.palet.*.toArgb() |\n| StatusActivity.kt | ✅ WarnaAktif.palet.*.toArgb() |\n| BlokirActivity.kt | ✅ WarnaAktif.palet.*.toArgb() |\n\n### Refactor hardcoded colors\n- Original: 106 hardcoded `Color(0x...)` di 8 file Compose\n- Compose files: ✅ zero hardcoded (semua pakai `Bg`, `Biru`, `Teks`, dst computed property)\n- View-based files: ✅ zero hardcoded (semua pakai `WarnaAktif.palet.*.toArgb()`)\n- Remaining 23 `Color(0x...)` di `Palet.kt` (DEFAULT definition — correct, must be literal) + `WarnaAktif.kt` (dark/light scheme constants)\n\n### Tambah tema baru\nDrop file JSON di `assets/tema/nama.json` → otomatis muncul di picker. No recompile.\n\n### Tambah bahasa baru\nBuat `values-xx/strings.xml` + 1 baris di language picker. Perlu recompile (APK split).","media":"[]","urut":39,"createdAt":"2026-10-11T09:44:43.337Z","updatedAt":"2026-10-12T03:14:07.660Z"}],"children":[],"parent":{"slug":"zx-parenting","judul":"ZX Parenting"}}