Attendance Sanchalan
Sanchalan Docs
Changelog

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/healthz with 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/attendance on :80 — standalone server block, proxies to the same 127.0.0.1:8092 upstream (no prefix to strip).
  • SSL is added by sudo /usr/local/sbin/sds-ops/flip-attendance-subdomain.sh via certbot --nginx once DNS A record (attendance.rajyasabha.digital → 103.224.23.106) propagates.
  • The flip script also rewrites APP_URL, sets SESSION_DOMAIN + SANCTUM_STATEFUL_DOMAINS + DIGILOCKER_REDIRECT_URI, recreates docker compose, and inserts a 301 from /pd/attendance/* on the legacy ft.rajyasabha.digital vhost 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 /doc Blade pages, mobile lib/config.dart default API_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=true punches, 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 dead cancelled status enum.
  • Owner can cancel their own pending or approved leave whose start_date is 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(); COMMUTED credits 2× HPL the same way it deducts.
  • Decision history gets an append-only {action: "cancelled", actor, at, note} entry; audit log emits attendance.leave.cancelled with was_approved flag.
  • 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) in lib/api/leave_api.dart.
  • 64/64 phpunit green (was 54): 10 new LeaveCancelTest cases including COMMUTED 2× 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 on attendance_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 report
    • GET team/absent — direct reports with no punch + no approved leave + no approved tour today
    • GET approvals/pending — pending leaves + tours from my reports
    • GET team/summary?from=&to= — punch-day count + clipped leave-day count per report (window capped at 92 days)
  • GET /v1/me now returns the user's resolved permissions array — clients hide UI affordances they don't have, server still enforces every gate.
  • Mobile: new lib/manager/manager_screen.dart with 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 ManagerApiTest cases — 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 against transit/keys/attendance. Mirrors the vani_prod backport.
  • FaceTemplateStore wrap/unwrap is now backend-pluggable. Per-row dek_wrap_method tag (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-deks rewraps 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-rw policy extended with encrypt/decrypt/rewrap caps + key metadata read. AppRole secret-id rotated. All via sudo /usr/local/sbin/sds-ops/wire-attendance-vault-transit.sh — idempotent.
  • Default unchanged: KEK_BACKEND=laravel_crypt. Operator opts in by setting KEK_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_assurance columns on attendance_punches with CHECK constraints.
  • PunchController accepts + records bio fields, audit-logs the assurance tier.
  • Mobile BiometricGate service wraps local_auth; signature only released after a fresh BiometricPrompt match. Cancellation short-circuits with verdict=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, plus face_match_score / liveness_score / face_template_version on attendance_punches. Verdict CHECK widened to include accepted_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), RBAC can_enroll_face.
  • Mobile: FaceEmbedder (MobileFaceNet TFLite), LivenessDetector (ML Kit), FaceCaptureScreen, FacePunchScreen, EnrollmentScreen.
  • Decision matrix: config/attendance.php with 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

DigiLocker eKYC (shipped)

  • attendance_digilocker_links table — Aadhaar-correlation hash, name+DOB match flags, encrypted access token, sanitised claims (no raw Aadhaar / DOB / photo bytes ever persisted).
  • DigiLockerClient service: PKCE OAuth2, eKYC XML parser (XXE-defused), DigiLockerController with 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 via flutter_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/mode reports 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. RBAC can_manage_geo_fences.
  • HolidayWebController — list, create, edit, delete, year filter. RBAC can_manage_holidays.
  • Admin nav: new Geofences tab; Holidays repointed to the manage page.

Mobile nav rewrite (shipped)

  • Hamburger toggle on <md viewports → 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-scroll for 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_score added to AttendancePunch fillable + casts; geofence schema NOT-NULL polygon_geojson handled 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; HasApiTokens on User; users.employee_id column added with email-based auto-link in SSO callback.
  • Mobile PKCE endpoints: POST /auth/sso/mobile/redirect and POST /auth/sso/mobile/exchange with SBIAM_MOBILE_CLIENT_ID + redirect_uri allowlist.
  • Bug fixes: REDIS_CLIENT mismatch (predis → phpredis), imei_salt config wired, verdict column widened 16→32, partial unique index on (employee_id, nonce) WHERE accepted for 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 AllowOverride patch in Dockerfile, bind-mount perm fix, Blueprint::checkDB::statement ALTER ... ADD CHECK rewrite.
  • 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

  1. sb-iam mobile client_id registration (operator action).
  2. Mobile compile + APK signing on a dev workstation (server is too lean for Android SDK).
  3. FCM push notifications for leave/tour decisions.
  4. Manager mobile dashboard tab (approver workflow on phone).
  5. Hardware attestation (Play Integrity / DeviceCheck) replacing soft anti-spoof signals.
  6. OpenAPI annotations on Sso + Admin* controllers.
  7. Production cutover items (see Runbook §6).