Repository navigation
Conversation
… dropdown closes all others
…ons for lag-free 60fps accordion
…an (Anti Mati Listrik), dan Audit Trail Log
… into Redis integration branch
…esiDicari loading state with async Redis recovery
…string, unknown> to pass CI lint
….ts for 100% clean CI typecheck
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe change replaces module and course entities with unit-owned topics, adds cohort-based exam access, introduces calculator and medical reference tools, and adds Redis-backed session storage. Academic routes, backups, seeds, authorization, repositories, and participant management are updated. ChangesAcademic model migration
Exam tools and sessions
Estimated code review effort: 5 (Critical) | ~90+ minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 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: 15
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/routes/_authenticated/admin.users.roles.tsx (1)
35-43: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRestrict
/admin/users/rolestosuper_adminand useunitsfor the new entity.
Loadercalls useauth-only_authenticatedwithout a role guard, whilegetUnitAcademikList()returns[]unlesscaller.role === "super_admin". Any non-super_admin who reaches/admin/users/rolesgets an empty unit list, and the topic group for each manager never renders. Add the role guard before the route, then also usegetUnitAkademikList()forgetUsersList(). Rename the loader binding and UI labels frommodulstounitsbecause the section now maps topics byunitAkademik, not modul.🤖 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/routes/_authenticated/admin.users.roles.tsx` around lines 35 - 43, Restrict the `/admin/users/roles` route with a `super_admin` role guard before its loader runs. Update `Loader` to obtain and pass the unit list from `getUnitAkademikList()` into `getUsersList()` as required, rename the `moduls` binding and all related UI labels/references to `units`, and keep topic grouping based on each manager’s `unitAkademik`.
🟡 Minor comments (7)
src/routes/_authenticated/admin.peserta.index.tsx-241-241 (1)
241-241: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUpdate the empty-state column span.
The table now has seven columns, but the empty-state cell at Line 292 still uses
colSpan={6}. Set it to7so the empty state spans the full table.Proposed fix
-<TableCell colSpan={6} className="h-48 text-center"> +<TableCell colSpan={7} className="h-48 text-center">Also applies to: 259-259
🤖 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/routes/_authenticated/admin.peserta.index.tsx` at line 241, Update the empty-state table cell’s colSpan from 6 to 7 in the table containing the “Tahun Angkatan” TableHead, including the corresponding occurrence noted near the other header, so the empty state spans all seven columns.src/lib/server/redis/service.ts-146-159 (1)
146-159: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winClamp the computed TTL before calling
SET ... EX.
durationMinutesreaches this method from the request body ofsetSessionTimerServerwithout validation. Redis rejects an expire value that is zero or negative with an error. AdurationMinutesof-60or lower producesdurationMinutes * 60 + 3600 <= 0, and theSETthrows. Thecatchblock is empty, so the call then writes the same non-positive TTL intosetFallbackon Line 158, whereDate.now() + ttl * 1000yields an already-expired entry. The timer is lost with no log line.Clamp the TTL and reject a non-finite
durationMinutes.🐛 Proposed fix
async setSessionTimer(sesiId: string, endsAt: number, durationMinutes: number): Promise<void> { const key = REDIS_KEYS.sessionTimer(sesiId); + const safeMinutes = Number.isFinite(durationMinutes) + ? Math.min(Math.max(durationMinutes, 1), 24 * 60) + : 60; + const ttlSeconds = safeMinutes * 60 + 3600; const data = JSON.stringify({ endsAt, durationMinutes, startedAt: Date.now() }); if (this.isConnected && this.redis) { try { - await this.redis.set(key, data, "EX", durationMinutes * 60 + 3600); + await this.redis.set(key, data, "EX", ttlSeconds); return; - } catch { - // fallback + } catch (err) { + console.warn("[Redis] Failed to set session timer, using fallback:", err); } } - this.setFallback(key, data, durationMinutes * 60 + 3600); + this.setFallback(key, data, ttlSeconds); }🤖 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/lib/server/redis/service.ts` around lines 146 - 159, Update setSessionTimer to reject non-finite durationMinutes and clamp the computed TTL to a positive minimum before using it with redis.set or setFallback. Reuse the same validated TTL for both storage paths so invalid durations cannot produce Redis errors or already-expired fallback entries.src/routes/_authenticated/admin.index.tsx-80-85 (1)
80-85: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse consistent Indonesian scale abbreviations.
The formatter mixes locales.
rbis the Indonesian abbreviation for ribu (thousand), butMis the English abbreviation for million. In Indonesian shorthand,Mreads as miliar (billion), and million isjt. The decimal separator is also a period instead of the Indonesian comma.🌐 Proposed fix for locale-consistent formatting
const formatNumber = (num: number) => { - if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`; - if (num >= 1000) return `${(num / 1000).toFixed(1)}rb`; - return num.toString(); + if (num >= 1_000_000) return `${(num / 1_000_000).toLocaleString("id-ID", { maximumFractionDigits: 1 })} jt`; + if (num >= 1_000) return `${(num / 1_000).toLocaleString("id-ID", { maximumFractionDigits: 1 })} rb`; + return num.toLocaleString("id-ID"); };🤖 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/routes/_authenticated/admin.index.tsx` around lines 80 - 85, Update formatNumber to use Indonesian abbreviations consistently: represent millions with “jt” and thousands with “rb”, and replace the decimal point with an Indonesian comma in formatted values. Preserve the existing one-decimal rounding and plain numeric output for values below one thousand.src/routes/_authenticated/admin.tsx-189-195 (1)
189-195: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe rule-key fallback highlights the wrong item when two items share one rule key.
/admin/pesertaand/admin/peserta/onlineboth resolve to thepesertarule key. For a pathname such as/admin/peserta/kartu, the exact/admin/pesertaitem returnsfalseat line 190, then the fallback at lines 192-194 marks "Pantau Ujian Live" as active. The dashboard "Kartu Peserta" shortcut links to that path, so the state is reachable.Restrict the fallback to items whose own path is a prefix of the pathname, or to the single longest-matching item in the group.
🐛 Proposed fix for the active-item fallback
function isNavItemActive(item: NavItem, pathname: string) { if (item.exact) return pathname === item.to; if (pathname === item.to || pathname.startsWith(`${item.to}/`)) return true; + // Only fall back to rule-key matching when the item is not a deeper sibling + // under a shared rule (e.g. /admin/peserta vs /admin/peserta/online). + if (!pathname.startsWith(`${item.to.split("/").slice(0, 3).join("/")}/`)) return false; const rule = resolveAdminRouteRule(pathname); const itemRule = resolveAdminRouteRule(item.to); return !!(rule && itemRule && itemRule.key === rule.key); }Prefer selecting the longest matching
item.toacross the group if that fits the navigation model better.🤖 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/routes/_authenticated/admin.tsx` around lines 189 - 195, Update isNavItemActive so the rule-key fallback cannot activate an item whose item.to is not a path prefix of pathname; require the item path to match as an exact path or directory prefix before applying the shared rule-key comparison. Preserve exact-item handling and prevent shorter same-rule entries such as /admin/peserta from competing with more specific paths.src/routes/_authenticated/admin.analitik.index.tsx-142-144 (1)
142-144: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRemove the duplicate exam name.
Line 139 already renders
u.nama. Lines 142-144 render the same value again. Remove this block or replace it with distinct metadata.🤖 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/routes/_authenticated/admin.analitik.index.tsx` around lines 142 - 144, Remove the duplicate u.nama rendering from the nearby h3 block in the relevant component, since the exam name is already rendered earlier. Leave the existing first rendering intact and only retain this block if it is replaced with distinct metadata.src/lib/server/backup/functions.ts-77-77 (1)
77-77: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winRemove the invalid type assertion for
angkatanIds.
stringifyJson()returnsstring, while theUjianPrisma field expectsString; the existing backup import other string-array fields the same way.Proposed fix
- angkatanIds: stringifyJson(item.angkatanIds) as unknown as string[], + angkatanIds: stringifyJson(item.angkatanIds),🤖 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/lib/server/backup/functions.ts` at line 77, Update the angkatanIds assignment in the backup Ujian import to remove the unknown/string[] type assertion and preserve the string returned by stringifyJson(item.angkatanIds), matching the handling of the other string-array fields.src/routes/_authenticated/admin.topik.$id.soal.tsx-30-30 (1)
30-30: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDo not block question management when the unit cannot be resolved.
Line 43 treats a missing
unitas a missing topic and reports "Topik tidak ditemukan". A topic whoseunitIddid not migrate then becomes unmanageable, and the message names the wrong cause. Line 339 already tolerates a missing unit in the move dialog. Gate only ontopik, and render the breadcrumb conditionally.🐛 Proposed fix
- if (!topik || !unit) return ( + if (!topik) return (Then guard the breadcrumb:
- <Link to="/admin/modul/$id/topik" params={{ id: unit.id }} className="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"> - {unit.nama} - </Link> + {unit && ( + <Link to="/admin/modul/$id/topik" params={{ id: unit.id }} className="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"> + {unit.nama} + </Link> + )}Also applies to: 43-48
🤖 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/routes/_authenticated/admin.topik`.$id.soal.tsx at line 30, Update the question-management route around the `unit` lookup and the topic-not-found guard so access is gated only by `topik`; a missing `unit` must not trigger the “Topik tidak ditemukan” response. Render the breadcrumb conditionally when `unit` is available, while preserving the existing missing-topic handling and downstream behavior.
🧹 Nitpick comments (5)
src/components/cbt/CheatSheets.tsx (1)
207-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove or implement the non-functional controls.
Inv,Keypad, andHistoryon Lines 209-217 aredivelements withcursor-pointerand no handler. TheAnsbutton on Line 259 has an emptyonClick. Each element looks interactive and does nothing. During an exam this costs the participant time.The
divelements are also not reachable by keyboard and have no role, so keyboard users get no feedback at all.Remove these controls, or implement them and convert the
divelements tobuttonelements.handleDeleteon Line 150 is also defined but never bound to a control.Also applies to: 259-259
🤖 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/components/cbt/CheatSheets.tsx` around lines 207 - 218, Remove the non-functional Inv, Keypad, History, and Ans controls, or implement their interactions fully. If retained, convert the interactive divs to accessible button elements with keyboard support and meaningful handlers, and bind the existing handleDelete function to the appropriate control instead of leaving it unused or exposing empty onClick behavior.src/routes/_authenticated/admin.index.tsx (1)
330-350: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDerive the telemetry values instead of hardcoding them.
SQLite Local,Prisma Client, andv1.2.0are static strings in the view. The version label will drift frompackage.jsonafter the next release, and the database label will be wrong if the deployment changes provider. Source these values from build-time constants or a server function.🤖 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/routes/_authenticated/admin.index.tsx` around lines 330 - 350, Update the System Environment Telemetry block in the admin page to use existing build-time constants or a server-provided configuration source for the database provider, ORM engine, and application version instead of hardcoded “SQLite Local”, “Prisma Client”, and “v1.2.0” values. Preserve the current labels and styling while ensuring the displayed values reflect the deployed configuration and package version.src/routes/_authenticated/admin.tsx (1)
339-342: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winUse the generated route-path union for admin navigation links.
NavItem["to"],pendingTasks[].route, andShortcutCard.hrefcurrently model valid admin paths as plainstring, so renamed or deleted links can pass type checks and still navigate to a nonexistent route. UseFileRouteTypes["fullPaths"]or a derived admin route union for these fields so link failures are caught at build time.🤖 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/routes/_authenticated/admin.tsx` around lines 339 - 342, The admin navigation types currently allow arbitrary strings, so invalid or removed routes bypass type checking. In src/routes/_authenticated/admin.tsx at lines 339-342 and src/routes/_authenticated/admin.index.tsx at lines 283 and 416-419, update NavItem["to"], pendingTasks[].route, and ShortcutCard.href to use FileRouteTypes["fullPaths"] or a derived admin route union, and remove the cast that bypasses Link route validation.src/lib/server/modul/functions.ts (1)
60-72: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winValidate the topik payload with
TopikSchemainstead of casting toany.
payloadisz.any(), and the casts remove the last type check.TopikSchemanow requiresunitId, so a request that still sendsmodulIdwrites a topic with no unit and produces orphan rows in the new model. Parse the payload before the transaction.♻️ Proposed validation
+ const topikList = + action === "bulkSet" + ? TopikSchema.array().parse(payload) + : null; + const topikItem = + action === "upsert" ? TopikSchema.parse(payload) : null; + await prisma.$transaction(async (tx) => { if (action === "remove") await tx.topik.delete({ where: { id: String(payload.id) } }); else if (action === "bulkSet") { await tx.topik.deleteMany(); - await tx.topik.createMany({ data: payload as any }); - } else - await tx.topik.upsert({ - where: { id: payload.id }, - update: payload as any, - create: payload as any, - }); + await tx.topik.createMany({ data: topikList! }); + } else + await tx.topik.upsert({ + where: { id: topikItem!.id }, + update: topikItem!, + create: topikItem!, + }); });Import
TopikSchemafrom@/lib/cbt/typesin addition to the existing type imports.🤖 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/lib/server/modul/functions.ts` around lines 60 - 72, Validate the topik payload with TopikSchema before entering the prisma.$transaction callback, replacing the any-based casts used by the remove, bulkSet, and upsert branches. Import TopikSchema from `@/lib/cbt/types` and use the parsed payload so unitId is required and legacy modulId-only requests cannot create orphan topics.src/routes/_authenticated/admin.modul.tsx (1)
35-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the unreachable branch in the
shownfilter.
canEditisisUnrestricted(user), andallowedTopikIdSetreturnsnullonly whenisUnrestricted(user)is true. Line 40 therefore already handles every case whereallowedSetisnull, and thereturn trueat line 45 is unreachable. ThetopikRepo.all()call also runs once per unit; hoist it out of the filter.♻️ Proposed simplification
+ const allTopiks = topikRepo.all(); const shown = units.filter(u => { const isQueryMatch = !query || u.nama.toLowerCase().includes(query.toLowerCase()); if (!isQueryMatch) return false; - - // Check if operator has access to any topik in this unit (or if unrestricted) - if (canEdit) return true; - const unitTopiks = topikRepo.all().filter((t) => t.unitId === u.id); - if (allowedSet) { - return unitTopiks.some(t => allowedSet.has(t.id)); - } - return true; + // Unrestricted operators see every unit. + if (!allowedSet) return true; + return allTopiks.some((t) => t.unitId === u.id && allowedSet.has(t.id)); });🤖 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/routes/_authenticated/admin.modul.tsx` around lines 35 - 46, Update the shown filter to remove the unreachable fallback return after the allowedSet check, relying on canEdit for unrestricted users and allowedSet membership otherwise. Hoist the topikRepo.all() result outside the units.filter callback, then reuse it when determining each unit’s accessible topics.
🤖 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 `@scratch/seed-clinical.ts`:
- Around line 8-21: Update the clinical seed setup to fetch the required
identifier from the UnitAkademik model instead of calling
prisma.modul.findFirst(), and rename the local identifier accordingly. In the
prisma.topik.create data, replace modulId with the Topik model’s unitId field
and pass the UnitAkademik identifier, preserving the existing missing-record
validation.
In `@src/components/cbt/CheatSheets.tsx`:
- Around line 83-131: Update evaluateMathExpression to allow and parse the ^
operator with appropriate power precedence, and reject any expression where pos
does not equal tokens.length after parseExpr completes. Resolve the keypad
conflict by changing the mod button’s behavior or label so it performs modulo
rather than the existing percent expansion, while preserving percent parsing for
percentage inputs.
In `@src/lib/cbt/access.ts`:
- Around line 87-101: Align visibleUjians with ujianTouchesAllowed by applying
the same every-topic authorization rule when filtering restricted users,
replacing the current some-based predicate while preserving unrestricted-user
behavior.
In `@src/lib/server/redis/service.ts`:
- Around line 16-31: Prevent the module-level Redis fallback cache from growing
indefinitely: in the service constructor, start a periodic sweep that removes
expired entries from memoryFallback and call unref() on the interval timer. Also
update clearSessionBuffer to delete the audit log key alongside the existing
answer and timer keys, while preserving current fallback expiration behavior in
setFallback and getFallback.
- Around line 71-104: Update saveTempAnswer to reduce per-save Redis work:
record only soalId in the ANSWER_UPDATED audit entry instead of spreading
jawabanData, and pipeline the audit list append/expiry together with the answer
hash write/expiry so they execute in a single Redis round trip. Preserve the
existing fallback-memory behavior when Redis is unavailable or the pipelined
operation fails.
- Around line 33-59: Update the Redis client setup in the constructor so
retryStrategy always returns a capped numeric backoff instead of null, allowing
reconnection after transient failures. Set isConnected from the Redis ready
event rather than connect, and add an end-event handler that logs when
reconnection has finally stopped while preserving the disconnected state.
In `@src/routes/_authenticated/admin.modul.import.tsx`:
- Around line 31-41: Both import routes incorrectly use unit-filtered topics for
the empty-state guard, preventing switching units when the first unit has no
topics. In src/routes/_authenticated/admin.modul.import.tsx lines 31-41, base
the guard on visibleTopiks(user).length and disable import buttons when topikId
is empty. In src/routes/_authenticated/admin.modul.import-word.tsx lines 89-96,
use the same all-visible-topics guard and preserve the existing if (!topikId)
check in loadFile for the selected-unit validation.
In `@src/routes/_authenticated/admin.modul.tsx`:
- Line 31: Update the unit list initialization in the component containing the
useState call to read unitAkademikRepo.all() during each render instead of
storing the result in state. Keep the existing units consumers unchanged so the
list reflects repository hydration consistently with the live topikRepo and
soalRepo reads.
In `@src/routes/_authenticated/admin.peserta.index.tsx`:
- Line 413: Update upsertUserServer to persist angkatan in both Prisma update
and create payloads so PesertaDialog.save stores the cohort year. In the Excel
import path, preserve existingUser?.angkatan when the import does not provide a
value, or explicitly define blank-versus-omitted behavior so re-importing cannot
unintentionally clear an existing cohort.
In `@src/routes/_authenticated/admin.tsx`:
- Around line 328-334: Add inert={!isOpen} to the nav group panel div identified
by its nav-group-${group.id}-panel id, so collapsed panels and their nested
links are removed from keyboard focus and assistive technology while closed.
- Around line 116-119: Update ADMIN_ROUTE_RULES to add an analitik entry
matching the existing /admin/analitik navigation and route-tree path, so
resolveAdminRouteRule("/admin/analitik") resolves correctly. If route access is
keyed by NavKey or role configuration, also add analitik to those definitions to
preserve independent analytics access.
In `@src/routes/_authenticated/admin.ujian`.$id.tsx:
- Around line 452-506: Update the isUmum switch handler in the Ujian form to
clear both groupIds and angkatanIds when enabling the public-exam mode,
preventing stale restrictions from blocking mahasiswa access. When disabling
isUmum, preserve the existing restriction arrays rather than resetting them.
In `@src/routes/_authenticated/admin.ujian.tsx`:
- Around line 95-101: Update submitAdd around ujianRepo.flush() to handle both a
rejected promise and an { ok: false } result: show the failure toast, keep the
list view open, remove the optimistic entry through setList, and reset isAdding.
Only append the entry, show the success toast, close the dialog, and navigate to
the edit route after a successful flush.
In `@src/routes/_authenticated/peserta.ujian`.$id.kerjakan.tsx:
- Around line 324-327: Update the Redis buffering flow around updateJawaban and
flushRedisSave: assign pendingRedisPayload.current before arming redisSaveTimer,
and have the timer invoke flushRedisSave. In handleNavigateIdx, flush the
pending Redis write instead of only clearing its timer; in submit, invoke
flushRedisSave before sesiRepo.flush() and ensure redisSaveTimer is cleared so
no delayed save recreates the submitted session buffer.
- Around line 211-217: Split the server timer synchronization from the main
timer/interval effect: place the setSessionTimerServer call in a dedicated
useEffect keyed only by the session identifier and endsAt (plus the required
session/test values), so answer updates do not trigger new POSTs. Narrow the
existing interval effect’s dependencies to stable values such as sesi?.id and
the relevant timer inputs, preserving its early-return behavior and preventing
interval recreation on every updateJawaban change.
---
Outside diff comments:
In `@src/routes/_authenticated/admin.users.roles.tsx`:
- Around line 35-43: Restrict the `/admin/users/roles` route with a
`super_admin` role guard before its loader runs. Update `Loader` to obtain and
pass the unit list from `getUnitAkademikList()` into `getUsersList()` as
required, rename the `moduls` binding and all related UI labels/references to
`units`, and keep topic grouping based on each manager’s `unitAkademik`.
---
Minor comments:
In `@src/lib/server/backup/functions.ts`:
- Line 77: Update the angkatanIds assignment in the backup Ujian import to
remove the unknown/string[] type assertion and preserve the string returned by
stringifyJson(item.angkatanIds), matching the handling of the other string-array
fields.
In `@src/lib/server/redis/service.ts`:
- Around line 146-159: Update setSessionTimer to reject non-finite
durationMinutes and clamp the computed TTL to a positive minimum before using it
with redis.set or setFallback. Reuse the same validated TTL for both storage
paths so invalid durations cannot produce Redis errors or already-expired
fallback entries.
In `@src/routes/_authenticated/admin.analitik.index.tsx`:
- Around line 142-144: Remove the duplicate u.nama rendering from the nearby h3
block in the relevant component, since the exam name is already rendered
earlier. Leave the existing first rendering intact and only retain this block if
it is replaced with distinct metadata.
In `@src/routes/_authenticated/admin.index.tsx`:
- Around line 80-85: Update formatNumber to use Indonesian abbreviations
consistently: represent millions with “jt” and thousands with “rb”, and replace
the decimal point with an Indonesian comma in formatted values. Preserve the
existing one-decimal rounding and plain numeric output for values below one
thousand.
In `@src/routes/_authenticated/admin.peserta.index.tsx`:
- Line 241: Update the empty-state table cell’s colSpan from 6 to 7 in the table
containing the “Tahun Angkatan” TableHead, including the corresponding
occurrence noted near the other header, so the empty state spans all seven
columns.
In `@src/routes/_authenticated/admin.topik`.$id.soal.tsx:
- Line 30: Update the question-management route around the `unit` lookup and the
topic-not-found guard so access is gated only by `topik`; a missing `unit` must
not trigger the “Topik tidak ditemukan” response. Render the breadcrumb
conditionally when `unit` is available, while preserving the existing
missing-topic handling and downstream behavior.
In `@src/routes/_authenticated/admin.tsx`:
- Around line 189-195: Update isNavItemActive so the rule-key fallback cannot
activate an item whose item.to is not a path prefix of pathname; require the
item path to match as an exact path or directory prefix before applying the
shared rule-key comparison. Preserve exact-item handling and prevent shorter
same-rule entries such as /admin/peserta from competing with more specific
paths.
---
Nitpick comments:
In `@src/components/cbt/CheatSheets.tsx`:
- Around line 207-218: Remove the non-functional Inv, Keypad, History, and Ans
controls, or implement their interactions fully. If retained, convert the
interactive divs to accessible button elements with keyboard support and
meaningful handlers, and bind the existing handleDelete function to the
appropriate control instead of leaving it unused or exposing empty onClick
behavior.
In `@src/lib/server/modul/functions.ts`:
- Around line 60-72: Validate the topik payload with TopikSchema before entering
the prisma.$transaction callback, replacing the any-based casts used by the
remove, bulkSet, and upsert branches. Import TopikSchema from `@/lib/cbt/types`
and use the parsed payload so unitId is required and legacy modulId-only
requests cannot create orphan topics.
In `@src/routes/_authenticated/admin.index.tsx`:
- Around line 330-350: Update the System Environment Telemetry block in the
admin page to use existing build-time constants or a server-provided
configuration source for the database provider, ORM engine, and application
version instead of hardcoded “SQLite Local”, “Prisma Client”, and “v1.2.0”
values. Preserve the current labels and styling while ensuring the displayed
values reflect the deployed configuration and package version.
In `@src/routes/_authenticated/admin.modul.tsx`:
- Around line 35-46: Update the shown filter to remove the unreachable fallback
return after the allowedSet check, relying on canEdit for unrestricted users and
allowedSet membership otherwise. Hoist the topikRepo.all() result outside the
units.filter callback, then reuse it when determining each unit’s accessible
topics.
In `@src/routes/_authenticated/admin.tsx`:
- Around line 339-342: The admin navigation types currently allow arbitrary
strings, so invalid or removed routes bypass type checking. In
src/routes/_authenticated/admin.tsx at lines 339-342 and
src/routes/_authenticated/admin.index.tsx at lines 283 and 416-419, update
NavItem["to"], pendingTasks[].route, and ShortcutCard.href to use
FileRouteTypes["fullPaths"] or a derived admin route union, and remove the cast
that bypasses Link route validation.
🪄 Autofix
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: a0803d38-481d-46ab-a5cf-59ca0fd58dd1
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (40)
package.jsonprisma/schema.prismascratch/seed-clinical.tssrc/components/cbt/CheatSheets.tsxsrc/lib/cbt/access.tssrc/lib/cbt/backup.tssrc/lib/cbt/repos.tssrc/lib/cbt/types.tssrc/lib/server/akademik/functions.tssrc/lib/server/backup/functions.tssrc/lib/server/db/auth.tssrc/lib/server/db/seed-shared.mjssrc/lib/server/modul/functions.tssrc/lib/server/redis/functions.tssrc/lib/server/redis/service.tssrc/lib/server/repos/mappers.tssrc/lib/server/repos/snapshot.tssrc/lib/server/ujian/functions.tssrc/lib/server/users/functions.tssrc/routeTree.gen.tssrc/routes/_authenticated/admin.akademik.mata-kuliah.tsxsrc/routes/_authenticated/admin.analitik.$id.tsxsrc/routes/_authenticated/admin.analitik.analisis.tsxsrc/routes/_authenticated/admin.analitik.index.tsxsrc/routes/_authenticated/admin.analitik.rekap.tsxsrc/routes/_authenticated/admin.evaluasi.index.tsxsrc/routes/_authenticated/admin.index.tsxsrc/routes/_authenticated/admin.modul.$id.topik.tsxsrc/routes/_authenticated/admin.modul.import-word.tsxsrc/routes/_authenticated/admin.modul.import.tsxsrc/routes/_authenticated/admin.modul.tsxsrc/routes/_authenticated/admin.pengaturan.tsxsrc/routes/_authenticated/admin.peserta.index.tsxsrc/routes/_authenticated/admin.topik.$id.soal.tsxsrc/routes/_authenticated/admin.tsxsrc/routes/_authenticated/admin.ujian.$id.tsxsrc/routes/_authenticated/admin.ujian.tsxsrc/routes/_authenticated/admin.users.roles.tsxsrc/routes/_authenticated/peserta.ujian.$id.index.tsxsrc/routes/_authenticated/peserta.ujian.$id.kerjakan.tsx
💤 Files with no reviewable changes (4)
- src/routes/_authenticated/admin.akademik.mata-kuliah.tsx
- src/lib/server/users/functions.ts
- src/lib/cbt/backup.ts
- src/routeTree.gen.ts
📜 Review details
🔇 Additional comments (30)
src/routes/_authenticated/admin.peserta.index.tsx (1)
378-378: LGTM!Also applies to: 391-391, 443-459
src/lib/server/ujian/functions.ts (1)
85-86: LGTM!Also applies to: 101-102, 111-112
src/routes/_authenticated/admin.ujian.$id.tsx (1)
176-186: LGTM!Also applies to: 346-346, 367-370, 560-582
src/routes/_authenticated/admin.ujian.tsx (1)
51-55: LGTM!Also applies to: 277-314
src/components/cbt/CheatSheets.tsx (1)
17-81: LGTM!src/routes/_authenticated/peserta.ujian.$id.kerjakan.tsx (1)
77-131: LGTM!Also applies to: 156-185, 269-287, 347-355, 601-602, 679-680
src/lib/server/redis/functions.ts (2)
10-14: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftBroken Authentication (CWE-807)
Reachability: External
Reachability path
● Entry src/routes/_authenticated/peserta.ujian.$id.kerjakan.tsx:77 CheatSheetAction │ ▼ ● Sink src/lib/server/redis/functions.tsThe timer is not server-authoritative, because the client supplies
endsAt.
service.tsdocumentssetSessionTimeras a "Server-Authoritative Timer" that "prevents client-side tampering". This handler acceptsendsAtanddurationMinutesfrom the request body and stores them without validation. A participant can post a laterendsAtand extend the stored deadline.Derive
endsAton the server from the persisted session start time and the examdurasiMenit. Accept onlysesiIdfrom the client.#!/bin/bash # Find every consumer of the stored timer to size the change. rg -n -C5 'getSessionTimer|setSessionTimer' src
4-44: 🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy liftAuthorization Bypass (CWE-862): Missing Authorization
Reachability: External
Reachability path
● Entry src/routes/_authenticated/peserta.ujian.$id.kerjakan.tsx:77 CheatSheetAction │ ▼ ● Sink src/lib/server/redis/functions.tsAdd authentication and session-ownership checks to these five server functions.
createServerFn({ method: "POST" })publishes an HTTP endpoint. None of these five handlers identifies the caller, and none checks that the caller ownssesiId. The validators are identity functions, so the raw request body reaches the handler unchanged.The attack path is direct. A client posts an arbitrary
sesiId:
getTempAnswersServerreturns another participant's in-progress answers.saveTempAnswerServeroverwrites another participant's buffered answers, which are then restored into that participant's session by the recovery code inpeserta.ujian.$id.kerjakan.tsx.clearSessionBufferServerdestroys another participant's recovery buffer.logAuditServerwrites arbitrary entries into any session's audit list.The sibling module
src/lib/server/ujian/functions.tscallsrequireCaller()and then authorizes the action. These handlers must do the same and must also confirm that the session belongs to the caller.🛡️ Proposed fix pattern
import { createServerFn } from "`@tanstack/react-start`"; +import { z } from "zod"; +import { requireCaller } from "`@/lib/server/db/auth`"; +import { prisma } from "`@/lib/server/db/client`"; import { redisService } from "./service"; +async function requireOwnedSesi(sesiId: string) { + const caller = await requireCaller(); + if (!caller) throw new Error("Unauthorized"); + const sesi = await prisma.sesiUjian.findUnique({ + where: { id: sesiId }, + select: { pesertaId: true }, + }); + if (!sesi) throw new Error("Not found"); + if (sesi.pesertaId !== caller.id && caller.role !== "super_admin") { + throw new Error("Forbidden"); + } +} + export const getTempAnswersServer = createServerFn({ method: "POST" }) - .validator((sesiId: string) => sesiId) + .validator(z.string().min(1)) .handler(async ({ data: sesiId }) => { + await requireOwnedSesi(sesiId); return redisService.getTempAnswers(sesiId); });Apply the same guard and a
zodschema tosetSessionTimerServer,saveTempAnswerServer,clearSessionBufferServer, andlogAuditServer. Adjust the model and field names to match the repository.#!/bin/bash # Confirm no global server-function middleware supplies authentication, # and show the authorization pattern used by other server functions. fd -e ts -e tsx . src --exec rg -ln 'createMiddleware|globalMiddleware|registerGlobalMiddleware' {} rg -n -C4 'requireCaller' src/lib/server | head -60 fd -g 'start.ts' -g 'start.tsx' -g 'router.tsx' . srcsrc/routes/_authenticated/admin.pengaturan.tsx (1)
66-66: 📐 Maintainability & Code QualityNo change needed.
neo-readyis defined insrc/styles.cssunder[data-theme='neobrutalism'], so the class has visual behavior for the target theme.src/routes/_authenticated/admin.tsx (1)
212-238: 🩺 Stability & AvailabilityNo auto-expand loop here.
configRepo.get()returns the module-levelcache.configreference, and currentconfigRepo.set()paths assign through that same cache without recreating the object, so this memo/effect dependency pattern does not create the described render loop.> Likely an incorrect or invalid review comment.package.json (1)
60-60: LGTM!prisma/schema.prisma (2)
45-45: LGTM!Also applies to: 56-56, 83-85, 135-136
124-125: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winAuthorization Bypass (CWE-862): Missing Authorization
Reachability: External
Apply the cohort policy before returning participant exams.
angkatanIdsis persisted here, but the supplied participant snapshot checks onlygroupIdsandcaller.unitId. A participant in an allowed unit but outside the configured cohort receives the exam and its token rows. Use one server-side predicate that evaluates cohort restrictions and theisUmumpolicy in the snapshot and exam-start paths.#!/bin/bash set -euo pipefail ast-grep outline src/lib/server/repos/snapshot.ts --items all --match 'pesertaSnapshot' ast-grep outline src/lib/server/db/auth.ts --items all --match 'pesertaCanTouchUjian' ast-grep outline src/lib/cbt/access.ts --items all --match 'isParticipantAssignedToExam' rg -n -C 8 'angkatanIds|isUmum|groupIds|pesertaSnapshot|pesertaCanTouchUjian|isParticipantAssignedToExam' \ prisma/schema.prisma \ src/lib/server/repos/snapshot.ts \ src/lib/server/db/auth.ts \ src/lib/cbt/access.tssrc/lib/server/repos/mappers.ts (1)
2-2: LGTM!Also applies to: 60-60, 96-96, 142-143, 152-153
src/lib/server/repos/snapshot.ts (1)
18-19: LGTM!Also applies to: 42-43, 142-156
src/lib/server/backup/functions.ts (1)
7-7: LGTM!src/lib/server/db/seed-shared.mjs (1)
222-227: LGTM!Also applies to: 573-573
src/lib/server/db/auth.ts (1)
28-28: LGTM!Also applies to: 90-90
src/routes/_authenticated/admin.analitik.analisis.tsx (1)
3-3: LGTM!Also applies to: 40-40, 121-123
src/routes/_authenticated/admin.analitik.index.tsx (1)
3-3: LGTM!Also applies to: 23-24
src/routes/_authenticated/admin.analitik.rekap.tsx (1)
3-3: LGTM!Also applies to: 117-119
src/routes/_authenticated/admin.evaluasi.index.tsx (1)
4-4: LGTM!Also applies to: 39-39, 50-50, 62-62, 135-135, 150-150, 170-170
src/routes/_authenticated/peserta.ujian.$id.index.tsx (2)
3-3: LGTM!Also applies to: 204-209
177-179: 🔒 Security & PrivacyInformation Disclosure (CWE-209): Generation of Error Message Containing Sensitive Information
Reachability: External
Verify that unexpected error messages are safe for participants.
This path exposes every non-assignment
Error.messagein a participant toast. Keep a fixed user-safe message unless all errors fromfindOrCreateSesiandstartSesiare safe to disclose.#!/bin/bash set -euo pipefail ast-grep outline src/lib/cbt/exam.ts --items all --match 'findOrCreateSesi|startSesi' rg -n -C 5 'findOrCreateSesi|startSesi|throw new Error|throw new [A-Za-z]+Error' src/lib/cbt/exam.tssrc/lib/cbt/types.ts (1)
57-57: LGTM!Also applies to: 70-70, 124-125, 134-135
src/lib/cbt/repos.ts (1)
5-10: LGTM!Also applies to: 93-104, 162-193, 275-281
src/lib/server/akademik/functions.ts (1)
8-8: LGTM!src/routes/_authenticated/admin.modul.$id.topik.tsx (1)
3-3: LGTM!Also applies to: 20-38, 40-43, 79-83
src/routes/_authenticated/admin.analitik.$id.tsx (1)
4-4: LGTM!Also applies to: 63-68
src/lib/cbt/access.ts (1)
128-136: 🔒 Security & Privacy | ⚡ Quick winAuthorization Bypass (CWE-862): Missing Authorization
Reachability: External
Reachability path
● Entry src/routes/_authenticated/admin.ujian.tsx:51 add │ ▼ ● Hop src/routes/_authenticated/admin.modul.$id.topik.tsx │ ▼ ● Sink src/lib/cbt/access.tsConfirm the fail-open default for exams without restrictions.
If
groupIdsandangkatanIdsare both empty andisUmumis false, the function returns true. Every authenticated participant then passes the check atsrc/routes/_authenticated/peserta.ujian.$id.index.tsxline 54.UjianSchemadefaults both fields to empty, so an exam saved without a cohort selection is open to all participants. Confirm that this is the intended product rule, or requireisUmumfor unrestricted exams.Run the following script to check whether the exam editor always writes a restriction:
#!/bin/bash # Description: Inspect how angkatanIds, groupIds, and isUmum are set when an exam is created or edited. rg -n -C 6 'angkatanIds|isUmum' --glob 'src/routes/**' --glob 'src/lib/server/ujian/**'
| const modulId = (await prisma.modul.findFirst())?.id; | ||
| if (!modulId) throw new Error("No modul found"); | ||
|
|
||
| const adminId = (await prisma.user.findFirst())?.id; | ||
| if (!adminId) throw new Error("No admin found"); | ||
|
|
||
| const topikId = uid(); | ||
| await prisma.topik.create({ | ||
| data: { | ||
| id: topikId, | ||
| modulId, | ||
| nama: "Keperawatan & Farmasi - Hitungan & Nilai Normal" | ||
| } | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm the Topik model fields and whether a Modul model still exists.
fd -g 'schema.prisma' --exec rg -n -A20 'model (Topik|Modul|UnitAkademik)' {}Repository: mannnrachman/cbt-man
Length of output: 1465
🏁 Script executed:
#!/bin/bash
# Inspect the seed script changes and any remaining seed references to modul/modulId.
set -euo pipefail
printf '--- scratch/seed-clinical.ts ---\n'
if [ -f scratch/seed-clinical.ts ]; then
nl -ba scratch/seed-clinical.ts | sed -n '1,80p'
else
echo 'scratch/seed-clinical.ts not found'
fi
printf '\n--- repository seed files mentioning modul/modulId/unitId ---\n'
rg -n 'modulId|prisma\.modul|unitId|prisma\.unitAkademik|unitAkademik' .
printf '\n--- files containing model Modul ---\n'
rg -n 'model Modul' .Repository: mannnrachman/cbt-man
Length of output: 249
🏁 Script executed:
#!/bin/bash
# Inspect the seed script changes and any remaining seed references to modul/modulId.
set -euo pipefail
printf '%s\n' '--- scratch/seed-clinical.ts ---'
if [ -f scratch/seed-clinical.ts ]; then
nl -ba scratch/seed-clinical.ts | sed -n '1,80p'
else
echo 'scratch/seed-clinical.ts not found'
fi
printf '%s\n' ''
printf '%s\n' '--- repository seed files mentioning modul/modulId/unitId ---'
rg -n 'modulId|prisma\.modul|unitId|prisma\.unitAkademik|unitAkademik' .
printf '%s\n' ''
printf '%s\n' '--- files containing model Modul ---'
rg -n 'model Modul' .Repository: mannnrachman/cbt-man
Length of output: 231
🏁 Script executed:
#!/bin/bash
# Inspect the seed script and relevant model references without nl.
set -euo pipefail
echo '--- scratch/seed-clinical.ts ---'
if [ -f scratch/seed-clinical.ts ]; then
awk '{printf "%5d\t%s\n", NR, $0}' scratch/seed-clinical.ts | sed -n '1,80p'
else
echo 'scratch/seed-clinical.ts not found'
fi
echo ''
echo '--- repository file references to modul/modulId/unitId ---'
rg -n 'modulId|unitId|prisma\.unitAkademik|prisma\.modul' . || true
echo ''
echo '--- files containing model Modul ---'
rg -n 'model Modul' . || trueRepository: mannnrachman/cbt-man
Length of output: 15636
Update the clinical seed to use the UnitAkademik model.
Topik now stores unitId, UnitAkademik exists, and model Modul is no longer present. Reading prisma.modul.findFirst() and creating prisma.topik with modulId will fail before the script can run.
🐛 Proposed change
- const modulId = (await prisma.modul.findFirst())?.id;
+ const unitId = (await prisma.unitAkademik.findFirst())?.id;
if (!modulId) throw new Error("No modul found");
+ if (!unitId) throw new Error("No unit akademik found");
await prisma.topik.create({
data: {
id: topikId,
- modulId,
+ unitId,
nama: "Keperawatan & Farmasi - Hitungan & Nilai Normal"
}
});🤖 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 `@scratch/seed-clinical.ts` around lines 8 - 21, Update the clinical seed setup
to fetch the required identifier from the UnitAkademik model instead of calling
prisma.modul.findFirst(), and rename the local identifier accordingly. In the
prisma.topik.create data, replace modulId with the Topik model’s unitId field
and pass the UnitAkademik identifier, preserving the existing missing-record
validation.
| function evaluateMathExpression(expr: string): number { | ||
| const sanitized = expr.replace(/÷/g, "/").replace(/×/g, "*").replace(/−/g, "-"); | ||
| if (!/^[0-9\s.+\-*/%()]+$/.test(sanitized)) { | ||
| throw new Error("Invalid math expression"); | ||
| } | ||
|
|
||
| const expanded = sanitized.replace(/(\d+(?:\.\d+)?)%/g, "($1/100)"); | ||
| const tokens = expanded.match(/\d+(?:\.\d+)?|[+\-*/()]/g); | ||
| if (!tokens) throw new Error("No tokens"); | ||
|
|
||
| let pos = 0; | ||
|
|
||
| function parsePrimary(): number { | ||
| if (pos >= tokens!.length) throw new Error("Unexpected end"); | ||
| const token = tokens![pos++]; | ||
| if (token === "(") { | ||
| const val = parseExpr(); | ||
| if (tokens![pos++] !== ")") throw new Error("Missing closing paren"); | ||
| return val; | ||
| } | ||
| const num = parseFloat(token); | ||
| if (isNaN(num)) throw new Error("Invalid number: " + token); | ||
| return num; | ||
| } | ||
|
|
||
| function parseMultiplicative(): number { | ||
| let left = parsePrimary(); | ||
| while (pos < tokens!.length && (tokens![pos] === "*" || tokens![pos] === "/")) { | ||
| const op = tokens![pos++]; | ||
| const right = parsePrimary(); | ||
| left = op === "*" ? left * right : (right !== 0 ? left / right : NaN); | ||
| } | ||
| return left; | ||
| } | ||
|
|
||
| function parseExpr(): number { | ||
| let left = parseMultiplicative(); | ||
| while (pos < tokens!.length && (tokens![pos] === "+" || tokens![pos] === "-")) { | ||
| const op = tokens![pos++]; | ||
| const right = parseMultiplicative(); | ||
| left = op === "+" ? left + right : left - right; | ||
| } | ||
| return left; | ||
| } | ||
|
|
||
| const result = parseExpr(); | ||
| if (isNaN(result) || !isFinite(result)) throw new Error("Math error"); | ||
| return result; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
The parser and the keypad do not agree. Three defects follow.
^always fails. Thexʸbutton on Line 234 inserts^. The allowlist on Line 85 rejects^, socalculatethrows and the display becomesError. The power button is unusable.- Leftover tokens are accepted silently.
parseExprreturns after the top-level loop ends. The code never checks thatpos === tokens.length. Example:5%3expands to(5/100)3, tokenizes to( 5 / 100 ) 3, and returns0.05. The trailing3is discarded and the participant sees a wrong answer with no error. - The
modbutton computes percent. Line 251 inserts%, and Line 89 rewritesn%into(n/100).modtherefore never performs a modulo operation.
Participants use this tool for scored clinical calculations, so a silently wrong result is worse than an error.
🐛 Proposed fix: reject leftover tokens and support `^`
function evaluateMathExpression(expr: string): number {
const sanitized = expr.replace(/÷/g, "/").replace(/×/g, "*").replace(/−/g, "-");
- if (!/^[0-9\s.+\-*/%()]+$/.test(sanitized)) {
+ if (!/^[0-9\s.+\-*/%^()]+$/.test(sanitized)) {
throw new Error("Invalid math expression");
}
const expanded = sanitized.replace(/(\d+(?:\.\d+)?)%/g, "($1/100)");
- const tokens = expanded.match(/\d+(?:\.\d+)?|[+\-*/()]/g);
+ const tokens = expanded.match(/\d+(?:\.\d+)?|[+\-*/^()]/g);
if (!tokens) throw new Error("No tokens");+ function parsePower(): number {
+ const base = parsePrimary();
+ if (pos < tokens!.length && tokens![pos] === "^") {
+ pos++;
+ return Math.pow(base, parsePower()); // right associative
+ }
+ return base;
+ }
+
function parseMultiplicative(): number {
- let left = parsePrimary();
+ let left = parsePower();
while (pos < tokens!.length && (tokens![pos] === "*" || tokens![pos] === "/")) {
const op = tokens![pos++];
- const right = parsePrimary();
+ const right = parsePower();
left = op === "*" ? left * right : (right !== 0 ? left / right : NaN);
}
return left;
} const result = parseExpr();
+ if (pos !== tokens.length) throw new Error("Unexpected trailing input");
if (isNaN(result) || !isFinite(result)) throw new Error("Math error");
return result;
}Also relabel or reimplement the mod button on Line 251, because % is bound to percent.
🤖 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/components/cbt/CheatSheets.tsx` around lines 83 - 131, Update
evaluateMathExpression to allow and parse the ^ operator with appropriate power
precedence, and reject any expression where pos does not equal tokens.length
after parseExpr completes. Resolve the keypad conflict by changing the mod
button’s behavior or label so it performs modulo rather than the existing
percent expansion, while preserving percent parsing for percentage inputs.
| export function ujianTouchesAllowed(user: User | null | undefined, ujian: Ujian): boolean { | ||
| if (isUnrestricted(user)) return true; | ||
| const mkSet = new Set(user?.mataKuliahIds ?? []); | ||
| if (ujian.mataKuliahId && mkSet.has(ujian.mataKuliahId)) return true; | ||
| const set = new Set(user?.allowedTopikIds ?? []); | ||
| if (set.size === 0 && mkSet.size === 0) return true; // unrestricted (though handled above) | ||
| if (set.size === 0) return true; // unrestricted (though handled above) | ||
| return ujian.topicSets.length > 0 && ujian.topicSets.every((ts) => set.has(ts.topikId)); | ||
| } | ||
|
|
||
| export function visibleUjians(user: User | null | undefined) { | ||
| if (isUnrestricted(user)) return ujianRepo.all(); | ||
| const set = new Set(user?.allowedTopikIds ?? []); | ||
| const mkSet = new Set(user?.mataKuliahIds ?? []); | ||
| const all = ujianRepo.all(); | ||
| return all.filter((u) => | ||
| (u.mataKuliahId && mkSet.has(u.mataKuliahId)) || | ||
| u.topicSets.some((ts) => set.has(ts.topikId)) | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Align exam visibility with the edit gate.
visibleUjians uses some, but ujianTouchesAllowed uses every. A restricted operator therefore sees an exam in the list that the editor rejects. src/routes/_authenticated/admin.ujian.$id.tsx calls ujianTouchesAllowed at lines 83 and 105 to block that exam. Choose one rule for both functions.
♻️ Proposed change to make listing match the edit gate
return all.filter((u) =>
- u.topicSets.some((ts) => set.has(ts.topikId))
+ u.topicSets.length > 0 && u.topicSets.every((ts) => set.has(ts.topikId))
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function ujianTouchesAllowed(user: User | null | undefined, ujian: Ujian): boolean { | |
| if (isUnrestricted(user)) return true; | |
| const mkSet = new Set(user?.mataKuliahIds ?? []); | |
| if (ujian.mataKuliahId && mkSet.has(ujian.mataKuliahId)) return true; | |
| const set = new Set(user?.allowedTopikIds ?? []); | |
| if (set.size === 0 && mkSet.size === 0) return true; // unrestricted (though handled above) | |
| if (set.size === 0) return true; // unrestricted (though handled above) | |
| return ujian.topicSets.length > 0 && ujian.topicSets.every((ts) => set.has(ts.topikId)); | |
| } | |
| export function visibleUjians(user: User | null | undefined) { | |
| if (isUnrestricted(user)) return ujianRepo.all(); | |
| const set = new Set(user?.allowedTopikIds ?? []); | |
| const mkSet = new Set(user?.mataKuliahIds ?? []); | |
| const all = ujianRepo.all(); | |
| return all.filter((u) => | |
| (u.mataKuliahId && mkSet.has(u.mataKuliahId)) || | |
| u.topicSets.some((ts) => set.has(ts.topikId)) | |
| ); | |
| } | |
| export function ujianTouchesAllowed(user: User | null | undefined, ujian: Ujian): boolean { | |
| if (isUnrestricted(user)) return true; | |
| const set = new Set(user?.allowedTopikIds ?? []); | |
| if (set.size === 0) return true; // unrestricted (though handled above) | |
| return ujian.topicSets.length > 0 && ujian.topicSets.every((ts) => set.has(ts.topikId)); | |
| } | |
| export function visibleUjians(user: User | null | undefined) { | |
| if (isUnrestricted(user)) return ujianRepo.all(); | |
| const set = new Set(user?.allowedTopikIds ?? []); | |
| const all = ujianRepo.all(); | |
| return all.filter((u) => | |
| u.topicSets.length > 0 && u.topicSets.every((ts) => set.has(ts.topikId)) | |
| ); | |
| } |
🤖 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/lib/cbt/access.ts` around lines 87 - 101, Align visibleUjians with
ujianTouchesAllowed by applying the same every-topic authorization rule when
filtering restricted users, replacing the current some-based predicate while
preserving unrestricted-user behavior.
| private memoryFallback: Map<string, { data: unknown, expiresAt: number | null }> = new Map(); | ||
|
|
||
| private setFallback(key: string, data: unknown, ttlSeconds?: number) { | ||
| const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null; | ||
| this.memoryFallback.set(key, { data, expiresAt }); | ||
| } | ||
|
|
||
| private getFallback(key: string): unknown | null { | ||
| const entry = this.memoryFallback.get(key); | ||
| if (!entry) return null; | ||
| if (entry.expiresAt && Date.now() > entry.expiresAt) { | ||
| this.memoryFallback.delete(key); | ||
| return null; | ||
| } | ||
| return entry.data; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
memoryFallback grows without bound.
getFallback removes an expired entry only when that exact key is read again. Nothing sweeps the map. Audit log keys are written with a 7-day TTL on Line 203 and are never read on the normal path, and clearSessionBuffer deletes only the answer key and the timer key. In a long-running server the map therefore accumulates one entry per session that ever used the fallback, each holding every buffered answer.
redisService is a module-level singleton, so the map lives for the process lifetime.
Add a periodic sweep or a bounded cache, and delete the log key in clearSessionBuffer.
♻️ Proposed change
private getFallback(key: string): unknown | null {
const entry = this.memoryFallback.get(key);
if (!entry) return null;
if (entry.expiresAt && Date.now() > entry.expiresAt) {
this.memoryFallback.delete(key);
return null;
}
return entry.data;
}
+
+ private sweepFallback() {
+ const now = Date.now();
+ for (const [key, entry] of this.memoryFallback) {
+ if (entry.expiresAt && now > entry.expiresAt) this.memoryFallback.delete(key);
+ }
+ }Start the sweep on an interval in the constructor, and call unref() on the timer so it does not hold the process open.
🤖 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/lib/server/redis/service.ts` around lines 16 - 31, Prevent the
module-level Redis fallback cache from growing indefinitely: in the service
constructor, start a periodic sweep that removes expired entries from
memoryFallback and call unref() on the interval timer. Also update
clearSessionBuffer to delete the audit log key alongside the existing answer and
timer keys, while preserving current fallback expiration behavior in setFallback
and getFallback.
| constructor() { | ||
| const redisUrl = process.env.REDIS_URL || "redis://127.0.0.1:6379"; | ||
| try { | ||
| this.redis = new Redis(redisUrl, { | ||
| maxRetriesPerRequest: 1, | ||
| enableOfflineQueue: false, | ||
| retryStrategy: (times) => { | ||
| if (times > 5) return null; // stop retrying after 5 attempts to avoid blocking | ||
| return Math.min(times * 100, 3000); | ||
| }, | ||
| }); | ||
|
|
||
| this.redis.on("connect", () => { | ||
| this.isConnected = true; | ||
| console.log("[Redis] Connected successfully to Redis server."); | ||
| }); | ||
|
|
||
| this.redis.on("error", (err) => { | ||
| if (this.isConnected) { | ||
| console.warn("[Redis] Disconnected or Connection Error:", err.message); | ||
| } | ||
| this.isConnected = false; | ||
| }); | ||
| } catch { | ||
| this.isConnected = false; | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
ioredis retryStrategy return null stop reconnecting ready end events
💡 Result:
In ioredis, the retryStrategy function dictates the behavior when a connection to Redis is lost [1][2]. If this function returns a value that is not a number (such as null, undefined, or false), ioredis will stop attempting to reconnect [1][3][2]. When the retryStrategy returns a non-numeric value, the client emits an end event, indicating that no further automatic reconnection attempts will be made [4][5]. At this point, the connection is considered permanently closed unless you manually call redis.connect [1][3][2]. To stop reconnecting, you can implement the strategy as follows [3]: const redis = new Redis({ retryStrategy(times) { // Returning null or false stops the reconnection process return null; } }); redis.on('end', => { console.log('Connection has ended and no further reconnections will be attempted.'); }); In this scenario, the 'end' event will be triggered after the retryStrategy returns a non-numeric value, effectively terminating the lifecycle of the automatic connection manager [4][5]. Any pending commands in the queue will be flushed with an error, typically reflecting that the connection has been closed [4].
Citations:
- 1: https://ioredis.readthedocs.io/en/stable/README/
- 2: https://github.com/redis/ioredis?tab=readme-ov-file
- 3: Connection is Closed redis/ioredis#913
- 4: https://github.com/redis/ioredis/blob/main/lib/redis/event_handler.ts
- 5: https://deepwiki.com/redis/ioredis/5.2-events
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate relevant files =="
fd -a 'service\.ts$|package\.json$' . | sed 's#^\./##' | head -100
echo
echo "== redis service file =="
cat -n src/lib/server/redis/service.ts
echo
echo "== package manifests with ioredis refs =="
rg -n '"ioredis"|ioredis' package.json $(git ls-files '*package*.json') || true
echo
echo "== usages of RedisService/isConnected/fallback =="
rg -n "RedisService|isConnected|memoryFallback|fallback" src -S || trueRepository: mannnrachman/cbt-man
Length of output: 13290
🌐 Web query:
ioredis 5.11.1 retryStrategy reconnect automatically end event ready event
💡 Result:
In ioredis 5.11.1, automatic reconnection is enabled by default [1][2]. The library attempts to reconnect whenever the connection to the Redis server is lost, provided the disconnection was not triggered manually by calling redis.disconnect or redis.quit [1][3]. The behavior of these reconnections is controlled by the retryStrategy option [1][2]. retryStrategy The retryStrategy is a function that determines the delay (in milliseconds) before the next reconnection attempt [1][2]. It receives a times argument, which indicates the number of reconnection attempts made so far [1][2]. If the function returns a number, the client will wait for that many milliseconds before attempting to reconnect [1][3]. If the function does not return a number (e.g., returns null or undefined), ioredis will stop attempting to reconnect, and the connection status will transition to end [1][4][2]. Events The following events are relevant to the connection lifecycle: ready: Emitted when the client is fully connected and ready to process commands [5]. If the enableReadyCheck option is true (default), this event is emitted only after the server reports it is ready (e.g., it has finished loading data from disk) [6][7]. If enableReadyCheck is false, it is emitted immediately after the connect event [5][8]. end: Emitted after the connection has closed and the client has stopped attempting further reconnections [4][5]. This occurs if the connection was closed manually, if the retryStrategy stopped returning a number, or if the connection failed permanently [1][4][5]. Summary of Reconnection Flow When the connection is lost, ioredis emits a close event [5]. If the connection was not closed manually, the client checks the retryStrategy [4]. If a valid delay is returned, it emits a reconnecting event and initiates a timer for the next attempt [4][5]. Upon successful reconnection, the client emits a connect event, followed by a ready event once the server is prepared to handle commands [5][6]. If all retries fail or the strategy dictates stopping, it eventually emits an end event [4][5].
Citations:
- 1: https://npmx.dev/package/ioredis/v/5.11.1
- 2: https://github.com/redis/ioredis
- 3: https://ioredis.readthedocs.io/en/stable/README/
- 4: https://github.com/redis/ioredis/blob/main/lib/redis/event_handler.ts
- 5: https://github.com/redis/ioredis?tab=readme-ov-file
- 6: https://ioredis.readthedocs.io/en/latest/API/
- 7: https://ioredis.readthedocs.io/en/stable/API/
- 8: https://github.com/redis/ioredis/blob/master/API.md
Keep the Redis client reconnecting after transient failures.
retryStrategy returns null after 5 attempts, so ioredis stops reconnection attempts and emits end. After that, no connect/ready event can fire, isConnected stays false, and all service calls fall back to process-local memory for the lifetime of the client. Keep retryStrategy returning a number with a capped backoff, update isConnected from ready, and log when the final end event occurs.
🤖 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/lib/server/redis/service.ts` around lines 33 - 59, Update the Redis
client setup in the constructor so retryStrategy always returns a capped numeric
backoff instead of null, allowing reconnection after transient failures. Set
isConnected from the Redis ready event rather than connect, and add an end-event
handler that logs when reconnection has finally stopped while preserving the
disconnected state.
|
|
||
| <div className="flex items-center justify-between rounded border p-2 bg-blue-50/50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800"> | ||
| <div> | ||
| <Label className="text-blue-700 dark:text-blue-400">Ujian Umum (Mata Kuliah Umum)</Label> | ||
| <p className="text-xs text-blue-600/70 dark:text-blue-400/70"> | ||
| Bila diaktifkan, ujian ini terbuka untuk semua prodi dan angkatan | ||
| </p> | ||
| </div> | ||
| <Switch checked={u.isUmum} onCheckedChange={(v) => set("isUmum", v)} /> | ||
| </div> | ||
|
|
||
| {!u.isUmum && ( | ||
| <> | ||
| <div className="space-y-1 mt-4"> | ||
| <Label className="text-xs">Program Studi / Unit (Kosong = Semua Prodi)</Label> | ||
| <div className="grid grid-cols-2 gap-2 sm:grid-cols-3"> | ||
| {groups.map((g) => ( | ||
| <label key={g.id} className="flex items-center gap-2 rounded border p-2 text-sm"> | ||
| <Checkbox | ||
| checked={u.groupIds.includes(g.id)} | ||
| onCheckedChange={(v) => | ||
| set( | ||
| "groupIds", | ||
| v ? [...u.groupIds, g.id] : u.groupIds.filter((x) => x !== g.id), | ||
| ) | ||
| } | ||
| /> | ||
| {g.nama} | ||
| </label> | ||
| ))} | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="space-y-2 mt-4 pt-4 border-t"> | ||
| <Label className="text-xs">Tahun Angkatan (Kosong = Semua Angkatan)</Label> | ||
| <div className="grid grid-cols-2 gap-2 sm:grid-cols-4"> | ||
| {availableAngkatan.map((angkatan) => ( | ||
| <label key={angkatan} className="flex items-center gap-2 rounded border p-2 text-sm"> | ||
| <Checkbox | ||
| checked={u.angkatanIds?.includes(angkatan) ?? false} | ||
| onCheckedChange={(v) => { | ||
| const current = u.angkatanIds ?? []; | ||
| set( | ||
| "angkatanIds", | ||
| v ? [...current, angkatan] : current.filter((x) => x !== angkatan) | ||
| ); | ||
| }} | ||
| /> | ||
| {angkatan} | ||
| </label> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </> | ||
| )} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect participant-side access logic for isUmum / angkatanIds handling.
fd -e ts -e tsx . src/lib/cbt src/lib/server --exec rg -n -C6 'isUmum|angkatanIds' {}Repository: mannnrachman/cbt-man
Length of output: 4599
🏁 Script executed:
#!/bin/bash
# Locate and inspect the admin edit component around the visibleUjians / participant access flow and the isUmum form state.
fd -e ts -e tsx . src routes --exec sh -c '
echo "== file: $1 =="
rg -n "visibleUjians|isParticipantAssignedToExam|setU|set\\(\"isUmum\"|isUmum|groupIds|angkatanIds" "$1" || true
' sh {}
# Read focused sections from route and library files.
for f in \
"src/routes/_authenticated/admin.ujian.$id.tsx" \
"src/lib/cbt/participant/ujian.ts"
do
if [ -f "$f" ]; then
echo "== $f line count =="
wc -l "$f"
echo "== $f isUmum visibility/access section =="
rg -n -C8 "isParticipantAssignedToExam|visibleUjians|isUmum|groupIds|angkatanIds" "$f"
fi
doneRepository: mannnrachman/cbt-man
Length of output: 12443
🏁 Script executed:
#!/bin/bash
# Read the core access implementation and focused route sections.
for f in \
"src/lib/cbt/access.ts" \
"src/lib/server/ujian/functions.ts" \
"src/routes/_authenticated/admin.ujian.tsx" \
"src/routes/_authenticated/admin.ujian.$id.tsx"
do
if [ -f "$f" ]; then
echo "== $f =="
wc -l "$f"
fi
done
echo "== src/lib/cbt/access.ts =="
sed -n '90,170p' src/lib/cbt/access.ts
echo "== src/lib/server/ujian/functions.ts visible/assignment sections =="
sed -n '350,410p' src/lib/server/ujian/functions.ts
echo "== src/routes/_authenticated/admin.ujian.tsx =="
sed -n '1,140p' src/routes/_authenticated/admin.ujian.tsx
echo "== src/routes/_authenticated/admin.ujian.$id.tsx form/save sections =="
sed -n '70,115p' src/routes/_authenticated/admin.ujian.$id.tsx
sed -n '180,215p' src/routes/_authenticated/admin.ujian.$id.tsx
sed -n '448,502p' src/routes/_authenticated/admin.ujian.$id.tsxRepository: mannnrachman/cbt-man
Length of output: 11086
Clear stale restrictions when enabling isUmum.
groupIds and angkatanIds can remain populated while isUmum is true. visibleUjians still returns the exam, but fetchUjianByIdServer checks row.groupIds directly for mahasiswa callers and returns Forbidden before exposing the public exam. Clear both arrays when the switch is enabled; when disabling, keep prior values if they may have been valid for future restriction edits.
🤖 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/routes/_authenticated/admin.ujian`.$id.tsx around lines 452 - 506, Update
the isUmum switch handler in the Ujian form to clear both groupIds and
angkatanIds when enabling the public-exam mode, preventing stale restrictions
from blocking mahasiswa access. When disabling isUmum, preserve the existing
restriction arrays rather than resetting them.
| ujianRepo.upsert(u); | ||
| await ujianRepo.flush(); | ||
| setList((current) => [...current, u]); | ||
| toast.success("Ujian baru dibuat — silakan edit"); | ||
| setIsAddDialogOpen(false); | ||
| setIsAdding(false); | ||
| navigate({ to: "/admin/ujian/$id", params: { id: u.id } }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Determine the return type of flush() on the repos.
rg -n -C10 'flush' src/lib/cbt/repos.tsRepository: mannnrachman/cbt-man
Length of output: 1248
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== route submitAdd context =="
cat -n src/routes/_authenticated/admin.ujian.tsx | sed -n '60,120p'
echo
echo "== MutationResult definition/usages =="
rg -n -C4 'type MutationResult|interface MutationResult|MutationResult|\.ok|flush\(' src/lib/cbt repos || true
echo
echo "== error handling in related files =="
rg -n -C6 'await .*Repo\.flush|result\.ok' src/routes -e 'tsx' | sed -n '1,220p'
echo
echo "== ujianRepo declaration context =="
cat -n src/lib/cbt/repos.ts | sed -n '1,260p'Repository: mannnrachman/cbt-man
Length of output: 9254
Handle all flush outcomes in submitAdd.
ujianRepo.flush() can return { ok: false } or reject. In each case, show the failure toast, keep the page on the list view, update setList to remove the optimistic entry, and reset isAdding so the add controls stay usable.
🤖 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/routes/_authenticated/admin.ujian.tsx` around lines 95 - 101, Update
submitAdd around ujianRepo.flush() to handle both a rejected promise and an {
ok: false } result: show the failure toast, keep the list view open, remove the
optimistic entry through setList, and reset isAdding. Only append the entry,
show the success toast, close the dialog, and navigate to the edit route after a
successful flush.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/routes/_authenticated/peserta.ujian.$id.index.tsx (1)
177-180: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winBring token claims under the same user-facing error guard.
claimExamToken()returns{ ok: false }in the normal token-error paths, but this handler only catches exceptions from session creation. IfclaimExamToken()throws,mulai()rejects with an unhandled participant-facing error. Wrap the earlier claim path in the same generic fallback or guarantee all server failures return{ ok: false, error }.🤖 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/routes/_authenticated/peserta.ujian`.$id.index.tsx around lines 177 - 180, Update mulai() so the claimExamToken() call is covered by the same generic participant-facing error guard as session creation. Handle thrown claim errors by showing the existing “Gagal memulai ujian” fallback and returning, while preserving the current { ok: false } handling for normal token failures.
🤖 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 `@src/lib/server/db/auth.ts`:
- Line 112: Remove the any cast at the isParticipantAssignedToExam authorization
call and make the argument satisfy its participant contract through a shared
minimal participant type or a typed adapter mapping unitId and angkatan.
Preserve the existing authorization behavior and avoid replacing the cast with
another broad assertion.
---
Nitpick comments:
In `@src/routes/_authenticated/peserta.ujian`.$id.index.tsx:
- Around line 177-180: Update mulai() so the claimExamToken() call is covered by
the same generic participant-facing error guard as session creation. Handle
thrown claim errors by showing the existing “Gagal memulai ujian” fallback and
returning, while preserving the current { ok: false } handling for normal token
failures.
🪄 Autofix
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: 8a4a7094-25fe-432b-8a7e-6736b10231c0
📒 Files selected for processing (9)
prisma/schema.prismasrc/lib/cbt/access.tssrc/lib/server/backup/functions.tssrc/lib/server/db/auth.tssrc/routes/_authenticated/admin.index.tsxsrc/routes/_authenticated/admin.peserta.index.tsxsrc/routes/_authenticated/admin.ujian.$id.tsxsrc/routes/_authenticated/admin.ujian.tsxsrc/routes/_authenticated/peserta.ujian.$id.index.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
- src/routes/_authenticated/admin.peserta.index.tsx
- prisma/schema.prisma
- src/lib/server/backup/functions.ts
- src/lib/cbt/access.ts
- src/routes/_authenticated/admin.index.tsx
- src/routes/_authenticated/admin.ujian.$id.tsx
📜 Review details
⚠️ CI failures not shown inline (2)
GitHub Actions: CI / CI: feat: revamp bank soal per jurusan, add angkatan filter checkboxes
Conclusion: failure
##[group]Run npm run lint -- --max-warnings=7
�[36;1mnpm run lint -- --max-warnings=7�[0m
shell: /usr/bin/bash -e {0}
##[endgroup]
> lint
> eslint . --max-warnings=7
/home/runner/work/cbt-man/cbt-man/scratch/seed-clinical.ts
##[error] 119:25 error Unexpected any. Specify a different type `@typescript-eslint/no-explicit-any`
GitHub Actions: CI / 0_CI.txt: feat: revamp bank soal per jurusan, add angkatan filter checkboxes
Conclusion: failure
##[group]Run npm run lint -- --max-warnings=7
�[36;1mnpm run lint -- --max-warnings=7�[0m
shell: /usr/bin/bash -e {0}
##[endgroup]
> lint
> eslint . --max-warnings=7
/home/runner/work/cbt-man/cbt-man/scratch/seed-clinical.ts
##[error] 119:25 error Unexpected any. Specify a different type `@typescript-eslint/no-explicit-any`
🧰 Additional context used
🪛 ESLint
src/lib/server/db/auth.ts
[error] 112-112: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🪛 GitHub Check: CI
src/lib/server/db/auth.ts
[failure] 112-112:
Unexpected any. Specify a different type
🔇 Additional comments (8)
src/lib/server/db/auth.ts (3)
71-71: 🎯 Functional CorrectnessNo callers remain after removing
operatorCanTouchModul.
28-28: 🔒 Security & PrivacyAuthorization Bypass (CWE-863): Incorrect Authorization
Complete the legacy permission migration before dropping
mataKuliahIds.If an existing operator has a non-empty
mataKuliahIdsvalue but no migrated topic permissions, the empty topic-permission branch can authorize every topic. If the new list is non-empty, removing the fallback can deny access that the stored permission still grants. Prove that the migration backfills equivalent topic permissions or clears the legacy field before enabling this path.#!/usr/bin/env bash set -euo pipefail rg -n -C 8 \ '\b(mataKuliahIds|topikIds|topicIds|operatorCanTouchTopikId)\b' \ --glob '*.ts' --glob '*.tsx' --glob '*.mjs' --glob 'schema.prisma'Also applies to: 57-58
90-96: 🔒 Security & PrivacyAuthorization Bypass (CWE-863): Incorrect Authorization
Fail closed for exams without topic scope.
operatorCanTouchTopicSetsreturnstruefor an empty list at Lines 65-68.parseJsonalso falls back to[]. Because this query removesmataKuliahId, a missing, malformed, or emptytopicSetsvalue can authorize an operator without a topic match unless an earlier guard rejects it. Reject unscoped records or prove that every reachable exam has valid topic sets.#!/usr/bin/env bash set -euo pipefail rg -n -C 10 \ '\b(operatorCanTouchUjian|operatorCanTouchTopicSets|topicSets|mataKuliahId)\b' \ --glob '*.ts' --glob '*.tsx' --glob '*.mjs' --glob 'schema.prisma'src/routes/_authenticated/admin.ujian.tsx (2)
70-74: Duplicate: handle everyujianRepo.flush()outcome.At Line [71],
add()ignores the flush result. If it returns{ ok: false }, Lines [72]-[74] still update the list, show success, and clearisAdding. If it rejects,isAddingremains set and the rejection is unhandled. This is the same unresolved issue from the previous review comment. Handle both outcomes before updating the UI.
8-8: LGTM!Also applies to: 42-69, 89-90, 114-115
src/routes/_authenticated/peserta.ujian.$id.index.tsx (3)
177-180: 🗄️ Data Integrity & IntegrationVerify token/session consistency after a start failure.
The token is claimed at Line [153] before
findOrCreateSesi(),startSesi(), andsesiRepo.upsert()at Lines [167]-[169]. If any of those operations fails, Lines [177]-[180] return without releasing or reconciling the token. Confirm that the server claim is idempotent for the same participant or provides a rollback path.
3-3: LGTM!Also applies to: 186-195, 204-215, 224-224, 337-337, 347-347, 362-362
177-180: 🔒 Security & PrivacyInformation Disclosure (CWE-209): Generation of Error Message Containing Sensitive Information
Reachability: External
Verify that token-claim errors are sanitized.
The generic message at Line [178] covers only errors after the token claim.
src/lib/cbt/repos.tspassesclaimExamTokenServer’sresult.errorthrough, and Line [155] displaysclaim.errordirectly. If that server error contains exception details, the participant can see internal information. Return stable user-facing token errors and map unexpected failures to the generic message.
|
|
||
| ); | ||
| const angkatanIds = parseJson<string[]>(ujian.angkatanIds, []); | ||
| return isParticipantAssignedToExam(caller as any, { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Remove the any cast from the authorization boundary.
The supplied CI and ESLint results report @typescript-eslint/no-explicit-any at Line 112. The cast also hides whether UserRow satisfies the helper contract for unitId and angkatan. Define a shared minimal participant type or map these fields into a typed adapter. Do not replace this with another broad assertion.
🧰 Tools
🪛 ESLint
[error] 112-112: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🪛 GitHub Check: CI
[failure] 112-112:
Unexpected any. Specify a different type
🤖 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/lib/server/db/auth.ts` at line 112, Remove the any cast at the
isParticipantAssignedToExam authorization call and make the argument satisfy its
participant contract through a shared minimal participant type or a typed
adapter mapping unitId and angkatan. Preserve the existing authorization
behavior and avoid replacing the cast with another broad assertion.
Source: Linters/SAST tools
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
scratch/seed-clinical.ts (1)
119-122: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winType
soalsinstead of suppressingno-explicit-any.
model SoalstoresTipeSoalandKesulitan, whilesoalsis inferred with literal strings. Declaresoalswith the Prisma-generatedSoalCreateInputtype for the seed insert, then remove bothas anycasts and their suppression comments.🤖 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 `@scratch/seed-clinical.ts` around lines 119 - 122, Type the soals seed data using Prisma’s generated SoalCreateInput before the seed insert, then remove the no-explicit-any suppression comments and both as any casts from tipe and kesulitan while preserving their existing values.
🤖 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 `@raw_admin.tsx`:
- Around line 55-61: Update the counts object so the value from
topikRepo.all().length is represented as a topic count rather than counts.modul,
and update the corresponding KPI display near the Modul Mata Kuliah label to use
the renamed topic count or derive the intended module/unit count instead. Keep
the other count fields unchanged.
- Line 156: Replace the corrupted “ΓÇó” separator with the intended bullet
separator in both subtitle strings: raw_admin.tsx lines 156-156 and 349-349.
Preserve the surrounding exam count and label text.
- Around line 40-41: Add a stateful time value in the component containing the
now calculation, and update it with a timer so exam categories are recomputed as
boundaries pass. Schedule refreshes around relevant exam start/end times, clean
up the timer on unmount or dependency changes, and use the refreshed state in
the active/upcoming/finished classification instead of the render-only
Date.now() value.
---
Nitpick comments:
In `@scratch/seed-clinical.ts`:
- Around line 119-122: Type the soals seed data using Prisma’s generated
SoalCreateInput before the seed insert, then remove the no-explicit-any
suppression comments and both as any casts from tipe and kesulitan while
preserving their existing values.
🪄 Autofix
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: 9efd40dc-c19a-4d51-81e5-733c446633e0
📒 Files selected for processing (8)
raw_admin.tsxraw_admin.txtscratch/seed-clinical.tsskills-lock.jsonsrc/lib/server/db/auth.tssrc/routes/_authenticated/admin.analitik.index.tsxtemp_admin.txtts_errors.txt
🚧 Files skipped from review as they are similar to previous changes (2)
- src/routes/_authenticated/admin.analitik.index.tsx
- src/lib/server/db/auth.ts
📜 Review details
🔇 Additional comments (2)
skills-lock.json (2)
1-4: LGTM!Also applies to: 9-10, 15-16, 21-22, 27-28, 33-34, 39-40, 45-47
5-8: 🔒 Security & PrivacyLLM Security (CWE-829): Inclusion of Functionality from Untrusted Control Sphere
Verify hash enforcement before loading these skills.
Each entry points to a
SKILL.mdinstruction file that can be loaded into an agent session. (docs.github.com) The lock recordscomputedHash, but this file does not prove that the loader verifies it.Confirm that the loader:
- Hashes the fetched file and rejects mismatches.
- Does not silently refresh the content or hash.
- Requires review when a hash changes.
If the loader does not enforce this control, pin each skill to an immutable commit or add explicit verification.
#!/bin/bash set -euo pipefail # Expect: the loader verifies computedHash before loading skill content. rg -n -C 6 \ 'skills-lock\.json|computedHash|skillPath|sourceType|skills (add|install|sync|update)' \ --glob '!node_modules/**' .Also applies to: 11-14, 17-20, 23-26, 29-32, 35-38, 41-44
| const now = Date.now(); | ||
| const ONE_WEEK = 7 * 24 * 60 * 60 * 1000; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files matching raw_admin.tsx:"
fd -a 'raw_admin\.tsx' . || true
file="$(fd 'raw_admin\.tsx' . | head -n 1 || true)"
if [ -n "${file:-}" ]; then
echo "File: $file"
wc -l "$file"
echo "--- outline ---"
ast-grep outline "$file" || true
echo "--- relevant sections ---"
sed -n '1,80p' "$file"
echo "--- usages of now/exam state/ONE_WEEK ---"
rg -n "now|ONE_WEEK|active|upcoming|finished|beginAt|endAt|setInterval|setTimeout|useEffect|useSchedule|useInterval|subscribe|reRender|sync" "$file" -C 3 || true
echo "--- imports and parent usage ---"
rg -n "raw_admin|RawAdmin|from ['\"].*raw_admin|RawAdminProps" . --glob '!node_modules' --glob '!dist' --glob '!build' -C 2 || true
fi
echo "--- repository/dash repo imports in file, if any ---"
if [ -n "${file:-}" ]; then
sed -n '1,40p' "$file" | rg -n "import|from" || true
fiRepository: mannnrachman/cbt-man
Length of output: 10375
Refresh time-dependent exam state.
now is computed only once during render, while the dashboard treats active/upcoming/finished categories from that fixed value. Add a stateful timer that refreshes around exam boundaries and clears on unmount so expired exams do not stay active and upcoming exams do not delay appearing.
🤖 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 `@raw_admin.tsx` around lines 40 - 41, Add a stateful time value in the
component containing the now calculation, and update it with a timer so exam
categories are recomputed as boundaries pass. Schedule refreshes around relevant
exam start/end times, clean up the timer on unmount or dependency changes, and
use the refreshed state in the active/upcoming/finished classification instead
of the render-only Date.now() value.
| const counts = { | ||
| peserta: pesertaList.length, | ||
| modul: topikRepo.all().length, | ||
| soal: soalList.length, | ||
| ujian: semuaUjian.length, | ||
| sesi: sesiRepo.all().length, | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not present a topic count as a module count.
Line 57 assigns topikRepo.all().length to counts.modul, but Line 172 displays it as Modul Mata Kuliah. Multiple topics can belong to one unit, so this KPI can overstate the module count.
Rename the KPI to topics, or derive the intended unit count.
🤖 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 `@raw_admin.tsx` around lines 55 - 61, Update the counts object so the value
from topikRepo.all().length is represented as a topic count rather than
counts.modul, and update the corresponding KPI display near the Modul Mata
Kuliah label to use the renamed topic count or derive the intended module/unit
count instead. Keep the other count fields unchanged.
mannnrachman
left a comment
There was a problem hiding this comment.
@Jedijedai terima kasih untuk pekerjaan Bank Soal per Jurusan dan filter Angkatan. Saya sudah review head ba5a084b55ea2c9cd72cd53c05be6441f2ad0eb1.
Saya meminta perubahan, tetapi jangan rebase atau mencoba membersihkan PR aggregate ini di tempat. Branch ini membawa 31 commit / 39 file dan mewarisi seluruh lineage #83 (Redis, CheatSheets/calculator, snapshot, dashboard/sidebar lama), sehingga merge atau rebase biasa tetap berisiko membawa kembali kode yang sudah ditolak.
Mohon buat PR replacement baru dari main terbaru dengan ruang lingkup hanya Bank Soal per Jurusan + Angkatan, lalu referensikan #87 untuk kredit/riwayat kontribusi.
Checklist replacement PR
-
Mulai bersih dari
maingit fetch origin main git switch -c feature/bank-soal-angkatan-v2 origin/main
Jangan cherry-pick Redis,
CheatSheets, snapshot peserta, dashboard/sidebar, atau penghapusanModul/MataKuliahdari branch ini. -
Jaga struktur data yang sudah ada
- Jangan menghapus/merombak
ModuldanMataKuliahdalam feature ini. - Jika perlu
User.angkatan,Ujian.angkatanIds, atauUjian.isUmum, tambahkan migration Prisma additive dan data-preserving. - Jangan gunakan
db pushuntuk menggantikan migration. - Jika desain benar-benar memerlukan migrasi
Modul/MataKuliah → UnitAkademik, jadikan itu PR desain/migrasi khusus dengan backfill dan rencana rollback—bukan bagian dari feature UI ini.
- Jangan menghapus/merombak
-
Satu aturan akses peserta di server
- Tentukan dengan jelas arti array kosong (direkomendasikan: kosong = tidak membatasi / semua peserta).
- Terapkan predicate yang sama untuk snapshot peserta, direct fetch ujian, pembuatan sesi/start exam, dan UI.
isUmumharus diizinkan server-side sebelum memeriksa restriction lama.- Ujian cohort/prodi hanya boleh terlihat dan dapat dibuka oleh peserta yang cocok.
-
Selaraskan akses operator
- Listing, direct fetch, editor, dan mutation harus memakai rule yang sama untuk ujian mixed-scope (gunakan seluruh topic set /
every, bukansome).
- Listing, direct fetch, editor, dan mutation harus memakai rule yang sama untuk ujian mixed-scope (gunakan seluruh topic set /
-
Perbaiki UX Bank Soal/import
- Jika unit pertama kosong, selector unit tetap harus tampil agar operator dapat memilih unit lain yang memiliki topik.
- Jangan menyimpan
unitAkademikRepo.all()hanya saat mount; pastikan hasil hydration dapat terlihat. - Tidak boleh ada file
scratch/, seed klinis sementara, atau artifact/temp file di replacement PR.
-
Tambahkan test terfokus
- migration deploy pada database baru/fixture yang merepresentasikan data lama;
- peserta: umum, prodi-only, angkatan-only, gabungan prodi+angkatan, dan restriction kosong;
- operator mixed-scope tidak dapat melihat/mengedit di luar cakupan;
- import ketika unit pertama tidak punya topik tetapi unit lain punya topik.
-
Validation sebelum minta review ulang
npm run lint -- --max-warnings=7 npm run typecheck npm run test:unit npm run build npx prisma validate npx prisma migrate deploy
Silakan biarkan branch ini tetap ada untuk referensi. Setelah replacement PR kecil dan CI hijau tersedia, mention saya di sana; saya akan review ulang dari head baru.
|
@Jedijedai terima kasih untuk kontribusi Bank Soal per Jurusan dan filter Angkatan. Setelah review pada head Fakta yang terverifikasi:
Silakan buat PR replacement baru dari Branch |
Migrate Topik to use unitId instead of modulId. Add checkbox filter for angkatan in Ujian Editor.
Summary by CodeRabbit