Lima feature bebas konflik. Tiap satu mesti ada: jenis, storage, server action, UI builder/dashboard, integrasi, ujian.
- Extend
ConditionalConfigdilib/types/forms.ts— tambahrules: ConditionRule[]danlogic: 'all' | 'any'. Backward compat: kalaufieldId+valuelegacy ada, normalize ke satu rule equals. - Tulis
lib/forms/conditions.ts— fungsi tulenevaluateConditional(field, formData, allFields)+normalizeConditional(legacy). Operator:equals,not_equals,contains,not_contains,is_empty,is_not_empty,gt,lt. - Replace UI di
components/forms/fields-editor/index.tsxdengan editor multi-rule. - Wire
isFieldVisiblediapp/(public)/form/[id]/client.tsxpanggilevaluateConditional. - Tests:
tests/conditional-logic.test.ts— 17 tests pass.
- Migration:
form_webhookstable (id, form_id, user_id, url, secret_encrypted, events, enabled, created_at). RLS owner-only. - Type
WebhookConfigdilib/types/webhooks.ts. -
lib/storage/webhooks.ts— CRUD (list per form, create, update, delete, recordResult). -
lib/webhooks/dispatch.ts— sign HMAC-SHA256, fire with timeout, retry × 3 backoff. -
actions/webhooks.ts—createWebhookAction,updateWebhookAction,deleteWebhookAction,testWebhookAction. - Hook into
submitFormActionselepasincrementSubmissionCount. - Builder UI:
components/forms/webhooks-card.tsx. - Tests:
tests/webhook-dispatch.test.ts— 9 tests pass.
- Migration:
response_edit_tokens+forms.edit_link_settingsjsonb column. - Type
EditLinkSettings(enabled, expiryDays, emailFieldId). - On submit (when enabled and email field present): create token row, email respondent dengan magic link.
- New route
app/(public)/edit/[token]/page.tsx— re-uses public form rendering with prefilled values. - Action
submitEditedResponseAction— verify token, find sheet row by_submission_id, update Sheet row, mark token used. - Email template
getEditLinkEmaildilib/email/index.ts. - Builder UI:
components/forms/edit-link-card.tsx. - Tests:
tests/edit-token.test.ts— 6 tests pass.
- Refactor capture/blob helpers ke
lib/certificates/render.ts. - Pure CSV parser
lib/csv/parse.ts— handle quoted fields, BOM, CRLF, embedded newlines. - New dashboard page
app/(dashboard)/certificates/builder/[id]/bulk/page.tsx+ client. - Client-side bulk generator: loop entries, render
CertificateRenderereach, capture, push toJSZip. Trigger download. - "Bulk Generate" sparkles button on certificate template card.
- Tests:
tests/csv-parse.test.ts— 13 tests pass.
- Tambah
aggregateUserAnalytics(rows, days)dilib/analytics/aggregate.ts. - Add
getUserAnalyticsSummary(days)diactions/analytics.ts— RLS-gated. - New component
components/dashboard/cross-form-analytics.tsx— 4 stat cards + 30d sparkline + top-3 forms. - Mount di
app/(dashboard)/forms/page.tsxantara<DashboardStats>dan page header. - Tests:
tests/cross-form-analytics.test.ts— 6 tests pass.
-
npm run lint— 0 warnings -
npm test— 87/87 pass across 10 suites (was 36/36) -
npm run build— clean, 43 routes (Next 16.2.6, Turbopack) - Update
memory.mddanlessons.md
Skop dihantar: 5/5 features. 51 ujian baru, 0 lint warnings, build clean.
Keputusan reka bentuk:
- Conditional Logic — ditolak shape baru penuh (rules[]) tetapi normalize legacy shape automatic, jadi tiada migration data perlu dijalankan untuk borang sedia ada. Pure evaluator senang ditest.
- Webhooks — rangkaian sama macam BCL inbound (HMAC-SHA256 hex), jadi pengguna boleh re-use receiver code corak yang sama. Per-attempt timeout 5s × 3 attempts dengan exponential backoff (500ms, 1s, 2s). 4xx short-circuit kerana receiver explicit reject.
- Edit Link — guna jsonb column
edit_link_settingsuntuk elak proliferation. Token single-use untuk had blast radius leak. Edit mode skip file uploads, webhooks, owner email — sengaja senyap supaya owner tak banjir notif. - Bulk Certificate — pure client-side via
jszip(sudah dalam deps). Refactorlib/certificates/render.tsjadi reusable supaya tidak duplicate kod render. CSV parser hand-rolled kerana zero new deps. - Cross-form Analytics — silently render nothing kalau tiada data, tak susahkan dashboard. Top-3 sahaja dalam widget — page analytics individu untuk drill-down.
Lessons baru ditambah ke lessons.md:
server-onlyperlu di-stub dalam Vitestz.ZodErrorv4 guna.issues[]bukan.errors[]- Type baru kena di-re-export dari
lib/types/index.ts - Bulk client-render perlu 2×
requestAnimationFramewait - CSV empty check perlu
.trim() - Snapshot rekey label → id bila prefill
- Magic-link routes mesti
robots: { index: false }
Tinggal (Fasa B & C) — lihat memory.md.
- Identify root cause of database error on registration (trigger
handle_new_userusing incorrect column nametotal_formsand missingmonthcolumn values) - Create a new migration file
supabase/migrations/20260605000000_fix_handle_new_user_trigger.sqlto fixhandle_new_usertrigger function - Test the build and lint of the project to ensure no regressions
- Document the changes in
memory.md,lessons.mdandtask.md
- Punca Masalah: Trigger
on_auth_user_createdyang menjalankan fungsipublic.handle_new_user()gagal kerana mencuba untukINSERTkepublic.usagemenggunakan nama kolumtotal_forms(yang sepatutnyaforms_created) serta tidak memasukkan nilai untuk kolummonthyang mempunyai constraintNOT NULL. Hal ini menyebabkan transaction pendaftaran pengguna (sign up) terbatal dan memaparkan "database error" kepada pengguna.
- Identify root cause of form creation blocking (strict check on
googleClientEmail/googlePrivateKeyinstead of allowinggoogleAccessTokenOAuth config) - Update
createFormActioninactions/forms.tsto allow either OAuth or Service Account configurations - Test the build and lint to ensure everything compiled correctly
- Document the changes in
memory.md,lessons.mdandtask.md
- Punca Masalah: Ketika pengguna mahu mencipta borang baru (
createFormAction), fungsi akan menyemak jika ada rekod tetapan yang sah. Namun, semakan sebelum ini hanya memeriksa kolum manual Service Account (googleClientEmail&googlePrivateKey). Ini menghalang pengguna yang menggunakan Google OAuth (yang hanya menyimpangoogleAccessToken) daripada mencipta borang. - Penyelesaian: Mengubah semakan di
createFormActionuntuk membenarkan penciptaan borang jika pengguna telah mengkonfigurasi sama ada Google OAuth (googleAccessTokenwujud) ATAU manual Service Account.
- Identify redundant trigger
on_auth_user_created_subscriptionexecutinghandle_new_user_subscription()onauth.users - Update migration
20260605000000_fix_handle_new_user_trigger.sqlto drop the redundant trigger and function - Redeploy to Vercel to sync migration files
- Check Vercel CLI version and link status
- Deploy the application to Vercel using Vercel CLI
- Document the deployment in
memory.mdandtask.md
- Identify the root cause of forms 500 error (
record "new" has no field "slug"trigger error onformstable because the database trigger was executinggenerate_short_codefunction which expectedNEW.slug) - Create a new migration file
supabase/migrations/20260605001000_fix_forms_short_code_trigger.sqlto separate the forms trigger from the short_links trigger - Redeploy to Vercel to sync migration files
Matlamat: Selepas responden submit borang, hantar emel pengesahan automatik kepada responden (bukan hanya kepada pemilik borang). Guna semula infra Resend + corak pemilih medan emel yang sama macam Edit Link. Tiada jadual DB baharu — hanya satu lajur jsonb pada forms.
- 1. Type
RespondentNotificationSettingsdilib/types/forms.ts(enabled, emailFieldId, message?, includeSummary?) + tambah keForm+ re-export dilib/types/index.ts. - 2. Migration
supabase/migrations/20260607010000_add_respondent_notification.sql— tambah lajurrespondent_notification jsonb. - 3. Pemetaan storage di
lib/storage/forms.ts— 2× fromRow + 1× toRow (respondent_notification). - 4. Template emel
getRespondentConfirmationEmail(formTitle, message?, summary?)dilib/email/index.ts. - 5. Hook fire-and-forget dalam
submitFormActionselepas blok edit-link. - 6. UI builder
components/forms/respondent-notification-card.tsx(cermin EditLinkCard) + mount diapp/builder/[id]/client.tsx. - 7. Tests
tests/respondent-notification.test.ts(template purity + ringkasan). - 8. Verifikasi:
npm run lint(0) +npm test(94/94) +npm run build(bersih).
- Keputusan reka bentuk: guna lajur
jsonbtunggal (respondent_notification) macamedit_link_settingsuntuk elak proliferasi lajur. Berasingan sepenuhnya daripadareceiveEmailNotifications(notifikasi pemilik) — dua aliran emel berbeza, dua toggle berbeza. - Keselamatan: nilai jawapan responden (subjek, ringkasan, mesej) di-escape HTML (
escapeHtml) sebelum disuntik ke template emel, untuk halang HTML/markup injection dalam emel pengesahan. Kunci dalaman (_submission_iddll, prefix_) ditapis daripada ringkasan. - Ketahanan: blok fire-and-forget — kegagalan emel tidak sesekali gagalkan submission (try/catch +
console.warn). Sama corak dengan blok edit-link & notifikasi pemilik. - Nota (di luar skop):
getNewSubmissionEmail(notifikasi pemilik sedia ada) TIDAK escape input pengguna — potensi HTML injection dalam emel pemilik. Tidak diubah dalam pass ini untuk kekal skop minimum; patut dibaiki berasingan.
- Escape semua nilai pengguna dalam
getNewSubmissionEmail(userName, formTitle, submissionData keys/values, googleSheetUrl href). - Escape
formTitledalamgetEditLinkEmailuntuk konsistensi. -
escapeHtml(function declaration, hoisted) boleh guna oleh semua template dalam fail.
- Type
PdpaSettings { enabled, consentText, policyUrl? }padaForm+ barrel. - Migration
20260607020000_add_pdpa_settings.sql: lajurpdpa_settings jsonb. - Storage mapping (2× fromRow + toRow).
- Helper tulen
lib/forms/pdpa.ts(requiresPdpaConsent,isConsentGiven,isPdpaSubmissionAllowed). - UI builder
pdpa-card.tsx+ mount selepas RespondentNotificationCard. - Public form: checkbox persetujuan wajib (block submit + disable butang jika tak tick); rakam
Persetujuan PDPA: Yadalam dbData. - Server-side:
submitFormActiontolak jika PDPA enabled tapi consent tiada (tak boleh bypass via scripting). - Tests
tests/pdpa.test.ts— 8 tests.
- Migration
20260607030000_add_audit_logs.sql: jadualaudit_logs+ index + RLS owner-only SELECT +prune_audit_logs(). - Type
lib/types/audit.ts+ barrel. -
lib/storage/audit.ts—logAudit()(resolve user, insert via admin) +listAuditLogs()(RLS). - Formatter tulen
lib/audit/format.ts(describeAuditAction,describeAuditLog,auditActionKind). - Hook log pada
createFormAction+deleteFormAction(sebelum redirect). - Dashboard
app/(dashboard)/audit/page.tsx+ pautan sidebar + route terlindung diproxy.ts. - Tests
tests/audit-format.test.ts— 7 tests.
- Jenis medan baharu
pagebreak(pemisah) diFormFieldType. - Helper tulen
lib/forms/pagination.ts(splitIntoPages,isMultiPage,findAdjacentNonEmptyPage,lastNonEmptyPageIndex). - Builder: dropdown jenis + butang "Add Page Break" + kecualikan pagebreak dari sumber syarat/required/conditional.
- Public form: render satu page setiap kali + butang Kembali/Seterusnya/Submit + indikator "Halaman X / Y"; validasi per-page pada Next; PDPA + Submit di page akhir; guard Enter; skip page kosong (conditional).
-
visibleFieldskecualikan pagebreak (tidak divalidasi/dihantar/dikira). - Tests
tests/pagination.test.ts— 10 tests.
-
npm run lint— 0 warnings. -
npm test— 121/121 pass across 14 suites (was 94). -
npm run build— clean, 44 routes (+/audit).
- Email escaping:
escapeHtmldiguna merentasgetNewSubmissionEmail,getEditLinkEmail,getRespondentConfirmationEmail. Nilai responden tak boleh lagi suntik markup ke emel. - PDPA: gate dikuatkuasakan dua lapis (client UX + server enforcement) supaya tak boleh dipintas. Consent direkod sebagai lajur mesra Sheet. Logik diekstrak ke fungsi tulen untuk ujian.
- Audit log: jadual immutable dari sisi klien (tiada polisi INSERT; tulis via service role sahaja). Hanya log create/delete (bukan update autosave yang bising). Formatter tulen +
force-dynamicpage. - Multi-page: guna
pagebreaksebagai pemisah dalam array sedia ada — tiada migration, backward-compatible (borang tanpa pagebreak = 1 page). Page kosong (akibat conditional) dilangkau automatik. Semua logik pagination tulen & diuji.
Matlamat: Memudahkan antara muka Form Builder untuk pengguna bukan teknikal dengan menyembunyikan tetapan lanjutan ("Validation Rules" dan "Conditional Logic") secara lalai menggunakan Accordion.
- 1. Import komponen Accordion di
components/forms/fields-editor/index.tsx. - 2. Kemas kini
SortableFielduntuk membungkus seksyen Validation dan Conditional dengan Accordion (collapsed by default). - 3. Tambah indikator lencana (badge) jika validation/conditional aktif supaya pengguna tahu ada peraturan aktif.
- 4. Kemas kini
ConditionalLogicEditoruntuk membuang tajuk berganda. - 5. Uji secara manual dan jalankan
npm testsertanpm run builduntuk memastikan tiada masalah.
Sembilan pembetulan risiko/kualiti dari audit penuh (lihat memory.md untuk butiran reka bentuk).
- Migration
20260701010000_add_form_responses.sql(jadual + partial index + prune + RLS owner-only SELECT). -
lib/storage/form-responses.ts— insert (idempotent, 23505=duplicate), markSynced, markSyncFailed({final}), listPendingSyncResponses (join forms+settings). -
submitFormAction: tulis DB dahulu → Sheets sync + webhooks + 3 emel dalamafter(). - Cron
/api/cron/sync-responses(*/10) + entrivercel.json.
- Migration
20260701020000_payment_webhook_idempotency.sql(processed_at+ backfill + uniqueprovider_reference). - Route: duplicate → 200
{duplicate:true}tanpa kesan sampingan;processed_atdiset serentak dengan status; SEMUA DB via admin client (fix anon/RLS silent failure). - Initiate:
PRO_PRICE+KLIK-${randomUUID()}+ buang fake phone.
-
lib/forms/validate-submission.ts(pure) — reuseevaluateConditional, skip layout-only, ReDoS cap. -
submitFormActionguna modul baharu.
- Client jana
_submission_key(randomUUID per page-load, sessionStorage); action guna sebagai submission_id (unique constraint menelan double-submit). - Key dikosong selepas success ("Submit another response" dapat key baru).
-
.github/workflows/ci.yml— lint → typecheck → test → build (push/PR master).
-
app/error.tsx,app/global-error.tsx,app/not-found.tsx.
-
lib/constants/pricing.ts(PRO_PRICE) — initiate, pricing page, modal, plan-card semua import dari satu tempat.
-
getFormById/getFormByShortCodedibalutcache().
- Skrip
typecheck; deps pembangunan dipindah ke devDependencies.
-
npm run lint— 0 warnings -
npm run typecheck— clean -
npm test— 206/206 (25 suites; was 171) -
npm run build— clean, 45 routes (+/api/cron/sync-responses)
- Write-first: DB ialah source of truth baharu; Sheet jadi "view" yang akhirnya konsisten (cron retry). Responden tidak pernah lagi kehilangan jawapan atau menunggu webhook lambat.
- Idempotency: ditetapkan sebelum sebarang geran supaya crash mid-handler tidak boleh double-grant; completed lama di-backfill
processed_at. - Admin client fix: webhook BCL tiada cookie — anon client + RLS owner-only = silent 404; service role satu-satunya pilihan betul.
- Tinggal (Fasa E cadangan): Turnstile optional per-form, zod di semua action files, dekomposisi client.tsx (1,354 baris) + builder client (1,551 baris), responses dashboard baca form_responses, export/backup UI dari form_responses, a11y audit builder, i18n konsisten (lang="ms" pada page English), renewal/cancel flow.