Plan: Transitioning to Forward Deployed Engineer / AI Engineer
백엔드 엔지니어가 FDE·AI 엔지니어로 전환하기 위한 단계별 로드맵
학생용 무료 시간표·숙제 앱, 순환일·눈날·지연·점심 파동 관리, 오프라인 사용 가능, 광고 없음
Free school schedule and homework app for students. Handles rotating cycle days, snow days, two-hour delays and lunch waves. Works offline, no ads.
교육TypeScript · CSSGitHub @Zzza38온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기A scheduling app
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Production domain: https://getquasarapp.com. The domain move runbook covers cutover and the notice at the previous address.
Phases 1 to 4 of PLAN.md: a personal school schedule and task tracker with a school community, friends-only one-to-one chat and one public Global chat room. The UI is a phone-first app with seven views (Today, Schedule, Tasks, Classes, School, People, Messages) at their own paths, structured editors for every schedule concept, and an always-visible sync status. UI_REDESIGN.md records the screen inventory and design decisions.
Built with Next.js App Router, TypeScript, Tailwind CSS with shadcn/ui (Radix primitives, lucide icons), tRPC, Google OAuth through NextAuth, SQLite, and IndexedDB. There is no demo login or production authentication bypass.
The shadcn-owned components live in src/components/ui/ (managed by components.json; add more with npx shadcn@latest add <name>). App-level composites built on them (Button, Field, Chip, Callout, Modal, Segmented, Section, …) live in src/components/primitives.tsx.
Requires Node.js 22.12+ (Node 24 recommended) and npm. npm is the only supported package manager: package-lock.json is the only lockfile, and CI and the Dockerfile install from it with npm ci. Do not install with pnpm (see docs/OPERATIONS.md).
npm ci
cp .env.example .env.local
# Fill in the variables below, then:
npm run dev
Open http://localhost:3000. Create a Google OAuth Web application client, configure its consent screen/test users, and add http://localhost:3000/api/auth/callback/google as an authorized redirect URI. Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, NEXTAUTH_URL, and a strong NEXTAUTH_SECRET in .env.local. Generate a secret with openssl rand -base64 32. Set OWNER_EMAIL to the owner's verified Google email to enable /admin; roles are enforced by the server. Names are entered during onboarding independently of Google profile names.
Google credentials and a public domain are required for real sign-in and deployment; neither is supplied in this repository. Schedule logic and synchronization tests run without credentials.
npm test
npm run typecheck
npm run build
npm start
Browser tests use an isolated temporary database (removed when the run ends; set E2E_KEEP_DATABASE=1 to keep it for debugging) and encrypted test-session fixtures, without changing production authentication. The test server (and scripts/ui-sweep.ts) listens on 127.0.0.1 only and blanks the scan-provider and VAPID keys, so production keys in .env.local do not reach it:
npx playwright install chromium
npm run test:e2e
Run a build first. Optionally set PLAYWRIGHT_CHROMIUM_EXECUTABLE to an existing Chromium binary. The build and development commands use Webpack; the container enables Next's standalone output with QUASAR_STANDALONE=1.
Shared-school editing and joining require a connection. Saved personal classes, tasks, assignments, custom schedules and overrides can be edited offline after a successful signed-in load. The service worker requires HTTPS except on localhost. Sync resumes when the app is open and connectivity returns; it does not require browser background-sync support.
Phase 3 adds the People view: school verification (automatic by school email domain, or by support review of a proof), a member directory with verified-only full names, friend requests, friends' classes and today's timetable, blocking and reporting. Locked schools get schedule-change voting in the School view, and the support admin page gains verification requests, member reports, passed proposals awaiting publication, and per-school email domains.
Phase 4 adds Messages: one-to-one chat between accepted friends, specified in docs/CHAT.md, plus one public Global chat room (CHAT.md §11) that every member with names entered can read and post in, across schools. The owner moderates the room directly (edit or remove any message with a reason everyone sees, audited), it has no block or report button of its own, but a block made in People or a one-to-one chat hides the blocked member's room messages from the blocker (one way), and each member can mute it. Slurs are censored to asterisks in both kinds of chat. On phones it opens from an icon in the top bar; on desktop it is a sidebar item. Chat is online only and polls for new messages; nothing about it is stored on the device except the unread count. Removing a friend or blocking closes the chat for both people at once and leaves a report-only row for 30 days. Reports freeze up to 30 messages from that chat, and the owner sees one-to-one messages only by opening the report, which is logged. The admin page gains Show messages, Hide message, timed messaging pauses and a Paused members section. Chat pushes say only "You have new messages.", wait 60 s, are capped at one per 10 minutes and 20 a day, and stay quiet from 22:00 to 07:00. Turn them off under Account → Notifications → Message notifications. The background worker also deletes old chat data on the schedule in docs/ARCHITECTURE.md.
Phase 2 adds private iCal subscriptions in Schedule, imported calendar items in Tasks and Schedule, task priorities and checklists, recurring tasks, and opt-in browser push reminders. The new controls use the existing cards, sheets, form fields and status indicators. Member directories, friends, chat and voting came later, in phases 3 and 4 above; phase 2 itself exposed no other student's name or schedule.
Add an iCal subscription under Schedule → Connected calendars. The calendar's time zone starts as the school's: imported due dates and times are converted into it (times the feed leaves unzoned are read in it). The task list and overdue checks read them in the school's zone, while reminders on imported items count from the calendar's zone, so a calendar set to another zone reminds by that zone's clock but turns overdue by the school's; change it only with a reason. Calendar refreshes match source identities to existing tasks and preserve completion, checklists, priorities and reminders. Changes to details that were also edited locally prompt a choice. Missing source items remain saved and marked as removed. Refresh failures keep the last successful data. Pausing stops automatic updates; removing a subscription keeps its items as regular tasks. Calendar subscriptions and source choices require a connection; saved tasks and their richer fields use the existing offline queue.
Task repeat settings support daily, weekly and monthly intervals and an optional inclusive end date. Task dates, like school dates, run from 1900 through 2199, and a repeat that would step past 2199 simply ends. Completing a task creates the next occurrence when the completion syncs. Monthly repeats retain the original day, clamping to shorter months. The next occurrence resets task/checklist completion and keeps the other task settings. Each completion records when it happened, so Tasks lists completed work most recent first, 50 at a time with Show more (tasks completed before this was recorded follow). Retries and reopening the same completed task never create another successor; deleting a successor does not cause it to be recreated. Imported items follow their source calendar rather than manual repeat settings.
Task reminders use the saved reminder time zone and due time, or 9:00 AM when only a date is set. Enable browser notifications from Account on each device. Reminders require HTTPS, browser permission, server VAPID configuration and the background worker. Browser/provider availability affects delivery; reminders are not alarms.
Run npm run worker alongside the web process with the same .env.local, database path and VAPID keys. It periodically refreshes calendars and delivers due reminders. When a new support item arrives (a correction request, feedback, a verification request, a report or a proposal awaiting support), the owner's enrolled browsers get a generic "A new support request is waiting" push that opens /admin and carries no request text, names or emails. Keep one supervised worker running in production. Configure VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY and VAPID_SUBJECT as described in .env.example; never commit private keys. Without VAPID keys, or with keys that are malformed or do not form a pair, calendar refreshing still works and browser notification enrollment remains unavailable. Manual calendar refresh is available in the app.
The current private installation has its worker and VAPID keys configured. Local automated checks do not establish delivery to a real device; enable browser reminders in Account and test a task reminder.
Students can add classes from photos of a printed or on-screen timetable (Classes → Scan timetable). A scan may carry up to three photos, for example both halves of a timetable. The browser downsizes each photo, the server sends them to any OpenAI-compatible vision model along with the school's period IDs and the grade-filtered class directory. The model can ask the server for up to six enlarged close-ups of parts of the photos (contrast-enhanced or rotated if it wants) before it answers, and it says the photo is too unclear instead of guessing; rows it is not sure of are flagged for the student to check. The student confirms or edits every row before anything is saved. A row keeps the model's directory match only when the printed name could be that class: the same words in any order, or a shortened name at the same level ("AP Chem" for "AP Chemistry"), never a different number, honors or AP level. Photos are not stored. Configure SCAN_API_URL, SCAN_MODEL and optionally SCAN_API_KEY, SCAN_MODEL_REASONING, SCAN_MODEL_TEMPERATURE, SCAN_MODEL_VARIANT (on OpenRouter :floor, the cheapest provider, by default) and SCAN_ZOOM as described in .env.example; the feature stays hidden until both the URL and model are set. Examples: OpenAI gpt-6-luna, DeepSeek deepseek-chat, OpenRouter (https://openrouter.ai/api/v1) openai/gpt-6-luna, or a local Ollama qwen3-vl:8b. Each account may run 10 scans per hour and 30 per day. The quotas count scans, not photos, so a three-photo scan uses one. Each scan is recorded in audit_log as schedule.scan. While a scan runs the student sees its steps, a timer and each close-up the model asks for: the browser sends a random progressId with the scan and polls scan.progress once a second, and the server keeps that state in memory only until the scan ends. On OpenRouter the scan also asks for a summary of the model's thinking (OpenAI models hide the thinking itself), shown as a one-line "What the scanner is thinking".
Type savanah outside editable fields to spawn 80 broken hearts. They bounce around the viewport, dodge the cursor using the I'm Shy physics, and fade away after 7 to 9 seconds. Reduced motion keeps them still. The effect is available to everyone and has no mod-menu entry.
Personal timetables follow shared school bell times. A recurring correction can be for the school or only for the student. Personal adjustments save changed slot fields, inherit untouched school times, and clear when the school adopts them. Matching adjustments from at least three other classmates in the same grade can suggest a school proposal. Full private timetable controls live under advanced settings and warn that school corrections will no longer reach that timetable.
A personal schedule without a saved lock preference starts locked. Unlock schedule requires confirmation before editing classes, assignments, or timetable settings. Lock schedule protects all of them again. The choice syncs with the account and survives reloads. Homework tasks stay editable.
Schedule following is available to everyone and does not depend on beta enrollment. Removing the former schedule-following beta does not disable it or replace saved adjustments. Existing queued edits and retry payloads keep their original revisions. Older clients that cannot read sparse adjustments receive a reload message before an incompatible write commits.
A beta lets chosen members try a change on the real site (https://getquasarapp.com) before everyone gets it. There is no separate beta URL or build: the beta code ships in the normal production build behind a per-account flag. A member joins in Account → Betas, and the flagged code turns on for them on every device. Everyone else keeps the current version.
A beta is private or public. A private beta is joined by typing its code, so only people you give the code to get in. A public beta has no code: it is listed in Account → Betas and anyone can join it with a tap. Betas are private unless you ask for public.
Betas are created with a command, not from /admin. The command edits the beta list, betas.json, which sits beside the database (data/betas.json locally, the data volume in Docker) or at BETAS_PATH. The list holds the join codes in plain text, so it is gitignored and never committed. The server re-reads it whenever it changes: creating, pausing or removing a beta takes effect immediately, without a deploy or restart.
Pick a key: lowercase letters, numbers and dashes, such as month-grid. Wrap the new UI in <Beta> from src/components/betas.tsx, with the current UI as the fallback:
import { Beta, useBeta } from '@/components/betas';
<Beta id="month-grid" fallback={<WeekSchedule state={state} />}>
<MonthGrid state={state} />
</Beta>
For logic rather than a whole component, branch on useBeta('month-grid'), which returns true only for members in that beta. If you need the flag on the server, new BetaService(service).joined(userId) in src/server/betas.ts lists the member's betas.
<Beta> also catches crashes. If the beta UI throws, the member sees the fallback with a notice that offers Leave beta and Report it, not a broken page.
Keep beta changes additive. All members share one database, so any migration a beta needs runs for everyone as soon as it deploys. Add columns and tables. Don't rename or drop anything a non-beta code path still reads.
Merge and deploy as usual. Until the beta exists in the list, nobody is in it, so the deploy changes nothing for anyone.
Run the command in the checkout whose server should have the beta. For the live site that is the production checkout, /srv/NAS/Private/GitHub/quasar: the command loads .env.local there, so it edits the same list the live server reads.
npm run beta -- create month-grid --name "Month grid" --description "Try the month view in Schedule." --max 30
--name and --description are what members see in Account. --max caps how many people can join, so a leaked code (or a popular public beta) can't pull in the whole school. Add --public to list the beta in Account → Betas with no code; everything below about codes is for private betas. Leave out --code and the command generates one that is easy to read out, like TQJKH-VMQWP. Pass --code "GRID-2026" to choose your own: 4 to 64 letters and numbers, where case, spaces and dashes don't matter (GRID-2026 and grid 2026 are the same code). The command warns about codes under 8 characters, since those are easy to guess.
The command prints the code. /admin → Betas also shows every private beta's code with a Copy button, so you can look it up any time.
https://getquasarapp.com/?beta=off (any view path works too) leaves every beta before the page renders, so it still works if a beta breaks the app badly enough that Account won't open.Feedback lands in /admin → Inbox with a "Beta feedback: " chip and a Diagnostics section. It counts toward the same five-open-requests limit as other support requests.
npm run beta -- list # every beta, its code and status, and where the list is
npm run beta -- pause month-grid # the code stops working; members already in stay in
npm run beta -- open month-grid # the code works again
npm run beta -- new-code month-grid # a fresh generated code; members stay in
npm run beta -- new-code month-grid --code "GRID-2027" # or one you choose
npm run beta -- public month-grid # listed in Account → Betas, no code; members stay in
npm run beta -- private month-grid # back to a (new) code; members stay in
npm run beta -- edit month-grid --name "Month view" --max none
npm run beta -- remove month-grid # ends the beta: it switches off for everyone in it
remove retires the key, and the command refuses to reuse it, so the old members can't end up in a new beta that happens to share the name.
/admin → Betas is read-only apart from Remove under a beta's Members, which takes one person out. It shows each beta's code, status, member count, members and open feedback. Joins, leaves and removals go to the audit log as beta.*.
A beta is done when its PR merges, unless you decide to keep testing it. Before merging, delete the <Beta>/useBeta branch in the PR and keep the new code path. Merge and deploy, then run npm run beta -- remove <key> in the production checkout. To abandon a beta instead, delete the new code path and remove it the same way.
The owner is the account in OWNER_EMAIL (pinned by OWNER_GOOGLE_SUB). Other people can help run Quasar with a staff role. Each role is a fixed set of permissions, defined in src/domain/staff.ts:
| Role | Can do |
|---|---|
administrator | Everything below, plus suspending accounts |
moderator | Reports and their chat messages, messaging pauses, removals from a school, Global chat moderation, and the appeals against pauses and removals |
support | Correction requests and beta feedback, verification, member search, opening a member's record, signing a member out and forgetting their browsers |
schedule-editor | Correction requests, passed proposals, shared school timetables and names, and every school's class directory and lunch menu. Changing a school's email domains needs verification, because domains verify students |
These stay with the owner: the whole audit log, betas, mod overrides, editing a member's profile, classes, tasks, feeds and friendships, and moving a member to another school. Staff see a member's record without tasks or feeds, and without the address and browser on audit rows.
Staff never act on the owner, on other staff or on themselves. Only the owner sees reports that involve staff, whether as reporter, reported member or a sender in a group report's messages, and only the owner sees requests, removals or record entries about staff. Staff need a Google sign-in from the last two hours, like the owner, for the console and for editing other schools' directories and menus. Their actions go to the audit log and journald with their address and browser. In Global chat, staff messages carry a role badge, and a moderation note names the role ("Removed by a moderator").
Chat text that moderation took out is shown by tier: member, then moderator, administrator and owner. Support and schedule editors count as members here. Only the server censors and removes. A member is never sent what was taken out. The server keeps the rest for those allowed to read it (src/server/moderation.ts):
Roles are granted with a command, never from /admin. No role can grant roles in the app, administrator included. Run it in the production checkout for the live site. A change applies at the account's next request, with no deploy or restart. The account must have signed in once, and every change goes to the audit log under the owner as staff.grant or staff.revoke.
npm run staff -- roles # every role and its permissions
npm run staff -- list # current staff
npm run staff -- grant [email protected] administrator # or an account ID; replaces any role they had
npm run staff -- revoke [email protected]
Every push to master and every pull request runs .github/workflows/ci.yml: typecheck, the unit suite and the production build on Node 22 (the engines floor) and Node 24 (what production and the Docker image run), then the Playwright suite in Chromium against the Node 24 build. A separate job builds both Docker images (the standalone app and the worker target), checks that the app container answers /api/health and that the worker container starts. Failed browser runs upload test-results/ (traces and screenshots) as an artifact.
Phase 2 verification completed on 2026-09-11: 138 unit/integration tests, all ten Chromium browser scenarios, TypeScript checking and the production build pass. Browser checks cover existing onboarding, schedule editing, offline sync, conflicts, authorization and responsive layouts, plus rich recurring tasks and completed imported calendar entries. Quasar and its background worker are running in production at https://quasar.ziona.dev/ through Cloudflare Tunnel (see operations); the private health endpoint and database integrity check pass. A verified database backup was saved before deployment. Google sign-in completion, a real school feed and delivery to a real browser remain interactive pilot checks. Docker execution was not tested because Docker is unavailable on this host; CI's Docker job now builds and smoke-tests both images instead.
src/domain/: shared Zod models, schedule engine, merge logic and tests.src/server/: Google auth, SQLite schema, domain service and tRPC authorization.src/client/: typed API and durable offline storage.src/components/: replaceable React presentation.Compatibility was checked against the Next.js installation guide, tRPC fetch adapter, and installed package declarations. Exact dependency versions are pinned in package-lock.json.
Choose a grade when joining a school; existing students can choose it from the setup reminder, Account, or School. Grade changes use the selected grade's bell schedule and lunch times. Personal timetable copies and explicit adjustments stay intact.
Classes → Browse school classes opens the searchable, grade-filtered directory. Select multiple classes, add them, then drag them onto school blocks (or tap a class and then a block; touch and hold to drag with a finger). Personal edits affect only your copy. Shared directory editing follows the school's permanent ten-member lock and support lock; the owner can manage directories in Support → Schools → Classes. Locked entries have a correction-request action. Directory changes and removals preserve existing personal copies.
The additive SQLite migration creates school_classes; existing schedules and tasks are retained. Only with the owner's explicit permission, publish their saved class names, rooms, and teachers with node --env-file=.env.local --import tsx scripts/seed-directory.ts. Back up first. This command uses the configured owner, is repeatable, and preserves personal class IDs, colors, assignments, and sync history. It does not publish other students' personal classes.