Skip to content

feat: read attendance from the IIIT Kottayam attendance portal - #23

Open
moneytosms wants to merge 15 commits into
Noelithub77:masterfrom
moneytosms:sms/fix-new-portal
Open

feat: read attendance from the IIIT Kottayam attendance portal#23
moneytosms wants to merge 15 commits into
Noelithub77:masterfrom
moneytosms:sms/fix-new-portal

Conversation

@moneytosms

Copy link
Copy Markdown

Why

IIIT Kottayam retired Moodle's mod/attendance and moved attendance to
https://attendance.iiitkottayam.ac.in. Courses are still enrolled on Moodle, so Moodle
stays the source for assignments, timeline, resources and faculty — only attendance moved.

Bunkialo derives its timetable from attendance records, so the app lost attendance, bunk
tracking and the timetable at once. Worse, fetchAllAttendance() began returning [] on
every sync, which the store wrote straight into state — wiping the cache, then bunk-store,
then the persisted timetable on the next launch.

The decision everything follows from

AttendanceRecord.date is regex-parsed in 16 call sites across 11 files. Most fail
silently on a format change; the worst is isPastOrCompleted returning false
universally, which makes filterPastBunks discard every bunk with no error.

So the adapter emits Moodle-style date strings (Thu 1 Jan 2026 9:00AM - 9:55AM) rather
than structured fields. timetable-inference.ts, timetable-store.ts, bunk-store's
merge logic, every timetable component and ICS export took zero changes.

Tests assert this by feeding adapter output through the real downstream parsers rather
than restating the expected string, so format drift fails loudly.

What's here

New

  • services/attendance-portal.ts — HTTP, auth, token and credential storage
  • services/attendance-portal-adapter.ts — payload to CourseAttendance; pure, type-only
    imports so it loads under node --test without an Expo runtime
  • components/settings/portal-settings-section.tsx, components/modals/portal-connect-modal.tsx
  • src/scripts/test-setup.mjs, test-stub.mjs — test harness behind npm test
  • docs/attendance-portal.md, docs/attendance-portal-recon.md, todo.md
  • 5 test files, 80 tests

Changed

  • stores/attendance-store.ts — source selection, empty-scrape guard, in-flight
    de-duplication, change detection, reconnect flag
  • stores/bunk-store.ts — semester auto-drop skipped for portal courses
  • stores/auth-store.ts — logout now clears the portal too
  • app/settings.tsx, types/attendance.ts, AGENTS.md, package.json

Notable fixes found along the way

  • Empty scrape wiped the cache. With the module gone, every sync destroyed the user's
    attendance, bunks and timetable. Now guarded.
  • Logout left portal credentials on the device. "Log out" was untrue for one of the
    two accounts the app holds.
  • Semester auto-drop hid current courses. semester-course-filter.ts hardcodes
    Aug–Nov, so on 1 August a course with late-July sessions read as entirely outside the
    term. Four of six courses vanished. The heuristic exists for Moodle's stale "in progress"
    list; the portal only ever returns the active term, so portal courses skip it. Moodle
    behaviour is unchanged and pinned by a test.
  • UTC-midnight dates. The portal sends 2026-07-31T00:00:00.000Z. Parsing that through
    local time rolls the day back west of UTC, producing the wrong weekday and filing the
    class on the wrong day. Now reads the YYYY-MM-DD prefix.
  • Navigation flash. Every fetch replaced courses and bumped lastSyncTime even when
    nothing changed, and attendance.tsx watches lastSyncTime to run syncFromLms then
    generateTimetable. Every navigation triggered a full recompute.

Security

  • Access token in memory only; refresh token and credentials in expo-secure-store with
    keychainAccessible: WHEN_UNLOCKED_THIS_DEVICE_ONLY.
  • Credentials use the shared Credentials type, same shape services/auth.ts writes.
  • The 2FA password is held in memory between the password and code steps, so an incomplete
    login never leaves a password on disk.
  • Concurrent 401s share one refresh promise — with token rotation, parallel refreshes
    invalidate each other and lock the user out.
  • Not applied to the existing lms_credentials entry: changing the accessibility class
    invalidates the keychain item and would log out every current user. That's a migration,
    not a hardening.

Request load

A naive refresh is 1 + N requests. In-flight de-duplication, unchanged-course caching
(the summary already reports present/total) and change detection bring the steady
state to 1 request. Pull to refresh passes force and bypasses all three. Every
request has a 30s timeout; fetch has none by default.

Testing

npm test                            80 pass
./node_modules/.bin/tsc --noEmit    3 errors, all pre-existing on master
npx expo lint                       0 errors, 28 warnings (unchanged)
npx expo export --platform android  clean

Verified on a real device via Expo Go: six courses, timetable and dashboard populating,
bunk notes persisting across restarts.

Unit tests need Node 22.15+ for module.registerHooks and native type stripping.
Test files are *.test.mjs; the existing integration scripts are test-*.mjs and also
match node --test's default glob, so npm test scopes explicitly — a bare node --test
would run them against the live LMS with real credentials.

Known gaps

Tracked in todo.md:

  • dlCredited (duty-leave credited sessions) is ignored, so a course with duty leaves may
    show a percentage differing from the website. Needs a real example before guessing at
    the weighting.
  • Two courses sharing a course code would collapse into one. Latent; all current codes are
    unique.
  • 2FA is implemented from the bundle's shapes but untested against the real endpoint.

Note for review

The first commit (fix: import zustand and fuse.js types as types) is unrelated to the
feature and split out deliberately: StateStorage and IFuseOptions are types imported
as values, which Metro tolerates but Node's type stripping does not. Behaviour is
unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FNXAGPFfZxaD8rosWmhzCm

moneytosms and others added 15 commits August 1, 2026 14:21
StateStorage and IFuseOptions are types imported as values. Metro's
Babel transform tolerates it, but Node's native TypeScript type
stripping emits a real runtime import and the module fails to load.

Behaviour is unchanged; this only affects tooling that runs the sources
outside Metro.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moodle mod/attendance is dead; attendance moved to
attendance.iiitkottayam.ac.in. Documents the reverse-engineered API
surface and a 3-phase migration plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With Moodle's mod/attendance removed, fetchAllAttendance() returns []
on every sync. The store wrote that straight into state, wiping the
persisted cache; bunk-store.syncFromLms then dropped every LMS course
and timetable-store's rehydration regenerated from empty, destroying
the user's timetable.

Guard the empty result when a cache already exists. A genuinely empty
first sync is still allowed through.

Adds a node:test harness that imports the real .ts sources via a small
resolve hook, rather than duplicating logic into a script. Run with
npm test.

storage.ts: StateStorage is a type; import it as one so Node's type
stripping does not emit a runtime import. Behaviour unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moodle's mod/attendance is gone; attendance now lives at
attendance.iiitkottayam.ac.in. Adds a client for its JSON API and an
adapter onto the CourseAttendance shape the app already uses.

The adapter emits Moodle-style date strings ('Thu 1 Jan 2026 9:00AM -
9:55AM') rather than structured fields. AttendanceRecord.date is
regex-parsed in 16 places and most fail silently on a format change, so
emitting the format they already read leaves the entire downstream
pipeline untouched: inference, bunk merge, conflict resolution and ICS
export need no changes. Tests assert this by feeding adapter output
through the real parsers rather than restating the format.

Portal course ids are resolved onto Moodle ids by course code, so a
portal id never reaches persisted state and no store migration is
needed.

Concurrent 401s share one refresh promise; without it the N+1 course
fan-out fires N refreshes and token rotation locks the user out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Live response confirms the portal sends a calendar date pinned to UTC
midnight ('2026-07-31T00:00:00.000Z') and 24-hour HH:MM times.

Reading that date through local-time getters rolls the day back for
anyone west of UTC, producing the wrong weekday and filing the class
under the wrong day of the timetable. Take the YYYY-MM-DD prefix and
build a local date from it instead, which is timezone-proof and shorter.

Drops the speculative 12-hour time branch now that the real format is
known. A meridiem time is rejected rather than guessed at, so a future
format change shows up as missing sessions instead of silently wrong
times.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings screen rather than the login screen, so the portal stays
optional and first-run is not blocked on a second password.

Connecting triggers fetchAttendance() and nothing else: syncFromLms and
timetable regeneration already run off lastSyncTime when the attendance
tab renders. A 2FA challenge is reported as unsupported rather than
failing opaquely.

Connection state is local component state seeded from
hasPortalCredentials(), not a store; it changes twice per install.

Documents the confirmed payload formats, the deviations from the
original plan, and the manual verification steps this UI still needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Integrates portal credential storage with the existing Moodle path:

- Uses the shared Credentials type from types/auth.ts, the same
  {username, password} shape services/auth.ts writes, with username
  holding the portal email.
- Adds a tryAutoLogin equivalent: a dead refresh token silently re-logs
  in from the stored password rather than dead-ending at a prompt.
  Without this the stored password had no purpose and should not have
  been kept at all. Credentials are cleared only when re-login also
  fails, so a changed portal password prompts instead of retrying a
  rejected one forever.
- Writes both secrets with keychainAccessible
  WHEN_UNLOCKED_THIS_DEVICE_ONLY, keeping them off device backups and
  unreadable while the phone is locked. Not applied to the existing
  lms_credentials entry, which would invalidate every current user's
  saved login.

Security fix: logout left portal credentials on the device. auth-store
cleared eight stores and the Moodle session but never the portal, so
logging out was untrue for one of the two accounts the app holds.

2FA: TOTP, email OTP and backup codes are implemented end to end. The
password is held in memory between the password step and the code step,
so an incomplete login never leaves a password on disk.

faculty-store.ts: IFuseOptions is a type; import it as one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects found reviewing the portal client:

persistSession wrote data.refresh unconditionally. A refresh response
need not rotate the token, and writing undefined throws on device.
Guard it, matching the portal's own client which leaves the stored
token alone when none is returned.

disconnectPortal did not clear the password held in memory between the
password step and the 2FA step. Abandoning the flow left it there for
the rest of the process lifetime.

Also updates AGENTS.md, which still described attendance as a Moodle
scrape: documents the portal flow, the Moodle fallback, the reason the
adapter emits Moodle-style date strings, and how to run the unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
debug.scraper is a no-op and every debug category is off by default, so
the portal integration logged nothing at all and a failure was
indistinguishable from an empty response.

Adds a PORTAL category, on by default in dev, and logs response key
names, course counts, per-course join results and fetch failures. Key
names and counts only, never payloads, tokens or credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The student attendance response is {student, overall, byCourse, recent}.
The course list is byCourse; `courses` is the faculty dashboard's key,
and reading it off the bundle's faculty render code meant
summary.courses was always undefined.

Result was an empty course array, which cascaded: no attendance, so no
bunk sync, so no timetable. Confirmed against a device 2026-08-01.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only two of six courses appeared in the app. The semester auto-drop in
bunk-store hid the rest.

utils/semester-course-filter.ts hardcodes the current semester as
Aug-Nov. On 1 Aug 2026 that window is one day old, so sessions dated
late July read as "outside the semester" and every course with at least
MIN_PARSEABLE_RECORDS (4) records was dropped. The two that survived
were the two with a single session each, below that threshold.

The heuristic exists because Moodle's "in progress" course list keeps
stale courses. The portal only ever returns the active term, so it has
nothing to catch and misfires at the start of every term. Adds an
optional CourseAttendance.source and skips the heuristic for portal
courses. Absent source still means Moodle, so persisted data and
existing behaviour are unchanged.

Also: percentage is not in the live payload (courseId, courseCode,
courseName, total, present, dlCredited), so derive it from
present/total rather than shipping undefined into the UI.

Also: debug.ts sanitize() truncates arrays at 6 entries, which cut the
logged key lists at exactly 6 and hid the `sessions` key, making the
diagnostics misleading. Log key names as a joined string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The portal section was rendered inside the collapsed Developer block,
so the only way to restore attendance was hidden behind a dev toggle.
Moves it to the top of Settings, above Dashboard.

Adds a 30s timeout to every portal request, matching the Moodle axios
instance. fetch has no default timeout, so a stalled portal previously
left isLoading true and the attendance tab spinning with no way out.
Falls back to a manual AbortController where AbortSignal.timeout is
unavailable.

Edge cases now covered: malformed and out-of-range times, sessions
ending before they start, missing sessions array, absent topic,
out-of-order sessions, duplicate and blank course codes, and portal
failures in both foreground and background (cache preserved, error
surfaced only in the foreground).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three robustness changes.

Reconnect prompt. performRefresh clears credentials when both the
refresh token and the stored password are rejected, which happens on a
portal password change or if 2FA is enabled later. Until now that was
silent: attendance simply stopped updating and the user was never told.
attendance-store now distinguishes a self-disconnection from an
ordinary network error, persists a portalDisconnected flag so it
survives a restart, raises it even for background fetches where errors
are otherwise swallowed, and Settings shows a warning row.

In-flight de-duplication. The dashboard, the attendance tab and the
timetable tab each trigger a fetch on mount. Each portal fetch is 1 + N
requests, so overlapping mounts multiplied straight onto the portal.
Concurrent callers now share one promise.

Unchanged-course caching. The summary already reports present and total
per course, so a course whose totals have not moved has no new sessions
to fetch. Reusing the cached records turns the usual refresh from 1 + N
requests into 1. Pull to refresh passes force, which bypasses both the
in-flight guard and the cache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Returning from Settings flashed the screen. Every fetch replaced the
courses array and bumped lastSyncTime even when nothing had changed, and
app/(tabs)/attendance.tsx watches lastSyncTime to run syncFromLms, which
rewrites bunk-store, which regenerates the timetable. So every
navigation kicked off a full recompute and re-rendered the tree for no
new data. Compare a cheap signature first and leave both alone when the
result is equivalent.

Replaces the migration plan, which has served its purpose, with
docs/attendance-portal.md: architecture, auth, request-load strategy,
refresh cadence, course identity, semester handling, failure modes.

Turns the PORTAL debug category off by default now that it is no longer
needed for diagnosis. It stays available in utils/debug.ts.

Records the dlCredited question and the other known gaps in todo.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverts the shared utils/debug.ts change. Portal logging now goes
through debug.scraper, which is a no-op by default like every other
category, so the integration ships silent and no shared utility is
modified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@moneytosms
moneytosms requested a review from Noelithub77 as a code owner August 1, 2026 08:54
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added Attendance Portal integration for viewing attendance data alongside existing course services.
    • Added secure portal connection settings with email/password, OTP, authenticator, and backup-code verification.
    • Added portal connection, reconnection, disconnection, and error-status handling.
    • Added forced refresh support for attendance and timetables.
  • Bug Fixes
    • Improved refresh reliability, caching, authentication recovery, and protection against replacing valid data with empty results.
    • Preserved portal courses during semester auto-drop processing.
  • Tests
    • Added comprehensive automated coverage for portal authentication, attendance conversion, refresh behavior, caching, and logout flows.
  • Documentation
    • Documented portal integration, authentication, attendance behavior, testing, and operational considerations.

Walkthrough

The change integrates the Attendance Portal for authentication and attendance retrieval. It converts portal data into existing attendance records, adds caching and Moodle fallback behavior, updates settings and refresh flows, and adds hermetic tests and documentation.

Changes

Attendance Portal integration

Layer / File(s) Summary
Portal contracts and attendance adapter
src/types/attendance.ts, src/services/attendance-portal-adapter.ts, src/services/attendance-portal-adapter.test.mjs, docs/attendance-portal-recon.md
Adds portal payload types and converts validated portal sessions and courses into Moodle-compatible attendance records.
Portal authentication and retrieval
src/services/attendance-portal.ts, src/services/attendance-portal.test.mjs
Adds secure credential storage, multi-step authentication, token refresh, authenticated requests, session retrieval, caching, and attendance aggregation.
Attendance state and refresh behavior
src/stores/attendance-store.ts, src/stores/bunk-store.ts, src/stores/auth-store.ts, src/app/(tabs)/timetable.tsx, src/components/attendance/sub_tabs/*, src/stores/*test.mjs
Adds portal source selection, forced refreshes, in-flight request sharing, cache preservation, disconnection state, portal logout, and portal-specific semester handling.
Portal settings and connection UI
src/app/settings.tsx, src/components/modals/*, src/components/settings/*
Adds connection, OTP, TOTP, backup-code, reconnect, disconnect, loading, and error UI.
Hermetic validation and integration documentation
AGENTS.md, docs/attendance-portal.md, package.json, src/scripts/*, src/stores/faculty-store.ts, src/stores/storage.ts, todo.md
Documents portal behavior and testing. Adds the Node test command, module resolution setup, native-module stubs, and type-only imports.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested labels: codex

Suggested reviewers: noelithub77

Poem

A rabbit checked the portal gate,
And turned old sessions into dates.
With tokens tucked and codes in line,
The timetable refreshed on time.
“Hop, hop!” the cached records sing—
New attendance takes its wing.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: reading attendance from the IIIT Kottayam attendance portal.
Description check ✅ Passed The description covers motivation, changes, validation, security risks, known gaps, testing, and documentation, although it uses different section headings than the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the codex label Aug 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (7)
docs/attendance-portal-recon.md (1)

47-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language to these fenced blocks.

markdownlint reports MD040 for the fences at lines 47 and 207. Use text for the endpoint list and the status enum block.

Also applies to: 207-207

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/attendance-portal-recon.md` at line 47, Add the text language identifier
to the fenced code blocks in the attendance portal reconciliation documentation,
specifically the endpoint list and status enum blocks around the referenced
sections, so both fences satisfy markdownlint MD040.

Source: Linters/SAST tools

src/services/attendance-portal.ts (1)

63-72: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Clear the fallback timer after the request settles.

The fallback path creates a setTimeout that is never cleared. Each request keeps a pending timer for the full 30 seconds, and the abort still fires after a successful response. Aborting a settled request is harmless, but the retained timer is avoidable. Return the controller so the caller can clear it, or clear it inside the fetch wrappers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/attendance-portal.ts` around lines 63 - 72, Update timeoutSignal
and its fetch callers to retain and clear the fallback timer when each request
settles, including successful and failed requests. Preserve the native
AbortSignal.timeout path, and ensure the fallback controller’s timer is
cancelled after the request completes.
src/stores/auth-store.test.mjs (1)

61-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Set the store to a logged-in state before you call logout.

isLoggedIn is already false in the initial store state, so the assertion at line 66 passes even if logout does not reset it. Set isLoggedIn to true first, so the assertion proves the reset. Also reset the store state in beforeEach; beforeEach currently resets the two module flags only, and store state persists between tests once a second test is added.

💚 Proposed refactor
 beforeEach(() => {
   portalDisconnected = false;
   moodleLoggedOut = false;
+  useAuthStore.setState({ isLoggedIn: false });
 });
 
 test("logout clears the attendance portal credentials too", async () => {
+  useAuthStore.setState({ isLoggedIn: true });
+
   await useAuthStore.getState().logout();
 
   assert.equal(moodleLoggedOut, true);
   assert.equal(portalDisconnected, true);
   assert.equal(useAuthStore.getState().isLoggedIn, false);
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/auth-store.test.mjs` around lines 61 - 67, Update the logout test
to set useAuthStore’s isLoggedIn state to true before invoking logout, so the
assertion verifies that logout resets it. Extend the test setup’s beforeEach to
reset the auth store state in addition to moodleLoggedOut and
portalDisconnected, preventing state leakage between tests.
src/services/attendance-portal.test.mjs (2)

364-378: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test does not verify the abort.

The stub resolves immediately, so the only assertions are that a signal object exists and that aborted is a boolean. The test name promises abort-on-hang behaviour. Assert that the signal reaches the aborted state, using fake timers so the test stays fast and hermetic. Otherwise rename the test to state what it checks: the request carries an abort signal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/attendance-portal.test.mjs` around lines 364 - 378, Update the
test around portal.fetchPortalAttendance to use fake timers and a fetch stub
that remains pending long enough for the configured timeout, then advance the
timers and assert that the captured signal is aborted. Preserve the existing
signal-presence assertion and restore timers after the test; only rename the
test instead if abort behavior cannot be exercised.

26-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the duplicated fetch stub.

Lines 26-37 and lines 42-53 hold the same body. beforeEach assigns queuedFetch, so the module-level assignment at line 26 never serves a test. Keep queuedFetch only. Two copies can drift and then behave differently depending on which one a test happens to use.

♻️ Proposed refactor
 /** Queue of [status, body] pairs, consumed in order. Records every call. */
 let queue = [];
 const calls = [];
 
-globalThis.fetch = async (url, init = {}) => {
-  calls.push({ url: String(url), method: init.method ?? "GET", init });
-  const next = queue.shift();
-  if (!next) throw new Error(`unexpected request: ${url}`);
-  const [status, body] = next;
-  return {
-    ok: status >= 200 && status < 300,
-    status,
-    json: async () => body,
-    text: async () => JSON.stringify(body),
-  };
-};
-
 const authHeader = (call) =>
   new Headers(call.init.headers ?? {}).get("authorization");
 
 const queuedFetch = async (url, init = {}) => {

Also applies to: 42-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/attendance-portal.test.mjs` around lines 26 - 37, Remove the
module-level globalThis.fetch stub and retain the fetch implementation assigned
through queuedFetch in beforeEach. Ensure all tests continue using the single
shared stub without changing its request tracking or queued-response behavior.
src/app/settings.tsx (1)

14-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider moving PortalChallenge to types/index.ts.

PortalChallenge mirrors the challenge kinds returned by portal.login, so it is a shared domain type consumed by both the settings screen and the modal. It currently lives in a component file. The coding guidelines require shared domain types to come from types/index.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/settings.tsx` around lines 14 - 17, Move the shared PortalChallenge
type from the portal-connect-modal module into types/index.ts, then update the
settings screen and modal imports to use the centralized type. Preserve the
existing PortalChallenge shape and behavior while removing the component-local
definition.

Source: Coding guidelines

src/stores/attendance-store.test.mjs (1)

200-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for a forced fetch that overlaps a pending fetch.

Both tests here run sequentially or without a force overlap. The store replaces inFlightFetch on a forced call, and the earlier run then clears the shared reference. A test that starts a non-forced fetch, starts a forced fetch before the first settles, and then asserts the guard still deduplicates a third call would cover that path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/attendance-store.test.mjs` around lines 200 - 227, Add a test
alongside the existing fetch concurrency tests that starts a non-forced fetch,
initiates a forced fetch before the first request settles, then invokes a third
non-forced fetch while the forced request is pending. Assert the third call
shares the current in-flight operation rather than starting another portal
fetch, and verify the expected portal call count after all promises resolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Around line 417-419: Move the live LMS scripts currently named test-* under
src/scripts/ outside Node’s default test discovery patterns, then update
AGENTS.md lines 417-419 and docs/attendance-portal.md lines 199-201 to document
the new safe layout and commands. Ensure both documents no longer imply that
these scripts are safely handled by the test glob while preserving the
distinction between *.test.mjs files and operational LMS scripts.

In `@docs/attendance-portal-recon.md`:
- Line 16: Update the attendance portal documentation link in
attendance-portal-recon.md to reference the existing docs/attendance-portal.md
file instead of the unavailable attendance-portal-migration-plan.md target.
- Line 5: Update the status statements in the attendance portal reconstruction
document, including the repeated line, to acknowledge the live authenticated
confirmations documented later, while preserving any genuinely unverified areas.
Reconcile the byCourse schema with src/types/attendance.ts and the adapter test
by marking percentage optional and including dlCredited, and ensure the related
confirmed/open-question descriptions remain consistent.

In `@docs/attendance-portal.md`:
- Around line 34-36: Update docs/attendance-portal.md lines 34-36 and AGENTS.md
lines 304-308 to state narrowly that parsing, inference, conflict resolution,
and ICS formats remain compatible, while noting the portal-specific bunk-store
semester filter; remove any claim that the entire downstream pipeline is
unchanged.
- Around line 17-20: Add a language identifier to every fenced code block in
docs/attendance-portal.md, including the blocks around the Moodle/Portal diagram
and the additional referenced sections. Use text for plain diagrams or a more
precise identifier where appropriate, while preserving the block contents.
- Around line 104-115: Update fetchPortalAttendance’s unchanged-course cache
check to also compare the summary’s dlCredited value with the cached course
record before skipping its request, keeping the cache reuse path only when all
three values are unchanged; otherwise fetch fresh attendance data.
- Around line 73-78: Update the SecureStore documentation to scope
keychainAccessible: WHEN_UNLOCKED_THIS_DEVICE_ONLY and its
locked-device/off-device-backup guarantees to iOS only. Add documented or tested
evidence for Android backup exclusion before claiming Android credentials are
protected from off-device backups, and retain the existing lms_credentials
migration caveat.
- Line 121: Update the fetchAttendance flow so force: true still reuses the
existing in-flight promise instead of starting another portal request, while
continuing to bypass cached results and clear cached courses after the forced
fetch completes.

In `@src/app/settings.tsx`:
- Around line 188-198: Update handlePortalDisconnectConfirm to catch
disconnectPortal failures, preserve the busy-state cleanup, and surface the
error through the app toast system so it remains visible when PortalConnectModal
is closed. Keep the success-only state updates (setIsPortalConnected(false) and
clearAttendance()) unchanged.
- Around line 389-402: Update the portal connection state handling around
PortalSettingsSection so isPortalConnected is refreshed when portalDisconnected
transitions to true, rather than only during the mount effect. Ensure
needsReconnect becomes true and the UI shows the reconnect action while the
screen remains mounted.

In `@src/components/modals/portal-connect-modal.tsx`:
- Around line 95-97: Update the close-control Pressable in the portal connect
modal to include accessibilityRole="button" and accessibilityLabel="Close",
while preserving its existing onClose handler, hitSlop, and icon rendering.

In `@src/components/settings/portal-settings-section.tsx`:
- Around line 34-69: Update the conditional rendering in the portal settings
section so needsReconnect takes priority over isConnected for both the action
row and the explanatory caption. Ensure the reconnect label, warning icon,
danger state, and signed-out message render whenever needsReconnect is true,
even if isConnected is also true; otherwise preserve the existing connected and
disconnected behavior.

In `@src/services/attendance-portal-adapter.ts`:
- Around line 81-91: Update parseSessionDate to validate the parsed month and
day ranges before constructing the Date, rejecting months outside 1–12 and days
outside 1–31 so malformed values return null. Preserve the existing invalid-Date
check and ensure formatMoodleDate receives null for these inputs, allowing the
caller to drop the row.

In `@src/services/attendance-portal.test.mjs`:
- Around line 55-61: Reset the shared writeOptions collection in the beforeEach
test setup alongside queue, calls, and vault. Ensure each test’s assertions in
the writeOptions checks inspect only entries produced by that test.

In `@src/services/attendance-portal.ts`:
- Around line 121-139: Validate every authentication response through one
narrow, non-any parser before use, and have persistSession, login,
completeChallenge, and performRefresh consume its validated result. Require
access to be a string, require intermediate when constructing the needs2fa
result, and preserve the existing handling for optional refresh and other
fields; reject malformed bodies before mutating tokens, storing credentials, or
continuing the authentication flow.

In `@src/stores/attendance-store.ts`:
- Line 113: Update src/stores/attendance-store.ts lines 113-113: in
fetchAttendance, declare usingPortal before the try block and move the
portal.hasPortalCredentials() await inside it so the catch always handles
credential-read failures and clears isLoading. Update src/app/settings.tsx lines
137-145: invoke fetchAttendance after the handlePortalConnect and
handlePortalSubmitCode try/catch blocks, or isolate the await in
finishPortalConnect with its own try/catch, so refresh failures cannot be
reported as sign-in failures.
- Around line 100-101: Update the inFlightFetch cleanup in fetchAttendance so an
earlier request’s finally callback clears the reference only when inFlightFetch
still points to that same promise. Preserve forced-fetch replacement and normal
deduplication by comparing promise identity before assigning null.

In `@src/stores/bunk-store.test.mjs`:
- Around line 22-27: Update the julySession fixture and the related sessions
around the second test to derive dates from the current date (or use mock.timers
to fix the clock), rather than hardcoding July 2026. Keep the generated dates
within the semester window expected by evaluateCoursesAgainstCurrentSemester so
the test remains deterministic across run dates.

---

Nitpick comments:
In `@docs/attendance-portal-recon.md`:
- Line 47: Add the text language identifier to the fenced code blocks in the
attendance portal reconciliation documentation, specifically the endpoint list
and status enum blocks around the referenced sections, so both fences satisfy
markdownlint MD040.

In `@src/app/settings.tsx`:
- Around line 14-17: Move the shared PortalChallenge type from the
portal-connect-modal module into types/index.ts, then update the settings screen
and modal imports to use the centralized type. Preserve the existing
PortalChallenge shape and behavior while removing the component-local
definition.

In `@src/services/attendance-portal.test.mjs`:
- Around line 364-378: Update the test around portal.fetchPortalAttendance to
use fake timers and a fetch stub that remains pending long enough for the
configured timeout, then advance the timers and assert that the captured signal
is aborted. Preserve the existing signal-presence assertion and restore timers
after the test; only rename the test instead if abort behavior cannot be
exercised.
- Around line 26-37: Remove the module-level globalThis.fetch stub and retain
the fetch implementation assigned through queuedFetch in beforeEach. Ensure all
tests continue using the single shared stub without changing its request
tracking or queued-response behavior.

In `@src/services/attendance-portal.ts`:
- Around line 63-72: Update timeoutSignal and its fetch callers to retain and
clear the fallback timer when each request settles, including successful and
failed requests. Preserve the native AbortSignal.timeout path, and ensure the
fallback controller’s timer is cancelled after the request completes.

In `@src/stores/attendance-store.test.mjs`:
- Around line 200-227: Add a test alongside the existing fetch concurrency tests
that starts a non-forced fetch, initiates a forced fetch before the first
request settles, then invokes a third non-forced fetch while the forced request
is pending. Assert the third call shares the current in-flight operation rather
than starting another portal fetch, and verify the expected portal call count
after all promises resolve.

In `@src/stores/auth-store.test.mjs`:
- Around line 61-67: Update the logout test to set useAuthStore’s isLoggedIn
state to true before invoking logout, so the assertion verifies that logout
resets it. Extend the test setup’s beforeEach to reset the auth store state in
addition to moodleLoggedOut and portalDisconnected, preventing state leakage
between tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 713c899e-baa5-46b9-bb24-5706839ad2e5

📥 Commits

Reviewing files that changed from the base of the PR and between 5331b73 and e7faaf1.

📒 Files selected for processing (28)
  • AGENTS.md
  • docs/attendance-portal-recon.md
  • docs/attendance-portal.md
  • package.json
  • src/app/(tabs)/timetable.tsx
  • src/app/settings.tsx
  • src/components/attendance/sub_tabs/all-bunks-content.tsx
  • src/components/attendance/sub_tabs/courses-content.tsx
  • src/components/modals/index.ts
  • src/components/modals/portal-connect-modal.tsx
  • src/components/settings/index.ts
  • src/components/settings/portal-settings-section.tsx
  • src/scripts/test-setup.mjs
  • src/scripts/test-stub.mjs
  • src/services/attendance-portal-adapter.test.mjs
  • src/services/attendance-portal-adapter.ts
  • src/services/attendance-portal.test.mjs
  • src/services/attendance-portal.ts
  • src/stores/attendance-store.test.mjs
  • src/stores/attendance-store.ts
  • src/stores/auth-store.test.mjs
  • src/stores/auth-store.ts
  • src/stores/bunk-store.test.mjs
  • src/stores/bunk-store.ts
  • src/stores/faculty-store.ts
  • src/stores/storage.ts
  • src/types/attendance.ts
  • todo.md

Comment thread AGENTS.md
Comment on lines +417 to +419
Test files are named `*.test.mjs`. Note the scripts below are named `test-*.mjs` and also
match `node --test`'s default glob, so `npm test` scopes explicitly -- a bare
`node --test` would execute them against the live LMS with real credentials.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^AGENTS\.md$|^docs/attendance-portal\.md$|test-.*\.mjs|.*\.test\.mjs$)' || true

echo "== package scripts =="
if [ -f package.json ]; then
  node -e "const p=require('./package.json'); console.log(JSON.stringify(p.scripts,null,2))"
fi

echo "== matching Node default test tests files references =="
rg -n "node --test|default glob|\\.test\\.mjs|test-.*\\.mjs|default test" AGENTS.md docs/attendance-portal.md docs 2>/dev/null || true

echo "== candidate migration scripts =="
find . -path './node_modules' -prune -o -name 'test-[!.]*.mjs' -print -o -path './.git' -prune -o -name '*.test.mjs' -print 2>/dev/null | sort

Repository: Noelithub77/bunkialo2

Length of output: 3507


🌐 Web query:

Node.js node --test default glob pattern test files

💡 Result:

When you run the Node.js test runner using the --test flag without specifying explicit files or patterns, it automatically searches the current directory and its subdirectories for files matching the following default glob patterns [1]: - /.test.{cjs,mjs,js} - /-test.{cjs,mjs,js} - /_test.{cjs,mjs,js} - /test-.{cjs,mjs,js} - /test.{cjs,mjs,js} - /test//.{cjs,mjs,js} If you are using TypeScript or have enabled strip-types support (e.g., via --no-strip-types, which is default in some versions, or specific configuration), the runner also includes corresponding patterns for TypeScript files [1][2]: - /.test.{cts,mts,ts} - /-test.{cts,mts,ts} - /_test.{cts,mts,ts} - /test-.{cts,mts,ts} - /test.{cts,mts,ts} - /test//.{cts,mts,ts} Important usage notes: - Custom Glob Patterns: You can provide your own glob patterns as final arguments to the command (e.g., node --test "/.spec.js"). These follow glob(7) behavior [1]. - Quote Your Patterns: When passing glob patterns via the command line, always enclose them in double quotes (e.g., "/.test.js"). This prevents your shell from performing glob expansion before the command receives the patterns, which ensures cross-platform consistency and prevents the command from failing in environments like npm scripts [1][3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Node availability =="
node -v 2>/dev/null || true

echo "== Node default discovery sample =="
mkdir -p /tmp/node-test-glob-sample/pkg
cat >/tmp/node-test-glob-sample/pkg/should-not-run.mjs <<'EOF'
console.log('should not run');
EOF
cat >/tmp/node-test-glob-sample/pkg/test-would-run.mjs <<'EOF'
console.log('would run');
EOF
cd /tmp/node-test-glob-sample/pkg
node --test --list || true

echo "== inspect live LMS access references in candidate scripts =="
rg -n "LMS|credentials|secret|token|assign.*submit|playwright|download" src/scripts/test-* src/scripts/test-setup.mjs src scripts 2>/dev/null || true

Repository: Noelithub77/bunkialo2

Length of output: 315


Move live LMS scripts out of Node’s default test file globs.

test-* files live in src/scripts/, and node --test discovers them as tests. If a developer runs a bare node --test, these network-dependent scripts can execute against the live LMS. Rename or relocate them outside the default test file patterns, then update the instructions in AGENTS.md and docs/attendance-portal.md to describe the safe layout.

[high_effort_and_high rewarded]

📍 Affects 2 files
  • AGENTS.md#L417-L419 (this comment)
  • docs/attendance-portal.md#L199-L201
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 417 - 419, Move the live LMS scripts currently named
test-* under src/scripts/ outside Node’s default test discovery patterns, then
update AGENTS.md lines 417-419 and docs/attendance-portal.md lines 199-201 to
document the new safe layout and commands. Ensure both documents no longer imply
that these scripts are safely handled by the test glob while preserving the
distinction between *.test.mjs files and operational LMS scripts.

Source: Coding guidelines


**Target**: `https://attendance.iiitkottayam.ac.in`
**Date**: 2026-08-01
**Status**: Recon complete. No live authenticated response captured.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the status line; it contradicts the confirmed sections.

Line 5 states that no live authenticated response was captured. Line 32 repeats it. But line 109 states the top-level attendance keys were "confirmed live 2026-08-01", line 147 states SessionRecord was "Confirmed against a live response, 2026-08-01", and line 247 marks open question 1 as answered on that date. A reader who trusts the header will treat confirmed field names as inferred.

Also reconcile the byCourse block at lines 118-125 with src/types/attendance.ts: the type marks percentage optional and adds dlCredited, and the adapter test at line 200 states the live keys include dlCredited.

📝 Proposed documentation fix
-**Status**: Recon complete. No live authenticated response captured.
+**Status**: Recon complete. Bundle-derived, with the attendance summary and
+`SessionRecord` shapes confirmed against a live authenticated response on 2026-08-01.
   byCourse: {
     courseId: string;
     courseCode: string;
     courseName: string;
     present: number;
     total: number;
-    percentage: number;   // rendered with .toFixed(1)
+    dlCredited?: number;  // duty-leave sessions credited as attended
+    percentage?: number;  // rendered with .toFixed(1); not always present
   }[];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/attendance-portal-recon.md` at line 5, Update the status statements in
the attendance portal reconstruction document, including the repeated line, to
acknowledge the live authenticated confirmations documented later, while
preserving any genuinely unverified areas. Reconcile the byCourse schema with
src/types/attendance.ts and the adapter test by marking percentage optional and
including dlCredited, and ensure the related confirmed/open-question
descriptions remain consistent.

Source: Linters/SAST tools

Bunkialo derives its timetable from Moodle attendance records
(`utils/timetable-inference.ts`), so the timetable, bunk tracking and attendance
percentage all lose their data source. See
[attendance-portal-migration-plan.md](./attendance-portal-migration-plan.md) for the work.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify every relative Markdown link inside docs/ resolves to a real file.
fd -e md . docs | while IFS= read -r doc; do
  dir=$(dirname "$doc")
  rg -oN '\]\(\.{1,2}/[^)#]+' "$doc" | sed 's/^](//' | while IFS= read -r target; do
    resolved="$dir/$target"
    [ -e "$resolved" ] || echo "MISSING: $doc -> $target"
  done
done

Repository: Noelithub77/bunkialo2

Length of output: 241


Fix the missing attendance-portal-migration-plan.md link.

The link target does not exist, so readers will hit a broken docs page. Point it at docs/attendance-portal.md if the migration-plan file is unavailable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/attendance-portal-recon.md` at line 16, Update the attendance portal
documentation link in attendance-portal-recon.md to reference the existing
docs/attendance-portal.md file instead of the unavailable
attendance-portal-migration-plan.md target.

Comment thread docs/attendance-portal.md
Comment on lines +17 to +20
```
Moodle ──> assignments, timeline, resources, faculty, dashboard "Upcoming"
Portal ──> attendance, bunks, timetable
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to the fenced blocks.

markdownlint-cli2 reports MD040 for these four fences. Use text or a more precise language on each opening fence.

Also applies to: 38-40, 65-71, 96-99

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/attendance-portal.md` around lines 17 - 20, Add a language identifier to
every fenced code block in docs/attendance-portal.md, including the blocks
around the Moodle/Portal diagram and the additional referenced sections. Use
text for plain diagrams or a more precise identifier where appropriate, while
preserving the block contents.

Source: Linters/SAST tools

Comment thread docs/attendance-portal.md
Comment on lines +34 to +36
So the adapter emits Moodle-style date strings and the entire downstream pipeline is
untouched. Inference, clustering, conflict resolution, bunk merge and ICS export took
**zero changes**.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Describe the unchanged contract narrowly in both documents.

The date-format-dependent consumers remain compatible, but the portal path adds a source-specific bunk-store semester filter.

  • docs/attendance-portal.md#L34-L36: state that parsing, inference, conflict resolution, and ICS formats remain compatible without claiming that the entire downstream pipeline is unchanged.
  • AGENTS.md#L304-L308: make the same distinction in the project implementation flow.
📍 Affects 2 files
  • docs/attendance-portal.md#L34-L36 (this comment)
  • AGENTS.md#L304-L308
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/attendance-portal.md` around lines 34 - 36, Update
docs/attendance-portal.md lines 34-36 and AGENTS.md lines 304-308 to state
narrowly that parsing, inference, conflict resolution, and ICS formats remain
compatible, while noting the portal-specific bunk-store semester filter; remove
any claim that the entire downstream pipeline is unchanged.

Comment on lines +55 to +61
beforeEach(async () => {
queue = [];
calls.length = 0;
vault.clear();
globalThis.fetch = queuedFetch;
await portal.__resetForTests();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reset writeOptions in beforeEach.

beforeEach clears queue, calls and vault, but not writeOptions. So the array accumulates every write from every earlier test. Two effects: the loop at line 95 asserts on options that the current test did not produce, and assert.ok(writeOptions.length >= 2) can pass from stale entries even if this login wrote nothing. A failure then points at the wrong test.

💚 Proposed fix
 beforeEach(async () => {
   queue = [];
   calls.length = 0;
+  writeOptions.length = 0;
   vault.clear();
   globalThis.fetch = queuedFetch;
   await portal.__resetForTests();
 });

Also applies to: 88-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/attendance-portal.test.mjs` around lines 55 - 61, Reset the
shared writeOptions collection in the beforeEach test setup alongside queue,
calls, and vault. Ensure each test’s assertions in the writeOptions checks
inspect only entries produced by that test.

Comment on lines +121 to +139
const persistSession = async (
data: { access: string; refresh?: string },
credentials?: Credentials,
): Promise<void> => {
accessToken = data.access;
// A refresh response need not rotate the token. Writing undefined would throw
// on device, and clearing it would lock the user out on the next cold start.
if (data.refresh) {
await SecureStore.setItemAsync(REFRESH_KEY, data.refresh, KEYCHAIN_OPTIONS);
}
if (credentials) {
await SecureStore.setItemAsync(
CREDENTIALS_KEY,
JSON.stringify(credentials),
KEYCHAIN_OPTIONS,
);
}
debug.scraper("Portal session established");
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the auth response body before you persist it.

await response.json() is typed any, so every field read after it is unchecked. persistSession then assigns data.access to accessToken without proof that it is a string. Three consequences:

  1. A 200 response with an unexpected body sets accessToken to undefined. authedGet then sends no Authorization header, receives 401, runs refreshAccess, and can reach disconnectPortal, which deletes the stored credentials. A single malformed success response signs the user out.
  2. login returns { kind: "needs2fa", intermediate: data.intermediate } even when intermediate is missing. The settings screen stores undefined and the later submitTotp call posts an undefined intermediate.
  3. The project guidelines forbid any.

Add one narrow parser and route all three call sites through it.

🛡️ Proposed fix: parse the response before use
+type PortalSessionPayload = { access: string; refresh?: string };
+
+const parseSessionPayload = (data: unknown): PortalSessionPayload => {
+  const body = data as Record<string, unknown> | null;
+  const access = body?.access;
+  const refresh = body?.refresh;
+  if (typeof access !== "string" || access.length === 0) {
+    throw new PortalError(502, "Portal returned no access token");
+  }
+  return {
+    access,
+    refresh: typeof refresh === "string" && refresh ? refresh : undefined,
+  };
+};
+
+type PortalChallengePayload = {
+  needs2fa?: boolean;
+  needsEmailOtp?: boolean;
+  intermediate?: unknown;
+};
+
 /** Never logs the token or the password. */
 const persistSession = async (
-  data: { access: string; refresh?: string },
+  data: PortalSessionPayload,
   credentials?: Credentials,
 ): Promise<void> => {
-  const data = await response.json();
+  const data = (await response.json()) as PortalChallengePayload;
 
   if (data.needs2fa || data.needsEmailOtp) {
+    if (typeof data.intermediate !== "string" || !data.intermediate) {
+      throw new PortalError(502, "Portal returned no challenge token");
+    }
     pendingCredentials = { username: email, password };
     return {
       kind: data.needs2fa ? "needs2fa" : "needsEmailOtp",
       intermediate: data.intermediate,
     };
   }
 
-  await persistSession(data, { username: email, password });
+  await persistSession(parseSessionPayload(data), {
+    username: email,
+    password,
+  });
   return { kind: "success" };

Apply the same treatment in completeChallenge (line 174) and performRefresh (line 214):

-  const data = await response.json();
-  await persistSession(data, pendingCredentials ?? undefined);
+  const data = parseSessionPayload(await response.json());
+  await persistSession(data, pendingCredentials ?? undefined);

As per coding guidelines: "Use TypeScript strict mode, never use any".

Also applies to: 150-160, 173-174, 212-214

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/attendance-portal.ts` around lines 121 - 139, Validate every
authentication response through one narrow, non-any parser before use, and have
persistSession, login, completeChallenge, and performRefresh consume its
validated result. Require access to be a string, require intermediate when
constructing the needs2fa result, and preserve the existing handling for
optional refresh and other fields; reject malformed bodies before mutating
tokens, storing credentials, or continuing the authentication flow.

Source: Coding guidelines

Comment on lines 100 to +101
fetchAttendance: async (options) => {
if (inFlightFetch && !options?.force) return inFlightFetch;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Guard the in-flight reference by promise identity.

A forced fetch replaces inFlightFetch while an earlier run is still pending. When that earlier run settles, its finally callback sets inFlightFetch = null, although the reference now points to the forced run. The dedup guard then releases early, so a concurrent burst can start an extra 1 + N portal request set.

Compare the identity before clearing.

🐛 Proposed fix to clear only the current promise
-        inFlightFetch = run().finally(() => {
-          inFlightFetch = null;
-        });
-        return inFlightFetch;
+        const current = run().finally(() => {
+          if (inFlightFetch === current) inFlightFetch = null;
+        });
+        inFlightFetch = current;
+        return current;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/attendance-store.ts` around lines 100 - 101, Update the
inFlightFetch cleanup in fetchAttendance so an earlier request’s finally
callback clears the reference only when inFlightFetch still points to that same
promise. Preserve forced-fetch replacement and normal deduplication by comparing
promise identity before assigning null.

} else {
set({ isLoading: true, error: null });
}
const usingPortal = await portal.hasPortalCredentials();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

fetchAttendance can reject, and the portal connect flow reports that as a sign-in failure. The shared root cause is the credential read at Line 113 of src/stores/attendance-store.ts, which sits outside the try block, so a secure-storage rejection escapes run() and rejects the promise returned by fetchAttendance.

  • src/stores/attendance-store.ts#L113-L113: move the portal.hasPortalCredentials() await inside the try block and declare usingPortal before it, so the catch block always handles the failure and clears isLoading.
  • src/app/settings.tsx#L137-L145: call fetchAttendance() after the try/catch of handlePortalConnect and handlePortalSubmitCode, or wrap the await fetchAttendance() inside finishPortalConnect in its own try/catch. A refresh failure must not produce the message "Could not sign in. Check your email and password." after the credentials are stored.
📍 Affects 2 files
  • src/stores/attendance-store.ts#L113-L113 (this comment)
  • src/app/settings.tsx#L137-L145
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/attendance-store.ts` at line 113, Update
src/stores/attendance-store.ts lines 113-113: in fetchAttendance, declare
usingPortal before the try block and move the portal.hasPortalCredentials()
await inside it so the catch always handles credential-read failures and clears
isLoading. Update src/app/settings.tsx lines 137-145: invoke fetchAttendance
after the handlePortalConnect and handlePortalSubmitCode try/catch blocks, or
isolate the await in finishPortalConnect with its own try/catch, so refresh
failures cannot be reported as sign-in failures.

Comment on lines +22 to +27
const julySession = (i) => ({
date: `Fri ${24 + i} Jul 2026 11:30AM - 1:30PM`,
description: "",
status: "Present",
points: "",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Derive the session dates from the current date instead of hardcoding July 2026.

evaluateCoursesAgainstCurrentSemester computes the semester window from the current date. The fixture dates are absolute (July 2026), so the outcome of the second test depends on when the suite runs. In a run inside the Aug–Nov 2026 window the July sessions are outside the window and the course is auto-dropped. In a run during July 2026, or after the next window rolls over, the classification changes and the assertion can fail.

Compute the dates relative to new Date(), or install a fake clock with mock.timers, so the test stays deterministic.

Also applies to: 58-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/bunk-store.test.mjs` around lines 22 - 27, Update the julySession
fixture and the related sessions around the second test to derive dates from the
current date (or use mock.timers to fix the clock), rather than hardcoding July
2026. Keep the generated dates within the semester window expected by
evaluateCoursesAgainstCurrentSemester so the test remains deterministic across
run dates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant