Files
tasq/docs/superpowers/specs/2026-09-27-programmer-oversight-design.md

23 KiB
Raw Permalink Blame History

Programmer Oversight — Design Spec

Date: 2026-09-27 · Branch: feat/programmer-tasks · Status: Draft for review

Adds an oversight layer to the Programmer Tasks module: daily Day Sheet approvals with a remarks ↔ justification loop, a live admin Monitor, a PDF Accomplishment Report, and a gamified Mine dashboard.

1. Background

The Programmer Tasks module records programmers' dev work: tasks with a pause-aware timer, per-contributor work logs (assignee progress notes + helper-entered minutes), comments, and projects. Today:

  • The admin cannot see who is working on what right now, or who is helping whom.
  • Nobody reviews or approves programmers' daily work.
  • There is no Accomplishment Report.
  • The "Mine" tab is a flat list of cards.

2. Goals

  1. Admin sees each programmer's live state (working / paused / idle / away), current task, today's time, and help given/received.
  2. Each programmer's work is reviewed per day: approve, or disapprove with remarks; the programmer justifies (and may append corrections) until approved.
  3. Programmers generate their own Accomplishment Report (PDF, date-filtered); the admin generates one for any programmer.
  4. The Mine tab becomes a motivating dashboard with stats, a streak, badges, and tasks separated into In progress / Paused / Not started / Completed / Cancelled.

Non-goals

  • Live "help sessions" (help is derived from work logs after the fact).
  • XP/levels and team leaderboards.
  • Editing or deleting existing work-log entries (they stay append-only).
  • Migrating the existing DTR / task PDFs to the new shared PDF helpers.

3. Decisions

Topic Decision
Approval unit Day Sheet — one per programmer per day; admin can flag task rows in it
Submission Auto-submit at day end, plus manual early submit
Same-day approval Allowed; later work that day reopens the sheet as "amended after approval"
On disapprove Programmer justifies and may append corrective entries; originals are immutable
Late entries Back-dated work logs allowed on pending or disapproved days, never on approved days; no backfilling days that have no sheet
Helping Derived from work logs (helper = work-log author ≠ task assignee)
Report scope Toggle when generating: approved days only (default) or include unapproved days (marked)
Report layout By day: Date · Task/Project · Accomplishment · Time, then summary and signatures
Gamification Stat tiles + streak + badges
Notifications In-app + push
Monitor states Working / Paused / Idle / On leave / Pass slip
Tabs Programmer: Mine · All · Day Sheets — Admin: Monitor · Approvals · Mine · All
Core approach Day-sheet table + append-only review thread + SECURITY DEFINER RPCs + pg_cron auto-submit + approval snapshot

"Admin" means profiles.role = 'admin' only. The existing isAdminProvider (lib/providers/profile_provider.dart:305) also returns true for programmers, and the module's RLS treats both roles alike. All approval and monitor gating uses a new isStrictAdminProvider on the client and role = 'admin' in SQL.

4. Existing code this builds on

Fact Where
Task status is queued | in_progress | completed | cancelled. Paused = in_progress whose latest lifecycle activity-log event is paused. Not started = queued. lib/models/programmer_task.model.dart:39-47
Work logs are append-only; the day is known only from created_at; assignee rows have minutes = null, helper rows have minutes. supabase/migrations/20260927140000_add_programmer_projects_worklogs_comments.sql:90-111
Duration core computeEffectiveDuration lib/utils/task_duration.dart:38-75
Ledger computeTaskLedger credits all clock time to the current assignee — the per-day split must not do this. lib/utils/programmer_task_ledger.dart:46-78
ReportDateFilter is hard-wired to reportDateRangeProvider; ReportDateRange.end is exclusive. lib/screens/reports/report_date_filter.dart, lib/providers/reports_provider.dart:13-51
Per-person PDF with letterhead + preview dialog (period label is off by one day at :211). lib/screens/attendance/dtr_pdf.dart
DTO + pure PDF builder pattern lib/screens/attendance/work_log_pdf.dart
Notification type is free text; render + tap switch; push via send_fcm. lib/screens/notifications/notifications_screen.dart:63-140, lib/services/notification_bridge.dart:97,152, lib/providers/notifications_provider.dart:376
Scheduled push: pg_cron → scheduled_notifications (schedule_id nullable) → edge function notify_type switch. supabase/functions/process_scheduled_notifications/index.ts
SECURITY DEFINER RPC that writes status + notifications atomically respond_shift_swap, supabase/migrations/20260322190000_pm_oncall_companion_swap.sql
Per-user "on leave / pass slip today" logic lib/screens/dashboard/dashboard_screen.dart:371-433

5. Day Sheets

5.1 What a Day Sheet contains

One row per task the programmer worked on that day:

  • task title, category, project;
  • time that day:
    • Assignee rows: timer intervals (started/resumed → paused/completed/cancelled) clipped to Manila day boundaries, credited to whoever was assignee at that moment (walk created / assigned / reassigned events). A later reassignment never changes a past day. adjustment events count against the day they were created.
    • Helper rows: the helper's entered minutes, labelled "Helped ".
  • notes: that day's work-log entries for the task.

The admin flags rows when disapproving.

5.2 Schema

New migration supabase/migrations/20260927150000_add_programmer_day_sheets.sql.

programmer_day_sheets

Column Type Notes
id uuid pk
programmer_id uuid → profiles
work_date date Manila calendar day
status text check draft | pending | disapproved | approved, default draft
submitted_at timestamptz
auto_submitted bool default false
reviewed_by uuid → profiles last admin to approve/disapprove
reviewed_at timestamptz
resubmissions int default 0; incremented by justify
approved_snapshot jsonb rows as approved (see 5.5)
created_at, updated_at timestamptz

Unique (programmer_id, work_date).

programmer_day_sheet_events — append-only review thread

Column Type Notes
id uuid pk
sheet_id uuid → sheets, on delete cascade
actor_id uuid null null = system
kind text check submitted | auto_submitted | disapproved | justified | approved | amended
body text remarks / justification; check non-empty for disapproved and justified
flagged_task_ids uuid[] default '{}'
created_at timestamptz

programmer_task_work_logs.work_date — date not null default (now() at time zone 'Asia/Manila')::date. Existing rows are backfilled from created_at. A BEFORE INSERT guard trigger allows:

  • work_date = today (Manila); or
  • a past work_date only if the author's sheet for that date is pending or disapproved.

Anything else raises an error.

notifications.day_sheet_id — uuid null references programmer_day_sheets on delete cascade.

RLS: sheets and events are readable by the owner and by role = 'admin'. There are no client INSERT/UPDATE/DELETE policies; every write goes through the RPCs or triggers below. Both tables are added to the supabase_realtime publication.

5.3 Status transitions

Each transition is a SECURITY DEFINER RPC with set search_path = public, explicit auth.uid(), role, and current-status checks. Each one atomically updates the sheet, inserts a thread event, and inserts notification rows, then returns the recipient user ids so the client can call send_fcm. A stale status raises already_reviewed.

RPC Who From → To Rule
submit_day_sheet(p_date) owner draft → pending Any time, including early. Upserts the row.
disapprove_day_sheet(p_sheet, p_remarks, p_flagged_task_ids) admin pending → disapproved Remarks required.
justify_day_sheet(p_sheet, p_body) owner disapproved → pending Justification required; resubmissions += 1.
approve_day_sheets(p_items) — [{sheet_id, snapshot}] admin pending or disapproved → approved Same-day allowed. Bulk-capable. Stores the snapshot.
 draft ──submit / auto──▶ pending ──approve──▶ approved
                           │   ▲                 │
                  disapprove   justify           │ new work on the same day
                           ▼   │                 │ (system "amended")
                        disapproved ◀────────────┘ (back to pending)

5.4 Automatic behaviour

  • Draft creation. Triggers upsert a draft sheet when a work log is inserted (for the author, on work_date) and when a lifecycle activity-log event is inserted (for the task's assignee, on the event's Manila date). Only for role = 'programmer'.

  • Amended after approval. A work log or lifecycle event landing on an approved sheet for today sets it back to pending and adds a system amended event. This is not counted as a disapproval. The Approve button warns the admin when that programmer has a timer running.

  • Nightly auto-submit — auto_submit_day_sheets() via pg_cron at 00:05 Manila:

    1. For assignees of tasks still running, upsert yesterday's sheet (reopening it as amended if it was approved).
    2. Flip every draft sheet with work_date < today to pending (auto_submitted = true, auto_submitted event).
    3. If anything is pending, insert one scheduled_notifications row per admin with notify_type = 'day_sheet_digest', scheduled for 08:00 Manila.

    The cron registration uses the same DO $$ … EXCEPTION guard as existing cron migrations.

5.5 Approval snapshot

On approval the admin client sends the rows it displayed:

{
  "v": 1,
  "total_seconds": 27900,
  "rows": [
    {
      "task_id": "…", "title": "…", "category": "Bug Fix", "project_name": "HIS",
      "kind": "assignee", "helped_name": null,
      "seconds": 11400, "notes": ["Fixed session timeout…"]
    }
  ]
}

Approved days are thereby fixed records: the report and stats read the snapshot, so later renames or reassignments do not change what was approved.

6. Approval UX

6.1 Tabs

programmer_tasks_list_screen.dart shows tabs by role:

  • Programmer: Mine · All · Day Sheets
  • Admin: Monitor · Approvals · Mine · All

6.2 Day Sheets tab (programmer)

  • Today card — today's rows (live), total time, Draft/Submitted chip, Submit day.
  • Needs your attention — disapproved sheets, pinned, warning style.
  • History — sheets by date with status filter chips (AppStatusSummaryRow).
  • App bar: Accomplishment Report.

6.3 Sheet detail — /programmer-tasks/day-sheets/:id (both roles)

  • Header: date, programmer, status, total time, "Round N" when resubmitted.
  • Rows table: task · time · notes. Flagged rows are highlighted (AppStatusColors warning + flag icon).
  • Thread timeline, styled like widgets/activity_timeline.dart.
  • Programmer, when disapproved:
    • Add correction — the existing work-log dialog with workDate preset to that day, flagged tasks listed first.
    • Justification field (cannot be empty; modeled on the checkout-justification dialog, attendance_screen.dart:1462-1510).
    • Resubmit.
  • Admin, when pending or disapproved:
    • Row checkboxes for flagging.
    • Disapprove — dialog lists the flagged rows; remarks required.
    • Approve — warns if a timer is running. The admin client builds the snapshot.

6.4 Approvals tab (admin)

  • Summary chips: Pending · Awaiting justification · Approved.
  • Programmer filter and the shared date filter.
  • Cards grouped by date: programmer, total time, task count, Early/Auto badge, Round N.
  • Multi-select → Approve selected. Disapproval stays per sheet because it needs remarks.

7. Admin Monitor

  • Summary row: Working · Paused · Idle · On leave/Pass slip · Sheets pending.
  • One card per programmer (grid: 1 column on mobile, 2–3 on desktop):
    • State pill: Working (success) / Paused (warning) / Idle (neutral) / On leave or Pass slip (info; overrides the others).
    • Current task: title, category/project TechChips, live "running 1h 12m today".
    • Today's total; in-progress / paused / not-started counts; today's sheet status.
    • Help chips: "Helped → Ben · task · 45m", "Helped by ← Ana · 30m".
  • Drill-down (tap a card): ProgrammerDashboard(userId, readOnly: true), their recent Day Sheets, and Generate Accomplishment Report with the programmer preselected.
  • Who's helping whom panel: helper → assignee · task · minutes · time, with a day stepper (defaults to today). The working/paused state is always live.

Data

  • New programmerRunStatesProvider: realtime stream of lifecycle activity logs for all in_progress task ids → latest lifecycle event per task → Working vs Paused. It also replaces the Mine tab's myRunningProgrammerTaskIdProvider inference, which currently marks every in-progress task as paused when offline.
  • Today's time: the per-day split (5.1).
  • Help: work logs with work_date = day whose author ≠ task assignee.
  • Leave / pass slip: extract the per-user "today" logic from dashboard_screen.dart:371-433 into lib/utils/staff_presence.dart, used by both the dashboard and the Monitor.
  • Admin-only. Programmers keep the existing All tab and get no live view of teammates.

8. Mine dashboard

A single widget, ProgrammerDashboard(userId, {readOnly}), used for the Mine tab and for the Monitor drill-down.

  1. Now working hero — the running task with a live timer (today + total) and Pause/Complete. If nothing is running: "Nothing running" and Start on the top not-started task (via startProgrammerTaskWithPausePrompt).

  2. Stat tiles (AppMetricCard):

    • Today's focus — ring against an 8h target (pattern: reports/widgets/conversion_rate_card.dart:43-55).
    • Completed this week, with change vs last week.
    • Streak — consecutive weekdays whose sheet is pending or approved. Weekends and approved-leave days are skipped (neither count nor break). A disapproved day breaks the streak until justified. Today counts once submitted. Shows best streak too.
    • First-pass approval rate (last 30 days) — approved sheets with no disapproved event ÷ reviewed sheets.
    • Help given this month.
    • Streak and approval rate are hidden for admins (admins have no sheets).
  3. Badges — computed from data each time (no table). Locked badges are greyed out with progress (e.g. 7/10).

    Badge Unlocks at
    First Finish first completed task
    Finisher 10 / 50 / 100 completed tasks
    Bug Squasher 10 completed Bug Fix tasks
    Deep Work 100 / 500 approved hours
    On a Roll best streak 5 / 10 / 20
    Team Player 10h help given
    Clean Week a Mon–Fri week with every day approved first time
  4. Task sections (AppSectionHeader + count): In progress · Paused · Not started · Completed (collapsed; last 10 + Show all) · Cancelled (collapsed). The list's private _TaskCard is extracted to widgets/programmer_task_card.dart.

Data: hour-based stats and badges sum approved snapshots; "today" figures come from the live per-day split; counts come from programmerTasksProvider filtered to the user. Calculations live in pure functions (computeProgrammerStats, computeBadges).

9. Accomplishment Report (PDF)

Entry points: Day Sheets tab app bar (programmer, self); Approvals tab app bar and Monitor drill-down (admin).

Generate dialog

  • Programmer picker — admin only; searchable single-select of role = 'programmer' from profilesProvider.
  • Date filter — ReportDateFilter.controlled, default "This Month".
  • Toggle — "Include unapproved days (marked)", off by default.
  • Generate → preview dialog with Print and Download.

Date filter reuse: add ReportDateFilter.controlled({value, onChanged}) to lib/screens/reports/report_date_filter.dart. _DateFilterDialog returns the chosen ReportDateRange instead of writing the provider. The default constructor keeps binding reportDateRangeProvider, so the Reports screen is unchanged.

Layout (A4 MultiPage, Roboto, CRMC letterhead):

  1. Info box — name, position, period (inclusive end date: end − 1 day).
  2. By-day table — Date | Task/Project | Accomplishment | Time.
    • Accomplishment = that day's work-log notes (Quill delta → plain text); "(no notes)" when a row has time but no notes.
    • Help rows read "Helped Ana — ".
    • When unapproved days are included, they carry a Pending/Disapproved tag.
  3. Footnote (approved-only mode) — "N days in the period are not included (x pending, y disapproved)".
  4. Summary — days reported, total time, tasks completed in the period, help given, time by category.
  5. Signatures — "Prepared by" the programmer; "Approved by" the reviewing admin's name if one admin approved every included day, otherwise a blank line.

Data: approved days come from approved_snapshot; unapproved days (when included) are computed live. buildAccomplishmentReportData(...) (pure) produces a DTO; buildAccomplishmentReportPdf(dto) only draws.

Shared helpers (used by new code only): lib/utils/pdf/pdf_letterhead.dart (fonts, logo, letterhead) and lib/widgets/pdf_preview_dialog.dart (pdfrx preview, Printing.layoutPdf, Printing.sharePdf, modeled on _DtrPdfDialog).

10. Notifications

Type Recipient Trigger
day_sheet_submitted all admins programmer submits early
day_sheet_digest each admin, 08:00 nightly auto-submit left sheets pending; opens Approvals
day_sheet_disapproved programmer admin disapproves (includes remarks)
day_sheet_justified the reviewing admin programmer justifies and resubmits
day_sheet_approved programmer admin approves (single or bulk)

RPCs insert the notification rows and return recipients; the client sends push through NotificationsController.sendPush. The digest goes through scheduled_notifications with a new case in process_scheduled_notifications. New render cases and tap routing go into notifications_screen.dart and notification_bridge.dart, and day_sheet_id is added to notification_item.model.dart.

11. Error handling

  • Stale status (e.g. two admins act at once) → "This sheet was already reviewed" and the view refreshes.
  • Empty remarks/justification → blocked in the dialog and by the DB check.
  • Disallowed back-dated entry → the guard trigger's error is shown as a clear message.
  • Push failure → non-blocking; logged, as in existing flows.
  • Offline → Day Sheets are online-only (like work logs); actions are disabled with a banner.

12. Files

New (each under 500 lines):

  • supabase/migrations/20260927150000_add_programmer_day_sheets.sql
  • lib/models/programmer_day_sheet.model.dart, lib/models/programmer_day_sheet_event.model.dart (plain online models)
  • lib/providers/programmer_day_sheets_provider.dart (streams, RPC controller, push)
  • lib/providers/programmer_run_states_provider.dart
  • lib/utils/programmer_daily_time.dart (per-day split with assignee attribution)
  • lib/utils/programmer_stats.dart (stats, streak, badges)
  • lib/utils/staff_presence.dart
  • lib/utils/pdf/pdf_letterhead.dart, lib/widgets/pdf_preview_dialog.dart
  • lib/screens/programmer_tasks/dashboard/ — dashboard, hero, stat tiles, badges, sections
  • lib/screens/programmer_tasks/monitor/ — monitor tab, programmer card, help panel
  • lib/screens/programmer_tasks/day_sheets/ — Day Sheets tab, Approvals tab, detail screen, disapprove/justify dialogs
  • lib/screens/programmer_tasks/report/ — generate dialog, report data, report PDF
  • lib/screens/programmer_tasks/widgets/programmer_task_card.dart

Modified:

  • lib/screens/programmer_tasks/programmer_tasks_list_screen.dart — role tabs, card extract
  • lib/models/programmer_task_work_log.model.dart, lib/providers/programmer_task_work_logs_provider.dart, lib/screens/programmer_tasks/widgets/work_log_section.dart — workDate
  • lib/screens/reports/report_date_filter.dart — controlled constructor
  • lib/providers/profile_provider.dart — isStrictAdminProvider
  • lib/routing/app_router.dart — day-sheet detail route
  • lib/screens/notifications/notifications_screen.dart, lib/services/notification_bridge.dart, lib/models/notification_item.model.dart
  • supabase/functions/process_scheduled_notifications/index.ts — digest case
  • lib/screens/dashboard/dashboard_screen.dart — use staff_presence.dart

13. Delivery slices

Each slice ships independently on feat/programmer-tasks.

  1. Mine dashboard — run-state provider, per-day split util, dashboard. No schema change; fixes the offline "everything paused" bug. Approval-based stats appear once slice 3 lands.
  2. Admin Monitor — plus the staff_presence.dart extraction.
  3. Day Sheets — migration, RPCs, cron, Day Sheets/Approvals tabs, detail screen, notifications. The user applies the migration and redeploys process_scheduled_notifications (the Supabase migration tool is blocked in this environment).
  4. Accomplishment Report — date-filter refactor, PDF helpers, report.

14. Testing and verification

  • flutter analyze lib clean; flutter test passes apart from the three known, unrelated failures (theme_overhaul, profile_screen, user_management).
  • Unit (pure Dart, TDD): per-day split (midnight crossing, reassignment attribution, pauses, adjustments); stats and streak (weekends, leave, disapproved days); badges; report data (approved-only vs marked, snapshot vs live, footnote, inclusive period).
  • Controller (mock-first, pattern of test/programmer_tasks_controller_test.dart): RPC calls, push invocation, stale-status error mapping.
  • Widget: Disapprove dialog requires remarks; dashboard sections; tabs by role.
  • SQL checklist after the migration is applied (rolled-back transactions with request.jwt.claims set per role): every transition allowed/denied by role and status; back-dated work-log guard; amended-after-approval; auto_submit_day_sheets() called directly; get_advisors security check.
  • End-to-end via the QA harness (programmer, then admin; light and dark): log work → submit early → admin flags and disapproves → programmer adds a correction and justifies → admin approves → both generate the PDF (approved-only and marked) → Monitor shows Working/Paused/On leave and help edges → notifications render and route.
  • flutter build web succeeds.