┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ JavaFX UI │ │ REST Controllers │ │ RefreshablePanel│
│ │ Components │ │ (TaskRestController)│ │ Interface │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Simplified Service Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ TaskService │ │ StudyService │ │ProjectService│ │
│ │ CategoryService │ │ ReminderService │ │ ...more │ │
│ │ (Business Logic │ │ Orchestration) │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Active Record Domain Models │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ Task │ │ StudySession │ │ Project │ │
│ │ • save() │ │ • save() │ │ • save() │ │
│ │ • delete() │ │ • delete() │ │ • delete() │ │
│ │ • findAll() │ │ • findAll() │ │ • findAll() │ │
│ │ • findById() │ │ • findById() │ │ • findById()│ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ ProjectSession │ │ TaskPriority │ │TaskCategory │ │
│ │ • save() │ │ TaskStatus │ │DailyReflection│
│ │ • delete() │ │ ProjectStatus │ │ ...more │ │
│ │ • findAll() │ │ (Value Objects)│ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────┐
│ H2 Database │
│ (Direct JDBC) │
└─────────────────┘
When the optional Google Drive integration is configured, the persistence layer gains an additional offline-first sync loop:
- Bootstrap – before Spring Boot initializes the datasource, the
GoogleDriveBootstrapdownloads the lateststudysync.mv.dbfrom the signed-in Google Drive account (if cached credentials exist). - Runtime – StudySync continues to operate against the local H2 file for fast, offline reads/writes. Users can trigger a manual upload from the Profile → Google Drive Sync panel at any time.
- Shutdown –
GoogleDriveServiceuploads the updated H2 file to the user's privateStudySyncDrive folder during bean destruction, ensuring multi-device availability without a StudySync backend.
OAuth credentials live on the user's machine (~/.studysync/google) and the cloud copy resides inside the user's own Drive (My Drive/StudySync/studysync.mv.db).
- Active Record Pattern: Models handle their own database operations
- Simplified Services: Services focus on business logic and orchestration
- No Database Service Layer: Eliminated unnecessary abstraction layer
- Self-Managing Models:
task.save(),project.delete(),StudySession.findAll() - Mutable Domain Objects: Task, Project, StudySession are classes with behavior
- Real-time Session Tracking: Live timers with Duration-based calculations
- Direct JDBC: Models use JdbcTemplate directly for database operations
Tasks support an optional recurrence schedule via a recurring_pattern column:
- Format:
"intervalWeeks:daysOfWeek"— e.g."1:1,3,5"= every week on Mon, Wed, Fri;"2:1,4"= every 2 weeks on Mon, Thu - NULL means a one-off (non-recurring) task
Task.isRecurring()andTask.getRecurringSummary()provide runtime helpersTaskService.recurringTaskAppliesTo(task, date, referenceMonday)checks whether a recurring task should appear on a given date- When editing,
""(empty string) inTaskUpdate.recurringPatternsignals "clear the pattern", whilenullmeans "keep existing"
Study goals can be planned for future dates:
StudyService.getStudyGoalsForFutureDate(date)returns goals for a specific future date without delay processing- The Study Planner's "Add Goal" dialog includes a DatePicker (today + future only) with quick "Today" / "Tomorrow" buttons
- Calendar and Daily views branch on past/today/future when loading goals
processAllDelayedGoals() performs a full table scan and write operations. To avoid redundant work on every UI refresh, a lastDelayProcessingDate field ensures it runs at most once per calendar day.
Since spring.sql.init.mode: always loads schema.sql on every startup:
- Tables use
CREATE TABLE IF NOT EXISTS(safe for existing DBs) - New columns are added via
ALTER TABLE ... ADD COLUMN IF NOT EXISTSat the bottom ofschema.sql - This avoids data loss and supports rolling upgrades without external migration tools