Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6b4c2d9
fix: harden DB persistence to prevent data loss on Drive sync
geokoko Mar 16, 2026
c499582
fix: render icon characters via Noto Emoji font for cross-platform su…
geokoko Mar 17, 2026
5ff6d90
fix: re-run delayed task/goal processing after Drive DB reload
geokoko Mar 17, 2026
718c2c1
fix: exclude postponed recurring tasks from missed detection
geokoko Mar 17, 2026
6dfc0ce
fix: re-plan dropdown filtering and empty box rendering
geokoko Mar 17, 2026
c1f4e3b
refactor: simplify repeated patterns and improve error handling
geokoko Mar 17, 2026
3e76549
fix: replace non-rendering emoji with basic Unicode symbols
geokoko Mar 17, 2026
ed93e56
fix: only show re-plan goals whose task is not in today's list
geokoko Mar 17, 2026
b051911
fix: move inline warning icon to setGraphic and reuse session error l…
geokoko Mar 17, 2026
2a7bcdd
fix: complete icon rendering sweep across all UI panels
geokoko Mar 17, 2026
fe5e4c5
fix: null-safety bugs in search filters, reminder toString, and date …
geokoko Mar 18, 2026
987b94e
fix: button icon overlap and empty combo-box dropdown entries
geokoko Mar 18, 2026
6e971a7
chore: gitignore CLAUDE.md to keep local-only config out of repo
geokoko Mar 20, 2026
737b2d4
feat: three-option exit dialog and local save button
geokoko Mar 21, 2026
0639539
feat: enforce single application instance via file lock
geokoko Mar 21, 2026
30a2d2b
fix: GNOME desktop integration — set WM_CLASS for window matching
geokoko Mar 21, 2026
110c840
fix: correct GNOME StartupWMClass to match JavaFX window class
geokoko Mar 21, 2026
e438418
fix: force dialog/stage dimensions on GNOME to prevent tiny windows
geokoko Mar 21, 2026
efbc535
fix: always run H2 SHUTDOWN on exit, remove debug diagnostics
geokoko Mar 21, 2026
22a1e07
fix: stop auto-carry-forward of delayed goals in planner queries
geokoko Mar 22, 2026
84883e5
feat: add goal history to tasks and soft-delete goals instead of remo…
geokoko Mar 26, 2026
d2edc13
fix: prevent achieved+failed inconsistency on goal soft-delete
geokoko Mar 26, 2026
c77002a
fix: mark re-planned unachieved goals as failed instead of letting th…
geokoko Mar 27, 2026
78b8b5f
fix: calendar hard-deletes goals, shows all statuses, adds mark-as-fa…
geokoko Mar 27, 2026
3b3224d
Merge pull request #24 from geokoko/feature/task-goal-history
geokoko Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ bin/
.classpath
.project

# Claude
CLAUDE.md

# OS
.DS_Store
Thumbs.db
Expand Down
2 changes: 1 addition & 1 deletion packaging/studysync.desktop
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ Icon=studysync
Terminal=false
Categories=Education;Office;ProjectManagement;
Keywords=study;task;project;calendar;academic;planner;
StartupWMClass=com-studysync-StudySyncApplication
StartupWMClass=com.studysync.application.StudySyncJavaFXApp
2 changes: 1 addition & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ Icon=studysync
Terminal=false
Categories=Education;Office;ProjectManagement;
Keywords=study;task;project;calendar;academic;planner;
StartupWMClass=com-studysync-StudySyncApplication
StartupWMClass=com.studysync.application.StudySyncJavaFXApp
EOF
else
print_info "Using desktop file from: $desktop_source"
Expand Down
75 changes: 60 additions & 15 deletions src/main/java/com/studysync/StudySyncApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.Path;

/**
* Main entry point for the StudySync desktop application.
*
Expand All @@ -31,42 +38,80 @@ public class StudySyncApplication {

/** The Spring application context instance shared across the application. */
private static ConfigurableApplicationContext springContext;


/** File lock held for the lifetime of the process to enforce single-instance. */
@SuppressWarnings("unused") // must stay referenced to prevent GC releasing the lock
private static FileLock instanceLock;
@SuppressWarnings("unused")
private static RandomAccessFile lockRaf;

/**
* Main method that starts the StudySync application.
*
* <p>This method performs the following initialization sequence:
* <ol>
* <li>Configures system properties for JavaFX compatibility</li>
* <li>Initializes the Spring Boot application context</li>
* <li>Launches the JavaFX application with Spring integration</li>
* </ol></p>
*
*
* @param args command line arguments passed to both Spring Boot and JavaFX
*/
public static void main(final String[] args) {
// Enforce single instance — exit immediately if another process holds the lock
if (!acquireInstanceLock()) {
System.err.println("StudySync is already running. Exiting.");
System.exit(1);
}

// Disable Spring Boot's automatic shutdown when main method ends
System.setProperty("java.awt.headless", "false");

// Attempt to sync Google Drive hosted data (if configured) before Spring initializes the database
GoogleDriveBootstrap.initialize();

// Configure Spring Boot for faster startup
SpringApplication app = new SpringApplication(StudySyncApplication.class);

// Enable lazy initialization for faster startup
app.setLazyInitialization(true);

// Reduce startup output
app.setLogStartupInfo(false);
app.setBannerMode(org.springframework.boot.Banner.Mode.OFF);

// Start Spring Boot application context
springContext = app.run(args);

// Launch JavaFX application
Application.launch(StudySyncJavaFXApp.class, args);
}

/**
* Attempts to acquire an exclusive file lock to prevent multiple instances.
* The lock file is created in the user's data directory and held for the
* lifetime of the JVM process (released automatically on exit).
*
* @return true if the lock was acquired (no other instance running)
*/
private static boolean acquireInstanceLock() {
try {
Path lockDir = Path.of(System.getProperty("user.home"), ".local", "share", "studysync");
Files.createDirectories(lockDir);
Path lockFile = lockDir.resolve(".studysync.lock");

// RandomAccessFile + FileLock held in static fields so GC cannot release them
lockRaf = new RandomAccessFile(lockFile.toFile(), "rw");
FileChannel channel = lockRaf.getChannel();
instanceLock = channel.tryLock();

if (instanceLock == null) {
// Another instance holds the lock — close the file before exiting
try { lockRaf.close(); } catch (IOException ignored) { }
lockRaf = null;
return false;
}
return true;
} catch (Exception e) {
// Catches IOException AND OverlappingFileLockException (from same-JVM
// re-entry, e.g. test harnesses calling main() twice).
System.err.println("Warning: could not check instance lock: " + e.getMessage());
return true; // allow startup if lock mechanism fails
}
}

/**
* Provides access to the Spring application context for JavaFX components.
Expand Down
28 changes: 18 additions & 10 deletions src/main/java/com/studysync/application/StudySyncJavaFXApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,32 +143,40 @@ public void start(final Stage primaryStage) {
event.consume(); // Prevent immediate close

Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Sync to Google Drive");
alert.setHeaderText("Save changes to Google Drive?");
alert.setContentText("Do you want to upload your latest data to Google Drive before exiting?");
alert.setTitle("Exit StudySync");
alert.setHeaderText("How would you like to exit?");
alert.setContentText("Choose how to save your data before closing.");

ButtonType buttonSave = new ButtonType("Save & Exit");
ButtonType buttonExit = new ButtonType("Exit Only");
ButtonType buttonDrive = new ButtonType("Push to Drive & Exit");
ButtonType buttonLocal = new ButtonType("Save Locally & Exit");
ButtonType buttonNoSave = new ButtonType("Exit without Saving");
ButtonType buttonCancel = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);

alert.getButtonTypes().setAll(buttonSave, buttonExit, buttonCancel);
alert.getButtonTypes().setAll(buttonDrive, buttonLocal, buttonNoSave, buttonCancel);

Optional<ButtonType> result = alert.showAndWait();

if (result.isPresent()) {
if (result.get() == buttonSave) {
logger.info("User chose to save and exit");
if (result.get() == buttonDrive) {
logger.info("User chose to push to Drive and exit");
driveService.setShutdownSaveEnabled(true);
Platform.exit();
} else if (result.get() == buttonExit) {
logger.info("User chose to exit without saving to Drive");
} else if (result.get() == buttonLocal) {
logger.info("User chose to save locally and exit");
driveService.saveLocally();
driveService.setShutdownSaveEnabled(false);
Platform.exit();
} else if (result.get() == buttonNoSave) {
logger.info("User chose to exit without saving");
driveService.setShutdownSaveEnabled(false);
Platform.exit();
} else {
logger.info("User cancelled exit");
}
}
} else {
// Not signed in — flush H2 to disk and exit
driveService.saveLocally();
Platform.exit();
}
} catch (Exception e) {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/studysync/config/DatabaseReloadService.java
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ public void reconnect() {
}
}

// Evict the validation connections so that subsequent queries from
// panels/services go through brand-new connections. Without this,
// the connections used during the retry loop above might still be
// in the pool and could serve cached data from the old H2 engine.
if (dataSource instanceof HikariDataSource hikari) {
HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
if (pool != null) {
pool.softEvictConnections();
logger.debug("Post-reconnect: evicted validation connections from pool");
}
}

// Run idempotent schema.sql to apply any missing migrations
runMigrations();

Expand Down
Loading
Loading