Changelog
What landed when. Most-recent first.
2026-04-28 — Public demo hub at /showcase (and root /)
- Single shareable demo URL:
https://attendance.rajyasabha.digital/(alias/showcase) — no sb-iam login. - Hero with live numbers (active employees, present today, punches today, L3 face matches), a phone mock illustrating the IN button + L3 face scan, a marquee strip of stack tags, and "right now" tiles (pending leaves, pending tours, active geofences, holidays loaded, identity tiers, phpunit count).
- "Five entry points, one stack" — surface cards link to
/demo,/mobile-demo,/admin,/api/documentation,/doc,/api/healthzwith thumbnail previews. - "A single punch, from finger to ledger" — five-step end-to-end flow (mobile → sign → API → decide → audit).
- Root
/no longer redirects to/admin; admin login picker now also surfaces demo links. DemoController@showcase+resources/views/demo/showcase.blade.php. Tailwind CDN, no build step. 64/64 phpunit still green.
2026-04-28 — Move to attendance.rajyasabha.digital
- Production canonical URL is now
https://attendance.rajyasabha.digital— own subdomain, no path prefix. - Nginx vhost staged at
/etc/nginx/sites-available/attendanceon :80 — standalone server block, proxies to the same127.0.0.1:8092upstream (no prefix to strip). - SSL is added by
sudo /usr/local/sbin/sds-ops/flip-attendance-subdomain.shviacertbot --nginxonce DNS A record (attendance.rajyasabha.digital → 103.224.23.106) propagates. - The flip script also rewrites
APP_URL, setsSESSION_DOMAIN+SANCTUM_STATEFUL_DOMAINS+DIGILOCKER_REDIRECT_URI, recreates docker compose, and inserts a 301 from/pd/attendance/*on the legacyft.rajyasabha.digitalvhost so partner integrations on the old URL still land softly. - Code/doc references to
ft.rajyasabha.digital/pd/attendance/*rewritten to the new canonical URL across 13 files (OpenAPI@OA\Server, all/docBlade pages, mobilelib/config.dartdefaultAPI_BASE, DigiLocker client/services defaults, demo seed printout, runbook). - DigiLocker redirect URI for NeGD agency registration is now
https://attendance.rajyasabha.digital/api/digilocker/callback— register this on the partner portal, not the legacy path.
2026-04-28 — Live demo dashboard
- Public read-only demo at
https://attendance.rajyasabha.digital/demo— no sb-iam login. Bookmarkable single page with hero tiles, today's punches by hour, 7-day attendance trend, identity-tier breakdown (L1/L2/L3), verdict distribution, recent activity feed, pending leaves & tours, active geofences, upcoming holidays, and a feature matrix. app/Http/Controllers/Web/DemoController.php— computes everything live from PG; demo "cohort" = distinct synthetic punchers in last 7 days (so present/absent ratios look like a real workplace, not the sparse 2,300-row roster).php artisan attendance:demo-seed— idempotent: wipes today + 6 prior days of_synthetic=truepunches, regenerates 420 today + ~3,000 across history, plus 8 leaves and 4 tours. Re-run anytime via the "↻ Refresh demo" button on the page.- Route group has CSRF + session middleware so the refresh button works from a stock browser.
2026-04-28 — Leave cancellation
- New endpoint
POST /v1/leaves/{id}/cancel— fills the previously deadcancelledstatus enum. - Owner can cancel their own
pendingorapprovedleave whosestart_dateis today or in the future. - Approver (
reporting_officer/appointing_authority/admin) may override-cancel an already-elapsed approved leave (mistaken approval, retroactive correction). - Cancelling an approved leave restores the deducted days via new
CcsLeaveEngine::creditBalance();COMMUTEDcredits 2× HPL the same way it deducts. - Decision history gets an append-only
{action: "cancelled", actor, at, note}entry; audit log emitsattendance.leave.cancelledwithwas_approvedflag. - Idempotent on already-cancelled / rejected / expired (returns 422
not_cancellable); 403 if neither owner nor approver; 404 on unknown id. - Mobile:
LeaveApi.cancel(leaveId, note)inlib/api/leave_api.dart. - 64/64 phpunit green (was 54): 10 new
LeaveCancelTestcases includingCOMMUTED2× HPL credit-back.
2026-04-28 — Manager dashboard (mobile + API)
- New permission key
can_view_team_attendance(reporting_officer / appointing_authority / hr_admin / admin). app/Services/TeamScope.php— resolves "who reports to me" via jsonb containment onattendance_employee_register.reporting_chain. Empty-team managers get empty results, never 500.- Endpoints under
/v1/manager/*:GET team/today— first-IN / last-OUT / status per direct reportGET team/absent— direct reports with no punch + no approved leave + no approved tour todayGET approvals/pending— pending leaves + tours from my reportsGET team/summary?from=&to=— punch-day count + clipped leave-day count per report (window capped at 92 days)
GET /v1/menow returns the user's resolvedpermissionsarray — clients hide UI affordances they don't have, server still enforces every gate.- Mobile: new
lib/manager/manager_screen.dartwith three tabs (Today / Absent / Approvals). Quick-action card "Manager" appears on dashboard only when the permission is present. - 54/54 phpunit green (was 46): 8 new
ManagerApiTestcases — auth/RBAC gating, today/absent scoping, pending-only filter, summary window validation, leave-day clipping math.
2026-04-28 — Vault transit KEK for face-template DEKs
- VaultTransit client (
app/Services/VaultTransit.php) — AppRole login (1h cached token) + encrypt/decrypt/rewrap againsttransit/keys/attendance. Mirrors the vani_prod backport. - FaceTemplateStore wrap/unwrap is now backend-pluggable. Per-row
dek_wrap_methodtag (laravel_crypt|vault_transit:<keyname>) is authoritative on read, so flipping the global config forward only affects newly written rows. - Backfill:
php artisan attendance:rewrap-face-deksrewraps existing DEKs without re-encrypting the embedding (blob_sha256 stays valid). Supports--dry-run,--limit,--include-retired. - Vault provisioning:
transit/keys/attendance(aes256-gcm96, non-exportable) created;sanchalan-rwpolicy extended with encrypt/decrypt/rewrap caps + key metadata read. AppRole secret-id rotated. All viasudo /usr/local/sbin/sds-ops/wire-attendance-vault-transit.sh— idempotent. - Default unchanged:
KEK_BACKEND=laravel_crypt. Operator opts in by settingKEK_BACKEND=vault_transit, recreating containers, and running the rewrap command. - Tests: 46/46 phpunit green (was 39); two new tests cover the Vault path and the rewrap flow with an in-memory VaultTransit fake. Live container round-trip against real Vault verified.
2026-04-27 — evening (Phase 1 + Phase 2.5 + DigiLocker + mobile-nav fix)
Phase 1 — device biometric (shipped)
- Migration:
bio_used,bio_method,identity_assurancecolumns onattendance_puncheswith CHECK constraints. - PunchController accepts + records bio fields, audit-logs the assurance tier.
- Mobile
BiometricGateservice wrapslocal_auth; signature only released after a fresh BiometricPrompt match. Cancellation short-circuits withverdict=rejected_biometric. - Live verified:
verdict=accepted bio_used=1 bio_method=fingerprint assurance=l2.
Phase 2.5 — self-hosted face match (shipped)
- Schema:
attendance_face_consent,attendance_face_templates, plusface_match_score/liveness_score/face_template_versiononattendance_punches. Verdict CHECK widened to includeaccepted_face_match,rejected_face_mismatch,rejected_liveness. - Services:
FaceMatcher(cosine, 512-d wire format),FaceTemplateStore(AES-256-GCM + envelope-encrypted DEK + tamper-detect via blob_sha256 + audit on every read),LivenessGate(per-cohort threshold + active challenge),IdentityPolicy(resolves required tier from cohort × context). - API:
POST /v1/punch/face-match(L3 punch),POST /v1/enrollment/face-template(HR-supervised),GET /v1/me/face-status,POST /v1/enrollment/{id}/withdraw. - Admin web:
/admin/enrollment(search + per-employee enroll/withdraw), RBACcan_enroll_face. - Mobile:
FaceEmbedder(MobileFaceNet TFLite),LivenessDetector(ML Kit),FaceCaptureScreen,FacePunchScreen,EnrollmentScreen. - Decision matrix:
config/attendance.phpwith per-cohort, per-context tier requirements. - Tests:
FaceMatcherTest,LivenessGateTest,IdentityPolicyTest,FaceTemplateStoreTest(encrypt/decrypt/match round-trip). - Live verified — three verdicts:
- genuine:
accepted_face_match, score=1.0000, liveness=0.9 - impostor:
rejected_face_mismatch, score=-0.0855 (under 0.65) - low-liveness:
rejected_liveness, liveness=0.3
- genuine:
DigiLocker eKYC (shipped)
attendance_digilocker_linkstable — Aadhaar-correlation hash, name+DOB match flags, encrypted access token, sanitised claims (no raw Aadhaar / DOB / photo bytes ever persisted).DigiLockerClientservice: PKCE OAuth2, eKYC XML parser (XXE-defused),DigiLockerControllerwith web (session) + mobile (token) endpoints.POST /v1/enrollment/face-template/digilocker— first-time face enrollment using a fresh (≤15 min) DigiLocker link with name + DOB matching the employee record.- Mobile
DigiLockerEnrollScreen: full PKCE flow viaflutter_web_auth_2, embeds the DigiLocker photo + a live face frame, averages, submits. - Doc page /doc/digilocker.
Parichay (sb-iam) SSO — verification
- Web flow + mobile PKCE flow already wired and code-reviewed;
GET /api/auth/modereports correct state. - Endpoints respond 404 when
sso_enabled=false(dev mode default) — confirms gating works as designed. - Doc page /doc/parichay with the full operator setup recipe.
Geofence + Holiday CRUD (shipped)
GeoFenceWebController— list, create, edit, deactivate; auto-synthesises a 16-point GeoJSON polygon from (centre, radius) when no polygon supplied. RBACcan_manage_geo_fences.HolidayWebController— list, create, edit, delete, year filter. RBACcan_manage_holidays.- Admin nav: new Geofences tab; Holidays repointed to the manage page.
Mobile nav rewrite (shipped)
- Hamburger toggle on
<mdviewports → slide-out drawer with full nav, role pill, sign-out. Backdrop click + Escape dismiss. - Sticky header so nav is reachable from deep pages.
- All admin tables wrapped in
.table-scrollfor horizontal overflow on narrow viewports. - Section indicator in mobile header shows current page label.
- Doc layout: same drawer pattern, plus prose tweaks (code word-break, mermaid overflow).
- All 16 admin pages and 15 doc pages render 200 with mobile drawer markup verified.
Other
- Bug fixes:
face_match_score+liveness_scoreadded toAttendancePunchfillable + casts; geofence schema NOT-NULLpolygon_geojsonhandled via auto-circle synthesis. - Tests: 39 PHPUnit (11 unit + 7 face/identity unit + 8 feature + 13 face-template + extras), all green.
2026-04-27 — afternoon (4-hour autonomous build)
Backend
- Worker + audit containers wired (new
sds-ops/wire-attendance-audit.sh); 4 healthy containers. - Sanctum personal-access-tokens published;
HasApiTokenson User;users.employee_idcolumn added with email-based auto-link in SSO callback. - Mobile PKCE endpoints:
POST /auth/sso/mobile/redirectandPOST /auth/sso/mobile/exchangewithSBIAM_MOBILE_CLIENT_ID+ redirect_uri allowlist. - Bug fixes:
REDIS_CLIENTmismatch (predis → phpredis),imei_saltconfig wired,verdictcolumn widened 16→32, partial unique index on(employee_id, nonce) WHERE acceptedfor replay protection. - New endpoints:
GET /v1/me,GET /v1/holidays, full/v1/tours/*subsystem (apply/list/decide). - L5-Swagger annotations on Punch/Device/Leave/Tour/PublicLookup; UI live at
/api/documentation.
Web admin (new)
- 9 Blade pages: Dashboard, Today, Late, Absent, Pending leaves, Pending tours, Devices, Holidays, Reports.
- Dev login with email + role picker; production redirects to sb-iam.
- Force-HTTPS scheme via AppServiceProvider so generated URLs work behind TLS-terminating nginx.
- CSV export with 90-day cap; leave + tour decide flows audit-logged.
Documentation (new — this site)
/doc/*— 11 Blade pages with Mermaid diagrams.
Mobile
- Full Flutter source repo: 30+ files, 10 screens (Login, Home, Punch, Today, Month grid, Leave×2, Tour×2, Holidays, Devices, Profile).
- P-256 keypair (raw r||s, std-base64); stable UUIDv4 + sha256-salted installation hash; Hive offline queue with 48h max age.
- API clients aligned to backend wire format exactly.
- Bootstrap script + Android manifest patcher + iOS plist patcher.
Tests
- 11 unit tests (PunchSignatureVerifier, AntiSpoofPolicy, GeoFenceCheck) + 7 feature tests (auth, RBAC, validation) — 18/18 green.
- 2 integration smoke scripts (
/tmp/punch-smoke.sh,/tmp/admin-smoke.sh).
Audit pipeline e2e verified
Each accepted/rejected/duplicate punch + leave + tour action emits to Redis stream → audit container drains → writes to sds_audit.audit_events. 19+ attendance events landed in the audit DB during smoke testing.
2026-04-27 — morning (initial scaffold)
- Laravel 12 skeleton, Apache + PHP 8.2 docker image, nginx-alpine reverse proxy on port 8092.
- 8 attendance migrations: employee register / devices / geo fences / punches / holidays / leave policies / leave balances / leave applications.
- 3 controllers (Punch, Device, Leave) with full request validation; 5 services (AuditClient, AntiSpoofPolicy, GeoFenceCheck, PunchSignatureVerifier, CcsLeaveEngine); 8 Eloquent models.
- Permission-keyed RBAC middleware +
config/permissions.php. - Apache
AllowOverridepatch in Dockerfile, bind-mount perm fix,Blueprint::check→DB::statement ALTER ... ADD CHECKrewrite. - SsoController ported from vani_dev; nginx vhost added; healthz green.
- Seed data: 2,300 synthetic employees, 13 holidays, 7 fences, 9 leave policies, opening balances.
What's open
- sb-iam mobile client_id registration (operator action).
- Mobile compile + APK signing on a dev workstation (server is too lean for Android SDK).
- FCM push notifications for leave/tour decisions.
- Manager mobile dashboard tab (approver workflow on phone).
- Hardware attestation (Play Integrity / DeviceCheck) replacing soft anti-spoof signals.
- OpenAPI annotations on Sso + Admin* controllers.
- Production cutover items (see Runbook §6).
