From d8661e84a013239fbc4386abd5472f6a70112e5e Mon Sep 17 00:00:00 2001 From: legrab Date: Thu, 16 Jul 2026 10:13:37 +0200 Subject: [PATCH 01/10] plan(Editors): provide a holistic plan to extend databank with editors plan(Follow-ups): plan an MVP-closure session to take off rough edges --- docs/plans/POST-CORE-MVP-FINDINGS-PLAN.md | 249 +++++++++ docs/plans/TEACHER-EDITING-AND-MANUAL-PLAN.md | 480 ++++++++++++++++++ 2 files changed, 729 insertions(+) create mode 100644 docs/plans/POST-CORE-MVP-FINDINGS-PLAN.md create mode 100644 docs/plans/TEACHER-EDITING-AND-MANUAL-PLAN.md diff --git a/docs/plans/POST-CORE-MVP-FINDINGS-PLAN.md b/docs/plans/POST-CORE-MVP-FINDINGS-PLAN.md new file mode 100644 index 0000000..ad8a43d --- /dev/null +++ b/docs/plans/POST-CORE-MVP-FINDINGS-PLAN.md @@ -0,0 +1,249 @@ +# Post-core MVP findings plan + +Status: detached follow-up backlog +Execution order: after the gates in [TEACHER-EDITING-AND-MANUAL-PLAN.md](./TEACHER-EDITING-AND-MANUAL-PLAN.md) +Scope: findings that improve coherence, depth, resilience, and polish without blocking the first teacher setup-to-insight loop + +## 1. Purpose and triage rule + +This plan records useful findings that do not belong in the critical teacher-flow plan. It is intentionally broader and less coupled: items may be selected independently after the core MVP is usable. + +Before implementing an item, recheck its impact. If it can cause data loss, break logout/navigation/presentation/live acknowledgement, alter delivered history, or prevent a teacher from completing a core journey, promote it to the core plan instead of treating it as polish. + +Priority labels: + +- **S1 — solidify:** high-value work for the first hardening cycle after core MVP acceptance; +- **S2 — deepen:** worthwhile product or engineering depth after S1; +- **S3 — explore:** optional or strategic work that needs separate discovery. + +## 2. Findings register + +### 2.1 Information architecture and navigation + +#### F-IA-01 — Group navigation by teacher intent (S1) + +The current sidebar is a flat list mixing daily work, creation shortcuts, outcomes, projects, administration, and help. Several implemented routes—Courses, Groups, Learners, Locations, and Annual Plans—are not exposed, while `Új feladat` receives a permanent top-level slot. + +After the core editors settle, reorganize the shell into stable groups such as Today, Plan, Teach, Review, Library, and Workspace. Use a single Create action for context-aware creation rather than one privileged direct shortcut. Preserve current route compatibility and presentation escape paths. + +Acceptance notes: + +- active state works for list, detail, create, and nested manual routes; +- collapsed and narrow-screen navigation remains keyboard accessible; +- no destination disappears solely because a feature flag is off; it is either hidden intentionally or explained as unavailable; +- a teacher can still reach logout and learner join without scrolling through a long content menu. + +#### F-IA-02 — Replace raw pathname headings with route metadata (S1) + +The top bar currently shows values such as `/guide` and `/lessons/lesson.demo-presentation`. Introduce typed route metadata/breadcrumbs so the shell displays localized entity names and parent context. Avoid duplicating a second page title when the page already supplies one. + +#### F-IA-03 — Clarify Library versus Assignments naming (S1) + +The Hungarian label `Feladatok` can mean reusable Exercises or assigned work, while `Új feladat` creates an Exercise and `Feladatok` opens Assignments. Validate terminology with teachers and use distinct, consistently applied labels such as reusable exercise/task versus assignment. Update routes only if redirects preserve bookmarks. + +#### F-IA-04 — Cross-entity recents, favorites, and command navigation (S3) + +Once normal lists and search are reliable, explore recent items, pinned/favorite materials, and a keyboard command/search surface. Do not build this before list discoverability and permissions have stable semantics. + +### 2.2 Visual, responsive, and interaction polish + +#### F-UX-01 — Responsive shell and dense-editor layouts (S1) + +Audit sidebar, table overflow, split previews, drag handles, and presentation controls at common laptop, tablet, and phone widths. Use the narrow layout for review and emergency lesson control, not only learner pages. Reset any temporary test viewport and add visual/browser coverage at agreed breakpoints. + +#### F-UX-02 — Accessibility pass (S1) + +- verify logical heading hierarchy and landmarks; +- provide visible focus states and skip-to-content; +- ensure error summaries announce and focus invalid fields; +- label icon-only move/drag actions with item context; +- provide keyboard alternatives for drag/drop; +- review contrast for chips, warnings, muted text, disabled states, and projected content; +- expose tables, live response counts, timers, and save state to assistive technology; +- honor reduced motion and avoid color-only status communication. + +Use automated checks as a floor and complete keyboard/screen-reader-oriented manual checks for critical pages. + +#### F-UX-03 — Standard feedback and destructive-action language (S1) + +Unify success, saving, warning, conflict, empty, error, and retry presentations. Replace browser-default or absent confirmations with focused dialogs for archive, discard, reset, assignment issue, assessment generation, reveal, and completion where consequences warrant them. Avoid notification noise for routine autosaves. + +#### F-UX-04 — Status and date presentation (S1) + +The UI currently exposes codes including `published`, `assigned`, `warning`, and ISO dates. Create shared Hungarian labels, status semantics, and locale/timezone-aware formatters. Show relative context only alongside an unambiguous date/time where deadlines or schedules matter. + +#### F-UX-05 — Deliberate empty and loading states (S2) + +Replace generic `Betöltés…`, blank sections, and demo-oriented prompts with skeletons or small state components that explain what is loading, what an empty list means, and the next valid action. Preserve errors instead of turning them into emptiness. + +#### F-UX-06 — Visual consistency audit (S2) + +After component behavior stabilizes, audit typography, spacing, form widths, card density, table alignment, action hierarchy, chip semantics, editor chrome, projected content, print styles, and dark-theme readiness. Consolidate repeated CSS patterns without introducing a parallel design framework. + +### 2.3 Internationalization and content language + +#### F-I18N-01 — Remove mixed-language product copy (S1) + +Visible copy currently mixes Hungarian with `Today`, `Fonat Guide`, `Deterministic delivery`, raw slide types, status codes, and English error message keys. Complete one coherent Hungarian MVP vocabulary first. Keep canonical English terms in the manual where they aid package or API understanding. + +#### F-I18N-02 — Introduce localization boundaries (S2) + +Move UI strings, status labels, validation messages, and domain names into feature-owned localization namespaces. Format dates, numbers, percentages, and plural forms through locale-aware helpers. Do not translate user-authored content automatically. + +#### F-I18N-03 — English interface (S3) + +Only after Hungarian terminology and workflows are stable, add an English interface and manual set with parity checks. Define fallback behavior and a missing-translation build report. + +### 2.4 Product depth beyond the first teacher loop + +#### F-PROD-01 — Advanced timetable depth (S2) + +Extend recurrence and overrides with cancellations, moved lessons, exceptions, historical views, term breaks, and better overlap details. Institutional room availability and automated timetable solving remain separate products. + +#### F-PROD-02 — Broader evidence workflows (S2) + +Add evidence capture from teacher observation, lesson notes, assignment answers, confidence, correction quality, and rubric decisions. Provide provenance and correction, never hidden automatic mutation. Explore learner and Concept timelines only after privacy and retention rules are explicit. + +#### F-PROD-03 — Assessment analysis depth (S2) + +Beyond the five core analyzers, explore cohort comparison, item-quality history, misconception clusters, coverage trends, export, moderation, and reduced/deferred assessment chains. Clearly separate descriptive evidence from causal claims. + +#### F-PROD-04 — Print and offline classroom resilience (S2) + +Complete print layouts for lessons, teacher sheets, assignments, assessments, and answer keys. Explore graceful offline/read-only lesson access and reconnect messaging without promising offline writes before conflict behavior is designed. + +#### F-PROD-05 — Project capability (S3) + +The Project surface is a feature-toggled fixture viewer. Run separate discovery for project authoring, contributor opportunities, challenge sequencing, evidence, outputs, and teacher controls. Keep it isolated until the common Resource/Exercise/Assignment contracts can be reused cleanly. + +#### F-PROD-06 — Rich assets and providers (S3) + +Define the local-rich asset provider, upload limits, MIME validation, accessible metadata, thumbnails, replacement/version behavior, and structured video/provider records. Maintain learner-safe projection and content-rights metadata. + +#### F-PROD-07 — Import/export user experience (S2) + +Complete staged package import, bounded ZIP validation, manifest/schema report, reference validation, diff, atomic apply, update impact, teacher-fork preservation, and round-trip export. Treat the existing raw workspace JSON export as an administrative diagnostic, not the finished teacher exchange workflow. + +### 2.5 Architecture and maintainability + +#### F-ARCH-01 — Decompose the Fastify application factory (S1) + +`apps/server/src/app.ts` owns authentication, generic collections, presentation, live, assignments, assessments, packages, and manual delivery. Move each growing workflow into its feature slice with route registration, schemas, application service, and tests. Keep the app factory responsible for composition and shared middleware. + +#### F-ARCH-02 — Replace `any` and generic entity assumptions (S1) + +The web app extensively uses `api`, generic tables, and open records. Generate or share typed response/command contracts, add query-key helpers, and model discriminated unions for Nodes, Exercises, slides, results, and feature errors. Avoid broad type assertions and duplicated frontend validation. + +#### F-ARCH-03 — API error and concurrency client (S1) + +The client currently collapses typed server errors into `Error(messageKey)`. Preserve error code, retryability, field errors, technical reference, and HTTP status. Add helpers for conditional writes and explicit conflict recovery. Centralize session-expiry handling without masking other 401/403 cases. + +#### F-ARCH-04 — Collection-specific validation (S1) + +Only Exercises have meaningful creation validation in the generic collection loop. Add schemas and invariants for every mutable aggregate, or remove that aggregate from generic writes. Prevent invalid references at the application boundary, not only in UI selectors. + +#### F-ARCH-05 — Collection-per-aggregate persistence (S2) + +Replace the optimistic whole-workspace snapshot before large data sets, multiple teachers, or high write concurrency. Design repositories and transaction boundaries around aggregates and immutable histories. Provide a migration, backup, and rollback plan; keep current snapshot fixtures readable during transition. + +#### F-ARCH-06 — Search and pagination correctness (S2) + +Current search serializes entire records, returns at most 100, and uses an ID cursor over unsorted arrays. Add explicit search projections, stable sort tuples, indexes, total/next metadata where useful, and UI pagination. Keep private fields out of search documents. + +#### F-ARCH-07 — Revision model completion (S2) + +Complete canonical revision records, scheduled impact notices, package revisions, fork ancestry, compatible-latest policies, and one resolver used by Lessons, Presentation, Assignments, Assessment generation, grading, and history views. The core plan establishes the safety baseline; this item completes operational depth and migration tooling. + +#### F-ARCH-08 — Audit and observability model (S2) + +Define structured audit events for publish/archive/fork, roster changes, assignment issue/review, assessment generation/override/regrade, resets, and account administration. Add correlation/reference IDs and privacy-aware structured logs. Avoid storing answer content or secrets in routine logs. + +### 2.6 Security, privacy, and administration + +#### F-SEC-01 — Capability enforcement and admin controls (S1) + +Derive explicit capabilities from fixed roles and enforce them in route workflows and navigation. Complete account disable/reset, temporary-password change, session invalidation, and audit UI. Do not introduce an arbitrary permission editor. + +#### F-SEC-02 — CSRF and mutation protection review (S1) + +Reassess cookie-session origin checks, explicit CSRF strategy, rate limits, content types, public endpoints, reveal/control tokens, and error disclosure after editor mutations expand. Add negative integration tests rather than relying on frontend behavior. + +#### F-SEC-03 — Learner privacy and retention (S2) + +Document and implement which administrative identities, pseudonyms, live nicknames, answers, evidence, grades, exports, and audit records are visible to each role and how long they are retained. Add privacy-safe export/deletion/anonymization workflows only after historical and legal constraints are decided. + +#### F-SEC-04 — Content and URL safety (S1) + +Apply one allowlist-based link/media policy to Markdown, Resources, manual pages, projected content, print output, and package imports. Keep raw HTML and executable package content disabled. Verify external links do not leak teacher-only context. + +### 2.7 Testing, release, and operations + +#### F-TEST-01 — Component and accessibility test layer (S1) + +Add focused tests for editor primitives, selectors, option lists, conflict panels, Markdown rendering, status/date formatters, and keyboard ordering. Keep E2E tests for user outcomes rather than every field permutation. + +#### F-TEST-02 — Browser matrix and visual regression (S2) + +Run critical journeys in supported Chromium plus at least one additional engine if the product commits to it. Add intentional visual snapshots for shell, dense editors, projection, print, and narrow layouts. Review changes rather than updating snapshots blindly. + +#### F-TEST-03 — Real Mongo integration profile (S1) + +Exercise atomic multi-record workflows, stale retries, delivery immutability, package staging, migration, and concurrent writes against the Docker Mongo replica set. Keep memory tests fast but do not use them as transaction evidence. + +#### F-OPS-01 — Backup, restore, and migration runbooks (S1) + +Document and test backups, restore drills, schema/data migration, demo reset boundaries, rollback, and environment separation. Ensure reset and import actions cannot target production accidentally. + +#### F-OPS-02 — Performance budgets (S2) + +Measure shell startup, large Library lists, editor load, Markdown/KaTeX rendering, lesson presentation transitions, live polling, assessment generation, and workspace persistence. Set practical budgets only after collecting a representative baseline. + +#### F-OPS-03 — Dependency and generated-artifact maintenance (S2) + +Continue the existing Node/npm/lockfile checks. Add scheduled dependency review for editor, Markdown, Fastify, Vite, MongoDB, and browser tooling; regenerate OpenAPI after route changes; and verify clean artifacts contain the manual Markdown source needed by the build. + +## 3. Suggested post-core execution waves + +### Wave 1 — MVP solidification + +Prioritize S1 items that make the accepted workflows coherent and supportable: + +- F-IA-01 through F-IA-03; +- F-UX-01 through F-UX-04; +- F-I18N-01; +- F-ARCH-01 through F-ARCH-04; +- F-SEC-01, F-SEC-02, and F-SEC-04; +- F-TEST-01, F-TEST-03, and F-OPS-01. + +Exit condition: the core journeys are consistently named, accessible, observable, type-safe at boundaries, validated on real MongoDB, and administratively recoverable. + +### Wave 2 — Depth and scale readiness + +Choose S2 items based on pilot-teacher evidence: + +- deeper timetable, evidence, assessment, print, and package workflows; +- localization boundaries; +- persistence, search, revisions, audit, privacy, browser matrix, and performance. + +Exit condition: the chosen pilot scale and support model have explicit capacity, privacy, history, and operational evidence. + +### Wave 3 — Strategic extensions + +Evaluate S3 items—command navigation, English UI, full Project authoring, rich assets—through separate product briefs. Do not let these reopen the core MVP scope without evidence. + +## 4. Intake template for new detached findings + +Record new findings with: + +- ID and short title; +- observed evidence and affected surface; +- why it does not block the core teacher loop; +- user/engineering risk; +- proposed direction, not a premature full design; +- priority and dependencies; +- measurable acceptance note; +- promotion trigger into the core plan. + +## 5. Completion rule + +This backlog is not expected to reach zero. A post-core wave is complete when its selected items have acceptance evidence, `npm run validate` passes, targeted integration/browser tests pass, and every unverified item is recorded in `IMPLEMENTATION-DEVIATIONS.md`. Unselected findings remain explicit rather than being implied as implemented. diff --git a/docs/plans/TEACHER-EDITING-AND-MANUAL-PLAN.md b/docs/plans/TEACHER-EDITING-AND-MANUAL-PLAN.md new file mode 100644 index 0000000..23ce15c --- /dev/null +++ b/docs/plans/TEACHER-EDITING-AND-MANUAL-PLAN.md @@ -0,0 +1,480 @@ +# Teacher editing and manual plan + +Status: proposed implementation plan +Primary audience: product and engineering +Scope: the first useful teacher journey and the editing surfaces required to repeat it safely + +## 1. Outcome + +Turn the current runnable demonstration into a teacher-usable MVP in which a new teacher can: + +1. understand Fonat's vocabulary and mental model; +2. configure a real course, group, learners, and teaching location without demo identifiers; +3. author and revise learning materials and all six supported exercise types; +4. assemble, preview, publish, present, leave, and resume a lesson; +5. create a real assignment, review attempts, return work, and accept a correction; +6. create an assessment blueprint, choose recipients, generate stable variants, grade them, and understand the resulting findings; +7. find contextual, Markdown-authored guidance for every teacher-facing concept and workflow. + +The plan preserves the already-working navigation, logout, presentation escape, live-answer acknowledgement, and immutable submission and assessment delivery snapshots. + +## 2. Evidence and current-state assessment + +This plan is based on the current React routes, Fastify collection routes, contracts, demo state, existing tests, Version 4 requirements, and a browser walkthrough of the teacher UI. + +| Teacher need | Current useful foundation | Blocking gap | +| ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Get oriented | Login, stable shell, Today page, three-card Guide | No onboarding journey, checklist, contextual help, searchable manual, or sufficient terminology; the Guide mixes Hungarian UI with English naming and is hardcoded in the API. | +| Configure classes | Generic create-by-title lists exist for courses, groups, learners, and locations | Those routes are absent from primary navigation, rows do not open, and teachers cannot edit rosters, subject, location, recurrence, or most fields. | +| Find and revise materials | Library search, Node detail rendering, Markdown renderer, relation selectors | Library rows are not links, creation only records a title and fixed `concept` type, Node detail exposes raw JSON, and no material editor exists. | +| Author exercises | A bespoke editor supports six exercise types, Crepe fields, preview, create, and patch | No exercise library/list path; existing editor hydration is unsafe; concepts are discarded on save; choice correctness uses letter input; validation, rubric, evidence policy, archive, revision impact, and explicit draft/publish actions are incomplete. | +| Build a lesson | Course selection, slide insertion, drag/drop, move, remove, save, publish status, presentation link | Existing slide content cannot be edited. New task/quiz slides silently use the first exercise. Several supported presentation slide types cannot be added. No duration, location, reusable-material picker, preview, or reliable diagnostics. | +| Plan the year | Annual-plan collection and phase fixtures exist | Only title creation is exposed; no plan detail, phase editor, calendar, ordering, coverage, or lesson projection. | +| Assign practice | Immutable attempts and return/resubmit/accept server workflow works | Teacher creation uses fixed demo course, exercise, learners, deadline, and feedback. Review displays IDs and raw JSON and cannot edit feedback or compare attempts. | +| Assess learning | Blueprint fixture, deterministic generation, stable deliveries, grading, and grades exist | No blueprint editor. Generation uses fixed seed and two demo learners. No slot diagnostics, recipient selection, manual grading/override, result explanation, regrade preview, or print flow. | +| Interpret outcomes | Findings, evidence, and grades are listed | Labels are detached from learners, concepts, exercises, and next actions; raw IDs and status codes remain visible. | +| Recover from mistakes | Optimistic version fields exist in data | The client does not consistently send or explain conflicts, has no unsaved-change guard, no save confirmation, and no archive/revision-impact workflow. | + +### Safety issue to fix before expanding editors + +`ContentEditor` is created once with the initial prop value. Existing data arrives later, but the editor instance is not synchronized. During the browser audit, an existing numeric exercise showed the placeholder prompt in the Crepe editor while its learner preview showed the persisted prompt. The adapter must be corrected and covered by a reload/edit/save test before teachers are asked to edit more entity types. + +## 3. Product boundary for the core plan + +### Included + +- first real workspace onboarding; +- guided organization editors needed by the first course; +- Concept and Resource material editors; +- relation management through selectors; +- safe and complete editing of the six supported Exercise types; +- practical Annual Plan and Lesson editing; +- assignment authoring and teacher review; +- assessment blueprint authoring, delivery selection, grading, and findings explanation; +- a file-backed, Markdown-rendered internal manual; +- teacher-facing names in place of ordinary raw internal identifiers; +- focused API, integration, and browser coverage for the above journeys. + +### Deferred from this plan + +- multi-teacher collaboration and fine-grained capabilities; +- package ZIP import/update workflow; +- a visual graph canvas; +- AI generation; +- advanced project authoring; +- institutional timetable solving; +- full localization beyond a consistent Hungarian MVP; +- large-dataset and multi-tenant architecture work. + +Deferred findings are recorded separately in [POST-CORE-MVP-FINDINGS-PLAN.md](./POST-CORE-MVP-FINDINGS-PLAN.md). + +## 4. Implementation principles + +1. Keep TypeScript end to end. Define schemas and DTOs for each editor rather than extending `any`-based generic forms. +2. Route handlers parse and authenticate, invoke feature workflows, and map typed `Result` values. New multi-step writes belong in the relevant feature slice. +3. Use selectors populated with teacher-facing names. Internal identifiers may appear in an advanced/debug view, never as the normal input mechanism. +4. Preserve history. Archive referenced content; publish revisions; retain immutable delivered snapshots. +5. Use one editor frame and common interaction language: Back/Cancel, save state, validation summary, Preview, Save draft, Publish, Archive. +6. Make empty states instructional and actionable. +7. Build the manual from Markdown source files. React components may provide navigation and rendering, but documentation prose must not be embedded in server or page component literals. +8. Add behavior in vertical slices with browser proof, not as disconnected CRUD screens. + +## 5. Shared editor foundation + +Complete this foundation before feature-specific editors. + +### 5.1 Typed editor contracts + +- Add explicit shared schemas and teacher-facing DTOs for Subject, Learner Group, Learner Profile, Enrollment/roster, Course, Location, educational Node variants, Relation, Annual Plan, Phase, Lesson, Assignment, Assessment Blueprint, and grading decisions. +- Separate create, draft-update, publish, archive, and delivered-read models where their rules differ. +- Replace open generic patching for these workflows with feature-owned commands. Keep the collection routes temporarily for read compatibility, then stop using them from normal editor UI. +- Return field errors, stale-write details, and reference diagnostics through typed `Result` values. + +### 5.2 `ContentEditor` reliability + +- Support asynchronous initial Markdown and documented replacement when the loaded value changes. +- Prevent replacement from overwriting local dirty edits. +- Add debounced change delivery, read-only behavior, focus, error state, theme integration, teardown, and optional Markdown source mode. +- Expose `ready`, `dirty`, and last-saved state to the parent editor. +- Cover Hungarian Unicode, headings, lists, tables, links, images, inline/block LaTeX, paste, undo/redo, source round-trip, read-only mode, and remount without duplicate editors. +- Add a regression test that loads an existing exercise after mount and proves an unchanged save retains its original Markdown. + +### 5.3 Standard editor shell + +Create reusable, accessible primitives rather than a generic JSON form: + +- `EditorPage` with breadcrumb, title, status, Back/Cancel, Save draft, Publish, Preview, and overflow actions; +- validation summary that links to fields and preserves server field errors; +- dirty-state navigation guard and explicit discard action; +- visible saving/saved/failed state and retry; +- optimistic concurrency conflict panel offering reload or a field-level comparison where practical; +- archive confirmation showing references and consequences; +- responsive split preview that becomes a tab on narrow screens; +- query invalidation and return routes that consistently reopen the saved entity. + +### 5.4 Shared selectors + +Build searchable, keyboard-usable selectors for Course, Group, Learner, Location, Concept, Resource, Exercise, Lesson, and Relation target. They must support loading, empty, error, archived/unavailable, and selected states. Selection displays titles and useful context, while payloads retain stable IDs. + +### 5.5 Revision and impact baseline + +- Introduce the canonical revision resolver before publishing reusable content from new editors. +- Draft edits remain mutable with optimistic concurrency. +- Publishing creates a revision and records who/when. +- Scheduled lessons and assignments show impact notices and require an explicit update choice. +- Completed lessons, submissions, and assessment deliveries continue resolving their pinned revision or immutable snapshot. +- Package-owned content is read-only until explicitly forked into teacher ownership. + +### Foundation exit criteria + +- Existing Exercise Markdown loads identically in editor and preview. +- Back navigation warns only when data is dirty. +- Two stale browser edits produce a comprehensible conflict rather than silent overwrite. +- Save draft, publish, and archive behaviors are consistent across two pilot entity editors. + +## 6. Release A: first-run workspace and teacher orientation + +### 6.1 First-run checklist and onboarding wizard + +Add an authenticated onboarding route that activates for a blank workspace and remains reopenable from Help. + +Steps: + +1. explain Workspace, Course, Group, Learner, and Lesson in plain Hungarian; +2. create or choose a Subject; +3. create the first Learner Group and school year; +4. add learners individually and by a reviewed paste/import table; +5. create a Location; +6. create a Course by selecting Subject, Group, default Location, and timezone; +7. optionally create a first Concept, Exercise, and Lesson through shortened versions of their real editors; +8. show a completion checklist with direct links to continue authoring or load the optional demo. + +Onboarding must use the same workflows as later editors and must not create hidden demo IDs. + +### 6.2 Organization editors + +- Learner Group: name, school year, status, roster summary, edit/archive. +- Learner Profile: display name/pseudonym, badge, teacher note, active status; administrative identity remains optional and privacy-aware. +- Roster: searchable add/remove, effective dates, inclusion/exclusion, clear active/history distinction. +- Location: name, room/description, active status. +- Course: title, Subject, Groups, explicit learner adjustments, default Location, timezone, active/archive state. +- Timetable entry: recurring weekday/start/end, Course, Location override, effective dates, cancellation/move override, overlap diagnostics. + +### 6.3 Today-page orientation + +- Replace demo-specific hero actions with current teacher actions: Finish setup, Create material, Create exercise, Plan lesson, Resume lesson, Review submissions. +- Use names instead of lesson/course IDs. +- Show why a finding or pending item matters and provide a direct next action. +- Keep the known-good presentation and logout paths unchanged. + +### Release A exit journey + +Starting from a blank workspace, a teacher creates a subject, group, two learners, location, course, and timetable entry; reloads; sees the named course and group in Today and Timetable; and opens the next authoring action without encountering an internal identifier. + +## 7. Release B: learning-material and exercise editors + +### 7.1 Library information architecture + +- Give the Library explicit filters/tabs for Concepts, Resources, Exercises, and optionally Collections. +- Add type, lifecycle, ownership, concept, and updated-date filters; teacher-facing sorting; and real cursor paging. +- Make the title and row actionable, with View, Edit/Fork, Duplicate, and Archive actions according to ownership and lifecycle. +- Replace the fixed create-by-title form with a Create menu that explains each material type. +- Keep internal IDs out of the default table. Offer them only in an advanced metadata panel. + +### 7.2 Concept editor + +Fields and behavior: + +- title, aliases/search terms, short summary, rich Markdown definition, lifecycle; +- optional subject/curriculum context supported by the current model; +- incoming/outgoing Relations separated and named; +- searchable relation target and relation-specific controls; +- duplicate, self-reference, invalid type, and archived-target prevention; +- preview with the same Markdown/KaTeX renderer used in lessons; +- Save draft, Publish, revision history/impact, Fork package content, Archive. + +### 7.3 Resource editor + +Support a deliberately bounded MVP set: + +- Markdown learning material; +- external link with provider, URL, display text, learner-safe description, and link validation; +- bundled accessible SVG/image metadata where the current asset profile permits it. + +Include title, summary, content/provider-specific fields, Concepts, intended use, learner/teacher visibility, rights/source metadata, preview, lifecycle, revision, fork, and archive. Do not accept executable HTML or scripts. + +### 7.4 Relation editor + +- Add, edit, and remove/archive `requires`, `covers`, `alternative-to`, and `extends` relations through localized selectors. +- Display both directions in material detail. +- Add any relation-specific contribution/similarity fields required by the schema. +- Explain relation meaning inline and link the term to the manual. + +### 7.5 Exercise catalogue and editor completion + +- Add an Exercises list route and make Exercises discoverable from Library and Lesson/Assignment/Assessment selectors. +- Begin creation with cards explaining the six supported types. +- Preserve the Crepe prompt and optional solution editors plus learner preview. +- Replace choice-option textareas and comma-separated correct letters with an option list editor: add/remove/reorder, stable option IDs, checkbox/radio correctness, and minimum-option rules. +- Manual response: response guidance, optional lightweight rubric, and teacher review behavior. +- Numeric: expected value, non-negative tolerance, optional unit, and examples of accepted/rejected answers. +- Accepted text: reorderable variants and explicit exact versus trim/case-fold normalization. +- Add Concept selector and contribution/evidence level; never reset `conceptIds` during an edit. +- Add expected duration, difficulty, evidence/feedback policy, validation summary, explicit Save draft/Publish, revision impact, duplicate, and archive. +- Changing exercise type must explain and confirm any answer data that would be discarded. +- Preview must use the same answer widgets and public projection rules as actual learner delivery. + +### Release B exit journey + +A teacher creates and publishes one Concept, one Markdown Resource, and all six Exercise types; relates the Resource and Exercises to the Concept; searches and reopens every item; edits and republishes one Exercise; and verifies the editor, preview, and persisted result retain identical content. + +## 8. Release C: annual planning and complete lesson editing + +### 8.1 Annual Plan editor + +- Guided creation from Course, school year/calendar bounds, teaching profile/lesson blueprint defaults where available. +- Phase list with add, rename, rich description, date range, order, move buttons, and drag/drop. +- Coverage selector for Concepts/Resources and an understandable coverage summary. +- Lesson projections with named Course, date, Phase, duration, and status. +- Diagnostics for gaps, impossible date ranges, and unscheduled lessons. +- Save draft and Publish with reload-safe ordering. + +### 8.2 Lesson editor redesign + +Keep the existing ordering controls, but make each row open a type-specific Activity/slide editor. + +Lesson-level fields: + +- intent/title, Course, date/time, duration, Location override, Phase, status; +- optional Blueprint/Profile once those defaults are modeled; +- teacher notes that never reach projection; +- full lesson preview and diagnostic summary. + +Supported insertion types for the MVP: + +- section introduction; +- Concept/Markdown definition selected from Library or authored inline as a new Resource; +- accessible image/SVG; +- normal Exercise selected from published supported Exercises; +- live quiz selected from supported live-capable Exercises; +- response status; +- results/leaderboard; +- solution/explanation; +- discussion prompt; +- timer with duration and end behavior; +- closure/homework. + +Every inserted item must be editable. Task/quiz insertion must require an explicit Exercise selection and never silently choose the first exercise. Unsupported types remain visible as unavailable with a reason. + +### 8.3 Diagnostics and preview + +- Sum duration against target lesson length. +- Detect missing content, missing exercise references, prerequisites, unsupported renderers, repetitive participation, solution-before-task, live quiz without results/reveal path, timer without valid duration, and missing closure. +- Distinguish errors that block publish from suggestions. +- Preview projected learner content and teacher-only guidance separately. +- Verify scheduled and completed revision resolution before publish. + +### Release C exit journey + +A teacher builds a lesson with at least three sections, a Concept, Resource, four distinct Exercise types, live quiz, response/results, solution, timer, discussion, and homework; edits every inserted item; reorders with mouse and keyboard/move buttons; publishes; reloads with exact order; previews; presents; pauses/leaves; resumes; and completes. + +## 9. Release D: assignments, review, assessments, and findings + +### 9.1 Assignment editor + +- Title/instructions in Markdown, Course or selected learner targets, one or more published Exercises or an Assessment, open/deadline date and local time, attempt limit, feedback release, evidence policy, status. +- Show recipient count and the resolved exercise revisions before assign. +- Save draft, preview learner view, assign, close/archive, and duplicate. +- Prevent deadlines before opening and unresolved/archived content. +- Replace all fixed demo course, learner, exercise, and deadline values. + +### 9.2 Submission review workspace + +- Queue filters by Course, Assignment, status, learner, and due state. +- Display learner, exercise, prompt snapshot, formatted answer, auto-check result, confidence/scaffold data when present, and attempt timeline. +- Compare returned and resubmitted attempts without modifying either snapshot. +- Teacher writes feedback, then returns or accepts; require a confirmation for status-changing actions. +- Where grading applies, create or confirm a Grade Entry with visible provenance. +- Replace raw answer JSON and internal IDs with teacher-facing rendering; keep technical metadata in an advanced panel. + +### 9.3 Assessment Blueprint editor + +- Title, Course, instructions, source filters, variant policy, and ordered slots. +- Slot editor for Concept, points, difficulty/source bounds, eligible exercise count, and deterministic shortfall explanation. +- Add/remove/reorder slots, total-points summary, Save draft, validate, Publish, duplicate, archive. +- Explain options when a slot is short: add content, allow repetition, widen criteria, reduce/defer the slot. Never relax silently. + +### 9.4 Generate and deliver assessment wizard + +- Choose Blueprint, target Course/learners, due window, A/B policy, and optional reproducible seed hidden under advanced settings. +- Preview selected questions, equivalence explanation, points, variant labels, and unresolved shortfalls before generation. +- Create stable learner-specific Deliveries through the canonical revision resolver. +- Provide online learner view and a print-ready preview. + +### 9.5 Grading and findings + +- Auto-grade supported types and queue manual responses. +- Show manual decision/override with required reason when it changes an automatic or official result. +- Preview regrade impact before changing Grade Entries. +- Explain easiest/hardest questions, strongest/weakest Concepts, omissions, attractive distractors, and follow-up learners. +- Findings recommend actions and link to supporting evidence; they never mutate plans or grades automatically. + +### Release D exit journey + +A teacher assigns two Exercises to the created Course, observes a learner draft survive reload, reviews a submitted immutable attempt, returns it with editable feedback, compares and accepts the resubmission; then creates a six-slot Blueprint, resolves any shortfall, generates stable A/B Deliveries for two selected learners, grades supported and manual items, records a Grade Entry, and opens an explained finding. + +## 10. Markdown-backed internal manual + +The manual is a product feature, not a single static help page. Establish its architecture early, then complete content alongside Releases A-D so the documentation never lags the UI. + +### 10.1 Source architecture + +- Store prose as UTF-8 Markdown under `apps/web/src/manual/hu/`, grouped by `getting-started`, `concepts`, `how-to`, `reference`, `troubleshooting`, and `tips`. +- Keep a small typed manifest for slug, order, category, title, summary, and keywords; import each `.md` body as raw build-time content. The prose itself must remain in Markdown files, not TSX, API route literals, fixture objects, or database seeds. +- Remove the hardcoded `/api/guide` response after the file-backed manual is in use. If an API is retained for future remote content, it must serve parsed Markdown documents from the same source rather than duplicate prose. +- Reuse the existing `Markdown` component, add GitHub-flavored Markdown support for tables/task lists, and keep KaTeX support. +- Do not enable raw HTML. Sanitize/validate URLs, distinguish external links, and provide accessible heading anchors and code/table overflow. +- Add a build/test check for duplicate slugs, broken internal links, missing titles/summaries, orphan pages, and terminology links. + +### 10.2 Manual UI + +- `/guide` becomes a searchable manual landing page with category navigation, beginner path, recently relevant topics, and terminology index. +- `/guide/:slug` renders a Markdown article with breadcrumb, table of contents, previous/next links, related terms, and links back to the relevant feature. +- Add contextual Help links to onboarding and every core editor. Links should open the precise article/heading, not the manual home. +- Search title, summary, keywords, headings, and body text locally at MVP scale. +- Add copy-link anchors and a print-friendly article view. +- Empty/error states remain usable if one article fails to load. + +### 10.3 Required terminology coverage + +The terminology section must define the Hungarian UI term, canonical English/domain term where useful, what it means to a teacher, how it differs from nearby terms, and a concrete example. Cover at least: + +- Workspace, Admin, teacher account, role, session; +- Subject, Course, Learner Profile, Learner Group, Enrollment, roster, explicit inclusion/exclusion, Teaching Location, timetable entry, override; +- Library, Node, Concept, Resource, Collection, owner, package-owned content, teacher fork; +- Relation and each supported type: requires, covers, alternative-to, extends; +- draft, published, archived, revision, pinned revision, immutable snapshot, optimistic conflict; +- Exercise, the six Exercise types, prompt, solution/explanation, option, accepted answer, tolerance, normalization, difficulty, expected duration, rubric, evidence policy; +- Annual Plan, Phase, Lesson, Lesson Blueprint, Lesson Layout, Activity/slide, diagnostic, prerequisite, teaching profile; +- Presentation Mode, projected view, Lesson Run, pause/leave, resume, complete, live session, join code, participant, reveal, response status, leaderboard; +- Assignment, learner draft, Submission, attempt, returned, resubmitted, accepted, feedback release; +- Learning Evidence, confidence, scaffold use, Finding, Grade Entry; +- Assessment, Assessment Blueprint, slot, source filter, shortfall, A/B variant, Delivery, automatic grading, manual override, regrade; +- content package, import/apply, update, Project, feature flag. + +Terms not yet implemented must be marked as planned or unavailable rather than described as usable. + +### 10.4 Required how-to coverage + +Write task-oriented articles with prerequisites, numbered steps, expected result, recovery notes, and related terminology: + +1. Start with a blank workspace. +2. Create a Course, Group, learners, roster, Location, and timetable entry. +3. Create, relate, publish, revise, fork, and archive a Concept or Resource. +4. Create and preview each of the six Exercise types. +5. Choose tolerance, accepted-text normalization, and correct choice options. +6. Build, diagnose, preview, publish, present, pause, resume, and complete a Lesson. +7. Start an anonymous live session, share the join code, reveal results safely, and recover from disconnection. +8. Create an Assignment, review attempts, return with feedback, accept a correction, and understand immutable history. +9. Build an Assessment Blueprint, resolve a shortfall, generate A/B Deliveries, grade, override with a reason, and review findings. +10. Understand revisions and why previously delivered work does not change. +11. Recover from validation errors, stale edits, missing content, session expiry, and disabled features. + +### 10.5 Required tips coverage + +Add short, cross-linked teacher tips, including: + +- draft first and publish only after learner preview; +- write measurable prompts and useful solutions; +- use Concepts and Relations to make reuse and assessment selection stronger; +- choose plausible distractors and avoid trick wording; +- use tolerance and units intentionally; +- balance lesson duration and participation modes; +- keep teacher notes out of projected content; +- reveal live results only when pedagogically appropriate; +- return work with one actionable correction target; +- interpret findings as prompts for judgment, not automatic decisions; +- duplicate/fork before making a context-specific variation; +- archive referenced content instead of deleting history. + +### 10.6 Documentation definition of done + +- Every primary navigation destination and core editor has an article and contextual link. +- Every visible teacher-facing domain term is present in the terminology index or links to a definition. +- No article claims a deferred feature is available. +- A browser test proves search, article rendering, table/task list/KaTeX output, anchor navigation, related links, and return to the editor. +- A content test catches broken manual links and missing glossary entries referenced by the UI. + +## 11. API and data changes by feature slice + +The exact route shape may evolve during implementation, but normal editor workflows should converge on feature-owned commands such as: + +- onboarding: workspace status and atomic step commands; +- organization: group, learner, roster, course, location, timetable create/update/archive; +- graph: Concept/Resource create/update/publish/archive/fork and Relation commands; +- exercises: draft update, publish revision, duplicate/fork, archive; +- planning: Annual Plan/Phase commands and Lesson draft/publish/preview diagnostics; +- assignments: draft, assign, close, recipient resolution, review commands; +- assessments: Blueprint validation/publish, generation preview/commit, grading decision, regrade preview; +- manual: build-time content index, with no mutable server workflow in the MVP. + +Each multi-record command must be atomic at the current store boundary, return a typed Result, and record enough audit/revision metadata for later migration away from the workspace snapshot. + +## 12. Verification strategy + +### Unit and component coverage + +- editor DTO validation and type-specific field rules; +- ContentEditor async hydration and dirty-value protection; +- stable option IDs and answer normalization; +- relation validity and duplicate prevention; +- revision resolution and impact calculation; +- lesson diagnostics and duration calculation; +- assessment slot eligibility/shortfall and deterministic generation; +- Markdown manifest, link, glossary, and renderer fixtures. + +### API/integration coverage + +- create/update/conflict/publish/archive/fork per reusable entity; +- blank onboarding without demo IDs; +- historical roster resolution and timetable overlap; +- assignment draft/submit/return/resubmit/accept snapshots; +- assessment generation, delivery immutability, grading, override, regrade preview, and findings; +- authorization and field-error mapping for every new command. + +### Browser journeys + +1. blank teacher onboarding and first course; +2. material plus six-exercise authoring, reload, edit, publish, and relation management; +3. full lesson build, diagnostics, preview, presentation escape/resume/complete; +4. assignment draft persistence and correction loop; +5. assessment blueprint through findings; +6. manual search, terminology, contextual help, and return to work; +7. regression journey for logout and live answer acknowledgement. + +Use at least two browser contexts where learner/teacher separation matters. Test keyboard ordering and a narrow viewport for core editors. + +### Required commands per implementation slice + +1. targeted unit/component tests; +2. targeted API integration tests; +3. targeted browser journey; +4. `npm run validate`; +5. record any unverified behavior in `IMPLEMENTATION-DEVIATIONS.md`. + +## 13. Delivery sequence and gates + +| Gate | Deliverable | May proceed when | +| ---- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| G0 | Editor safety foundation | Existing content hydrates correctly, dirty navigation and conflicts are safe, and revision resolution has a tested baseline. | +| G1 | Blank-workspace orientation and organization | A new teacher completes Release A without demo IDs or raw-ID input. | +| G2 | Materials and exercises | Release B browser journey passes for all supported types and reloads. | +| G3 | Planning and lesson authoring | Release C passes while preserving presentation leave/resume/complete behavior. | +| G4 | Assignment and assessment operations | Release D proves immutable history and stable deliveries end to end. | +| G5 | Manual completeness and MVP acceptance | Documentation definition of done and all regression journeys pass; remaining deviations are truthful. | + +Work on the manual source architecture in G0 and add/update articles in the same change set as each feature. Do not postpone all documentation writing until G5. + +## 14. Core MVP definition of done + +The core plan is complete only when a teacher unfamiliar with the repository can start from blank state and complete the setup → author → plan → teach → assign → assess → interpret loop using names and guided controls, can recover from validation and stale edits, and can find accurate in-product guidance for every step. Existing delivery and submission history must remain immutable, and all previously protected navigation/logout/live/presentation journeys must still pass. From 7f021dcee10c1d8114e617bc0ecedd1a4be1bb84 Mon Sep 17 00:00:00 2001 From: legrab Date: Thu, 16 Jul 2026 10:23:10 +0200 Subject: [PATCH 02/10] fix(editors): make rich content hydration safe --- apps/web/package.json | 1 + apps/web/src/api.ts | 28 +++++++++++- apps/web/src/components/ContentEditor.tsx | 54 ++++++++++++++++++++--- apps/web/src/pages/ExerciseEditorPage.tsx | 31 +++++++++++-- package-lock.json | 1 + tests/e2e/golden.spec.ts | 21 ++++++++- 6 files changed, 122 insertions(+), 14 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 521a3ac..8dea223 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,7 @@ "@dnd-kit/utilities": "3.2.2", "@hookform/resolvers": "5.4.0", "@milkdown/crepe": "7.21.3", + "@milkdown/kit": "7.21.3", "@radix-ui/themes": "3.3.0", "@tanstack/react-query": "5.101.2", "@tanstack/react-table": "8.21.3", diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts index a56f3f2..49c6e7a 100644 --- a/apps/web/src/api.ts +++ b/apps/web/src/api.ts @@ -9,6 +9,21 @@ export type Result = retryable: boolean; }; }; + +export class ApiError extends Error { + constructor( + message: string, + readonly code: string, + readonly status: number, + readonly retryable: boolean, + readonly fieldErrors: Record = {}, + readonly technicalReference?: string, + ) { + super(message); + this.name = "ApiError"; + } +} + export async function api( path: string, options: RequestInit = {}, @@ -26,8 +41,17 @@ export async function api( retryable: true, }, })); - if (!response.ok || data?.ok === false) - throw new Error(data?.error?.messageKey || `HTTP ${response.status}`); + if (!response.ok || data?.ok === false) { + const error = data?.error; + throw new ApiError( + error?.messageKey || `HTTP ${response.status}`, + error?.code || "HTTP", + response.status, + Boolean(error?.retryable), + error?.fieldErrors || {}, + error?.technicalReference, + ); + } return data?.ok === true ? data.value : data; } export const post = (path: string, body: unknown) => diff --git a/apps/web/src/components/ContentEditor.tsx b/apps/web/src/components/ContentEditor.tsx index 395b151..d1ebc62 100644 --- a/apps/web/src/components/ContentEditor.tsx +++ b/apps/web/src/components/ContentEditor.tsx @@ -1,34 +1,74 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { Crepe } from "@milkdown/crepe"; +import { replaceAll } from "@milkdown/kit/utils"; export function ContentEditor({ value, onChange, readOnly = false, + ariaLabel, }: { value: string; onChange: (value: string) => void; readOnly?: boolean; + ariaLabel?: string; }) { const root = useRef(null); + const crepe = useRef(undefined); + const currentMarkdown = useRef(value); + const applyingExternalValue = useRef(false); const latest = useRef(onChange); + const [ready, setReady] = useState(false); latest.current = onChange; useEffect(() => { if (!root.current) return; - const crepe = new Crepe({ root: root.current, defaultValue: value }); - crepe.on((listener) => { - listener.markdownUpdated((_ctx, markdown) => latest.current(markdown)); + const instance = new Crepe({ + root: root.current, + defaultValue: currentMarkdown.current, }); - void crepe.create().then(() => { - if (readOnly) root.current?.setAttribute("data-readonly", "true"); + instance.on((listener) => { + listener.markdownUpdated((_ctx, markdown) => { + currentMarkdown.current = markdown; + if (!applyingExternalValue.current) latest.current(markdown); + }); + }); + void instance.create().then(() => { + crepe.current = instance; + instance.setReadonly(readOnly); + const loadedMarkdown = instance.getMarkdown(); + if (loadedMarkdown !== currentMarkdown.current) { + applyingExternalValue.current = true; + instance.editor.action(replaceAll(currentMarkdown.current)); + queueMicrotask(() => { + applyingExternalValue.current = false; + }); + } + setReady(true); }); return () => { - void crepe.destroy(); + crepe.current = undefined; + void instance.destroy(); }; }, []); + useEffect(() => { + const instance = crepe.current; + if (!instance || value === currentMarkdown.current) return; + currentMarkdown.current = value; + applyingExternalValue.current = true; + instance.editor.action(replaceAll(value)); + queueMicrotask(() => { + applyingExternalValue.current = false; + }); + }, [value]); + useEffect(() => { + crepe.current?.setReadonly(readOnly); + }, [readOnly]); return (
); } diff --git a/apps/web/src/pages/ExerciseEditorPage.tsx b/apps/web/src/pages/ExerciseEditorPage.tsx index b0a61bc..5cebb82 100644 --- a/apps/web/src/pages/ExerciseEditorPage.tsx +++ b/apps/web/src/pages/ExerciseEditorPage.tsx @@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useMutation, useQuery } from "@tanstack/react-query"; import { useNavigate, useParams } from "react-router-dom"; -import { api, patch, post } from "../api"; +import { ApiError, api, patch, post } from "../api"; import { ContentEditor } from "../components/ContentEditor"; import { Markdown } from "../components/Markdown"; const types = [ @@ -40,6 +40,7 @@ export function ExerciseEditorPage() { }); const [prompt, setPrompt] = useState("Írd ide a feladat szövegét."); const [solution, setSolution] = useState(""); + const [hydrated, setHydrated] = useState(!id); const form = useForm({ resolver: zodResolver(schema), defaultValues: { @@ -66,6 +67,7 @@ export function ExerciseEditorPage() { optionsText: (e.options || []).map((o: any) => o.text).join("\n"), correctOptionIds: (e.correctOptionIds || []).join(","), }); + setHydrated(true); } }, [existing.data]); const selected = form.watch("exerciseType"); @@ -105,6 +107,14 @@ export function ExerciseEditorPage() { }, onSuccess: () => nav("/library"), }); + if (id && (existing.isLoading || !hydrated)) + return
Feladat betöltése…
; + if (existing.error) + return ( +
+ A feladat nem tölthető be: {existing.error.message} +
+ ); return ( <>
@@ -152,11 +162,19 @@ export function ExerciseEditorPage() {
{(selected === "single-choice" || selected === "multiple-choice") && ( <> @@ -222,6 +240,13 @@ export function ExerciseEditorPage() { {Object.keys(form.formState.errors).length > 0 && (
Ellenőrizd a megjelölt mezőket.
)} + {save.error && ( +
+ {save.error instanceof ApiError && save.error.code === "CONFLICT" + ? "A feladatot közben más módosította. Töltsd újra az oldalt, majd ellenőrizd a változásokat." + : `A mentés sikertelen: ${save.error.message}`} +
+ )}
Tanulói előnézet diff --git a/package-lock.json b/package-lock.json index 534743b..450d11b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "@dnd-kit/utilities": "3.2.2", "@hookform/resolvers": "5.4.0", "@milkdown/crepe": "7.21.3", + "@milkdown/kit": "7.21.3", "@radix-ui/themes": "3.3.0", "@tanstack/react-query": "5.101.2", "@tanstack/react-table": "8.21.3", diff --git a/tests/e2e/golden.spec.ts b/tests/e2e/golden.spec.ts index 9c6ac5c..0f21c8f 100644 --- a/tests/e2e/golden.spec.ts +++ b/tests/e2e/golden.spec.ts @@ -1,5 +1,6 @@ -import { test, expect } from "@playwright/test"; -test("login, presentation escape, and logout", async ({ page }) => { +import { test, expect, type Page } from "@playwright/test"; + +async function login(page: Page) { await page.goto("/login"); await page.getByLabel("E-mail").fill("admin@fonat.local"); await page.getByLabel("Jelszó").fill("fonat-demo"); @@ -7,6 +8,10 @@ test("login, presentation escape, and logout", async ({ page }) => { await expect( page.getByRole("heading", { name: /Ma a szálak/ }), ).toBeVisible(); +} + +test("login, presentation escape, and logout", async ({ page }) => { + await login(page); await page.getByRole("link", { name: "Pitagorasz-bemutató" }).click(); await expect( page.getByRole("button", { name: "Szünet és kilépés" }), @@ -17,3 +22,15 @@ test("login, presentation escape, and logout", async ({ page }) => { page.getByRole("heading", { name: "Belépés a munkatérbe" }), ).toBeVisible(); }); + +test("existing exercise Markdown hydrates the rich editor", async ({ + page, +}) => { + await login(page); + await page.goto("/exercises/exercise.missing-hypotenuse-6-8"); + const promptEditor = page.locator(".content-editor").first(); + await expect(promptEditor).toContainText( + "A befogók 6 cm és 8 cm. Mekkora az átfogó?", + ); + await expect(promptEditor).not.toContainText("Írd ide a feladat szövegét."); +}); From 8a764aed0f7bf95284e103ab2fd99bbe759eb2ac Mon Sep 17 00:00:00 2001 From: legrab Date: Thu, 16 Jul 2026 10:34:58 +0200 Subject: [PATCH 03/10] feat(authoring): add teacher setup and material editors --- IMPLEMENTATION-DEVIATIONS.md | 6 +- apps/server/src/app.ts | 141 ++++++++- apps/web/src/App.tsx | 42 +++ apps/web/src/components/Shell.tsx | 81 ++++-- apps/web/src/pages/EntityListPage.tsx | 76 ++--- apps/web/src/pages/ExerciseEditorPage.tsx | 249 +++++++++++++--- apps/web/src/pages/MaterialEditorPage.tsx | 200 +++++++++++++ apps/web/src/pages/NodeDetailPage.tsx | 47 ++- apps/web/src/pages/OrganizationEditorPage.tsx | 275 ++++++++++++++++++ apps/web/src/pages/SetupPage.tsx | 150 ++++++++++ apps/web/src/pages/TodayPage.tsx | 39 ++- apps/web/src/styles.css | 37 ++- packages/application/src/index.ts | 2 + packages/contracts/src/index.ts | 87 ++++-- tests/integration/api.test.ts | 31 ++ 15 files changed, 1323 insertions(+), 140 deletions(-) create mode 100644 apps/web/src/pages/MaterialEditorPage.tsx create mode 100644 apps/web/src/pages/OrganizationEditorPage.tsx create mode 100644 apps/web/src/pages/SetupPage.tsx diff --git a/IMPLEMENTATION-DEVIATIONS.md b/IMPLEMENTATION-DEVIATIONS.md index ca7aaf5..07bf0fc 100644 --- a/IMPLEMENTATION-DEVIATIONS.md +++ b/IMPLEMENTATION-DEVIATIONS.md @@ -7,6 +7,8 @@ This is a substantial runnable MVP modernized against the Version 4 specificatio - login, first-run bootstrap endpoint, server sessions, visible logout, cookie invalidation, disabled-user session invalidation; - stable shell, explicit presentation leave/complete actions, 404 recovery; - all six guided exercise types with Milkdown Crepe prompt/solution editors and learner preview; +- atomic blank-workspace onboarding for the first Subject, Group, learners, Location, Course, and Enrollments, plus guided organization editors; +- guided Concept/Resource material editing, actionable Library rows, named Relation management, and an Exercise catalogue with structured choice answers and Concept/evidence metadata; - 24 Grade 8 concepts, 18 authored exercises, five learner fixtures, multiple lessons, evidence, findings, assignments, assessment blueprint, and the complete ten-slide demo sequence; - live join code, scoped participant token, idempotent answer acceptance, polling, response table, teacher reveal, privacy-safe nickname leaderboard; - mutable assignment draft, immutable attempts, return/resubmit/accept flow; @@ -20,8 +22,8 @@ This is a substantial runnable MVP modernized against the Version 4 specificatio 1. **Persistence granularity:** MongoDB stores a bounded optimistic workspace snapshot, not separate aggregate collections with transaction-specific repositories. This is the largest architectural deviation. It preserves end-to-end behavior but is unsuitable for large collections and high concurrency. 2. **Package ZIP staging:** manifests and validation exist, but safe ZIP expansion, MIME checks, staging diff, transactional package update, and round-trip export are not complete. 3. **Revision model:** published exercise revision counters and immutable assessment snapshots exist. Full canonical revision records, scheduled impact notices, package-owned forks, and cross-workflow revision resolution are incomplete. -4. **Blank onboarding UI:** an Admin reset can create a blank workspace, and CRUD surfaces can rebuild it. A dedicated step-by-step onboarding wizard with foundation-package application is incomplete. -5. **Organization depth:** group/course/location creation works through generic guided forms, but historical course roster resolution, explicit inclusion/exclusion, and full timetable recurrence/override editing are simplified. +4. **Blank onboarding depth:** the guided setup creates the first real Subject, Group, learners, Location, Course, and Enrollments atomically without demo IDs. Foundation-package application and embedding the first Concept, Exercise, and Lesson directly in the wizard remain incomplete; the Today quick-start actions lead into their real editors instead. +5. **Organization depth:** guided Group, learner, Course, and Location editors work, but historical roster resolution, explicit inclusion/exclusion, bulk roster correction, and full timetable recurrence/override editing remain simplified. 6. **Assessment sophistication:** deterministic delivery, A/B labels, grading, and grades work. Slot ranking, meaningful equivalent alternatives, shortfall remediation UI, regrade preview, print stylesheet, and five analyzers are incomplete. 7. **Content package volume:** the runtime demo contains the important Grade 8 catalogue. The on-disk packages are contract examples rather than complete exported mirrors of every fixture. 8. **Authorization and CSRF:** role data, protected routes, secure cookie policy, origin checks, and rate limits exist. Fine-grained capabilities and an explicit CSRF token mechanism are incomplete. diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 0d94a4e..4fa13e7 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -11,6 +11,7 @@ import fastifyStatic from "@fastify/static"; import { existsSync } from "node:fs"; import path from "node:path"; import bcrypt from "bcryptjs"; +import { z } from "zod"; import { authenticate, createDemoState, @@ -312,6 +313,113 @@ export async function createApp( app.get("/api/workspace/summary", async () => ok(summarize(store.snapshot())), ); + app.get("/api/onboarding/status", async () => { + const state = store.snapshot(); + return ok({ + complete: + state.courses.length > 0 && + state.learnerGroups.length > 0 && + state.learners.length > 0, + counts: { + subjects: state.subjects.length, + groups: state.learnerGroups.length, + learners: state.learners.length, + locations: state.locations.length, + courses: state.courses.length, + }, + }); + }); + app.post("/api/onboarding/complete", async (req, reply) => { + const parsed = z + .object({ + subjectTitle: z.string().min(2), + groupTitle: z.string().min(1), + schoolYear: z.string().min(4), + learnerNames: z.array(z.string().min(1)).min(1), + locationTitle: z.string().min(2), + room: z.string().optional(), + courseTitle: z.string().min(2), + timezone: z.string().min(1).default(config.SCHOOL_TIMEZONE), + }) + .safeParse(req.body); + if (!parsed.success) + return reply + .code(400) + .send( + err( + "VALIDATION", + "onboarding.invalid", + false, + parsed.error.flatten().fieldErrors as Record, + ), + ); + const created = await store.mutate((state) => { + const now = clock.now().toISOString(); + const subject = { + id: id("subject"), + title: parsed.data.subjectTitle, + concurrencyVersion: 1, + createdAt: now, + updatedAt: now, + }; + const group = { + id: id("group"), + title: parsed.data.groupTitle, + schoolYear: parsed.data.schoolYear, + concurrencyVersion: 1, + createdAt: now, + updatedAt: now, + }; + const location = { + id: id("location"), + title: parsed.data.locationTitle, + room: parsed.data.room, + concurrencyVersion: 1, + createdAt: now, + updatedAt: now, + }; + const learners = parsed.data.learnerNames.map((name) => ({ + id: id("learner"), + title: name, + displayPseudonym: name, + administrativeIdentity: {}, + concurrencyVersion: 1, + createdAt: now, + updatedAt: now, + })); + const course = { + id: id("course"), + title: parsed.data.courseTitle, + subjectId: subject.id, + learnerGroupIds: [group.id], + defaultLocationId: location.id, + timezone: parsed.data.timezone, + status: "active", + concurrencyVersion: 1, + createdAt: now, + updatedAt: now, + }; + const enrollments = learners.map((learner) => ({ + id: id("enrollment"), + title: `${learner.title} – ${group.title}`, + learnerId: learner.id, + learnerGroupId: group.id, + status: "active", + startDate: now.slice(0, 10), + concurrencyVersion: 1, + createdAt: now, + updatedAt: now, + })); + state.subjects.push(subject); + state.learnerGroups.push(group); + state.locations.push(location); + state.learners.push(...learners); + state.enrollments.push(...enrollments); + state.courses.push(course); + return { subject, group, location, learners, course }; + }); + return reply.code(201).send(ok(created)); + }); app.get("/api/today", async () => { const state = store.snapshot(); return ok({ @@ -423,19 +531,44 @@ export async function createApp( const current = list[index]!; if (expected && Number(current.concurrencyVersion) !== expected) return err("CONFLICT", "common.staleWrite", true); - const next = { + const candidate = { ...current, ...(req.body as any), id: current.id, concurrencyVersion: Number(current.concurrencyVersion || 1) + 1, updatedAt: clock.now().toISOString(), }; - list[index] = next; - return ok(next); + if (route === "exercises") { + const parsed = exerciseSchema.safeParse(candidate); + if (!parsed.success) + return err( + "VALIDATION", + "exercise.invalid", + false, + parsed.error.flatten().fieldErrors as Record, + ); + Object.assign(candidate, parsed.data); + if ( + current.lifecycle !== "published" && + candidate.lifecycle === "published" + ) + candidate.currentRevision = + Number(current.currentRevision || 0) + 1; + } + list[index] = candidate; + return ok(candidate); }); return result.ok ? result - : reply.code(result.error.code === "CONFLICT" ? 409 : 404).send(result); + : reply + .code( + result.error.code === "CONFLICT" + ? 409 + : result.error.code === "VALIDATION" + ? 400 + : 404, + ) + .send(result); }); } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 3dc148c..0e72aa9 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -22,6 +22,9 @@ import { ProjectsPage } from "./pages/ProjectsPage"; import { GuidePage } from "./pages/GuidePage"; import { MathPlotPage } from "./pages/MathPlotPage"; import { NodeDetailPage } from "./pages/NodeDetailPage"; +import { MaterialEditorPage } from "./pages/MaterialEditorPage"; +import { OrganizationEditorPage } from "./pages/OrganizationEditorPage"; +import { SetupPage } from "./pages/SetupPage"; import { NotFoundPage } from "./pages/NotFoundPage"; function Protected() { const location = useLocation(); @@ -53,27 +56,66 @@ export function App() { } /> }> } /> + } /> } /> } /> + } /> + } /> } /> + } + /> } /> } /> } /> } /> } /> } /> + } + /> + } + /> } /> + } + /> + } + /> } /> + } + /> + } + /> } /> + } + /> + } + /> } diff --git a/apps/web/src/components/Shell.tsx b/apps/web/src/components/Shell.tsx index fc9c99f..cb49fa6 100644 --- a/apps/web/src/components/Shell.tsx +++ b/apps/web/src/components/Shell.tsx @@ -8,19 +8,43 @@ import { import { useQueryClient } from "@tanstack/react-query"; import { post } from "../api"; import { Logo } from "./Logo"; -const nav: Array<[string, string]> = [ - ["/", "Ma"], - ["/timetable", "Órarend"], - ["/library", "Könyvtár"], - ["/exercises/new", "Új feladat"], - ["/lessons", "Óratervek"], - ["/assignments", "Feladatok"], - ["/assessments", "Felmérések"], - ["/insights", "Elemzések"], - ["/projects", "Projektek"], - ["/admin", "Admin"], - ["/guide", "Útmutató"], -]; +const nav = [ + { + label: "Napi munka", + items: [ + ["/", "Ma"], + ["/timetable", "Órarend"], + ["/lessons", "Óratervek"], + ["/assignments", "Kiosztások"], + ["/assessments", "Felmérések"], + ["/insights", "Elemzések"], + ], + }, + { + label: "Tartalom", + items: [ + ["/library", "Könyvtár"], + ["/exercises", "Gyakorlófeladatok"], + ["/annual-plans", "Éves tervek"], + ["/projects", "Projektek"], + ], + }, + { + label: "Munkatér", + items: [ + ["/courses", "Kurzusok"], + ["/groups", "Csoportok"], + ["/learners", "Tanulók"], + ["/locations", "Helyszínek"], + ["/admin", "Admin"], + ["/guide", "Útmutató"], + ], + }, +] satisfies Array<{ label: string; items: Array<[string, string]> }>; + +const routeLabels: Record = Object.fromEntries( + nav.flatMap((group) => group.items), +); export function Shell() { const location = useLocation(); const navigate = useNavigate(); @@ -37,14 +61,20 @@ export function Shell() {
@@ -59,7 +89,14 @@ export function Shell() {
Aktuális hely - {location.pathname === "/" ? "Ma" : location.pathname} + {location.pathname === "/" + ? "Ma" + : routeLabels[ + Object.keys(routeLabels).find( + (route) => + route !== "/" && location.pathname.startsWith(route), + ) || "" + ] || "Szerkesztés"}
= { nodes: "Könyvtár", lessons: "Óratervek", @@ -15,11 +16,29 @@ const titles: Record = { learners: "Tanulók", locations: "Helyszínek", "annual-plans": "Éves tervek", + exercises: "Gyakorlófeladatok", }; +const statusLabels: Record = { + draft: "Piszkozat", + published: "Közzétett", + archived: "Archivált", + active: "Aktív", +}; + +const detailPath = (route: string, id: string) => { + if (route === "nodes") return `/library/${id}`; + if (route === "learner-groups") return `/groups/${id}`; + return `/${route}/${id}`; +}; + +const createPath = (route: string) => { + if (route === "nodes") return "/library/new"; + if (route === "learner-groups") return "/groups/new"; + return `/${route}/new`; +}; + export function EntityListPage({ route }: { route: string }) { const [search, setSearch] = useState(""); - const [newTitle, setNewTitle] = useState(""); - const qc = useQueryClient(); const q = useQuery({ queryKey: [route, search], queryFn: () => @@ -27,38 +46,34 @@ export function EntityListPage({ route }: { route: string }) { `/api/${route}?search=${encodeURIComponent(search)}&limit=100`, ), }); - const create = useMutation({ - mutationFn: () => - post( - `/api/${route}`, - route === "nodes" - ? { title: newTitle, type: "concept", lifecycle: "draft" } - : { title: newTitle, name: newTitle, status: "draft" }, - ), - onSuccess: () => { - setNewTitle(""); - qc.invalidateQueries({ queryKey: [route] }); - }, - }); const helper = createColumnHelper(); const columns = useMemo( () => [ helper.accessor((row) => row.title || row.name || row.id, { id: "title", header: "Megnevezés", - cell: (i) => {i.getValue()}, + cell: (i) => ( + + {i.getValue()} + + ), }), helper.accessor("lifecycle", { header: "Állapot", cell: (i) => ( - {i.getValue() || i.row.original.status || "aktív"} + {statusLabels[ + String(i.getValue() || i.row.original.status || "active") + ] || String(i.getValue() || i.row.original.status || "Aktív")} ), }), - helper.accessor("id", { - header: "Azonosító", - cell: (i) => {i.getValue()}, + helper.display({ + id: "actions", + header: "Művelet", + cell: (i) => ( + Megnyitás + ), }), ], [route], @@ -75,6 +90,9 @@ export function EntityListPage({ route }: { route: string }) { Szerkeszthető gyűjtemény

{titles[route] || route}

+ + Új elem +
@@ -83,19 +101,7 @@ export function EntityListPage({ route }: { route: string }) { value={search} onChange={(e) => setSearch(e.target.value)} /> -
- setNewTitle(e.target.value)} - /> - -
+ {q.data?.length || 0} találat
diff --git a/apps/web/src/pages/ExerciseEditorPage.tsx b/apps/web/src/pages/ExerciseEditorPage.tsx index 5cebb82..4ffa032 100644 --- a/apps/web/src/pages/ExerciseEditorPage.tsx +++ b/apps/web/src/pages/ExerciseEditorPage.tsx @@ -26,10 +26,16 @@ const schema = z.object({ acceptedUnit: z.string().optional(), correctValue: z.string().optional(), acceptedVariants: z.string().optional(), - optionsText: z.string().optional(), - correctOptionIds: z.string().optional(), + normalization: z.enum(["trim-casefold", "exact"]).default("trim-casefold"), + responseGuidance: z.string().optional(), + rubricMarkdown: z.string().optional(), + evidencePolicy: z.enum(["none", "light", "deep"]).default("light"), + contributionLevel: z + .enum(["introduces", "practices", "assesses"]) + .default("practices"), }); type Form = z.infer; +type ExerciseOption = { id: string; text: string }; export function ExerciseEditorPage() { const { id } = useParams(); const nav = useNavigate(); @@ -38,9 +44,20 @@ export function ExerciseEditorPage() { queryFn: () => api(`/api/exercises/${id}`), enabled: Boolean(id), }); + const concepts = useQuery({ + queryKey: ["concepts", "exercise-selector"], + queryFn: () => api("/api/nodes?search=&limit=100"), + select: (nodes) => nodes.filter((node) => node.type === "concept"), + }); const [prompt, setPrompt] = useState("Írd ide a feladat szövegét."); const [solution, setSolution] = useState(""); const [hydrated, setHydrated] = useState(!id); + const [options, setOptions] = useState([ + { id: crypto.randomUUID(), text: "Első lehetőség" }, + { id: crypto.randomUUID(), text: "Második lehetőség" }, + ]); + const [correctOptionIds, setCorrectOptionIds] = useState([]); + const [conceptIds, setConceptIds] = useState([]); const form = useForm({ resolver: zodResolver(schema), defaultValues: { @@ -49,8 +66,9 @@ export function ExerciseEditorPage() { expectedMinutes: 5, difficulty: 2, lifecycle: "draft", - optionsText: "Első lehetőség\nMásodik lehetőség", - correctOptionIds: "a", + normalization: "trim-casefold", + evidencePolicy: "light", + contributionLevel: "practices", }, }); useEffect(() => { @@ -58,14 +76,27 @@ export function ExerciseEditorPage() { const e = existing.data; setPrompt(e.promptMarkdown || ""); setSolution(e.solutionMarkdown || ""); + setOptions( + e.options?.length + ? e.options + : [ + { id: crypto.randomUUID(), text: "Első lehetőség" }, + { id: crypto.randomUUID(), text: "Második lehetőség" }, + ], + ); + setCorrectOptionIds(e.correctOptionIds || []); + setConceptIds(e.conceptIds || []); form.reset({ ...e, expectedValue: e.expectedValue, absoluteTolerance: e.absoluteTolerance, correctValue: String(e.correctValue ?? ""), acceptedVariants: (e.acceptedVariants || []).join("\n"), - optionsText: (e.options || []).map((o: any) => o.text).join("\n"), - correctOptionIds: (e.correctOptionIds || []).join(","), + normalization: e.normalization || "trim-casefold", + responseGuidance: e.responseGuidance || "", + rubricMarkdown: e.rubricMarkdown || "", + evidencePolicy: e.evidencePolicy || "light", + contributionLevel: e.contributionLevel || "practices", }); setHydrated(true); } @@ -73,10 +104,6 @@ export function ExerciseEditorPage() { const selected = form.watch("exerciseType"); const save = useMutation({ mutationFn: async (v: Form) => { - const options = (v.optionsText || "") - .split("\n") - .filter(Boolean) - .map((text, i) => ({ id: String.fromCharCode(97 + i), text })); const body: any = { title: v.title, exerciseType: v.exerciseType, @@ -85,12 +112,17 @@ export function ExerciseEditorPage() { expectedMinutes: Number(v.expectedMinutes), difficulty: Number(v.difficulty), lifecycle: v.lifecycle, - conceptIds: [], - options, - correctOptionIds: (v.correctOptionIds || "") - .split(",") - .map((x) => x.trim()) - .filter(Boolean), + conceptIds, + options: + v.exerciseType === "single-choice" || + v.exerciseType === "multiple-choice" + ? options.filter((option) => option.text.trim()) + : undefined, + correctOptionIds: + v.exerciseType === "single-choice" || + v.exerciseType === "multiple-choice" + ? correctOptionIds + : undefined, correctValue: v.correctValue === "true", expectedValue: v.expectedValue, absoluteTolerance: v.absoluteTolerance, @@ -98,7 +130,11 @@ export function ExerciseEditorPage() { acceptedVariants: (v.acceptedVariants || "") .split("\n") .filter(Boolean), - normalization: "trim-casefold", + normalization: v.normalization, + responseGuidance: v.responseGuidance, + rubricMarkdown: v.rubricMarkdown, + evidencePolicy: v.evidencePolicy, + contributionLevel: v.contributionLevel, concurrencyVersion: existing.data?.concurrencyVersion, }; return id @@ -177,16 +213,70 @@ export function ExerciseEditorPage() { /> {(selected === "single-choice" || selected === "multiple-choice") && ( - <> -