feat: read attendance from the IIIT Kottayam attendance portal - #23
feat: read attendance from the IIIT Kottayam attendance portal#23moneytosms wants to merge 15 commits into
Conversation
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>
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe 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. ChangesAttendance Portal integration
Estimated code review effort: 4 (Complex) | ~60 minutes Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 18
🧹 Nitpick comments (7)
docs/attendance-portal-recon.md (1)
47-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a language to these fenced blocks.
markdownlint reports MD040 for the fences at lines 47 and 207. Use
textfor 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 valueClear the fallback timer after the request settles.
The fallback path creates a
setTimeoutthat 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 winSet the store to a logged-in state before you call
logout.
isLoggedInis alreadyfalsein the initial store state, so the assertion at line 66 passes even iflogoutdoes not reset it. SetisLoggedIntotruefirst, so the assertion proves the reset. Also reset the store state inbeforeEach;beforeEachcurrently 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 valueThis test does not verify the abort.
The stub resolves immediately, so the only assertions are that a signal object exists and that
abortedis 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 winRemove the duplicated fetch stub.
Lines 26-37 and lines 42-53 hold the same body.
beforeEachassignsqueuedFetch, so the module-level assignment at line 26 never serves a test. KeepqueuedFetchonly. 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 winConsider moving
PortalChallengetotypes/index.ts.
PortalChallengemirrors the challenge kinds returned byportal.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 fromtypes/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 winAdd a test for a forced fetch that overlaps a pending fetch.
Both tests here run sequentially or without a force overlap. The store replaces
inFlightFetchon 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
📒 Files selected for processing (28)
AGENTS.mddocs/attendance-portal-recon.mddocs/attendance-portal.mdpackage.jsonsrc/app/(tabs)/timetable.tsxsrc/app/settings.tsxsrc/components/attendance/sub_tabs/all-bunks-content.tsxsrc/components/attendance/sub_tabs/courses-content.tsxsrc/components/modals/index.tssrc/components/modals/portal-connect-modal.tsxsrc/components/settings/index.tssrc/components/settings/portal-settings-section.tsxsrc/scripts/test-setup.mjssrc/scripts/test-stub.mjssrc/services/attendance-portal-adapter.test.mjssrc/services/attendance-portal-adapter.tssrc/services/attendance-portal.test.mjssrc/services/attendance-portal.tssrc/stores/attendance-store.test.mjssrc/stores/attendance-store.tssrc/stores/auth-store.test.mjssrc/stores/auth-store.tssrc/stores/bunk-store.test.mjssrc/stores/bunk-store.tssrc/stores/faculty-store.tssrc/stores/storage.tssrc/types/attendance.tstodo.md
| 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. |
There was a problem hiding this comment.
🔒 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 | sortRepository: 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:
- 1: https://nodejs.org/api/test.html
- 2: https://nodejs.org/docs/v22.11.0/api/test.html
- 3: node:test --test with glob patterns and npm can't find files nodejs/node#50658
- 4: https://blog.appsignal.com/2024/07/17/exploring-the-nodejs-native-test-runner.html
🏁 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 || trueRepository: 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. |
There was a problem hiding this comment.
📐 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. |
There was a problem hiding this comment.
📐 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
doneRepository: 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.
| ``` | ||
| Moodle ──> assignments, timeline, resources, faculty, dashboard "Upcoming" | ||
| Portal ──> attendance, bunks, timetable | ||
| ``` |
There was a problem hiding this comment.
📐 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
| 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**. |
There was a problem hiding this comment.
📐 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.
| beforeEach(async () => { | ||
| queue = []; | ||
| calls.length = 0; | ||
| vault.clear(); | ||
| globalThis.fetch = queuedFetch; | ||
| await portal.__resetForTests(); | ||
| }); |
There was a problem hiding this comment.
📐 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.
| 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"); | ||
| }; |
There was a problem hiding this comment.
🗄️ 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:
- A 200 response with an unexpected body sets
accessTokentoundefined.authedGetthen sends noAuthorizationheader, receives 401, runsrefreshAccess, and can reachdisconnectPortal, which deletes the stored credentials. A single malformed success response signs the user out. loginreturns{ kind: "needs2fa", intermediate: data.intermediate }even whenintermediateis missing. The settings screen storesundefinedand the latersubmitTotpcall posts an undefined intermediate.- 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
| fetchAttendance: async (options) => { | ||
| if (inFlightFetch && !options?.force) return inFlightFetch; |
There was a problem hiding this comment.
🚀 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(); |
There was a problem hiding this comment.
🩺 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 theportal.hasPortalCredentials()await inside thetryblock and declareusingPortalbefore it, so the catch block always handles the failure and clearsisLoading.src/app/settings.tsx#L137-L145: callfetchAttendance()after thetry/catchofhandlePortalConnectandhandlePortalSubmitCode, or wrap theawait fetchAttendance()insidefinishPortalConnectin its owntry/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.
| const julySession = (i) => ({ | ||
| date: `Fri ${24 + i} Jul 2026 11:30AM - 1:30PM`, | ||
| description: "", | ||
| status: "Present", | ||
| points: "", | ||
| }); |
There was a problem hiding this comment.
🎯 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.
Why
IIIT Kottayam retired Moodle's
mod/attendanceand moved attendance tohttps://attendance.iiitkottayam.ac.in. Courses are still enrolled on Moodle, so Moodlestays 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[]onevery 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.dateis regex-parsed in 16 call sites across 11 files. Most failsilently on a format change; the worst is
isPastOrCompletedreturningfalseuniversally, which makes
filterPastBunksdiscard every bunk with no error.So the adapter emits Moodle-style date strings (
Thu 1 Jan 2026 9:00AM - 9:55AM) ratherthan structured fields.
timetable-inference.ts,timetable-store.ts,bunk-store'smerge 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 storageservices/attendance-portal-adapter.ts— payload toCourseAttendance; pure, type-onlyimports so it loads under
node --testwithout an Expo runtimecomponents/settings/portal-settings-section.tsx,components/modals/portal-connect-modal.tsxsrc/scripts/test-setup.mjs,test-stub.mjs— test harness behindnpm testdocs/attendance-portal.md,docs/attendance-portal-recon.md,todo.mdChanged
stores/attendance-store.ts— source selection, empty-scrape guard, in-flightde-duplication, change detection, reconnect flag
stores/bunk-store.ts— semester auto-drop skipped for portal coursesstores/auth-store.ts— logout now clears the portal tooapp/settings.tsx,types/attendance.ts,AGENTS.md,package.jsonNotable fixes found along the way
attendance, bunks and timetable. Now guarded.
two accounts the app holds.
semester-course-filter.tshardcodesAug–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.
2026-07-31T00:00:00.000Z. Parsing that throughlocal 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-DDprefix.coursesand bumpedlastSyncTimeeven whennothing changed, and
attendance.tsxwatcheslastSyncTimeto runsyncFromLmsthengenerateTimetable. Every navigation triggered a full recompute.Security
expo-secure-storewithkeychainAccessible: WHEN_UNLOCKED_THIS_DEVICE_ONLY.Credentialstype, same shapeservices/auth.tswrites.login never leaves a password on disk.
invalidate each other and lock the user out.
lms_credentialsentry: changing the accessibility classinvalidates 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 steadystate to 1 request. Pull to refresh passes
forceand bypasses all three. Everyrequest has a 30s timeout;
fetchhas none by default.Testing
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.registerHooksand native type stripping.Test files are
*.test.mjs; the existing integration scripts aretest-*.mjsand alsomatch
node --test's default glob, sonpm testscopes explicitly — a barenode --testwould 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 mayshow a percentage differing from the website. Needs a real example before guessing at
the weighting.
unique.
Note for review
The first commit (
fix: import zustand and fuse.js types as types) is unrelated to thefeature and split out deliberately:
StateStorageandIFuseOptionsare types importedas values, which Metro tolerates but Node's type stripping does not. Behaviour is
unchanged.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FNXAGPFfZxaD8rosWmhzCm