SchoolMod 2.0 — desktop app rebuild - #3
Open
ZDStudios wants to merge 30 commits into
Open
Conversation
Ground-up rewrite of the browser-extension project into an Electron + React + TypeScript desktop app (Windows/macOS/Linux). Integrations - SEQTA: Microsoft SSO inside Electron's own browser (zero external deps), then the SEQTA JSON API directly — real name, student photo, timetable, assessments, grades, homework, notices, messages, reports. Python + Puppeteer kept as automatic fallbacks. - AI: Claude Code CLI (your Claude subscription) or OpenAI Codex (ChatGPT), one-click install + login. OpenAI-compatible wrapper as an advanced option. - Microsoft 365 via Graph device-code auth; Mathspace and Education Perfect. Study tools - Notebooks: local RAG over your own PDFs/DOCX/notes (BM25 + citations). - Flashcards: AI-generated, SM-2 spaced repetition. - Grades & averages, AI study planner, command palette, focus timer, to-do, scratchpad, daily AI brief. Notes - The predecessor's quiz-answer bots and network-filter bypass are intentionally not carried over. - Run with SCHOOLMOD_DIAG=1|concurrent|ai for a connectivity report. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fix "spawn claude ENOENT": npm's global bin is often absent from a GUI app's PATH, so findExecutable() now probes known install locations and remembers what it finds. Verified: PATH lacks %APPDATA%\npm yet Claude resolves and ai.ping/ai.chat both succeed. - Add OpenAI Codex (ChatGPT account) as a selectable AI provider alongside Claude Code and the OpenAI-compatible wrapper. - Port bell times from the original repo (Schools/Trinity College) as a live current-period tracker with a countdown to the next bell. - Assistant chat now fills the full tab height. - Strip UTF-8 BOM before parsing settings; a BOM silently reset every setting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Grafts the original repo's history onto the desktop rebuild so a pull request against main is possible and purely additive — every 1.x file (AutoBOT, Blooket, Kahoot, FortiGuard, Extension, Icons, Schools, Seqta, Mathspace, EducationPerfect, hw-bot.html, index.html) is preserved on this branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ling
The assistant is now an agent: it can call SchoolMod's own tools to read the
student's real SEQTA data and drive the app. Verified end to end —
"What assessments do I have coming up?" -> seqta_assessments -> real data
"What's my current average?" -> seqta_grades -> 66% + per-subject breakdown
"Set the accent colour to purple" -> app_set_accent -> applied
Tools: seqta_me/timetable/timetable_week/assessments/grades/notices/homework/
messages, bell_times, app_set_theme/set_accent/list_notebooks/create_notebook/
list_decks/create_flashcards/get_settings. Uses a text protocol rather than
provider-native function calling so it works on Claude, Codex and the wrapper
alike. Settings changed by a tool push a settings:changed event so the UI
updates live.
Microsoft 365 no longer needs an Azure app registration: it signs in through
Electron's own browser with the same school account as SEQTA and reads the web
apps. Verified: connect OK, 15 OneNote notebooks. Recent-files scraping is
weak (office.com's feed isn't plain anchors) — Graph mode remains available.
Also:
- Surface CLI notices ("you've hit your session limit", not-signed-in) as real
errors instead of returning them as if they were Claude's answer.
- Pin win/mac icon paths — the 1.x Icons/ folder at the repo root made
electron-builder mistake it for its icon directory and fail the build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rompt()) "New notebook" and "New deck" called window.prompt(), which Electron does not implement — it throws, so the click silently did nothing and both features looked completely broken. Replaced with a proper in-app PromptModal. Verified the rest of the pipeline was fine all along: notebook.create / addSourceText / ask (RAG) -> OK deck.create / generate (4 cards) -> OK Also stop Microsoft sitting on a loading spinner: the scraper polled up to 25s per attempt and recentFiles did it twice, so the lists spun for over a minute. Polling is now 11s and the OneDrive fallback only runs if the feed returned nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The onenote.com scraper was hitting a marketing landing page (0 results) — switched to the actual app at onenote.cloud.microsoft. Getting to real content took several confirmed root causes, each traced with a diagnostic before fixing (not guessed): 1. isSignedIn() was URL-only, so it false-positived on the logged-OUT marketing page at the same URL a signed-in session uses. Now checks page content and clicks "Sign in" when needed. 2. That check also false-positived on an EMPTY body mid-render (SPA shell loads before content) — added a "rendering, keep waiting" state. 3. The sidebar notebook rows are plain React text, not <a>/<button> elements (only ~2 real anchors exist on the page) — list scraping now parses document.body.innerText directly instead of guessing CSS selectors. 4. Clicking a name on the /copilotnotebooks landing page opens a Copilot Notebooks PROMO panel, not the notebook — now routes through "All Notebooks" to the real table first (matches the user's screenshot). 5. The real notebook row is a Fluent UI DataGrid item (role="row", not a link); a bare .click() didn't reach its handler — now finds the row ancestor and dispatches a full pointerdown/mousedown/pointerup/mouseup/ click sequence. 6. That click calls window.open() to a SharePoint Doc.aspx editor URL, which Electron silently blocks with no handler — now intercepted via setWindowOpenHandler and the same window navigates there instead. 7. The notebook editor renders inside an iframe, invisible to document.body.innerText on the outer page — content extraction now runs across every frame via WebFrameMain, with the MSAL TokenFactoryIframe (raw JS "content") explicitly excluded. Verified end to end against the real account: 3 notebooks listed, and readNotebook() returns real page titles (Course Outline, Geography/Civics/ History/Economics Homework Booklets) for the Humanities notebook. Added ms_onenote_notebooks / ms_onenote_read / ms_recent_files as agent tools, with explicit "never ask permission for a read-only action, always chain list->read in one turn" guidance — verified the agent now answers "what topics are in my Humanities notebook" with the real page list instead of just naming the notebook and asking to open it. Also: auto-scroll in the Assistant now sticks to the bottom through tool calls and streaming, and releases the moment the user scrolls up to read history (previously only fired on new messages, using "smooth" scroll that stuttered under rapid deltas). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a reusable WebFrame component (Electron <webview>, webviewTag enabled on
the main window) for viewing real school web apps INSIDE SchoolMod instead of
switching to an external browser:
- OneNote notebooks now open in a real, fully-interactive embedded editor —
clicking a notebook resolves its SharePoint edit URL (reusing the popup-
capture logic from readNotebook) and loads it in a webview on the same
signed-in persist:ms365 session, so it's exactly the real onenote.com
experience, just embedded.
- Word/Excel/PowerPoint/Teams/Outlook/OneDrive/To Do tiles open the same way.
- Mathspace and Education Perfect get their own "Open" buttons that embed
the site in-app rather than launching the OS browser.
Locked down via web-contents-created: no Node access in guest pages, and
navigation/popups restricted to an explicit allow-list of school-relevant
domains (anything else opens externally instead of loading in-app).
Multi-chat: the Assistant page now has a real chat list (create, switch,
rename, delete), each with its own message history, auto-titled from the
first message — replacing the single localStorage conversation. Old chats
migrate automatically on first load.
Verified via the agent diagnostic that theme control already worked both
directions: "Switch the app to dark mode" -> app_set_theme -> theme=dark,
then light -> theme=light. No code change needed there, just confirmation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Measured with instrumented timestamps (process.uptime()-based, immune to compiler import hoisting) rather than guessed: 1. ipc.ts eagerly imported every service module (openai, notebooks/rag, flashcards, msElectron, graph, claudeCli, codexCli, agent) at app boot, whether or not the user ever touched that feature. Converted to lazy, cached dynamic imports — one require the first time a feature is used, never again after. Measured: "module graph loaded" dropped from ~200ms to ~110-150ms, and total time to visible window dropped from ~615ms to ~420-510ms across repeated runs (roughly 25-30% faster). 2. index.ts's runDiagnostics() (SCHOOLMOD_DIAG-gated, dev-only) was importing seqta/claude at module top for every launch, diagnostic or not — moved those imports inside the function. 3. The bigger, real-world win: the PORTABLE .exe self-extracts its ~100MB payload on every single launch by default — measured 6-10+ seconds before the app even starts, vs ~0.6-0.8s for an installed/already-extracted app. Pinned portable.unpackDirName to a fixed name (was the per-build default uuid) so extraction can be reused across launches, and made the installer (Setup.exe) the explicitly recommended download in the README — verified end-to-end: silent-installed app shows its window in ~786ms on first ever launch. Left the SCHOOLMOD_PERF=1 timing instrumentation in place (mirrors the existing SCHOOLMOD_DIAG pattern) — it's a no-op unless that env var is set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ote import Assistant keeps running across tab switches: chat state (messages, streaming, active tool) moved out of the Assistant component into a zustand store (store/chat.ts) that lives above the route tree. Previously, navigating away mid-answer meant the still-arriving IPC stream chunks were setState calls on an unmounted component — silently dropped, so the rest of the answer was lost by the time you came back. Now the store's send() owns the IPC subscriptions and the promise chain itself, independent of any component's mount state, so a reply keeps streaming and gets persisted to localStorage no matter what tab you're on. Added a streamingChatId so the sidebar shows which specific chat is still answering if you've switched away from it. Computer access: new toggle in Settings, OFF by default. When on, the agent gets four tools — list a directory, read a text file, search filenames, open a path in its default app — all strictly READ-ONLY by design (no write, delete, move or execute, regardless of the toggle). Verified against the real Desktop folder: asked "list the files in my Desktop folder" -> correctly listed the real files with sizes. Direct import into Notebooks and Flashcards: a shared ImportSourceModal lets you pull SEQTA course/lesson content (by subject) or a OneNote notebook's pages straight in, no copy-paste. Backed by new seqta.courseContent() and seqta.subjectsList(), verified against the real account (23 subjects listed, course content fetched for Humanities). Also exposed as agent tools (seqta_subjects, seqta_course_content) so the assistant itself can pull course material into an answer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Write access: added computer_write_file (create/overwrite/append, makes missing parent folders). Still deliberately excludes deleting files/folders and running programs/commands — those remain unavailable regardless of the toggle. Updated the Settings copy, which previously and incorrectly promised "read-only, always." Verified with a ground-truth check independent of what the model claims: ran the agent, then read the file back from disk myself. ask: 'Write "SCHOOLMOD_WRITE_OK" to <tmp file>' tools called: computer_write_file GROUND TRUTH: file exists -> true | content -> "SCHOOLMOD_WRITE_OK" (An earlier run narrated a fictional "no such tool" failure without ever emitting the <tool> call — a one-off model inconsistency, not a code bug; re-run showed a clean, correct tool call on the first turn.) SEQTA: new "Courses" tab lists enrolled subjects (search-as-you-type); opening one shows its published files and lesson-plan content, reusing the seqta.courseContent()/subjectsList() backend already wired for notebook import. Verified against the real account (23 subjects). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ctions
Year filtering — SEQTA's /load/subjects returns EVERY period ever enrolled in,
not just the current one. Verified against the real account: 20 Year 7
subjects (2025S*) alongside the Year 8 ones. Crucially, a "code starts with 8"
guess would have been WRONG — 8AST/8ENS/8GAM are Year 8 codes from an earlier
semester. Filtering on SEQTA's own period string instead (it sorts
chronologically as text) correctly yields 13 current subjects, with a "Past
years" toggle to see the rest.
Lesson browsing — courseContent() now returns a structured lessons[] array
(term, week, title, notes, per-lesson files) rather than one flattened blob,
and the Courses tab has a lesson list + detail pane. Verified: 43 individual
lessons parsed for a real subject.
OneNote content — clicking a notebook showed nothing. Traced it: the embedded
WAC editor (onenoteframe.aspx?edit=0) renders ONLY its toolbar — stuck at
exactly 397 chars ("File/Home/Insert/Draw…") for 30s straight, zero
contenteditable nodes, no page canvas. It's a read-only viewer that never
loads the body, so no amount of waiting or selector-hunting would have fixed
it.
Fixed by reading the real structure from SharePoint's REST API instead: a
notebook is a folder in SiteAssets, sections are .one files inside, and Class
Notebooks nest them one level down in section-group folders (_Content Library,
_Collaboration Space, and the student's own name). Verified end to end —
readNotebook now returns ["Homework", "Using the Collaboration Space", "Using
the Content Library"] where it previously returned empty arrays.
(The onenote.com REST API was tried first and is cross-origin blocked;
SharePoint search also returns 0 rows for .one files, so the folder walk is
the path that actually works.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Desktop reminders before each period and for assessments due today/tomorrow, de-duplicated per day so the 30s tick can't spam. - System tray: closing the window keeps reminders alive; quit from the menu. - Start on login, launching quietly into the tray via --hidden. - Global quick-explain hotkey: copy anything, press it, and the assistant opens a fresh chat explaining it. - Backup export/restore. Credentials are deliberately excluded so a backup file is safe to email, and restore merges by id rather than overwriting. - Timetable + assessments export to .ics, hand-written to RFC 5545 (CRLF, escaped text, 75-octet folding). - Month calendar view of what's due, on the Planner. - Grade target calculator: the mark you need on what's left. - Study streak widget with a 12-week heatmap over the focus-timer log. - Command palette now searches your notebooks, decks, subjects and assessments, not just the app's fixed pages. Verified with SCHOOLMOD_DIAG=desktop: notifications supported, icon resolves, shortcut registers, and the generated .ics has matching BEGIN/END counts, CRLF endings, no over-length lines, and correctly escaped commas and semicolons. Also fixes pre-existing type errors that a typecheck surfaced along the way, including mcpClient re-exporting resolveCommand without binding it locally — which would have thrown at runtime on any MCP spawn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Toggles: the knob was `absolute` with no `left`, so it fell back to its
static position — which is centred inside a <button>. Measured in a
browser: the knob sat flush against the right edge when OFF, and hung
18px outside the pill when ON. Anchored it to `left-1` and moved the
travel to 0/20px, giving a symmetric 4px inset in both states.
Verified separately that toggle *persistence* was never broken: a new
SCHOOLMOD_DIAG=toggle flips all six booleans through setSettings() and
reads settings.json back off disk, both directions. All six passed, so
the bug really was only visual.
Bells: ported the rest of Schools/Trinity College/bell.html, which the
first pass had reduced to whole-minute counts. Adds second-precision
MM:SS countdown, 12-hour formatting, the Before School / School Ended /
Weekend phases with their exact labels, and the double-ring animation.
Two deliberate departures from the page's script:
- It subtracts 2000ms from the clock before comparing, running the
countdown two seconds behind real time. Dropped.
- It breaks out of its scan on the first later period, so a gap
between periods would report "School Ended". Trinity's schedules
are contiguous so it never fires there, but this version reports a
real 'between' phase instead of depending on that.
Verified by parsing the schedules out of bell.html and diffing against
the port (all 3 schedules match), then sweeping every 30s of Mon/Tue/
Fri/Sat for invariant violations (zero) and spot-checking the on-screen
strings at period boundaries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "Browse" tab renders the real SEQTA portal inside the app, so the
site's own pages are reachable without leaving SchoolMod — the same
treatment OneNote/Mathspace/Education Perfect already had.
It opens already signed in. The SSO partition ('seqta-sso') is
deliberately in-memory and is wiped on quit, so the webview can't reuse
it; prepareWebview() instead plants the JSESSIONID that ensure() has
already validated into a persistent partition. No second Microsoft
login.
The webview allowlist could not simply gain a hardcoded SEQTA domain —
every school runs its own host (Trinity's is students.trinity.wa.edu.au,
unrelated to seqta.com.au), so the host is derived from the configured
base URL at check time and applied to both the navigation and popup
guards.
WebFrame gained an `embedded` mode: it was `fixed inset-0`, i.e. a
fullscreen overlay, which cannot sit inside a tab. onClose is now
optional so a panel that lives in a tab has no stray close button.
Verified with SCHOOLMOD_DIAG=webview, which loads the real portal in the
partition and inspects the resulting DOM rather than assuming the cookie
was enough: final URL stayed on the portal (no redirect to
login.microsoftonline.com), title "SEQTA Learn", 1786 chars of real
content listing the current Year 8 subjects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lesson pane displayed a bare "1" for an entire lesson. Cause found
by dumping the real /seqta/student/load/courses payload
(SCHOOLMOD_DIAG=raw) instead of trusting the field names the parser had
been written against:
data.d — one entry per teaching week: { t: term, w: week, l: [...], n }
data.w — the lesson CONTENT, an array of arrays
`d[i].n` is an INDEX INTO data.w, not a note. The parser paired d[i]
with w[i] positionally and then rendered `item.n` as the lesson body,
so the body was literally the array index — "0", "1", "2".
Within a lesson, `t` is the title, `o` is the teacher-authored HTML body
and `r` the attachments. Measured on a real subject: 53 of 75 lessons
carry an `o` averaging ~61k chars. The document.contents module tree
only ever holds resources/mode — no prose — so `o` is the whole body and
the lessons without it are genuinely empty ("No Lesson- PCG/Assembly").
That HTML is the content, so it's now rendered rather than stripped to
text: tables, headings and colour-coded outcome boxes survive. It is
sanitised in the main process first (scripts, event handlers, frames,
javascript:/data: URLs) and drawn on a light surface, because SEQTA's
inline styles assume a white page — on the dark theme its unstyled text
would come out white-on-yellow. Plain text is still kept alongside for
search, the agent and notebook import.
Also fixes the embedded browser's toolbar running off the bottom of the
screen: it had a hardcoded 70vh that ignored the page header and tab bar
above it, so it now measures its own offset and fills what's left.
Verified against the real Science course: 75 lessons parsed, 53 with
HTML bodies, 0 whose body is a bare number (the bug), sample lesson
"8SCI01: Welcome to Year 8 Science" returning 1308 chars of real
outcomes text, and no <script>/on* handlers surviving sanitisation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Some lessons still read "No notes for this lesson" while others loaded.
Two different editors are in play across the same course:
- older lessons put rendered HTML in `o`
- newer ones leave `o` as an empty string and store the body in the
Lexical module tree, at document.contents -> modules[].content.html
The previous pass inspected modules[].content.VALUE, which only ever
holds file attachments, and concluded there was no prose in the module
tree. Wrong key — .content.html is where the body lives. Lessons now
fall back to walking the modules when `o` is empty.
On the real Science course this takes lesson bodies from 53/76 to
62/76. The remaining 14 are genuinely body-less: "No Lesson- Ash
Wednesday Mass", "Revision", "Practical: Physical Properties" — titles
a teacher created without content. Verified specifically against the
lesson reported as blank, "Kinetic Energy - Electrical, Thermal, Sound,
Light", which now returns 454 chars of real outcomes text.
Also: the SEQTA tab bar ran off the right edge once Browse made it 8
tabs, because every tab was flex-1 and forced onto one row. It now
wraps, so every tab stays reachable at any window width.
Worth noting for future work: d[i].n indexes past the end of data.w for
17 future weeks (T3W2-T4W8). That's correct — those weeks have no
content published yet — and they simply contribute no lessons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reports now open in-app: click one and the PDF renders inside the Reports tab (Chromium's built-in viewer via <webview plugins>), with Save PDF and an escape hatch to the system PDF app. Doing this surfaced that report downloads were broken and had been all along. openReport() fetched /seqta/student/files/stream?uuid=<uuid>, which returns HTTP 500 with an HTML error page for EVERY report uuid — verified across all 7. The old code never checked, wrote that HTML to a .pdf and handed it to the system viewer, so "Open PDF" produced a corrupt file rather than an error. Guessing at URL shapes went nowhere (12 candidates, all 404/500), so the answer came from SEQTA itself: loading its reports page in the signed-in partition and reading the anchors off the DOM. The real route is /seqta/student/load/file?type=report&file=<uuid> — the earlier probe had the right path but the wrong parameter names. Verified all 7 reports now return status 200 with a %PDF magic number, 84KB-500KB each, spanning Y07 and Y08, Interim/Semester/NAPLAN/Course Confirmation. Streams straight from SEQTA using the webview partition's existing session cookie rather than downloading first, so report cards no longer get left behind in the temp directory. Save PDF still writes a copy, but only where the student chooses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings -> AI features -> Enable AI features. Off means off: no request can reach a model. Enforced in the main process, not just by hiding buttons. Every channel that would reach a model (claude ping/status/install/login/chat/stream, agent chat, notebook ask/summarise/study-guide, deck generate) goes through a requireAi() guard and returns an error while disabled. The AI provider modules sit behind lazy() imports, so a blocked call also never loads them or spawns the CLI. The UI follows: the Assistant and Study Planner pages show an explanation instead of a dead page, the Assistant disappears from the sidebar and command palette, and the AI panels inside otherwise-useful pages (Mathspace tutor, Education Perfect coach, dashboard daily brief, notebook Q&A and summaries, flashcard generation) are hidden while their non-AI functionality stays. Quick-explain counts as an AI feature since it sends the clipboard to a model, so it no longer holds a system-wide hotkey while AI is off. The AI provider and computer-access sections are hidden too — configuring a provider you can't use is just noise. Nothing is destroyed: notebooks, sources and decks are kept, and flashcard review still works offline since spaced repetition is local. Verified with SCHOOLMOD_DIAG=aioff, driving the real IPC handlers: all 9 AI channels refuse with aiEnabled=false and all succeed with it true. The hotkey check initially read false in BOTH states — that turned out to be the installed app already holding Ctrl+Shift+E, not a bug; with it closed the result is false when off and true when on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuilds SchoolMod as a native desktop app (Electron + React + TypeScript).
Purely additive — no 1.x files are removed. Every original folder (AutoBOT, Blooket, Kahoot, FortiGuard, Extension, Icons, Schools, Seqta, Mathspace, EducationPerfect, hw-bot.html, index.html) is preserved.
What the app does
Download
https://github.com/ZDStudios/SchoolMod/releases/tag/v2.0.0
Not ported
The quiz auto-answer scripts, EP answer-extraction notes, Mathspace shop/avatar overrides and FortiGuard bypass were intentionally not rebuilt — they remain untouched in this branch and on main.