Repository navigation
Spec 10 Integrity Monitoring
Section of Quizzivy — Frontend Portal & Data Model Specification · Previous · Next
First-class requirement. Lives in src/features/integrity/, consumed by take-test (capture) and attempts (review).
Every signal produces an append-only event { kind, occurredAt, clientSeq, questionId?, meta? }.
| Kind | Source | Notes |
|---|---|---|
tab_hidden / tab_visible
|
document.visibilitychange |
Primary tab-switch signal; pair to compute away-duration. |
window_blur / window_focus
|
window blur/focus |
Catches alt-tab to another application — the Visibility API alone misses this. |
fullscreen_enter / fullscreen_exit
|
Fullscreen API | Only when requireFullscreen is on. |
copy / cut / paste
|
listeners on document
|
Always recorded; blocked only when blockCopyPaste is on. |
context_menu |
contextmenu |
Recorded; blocking off by default (it breaks assistive tooling). |
network_offline / network_online
|
navigator.onLine + fetch failures |
Distinguishes cheating from bad wifi. Matters for fairness. |
audio_play / audio_ended / audio_blocked / audio_seek
|
player (§11.4) | Gives the teacher listening behaviour. audio_seek is a jump the player put back on a recording that may not be skipped. The events only report: a question's play is counted by POST /app/attempts/:id/audio-play. |
start |
server-side | The attempt began. Written once, under the first session, at startedAt; it only gives that session a first row, so a flush it makes later is known to the server. It is no evidence that the tab is open. |
resume |
server-side | Re-entry into an in_progress attempt: reload, crash, device change. |
session_takeover |
server-side | Attempt opened in another tab/device. The teacher's timeline reads "Resumed in another tab or device". A tab that still holds the attempt's session never causes one: Continue in it reads the attempt with that session and resumes nothing (§9). |
page_hide |
pagehide |
Best-effort final flush via navigator.sendBeacon. |
Deliberately excluded: devtools-detection heuristics (window-size deltas, debugger timing). They false-positive on zoom, split-screen, and extensions, and are bypassed in seconds. Do not implement them.
Away-duration over count. A 2-second blur is a notification; a 90-second blur is a search. Store both endpoints so duration is visible, and only count a strike when an away episode exceeds minAwayMs (default 3000ms).
Announced, visible, never silent.
-
The intro page states the active rules in plain Vietnamese before starting, generated from the stored policy (§9). If
requireFullscreenis on, the intro's click that starts or resumes the attempt is what enters fullscreen (browsers require a gesture): "Bắt đầu" in the "Bắt đầu ngay?" dialog, or the intro's "Tiếp tục làm bài". Home's "Tiếp tục làm bài" (§9) goes straight to the paper and does not ask for fullscreen: the engine opens outside it and shows the fullscreen bar described below. A browser with no fullscreen is told so and takes the test as normal. -
Each counted absence opens the deck's
alertdialogwhen the student returns: "Bạn vừa rời trang làm bài", one body and one button, "Quay lại bài làm". The body says where the student stands and what happens next, and only what the server does:- within the allowance, "Lần này được tính là lần n trong m lần được phép…";
- past it under
flag, that the teacher has been told and the answers are safe (D8); - past it under
warn, the count and that the answers are safe, never naming the teacher; - with no absence allowed (
-1), that leaving is not allowed, with no number; - under
auto_submit, that the test is submitted after the allowance, and at the last one that the next absence submits it; - with no limit, that leaving is recorded, once a sitting.
The body does not state the seconds away or that the timer keeps running: the timer is on screen. The dialog does not close from the backdrop and has no close button; the button and
Escacknowledge it. -
A small persistent indicator shows remaining strikes when a limit is set: after the save line from 768, in the strip under the header below it (§9). Past the allowance it reads "Giáo viên đã được báo" under
flag.maxFocusLoss = 0retains the unlimited default;-1permits no counted departure; positive values permit that many departures. -
onLimitExceeded:warn= dialog only;flag= attempt marked for the admin, student told;auto_submit= immediate submission on exceeding the count, retaining answers for grading and recording the violation. There is no cancellation or extra strike. The final answer/event batch is saved before the server grades and closes. While offline, the attempt is locked locally, pending answers are retained and submission is retried with a visible notice. -
Fullscreen exit shows a bar under the header with a "Quay lại toàn màn hình" button, or one sentence on a browser with no fullscreen. It is a bar, never a dialog. Never trap the student:
Escalways works and there is always a visible way to leave and submit. -
Once the attempt is submitted on an assignment that requires fullscreen, by the student, the timer or an auto-submit, the engine leaves fullscreen itself. That exit is the app's, so from then on no
fullscreen_enterorfullscreen_exitis recorded, the student's own on the submitted screen included. A paper that is locked without a submit (taken over, already ended) stays as it is. -
Under
blockCopyPaste, a copy, cut or paste is stopped and recorded, and a danger toast says "Sao chép và dán đã bị tắt trong bài này": one toast, however often. -
Under five minutes the timer takes the danger tones (D8). A polite live region says the time left once when it passes 5:00 and once when it passes 1:00.
-
While the maintenance or sign-in-again overlay (§9) covers the engine, no focus change is recorded. An away episode already open when one appears is dropped, so time on an overlay never counts against the student.
-
The engine adopts the server's deadline from every autosave (
deadlineAt). It also adopts it from a timer submit that came too early (409 DEADLINE_NOT_REACHED; the server allows 5 s of grace). So a teacher's extension, or a maintenance window's, reaches an open attempt on its next autosave.
requireFullscreen: false, blockCopyPaste: true, maxFocusLoss: 0, onLimitExceeded: 'flag'. Conservative on purpose — the teacher opts into stricter modes per test.
What the teacher sees, by D8 (R4). One tone marks "Flagged" everywhere, the danger tokens (features/integrity/tones.ts): the dashboard's figure and its Recent activity marker, the assignment's Flagged count, the roster's row, the attempt sheet and the attempt page, and the flagged notification.
-
The Flagged count on Assignment detail's progress card (T-D4.22) counts the flagged attempts that are not voided: exactly the rows its filter shows, over the whole roster whatever the search. A flagged attempt that is later voided leaves the count.
-
The roster (the assignment's Students tab) shows Focus lost as "—" or "{n}×", in danger ink after a flag icon once the attempt is flagged, on a row in the danger's soft tone. Where the column is hidden, the same mark sits under the student's name.
-
The attempt sheet draws a compact timeline from the start (info) to "Submitted" (success, once handed in). Leaving the test and an automatic submission are danger, with the time away on the label ("Left fullscreen · 14 s"); every other kind keeps a neutral dot.
-
The rules keep §10.5's honest limits beside the integrity policy in the assignment wizard.
/teacher/attempts/:id → Integrity tab: ordered timeline with event kind, wall-clock time, offset from attempt start, duration for paired events, and the question on screen. Summary strip: total away-time, away episodes ≥ minAwayMs, paste count, resume count, audio replays. Neutral text — no red banners, no "CHEATING DETECTED". The teacher judges; the app reports. An audio play with no recorded end shows no duration and is not called ongoing.
Sessions are ordered by their earliest receivedAt, with sessionId breaking
ties. Within a session, start and resume come first and session_takeover last.
Within each priority, two events with different non-null clientSeq values
compare by clientSeq; otherwise by occurredAt and then event id. This order
is not a guarantee of wall-clock chronology across tabs or sessions.
The timeline draws no row for start: the synthetic "Started" row already
marks it, and an attempt whose only recorded event is start shows the empty
state.
Browser monitoring detects this tab losing focus. It cannot see a second device, a phone beside the laptop, a person in the room, or a printed sheet. The signals are evidence for a conversation, not proof. Do not build auto-zero or auto-ban features that assume otherwise.
- One
useIntegrityMonitorhook owns every DOM listener that records a signal or stops one, registered and torn down in a singleuseEffect. Four signals are recorded outside it, by a question's audio player throughrecordAudioEvent(§11.4):audio_playfrom its play callback,audio_endedwhen playback reaches the end,audio_blockedwhen the browser refuses to start it, andaudio_seekwhen the player puts a jump back. None changes a play count. A shared recording's play is written by the server (§11.5); the browser records its end and block withscope: groupandrecordingIdinmeta, without a question id. A known end duration is elapsed device wall time since the latest start, clamped to zero if the clock moved backwards; it is not total listening time. A resume overwrites that start; an end or block clears it. The timeline pairs the latest open play once, by question id first or otherwise by recording id. It accepts numericdurationMsfrom 0 through 86,400,000, truncating fractions to whole milliseconds; missing, malformed or out-of-range durations fall back to the nonnegative span between event clocks. Delayed play counts or clock skew can leave events unpaired. Overlapping sessions have no play-id matching. Other hooks listen to some of the same events and record and stop nothing:useClipboardNoticehas its owncopy,cutandpastelisteners ondocument, only to show §10.2's toast;useSaveStatuslistens toonlineandofflineonwindow, only for the save line (§9);useVersionWatchlistens tovisibilitychangeondocument, only to look for a newer build (§9). The monitor records and stops whether or not they run. - Events buffer in memory +
sessionStorage, flush with the autosave batch, and immediately onpagehideviasendBeacon. Leaving the engine while the attempt goes on sends what is buffered through the same beacon and keeps the sequence insessionStorage, so a return in the same session numbers on from it; the buffer is forgotten once the attempt has ended. A tab that begins a new session on an attempt whose stored buffer belongs to another session still holding events sends those events first, through the bearerPOST /app/attempts/:id/eventsunder their own session id, which the server accepts as one the attempt has recorded events under, in slices of at most 500, one after another; a refusal or a failure drops the rest quietly, and the new session is not held back. No request carries more than the contract's 500 events: an autosave or the pagehide beacon takes the oldest 500 and leaves the rest buffered, and an autosave that leaves events schedules the next one. The pagehide beacon takes the oldest events that fit about 60 kB, becausesendBeaconrefuses a body above 64 KiB, and removes them from the buffer only when the browser took them. Submitting, manually or byauto_submit, flushes again while events wait, so none is dropped when the attempt ends. A page that is unloading, not one that may return from the back/forward cache, records nothing afterpage_hide, so a reload leaves no open away episode. -
clientSequses an attempt-relative millisecond offset, calibrated bygetAttemptfromstartedAtandserverTime, and is never below the tab's stored next sequence. It increases within a tab. The offset component caps at 2,000,000,000; subsequent local increments can exceed that value. Response latency, clock changes and local increments can cause reordering or collisions across tabs, even for events in different milliseconds. Cross-tab chronology and uniqueness are not guaranteed. Older builds still send counters from zero, which the server accepts. The append-only dedup key remains(attempt, session, clientSeq); a repeated key is treated as a retry and not stored. AclientSeqabove 2,147,483,647 or below 0 answers400on every path that takes events, the beacon included, and writes nothing. - A flush names the session it was written under. The server accepts the
attempt's current session, or one that began with a
startorresumeand so has a row of its own (a superseded tab finishing its flush), and answers403 FORBIDDENfor any other id without writing. A flush never changes which session is current. - Failed background event flushes do not block answering or manual submission.
The immediate
auto_submitpolicy retries its final answer/event batch before confirming submission so the violation is retained with the answers. - An event transport failure alone does not block input. The explicit
auto_submitpolicy locks further answering once exceeded, while preserving and retrying the pending answers.
Section of Quizzivy — Frontend Portal & Data Model Specification · Previous · Next
Generated from docs/quizzivy-spec-v0.3.md. Edit that file in the repository; changes made on this page are overwritten.
Generated from the repository's documentation on every push to develop.
Start here
Product
Implementation plan
- 00 Architecture overview
- 10 Phase 0: Scaffold and contract
- 11 Phase 1: Auth, join, shells
- 12 Phase 2: Admin authoring
- 13 Phase 3: Assign and take
- 14 Phase 4: Grade and results
- 15 Phase 5: Hardening
- 16 Phase 4.5: The student at a desk
- 17 Word import: milestone plan
- 18 Word import: teacher workflow
- 19 Versioned content contract
- 20 Data model
- 30 Risk register
- 40 Open items
- 50 Non-functional requirements
- 60 Backend architecture
- 91 September 2026 issue remediation
- 92 Student layouts and interaction rules
Redesign (Phase R)
- 70 Redesign overview
- 71 R1: Foundations (v0.7.0)
- 72 R2: Access (v0.8.0)
- 73 R3: Student console (v0.9.0)
- 74 R4: Teacher workspace (v0.10.0)
- 74d D4: Deck update (v0.11.0)
- 74e E1: Moving a question across a group
- 75 R5: Admin console and email (v0.12.0)
- 76 R6: Question types and scoring (v0.13.0)
- 77 R7: Collaboration (v0.14.0)
- 78 R8: Schedule (v0.15.0)
- 79 R9: Insights (v0.16.0)
- 80 R10: Learn (v0.17.0)
- 81 R11: Landing, leads, v1.0 (v1.0.0)
Setup and operations
- CI
- Deploying
- DNS for quizzivy.com
- Google OAuth client
- Production operations
- Cloudflare R2 bucket
- Word import worker
- R4 rehearsal on a Neon branch
Design
Team
- Agent team
- Verification environment
- Team ledger
- R4 readiness
- R4 verification strategy
- T-R4.51 browser verification
Code READMEs