bf5ea860cc
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 29s
CI / test-python-klausur (push) Failing after 3m56s
CI / test-python-agent-core (push) Successful in 19s
CI / test-nodejs-website (push) Successful in 23s
Backend (school-service):
- tt_solution gains parent_solution_id (self-FK, ON DELETE SET NULL)
and seconds_limit columns via ALTER TABLE IF NOT EXISTS.
- CreateTimetableSolutionRequest accepts optional parent_solution_id
and seconds_limit (5-600s) with binding validation.
- CreateSolution checks parent ownership before INSERT so users can't
fork another tenant's plan.
- New PUT /timetable/lessons/:id/pin endpoint; ownership enforced via
the lesson's solution.created_by_user_id JOIN.
Solver:
- Lesson.pinned now carries @PlanningPin so Timefold leaves locked
cells untouched during the search.
- build_problem() takes optional parent_solution_id; if set, copies
pinned (class_id, subject_id, day, period, room) tuples onto fresh
Lesson objects via greedy first-fit matching. Surplus pinned rows
from curriculum changes are silently dropped.
- _build_factory(seconds) replaces the module-level factory so each
job honours its tt_solution.seconds_limit override.
- persist_solution writes lesson.pinned back so subsequent re-solves
inherit it.
Frontend (studio-v2):
- SolutionList grows three knobs in the create-form: Basieren auf
(parent dropdown, only completed solutions, disabled when none),
Sekunden-Limit (5-600), and the existing Name.
- PlanView cells get a pin/unpin button with optimistic update and
rollback on error. Pinned cells gain an amber ring.
- types.ts + api.ts mirror the new fields; lessonsApi.pin(id, bool).
- HelpPanel: collapsible 6-step Bedienungsanleitung explaining the
setup-to-plan workflow. Anchored at the top of /stundenplan above
the dev token banner.
- page.tsx switches to the same gradient + animated-blob background
used on /korrektur so /stundenplan stops looking like a slate-900
test page.
- JWT dev banner gets a step-by-step explanation of how to grab the
token from DevTools and a non-blocking success indicator (no more
alert()).
Tests:
- school-service: 6 new validator cases for parent_solution_id +
seconds_limit boundaries. 73 subtests total, all green.
- studio-v2: mockSchoolApi adds PUT /lessons/:id/pin route. 5 new
Playwright tests across two suites (parent-selector visibility +
options, seconds-limit input, pin button render, pin-icon flip).
Existing tests adjusted to the new help panel + JWT banner wording.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 lines
2.7 KiB
Go
64 lines
2.7 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TimetableSolution is one run of the solver — exactly one row per solve.
|
|
// Lessons attached via tt_lesson.solution_id.
|
|
type TimetableSolution struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
CreatedByUserID uuid.UUID `json:"created_by_user_id" db:"created_by_user_id"`
|
|
Name string `json:"name,omitempty" db:"name"`
|
|
Status string `json:"status" db:"status"`
|
|
HardScore *int `json:"hard_score,omitempty" db:"hard_score"`
|
|
SoftScore *int `json:"soft_score,omitempty" db:"soft_score"`
|
|
ErrorMessage string `json:"error_message,omitempty" db:"error_message"`
|
|
StartedAt *time.Time `json:"started_at,omitempty" db:"started_at"`
|
|
FinishedAt *time.Time `json:"finished_at,omitempty" db:"finished_at"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
ParentSolutionID *uuid.UUID `json:"parent_solution_id,omitempty" db:"parent_solution_id"`
|
|
SecondsLimit *int `json:"seconds_limit,omitempty" db:"seconds_limit"`
|
|
}
|
|
|
|
// TimetableLesson is one scheduled class-period in a solution.
|
|
type TimetableLesson struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
SolutionID uuid.UUID `json:"solution_id" db:"solution_id"`
|
|
ClassID uuid.UUID `json:"class_id" db:"class_id"`
|
|
SubjectID uuid.UUID `json:"subject_id" db:"subject_id"`
|
|
TeacherID uuid.UUID `json:"teacher_id" db:"teacher_id"`
|
|
RoomID *uuid.UUID `json:"room_id,omitempty" db:"room_id"`
|
|
DayOfWeek int `json:"day_of_week" db:"day_of_week"`
|
|
PeriodIndex int `json:"period_index" db:"period_index"`
|
|
Pinned bool `json:"pinned" db:"pinned"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
|
|
// Joined fields for display
|
|
ClassName string `json:"class_name,omitempty"`
|
|
SubjectName string `json:"subject_name,omitempty"`
|
|
TeacherName string `json:"teacher_name,omitempty"`
|
|
RoomName string `json:"room_name,omitempty"`
|
|
}
|
|
|
|
// CreateTimetableSolutionRequest kicks off a solve. The solver-service is
|
|
// invoked async — this endpoint only registers the solution row and queues
|
|
// the job.
|
|
//
|
|
// ParentSolutionID, if set, instructs the solver to seed the new problem
|
|
// with the parent solution's pinned lessons (Phase 7 plan versioning).
|
|
// SecondsLimit overrides the default 60s solver budget.
|
|
type CreateTimetableSolutionRequest struct {
|
|
Name string `json:"name"`
|
|
ParentSolutionID *string `json:"parent_solution_id,omitempty" binding:"omitempty,uuid"`
|
|
SecondsLimit *int `json:"seconds_limit,omitempty" binding:"omitempty,min=5,max=600"`
|
|
}
|
|
|
|
// UpdateLessonPinRequest toggles tt_lesson.pinned. Used by the Plan view's
|
|
// pin/unpin button.
|
|
type UpdateLessonPinRequest struct {
|
|
Pinned bool `json:"pinned"`
|
|
}
|