The load-time Byte Buddy agent cost ~420 ms on every launch before the
splash screen could show. FxThreadPlugin now weaves @FxThread at build time
(byte-buddy-maven-plugin, process-classes) with the same inlined advice and
method matcher as the agent, and marks each woven class @FxThreadWoven.
FxThreadAgent.install() only attaches when the application's classes run
from a directory (an IDE run), where an incremental compiler may have
overwritten woven classes, and then weaves only the classes without the
marker, so nothing is woven twice; -Dpholio.fxthread.agent=always|never
overrides the choice. A Maven-built jar starts with no agent: the splash's
first paint moves from ~0.83 s to ~0.47 s.
The measurements behind this are in .claude/plans/startup-time-analysis.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
SplashPreloader is a JavaFX Preloader, registered by GuiBootstrap through
javafx.preloader, so it only exists on the --ui path and shows as soon as
the toolkit is up, before the Spring context is built. It displays the
splash image with the application name, the build version read from
META-INF/build-info.properties, a status line and a progress bar, and fades
out once the main window is actually on screen.
StartupProgressReporter, a BeanPostProcessor added to the context before it
refreshes, turns the context's construction into real progress (beans
initialised over bean definitions) and status lines following the measured
phases: opening the library, loading components, preparing the interface.
The template's placeholder custom.property is dropped from build-info.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
The repackaged jar no longer ships the OpenJFX jars (every target runtime
provides JavaFX as modules, and they only held the build machine's
natives), Jilt (compile-time annotations, like Lombok) or the JUnit 6 and
AssertJ that devtoolsfx-connector wrongly declares at compile scope:
107 -> 89 libraries, 236.5 -> 223.4 MB. The POM version is reset to 0.0.1.
The README documents what takes space in the jar, and that the desktop
interface needs --ui.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
Sections are now the commit types (features, fixes, ...) with one sub-section
per scope, marked with a label icon instead of the puzzle piece.
ReleaseNotesTest, AGENTS.md and the release profile's comment follow the new
order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
The release profile gains two steps. notes writes
distrib/release_note/release-note-<version>.md from the Conventional
Commits since the previous v<version> tag (last 30 commits for the first
release), grouped by scope then type, duplicate first lines merged, with an
icon per type. publish uses that note as the GitHub release description and
attaches it; after the local tag, distrib commits the note in the distrib
worktree and pushes it. Covered by ReleaseNotesTest.
pom.xml is committed at 2026.9.0, so the next release is 2026.9.1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
./mvnw -Prelease validate bumps the project version (same month: build
number + 1, otherwise build 0 of the current month), runs a fresh
./mvnw verify, publishes pholio-<version>.jar and its SHA-256 as GitHub
release v<version> in Imag-In/Pholio through gh, then commits pom.xml and
tags v<version> locally. The non-Maven steps live in
tools/release/Release.java, a JDK-only single-file program; the version rule
is covered by ReleaseVersionTest.
AGENTS.md documents the new release flow and that source never goes to
GitHub (orphan distrib worktree, pre-push guard).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
sortpom rewrote pom.xml on every package (property order, schemaLocation
indentation, a closing </arg> tag), leaving the working tree dirty after
each build. Committing its output once keeps builds from modifying it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
ModalPane wraps its content in a ScrollPane, which was silently
corrupting rendering and mouse hit-testing for anything nested inside
it once its contents mutated rapidly: a TextField's own caret/text
could freeze mid-edit, and clicking a ListView row could stop
registering selection (also seen, independently, in the unrelated
Preferences dialog). None of it self-healed short of a full window
resize. A plain owned Stage (transparent, window-modal, centered and
kept in sync with the dialog's own size) shows neither symptom.
ModalService's public API is unchanged, so no caller needed touching.
Also in GeoLocationEditView: coalesce in-flight geocoding searches so
fast typing/deleting against a slow remote provider never queues more
than one call at a time, and back the debounce off to 500ms to stay
under LocationIQ's own per-second rate limit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016okXfGz39FtQawFWDYpQ5C
Moved LibraryCatalog's default-name message key next to app.name/
app.title as app.library.defaultName (grouped with the other app-level
strings, dropping the old library.defaultName). Also fixes two real
bugs the rename introduced along the way: the new key's value carried
literal quote characters ("My Photos" / "Mes Photos" instead of My
Photos / Mes Photos), which would have made it into the actual default
library name, and the leftover French library.defaultName had been
overwritten to "Doublons" -- French for "Duplicates", clearly a stray
edit given nav.tools.duplicates sits right above it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consuming the accessory's own MOUSE_RELEASED broke Button click handling:
Control's InputMap bails out once an event arrives already consumed, and
that race depended on the lazily-created Skin's registration order.
Consume one level up on the graphic instead, once the accessory's own
dispatch (Button included) has already fully run.
Also: the icon was stuck at AtlantaFX's theme default because Ikonli's
FontIcon always reports its size property as USER_AGENT origin, so a
size set in code never won the cascade — sized via a dedicated pholio.css
rule instead, same pattern as the thumbnail icons. The button now only
reveals on hovering the Photos title (forced visible while the library is
empty), and clicking the title itself — expand or collapse — navigates to
Photos whenever it isn't already the active destination.
Add real XMP read/write for face/animal regions and keywords, closing
the gap where Commons Imaging (EXIF/TIFF only) could never produce a real
dc:subject or mwg-rs:Regions packet. XmpPacketBuilder/XmpRegionParser use
Adobe's XMPCore to build and re-parse the RDF packet (verified end-to-end
against real JPEG/PNG/TIFF files and metadata-extractor's own reader);
XmpMetadataWriter plugs into CompositeMetadataWriter, which now fans a
write out to every capable delegate instead of stopping at the first,
since EXIF and XMP are independent halves of the same file's metadata.
MediaFaceRegion carries a resolved person name for serialization, and
MediaMetadataEditService.updateFaceRegions/PersonTagSyncService are wired
and tested but not yet called from anywhere — they're for the future
person-naming panel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JzvA5ySQUsYrMUTj7sHxFA
Add a face/animal recognition feature: local ONNX detection (YuNet+SFace
for faces with automatic person clustering, YOLOX-Nano for animal species)
behind an IRecognitionService seam mirroring IPlaceSearchService's local/
remote proxy pattern, with a separate engine seam so the ONNX backend can
be swapped later. Wired into the import/sync pipeline via TaskType.IMAGE_ANALYSIS
with a visible progress task, a startup backfill for existing libraries, and
a cheap perceptual-hash skip so metadata-only edits don't retrigger detection.
Detected regions are stored (new person/media_face_region tables) and shown
read-only in MediaInfoPane. Bundled ONNX weights (MIT/Apache-2.0) are tracked
via Git LFS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JzvA5ySQUsYrMUTj7sHxFA
java -jar target/pholio.jar (what jDeploy's installer ships) threw
ClassNotFoundException: net.bytebuddy.dynamic.Nexus from the moment any
org.icroco.pholio.* class loaded, right after FxThreadAgent self-attached.
Only ever showed up in the packaged jar, never from the IDE or the test
JVM's plain classpath, which is why it went unnoticed until the installer
was actually run.
Two changes needed together:
- spring-boot-maven-plugin: requiresUnpack for byte-buddy/byte-buddy-agent,
so the loader extracts them to real temp files instead of leaving Byte
Buddy's own jar-based bootstrapping nothing to write to inside a nested
jar-in-a-jar.
- FxThreadAgent: AgentBuilder.InitializationStrategy.NoOp instead of the
default SelfInjection, which registers every transformed type with Nexus
for redefinition bookkeeping this agent never needs (RedefinitionStrategy
stays disabled — every class here is instrumented once, on first load).
Verified by actually running the repackaged jar (it silently rendered a
blank/never-reached window before) against a real library.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWcuHVKWKo8YHEZSyfmT3b
Implement MetadataWriter for the everyday bitmap formats via Apache Commons
Imaging: CommonsImagingMetadataWriter rewrites the EXIF segment/directories
for JPEG and TIFF, PngChunkMetadataWriter hand-rolls the PNG eXIf chunk with
the same TIFF-structured EXIF payload (ExifTiffPatch holds the shared tag
mapping). CompositeMetadataWriter lets StandardImageMetaFactory route one
format per engine while still exposing a single writer per the factory
contract. GIF, BMP and WEBP remain unsupported.
Also fix MetadataPatch.isEmpty() ignoring geoLocation, and
MetadataExtractorReader.tagsOf() not reading back the Windows XPKeywords tag
the new writer uses for keywords.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWcuHVKWKo8YHEZSyfmT3b
Add MediaFile.processingFlags (METADATA_GENERATED, THUMBNAIL_GENERATED,
GEO_REFERENCE_GENERATED), packed into media_file.processing_flags as a
plain int since JDBC has no bitset column type. Wired into the live
pipeline (LibraryFolderService, MediaAnalysisService) so each flag is set
the moment its step actually succeeds, plus a startup task that backfills
already-imported rows by checking real metadata/thumbnail presence,
batched and non-destructive (only ever sets a bit, never clears one),
publishing a Notification with the count and elapsed time.
Switches MediaFile's builder from Lombok to Jilt: a record component this
central needed a defaulting compact constructor, which meant hand-picking
the generated static entry points anyway, so this was the natural point to
make the switch AGENTS.md now calls for project-wide.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQRmj6EdkdJygZSamMzH2Z
StatusBar.renderPath now leads with the file's owning LibraryFolder's directory name,
looked up by LibraryFolderService.findById off the FX thread and marshalled back via a
new @FxThread applyPath. Added LibraryFolderService.findById itself for this.
That lookup runs on every gallery selection change against rows that essentially never
change, so it's now @Cacheable (spring-boot-starter-cache, Caffeine-backed via
spring.cache.type=caffeine). Ids are per-database auto-increment and this app switches
which H2 file is open at runtime, so a stale entry from one library could otherwise
silently answer for another - remove(...) and a new onLibraryChanged listener (on
LibraryChangedEvent, whose javadoc already anticipated this) both evict the whole cache
rather than one entry, since either event makes every cached id suspect at once.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8a15CjTSwHXD3XCYQbrHt
Renames FileHasher to JdkSha256Hasher and introduces IFileHasher plus a second
ApacheMd5Hasher implementation (Apache Commons Codec), so JdkSha256Hasher's SHA-256
vs a non-JDK digest can actually be measured rather than assumed. Two @Component beans
now implement IFileHasher with neither marked @Primary, which leaves the app context
ambiguous (NoUniqueBeanDefinitionException) wherever IFileHasher is autowired -
tracked separately, not fixed by this commit.
Adds FileHasherBenchmark (JMH, test-scoped) comparing the two on a few representative
photo sizes, generated on the fly rather than committed as binary fixtures. Wires
jmh-core/jmh-generator-annprocess into pom.xml, plus exec-maven-plugin to run it
without a separate benchmarks module, and widens the NullAway exclusion regex to
also cover target/generated-test-sources, where JMH's generated runner classes land.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8a15CjTSwHXD3XCYQbrHt
MediaAnalysisService.extractMetadata now only reads metadata; LibraryFolderService
does the persisting, windowed via Stream.gather(Gatherers.windowFixed) into batched
saveAll calls instead of one save per file.
Task A's file hashing (the dominant cost on a NAS-backed library — full-file reads,
not the digest) now runs concurrently on a new HASH pool instead of a single-threaded
loop, cutting import time by close to an order of magnitude. Duplicate detection stays
a single sequential pass over the hashed results to avoid a same-scan race.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8a15CjTSwHXD3XCYQbrHt
StatusBar gains a Breadcrumbs (AtlantaFX) right of the library label: a
home icon, then one crumb per directory segment of the selected photo's
path, chevron-separated, ending in the filename. Hidden while nothing is
selected.
SelectionSource gains selectedMediaFile() alongside the existing
hasSelection(), and ViewportSelection mirrors it — the shared contract a
viewport view exposes its current file through. GalleryView implements it,
delegating both methods to ThumbnailGalleryPane.selectedFileProperty().
That property is set by an actual click handler on each thumbnail
(previously nothing selected anything at all), reusing the .photo-card
CSS class that already had hover/selected rules defined and unused.
Selection highlight uses a real :selected pseudo-class, refreshed
reactively per cell since ListCells are pooled and reused across rows.
New dependency: ikonli-material2-pack, for the home/chevron icons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJrVjrBdrUJYHYM9Hv7gX7
The build-time byte-buddy-maven-plugin weave (previous commit) broke
in practice: an IDE's own incremental compiler recompiles straight
into target/classes on every run by default, silently overwriting the
woven classes with plain ones the Maven plugin never touched. Nothing
failed at build time — StatusBar.applyLibraryStatus just ran on a
background thread with no guarantee at all, crashing with "Not on FX
application thread" the first time a folder was removed from a real,
IDE-launched run.
Replace it with FxThreadAgent, self-attaching at the first statement
of PholioApplication.main(), before Spring, JavaFX, or any of this
application's own classes are touched. A JVM agent instruments
classes as they load, independent of which compiler produced the
.class file — IDE, `mvn spring-boot:run`, or the packaged jar all get
the same weave.
The mechanism itself also changed, not just where it installs.
MethodDelegation + @SuperCall (the natural way to call the original
body from elsewhere) needs Byte Buddy to inject an auxiliary
"rebased original" class into the target's classloader, and every
injection strategy available on this JVM (reflection, Unsafe,
Instrumentation) throws IllegalStateException on it — confirmed
empirically, not assumed. FxThreadInterceptor now uses plain Advice
instead: inlined directly into the target method, so no injection is
needed at all. Off the FX thread, it skips the body for that call and
reflectively re-invokes the exact same (still-instrumented) method
from Platform.runLater — which re-enters the same thread check, and
this time passes. The instrumented method is its own deferred replay
target; no auxiliary class, no @SuperCall.
Also migrates every remaining hand-written FxUtils.onFxThread(...)
call site to @FxThread — ViewSwitcher, NavigationRail, AppHeaderBar,
LibraryFolderTree, ToastLayer, ModalService, TaskManager, ThemeManager,
LibraryImportCoordinator, ThumbnailGalleryPane — the work interrupted
by discovering the build-time bug above, now redone against the
working mechanism. Two of these are exactly the case a Spring AOP
proxy could never have handled: NavigationRail.rebuild and
ThemeManager.applyResolvedTheme are both called directly from their
own constructor, and ThumbnailGalleryPane is not a Spring bean at all.
FxThreadAgentTest reproduces the original crash end to end — a real
shown Stage, a real background thread, the real agent — and asserts
it no longer throws.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZpQK78WhohL1CqDip3PfM
Every scenegraph mutation reachable from a background thread was
wrapped in FxUtils.onFxThread(...) by hand, at each call site. Add
@FxThread so the guarantee lives on the method declaration instead.
Enforced by FxThreadPlugin, woven into method bodies at build time
via byte-buddy-maven-plugin (see pom.xml) rather than a runtime
mechanism. Two alternatives were spiked and ruled out first: AspectJ
compile-time weaving can't yet target this project's Java version and
doesn't run Lombok's annotation processor; a Spring AOP (CGLIB) proxy
can't intercept the self-invocation this codebase's actual call sites
are dominated by (an @EventListener calling the FX-touching method on
`this`). Bytecode instrumentation has neither limitation, confirmed
empirically before writing any of this: a scratch spike proved
interception reaches a private, self-invoked method and correctly
defers it via a captured @SuperCall, without re-entering the
interceptor.
FxThreadInterceptor is the woven replacement body; it delegates to
FxUtils.onFxThread, the single place the thread-check/dispatch
decision lives, whether reached through the annotation or still
written out by hand elsewhere. ArchitectureRulesTest gains a rule
requiring @FxThread methods return void, matching the fire-and-forget
Platform.runLater dispatch underneath.
Migrated MediaLibraryState.bump() (whole-method-wrap) and
StatusBar.applyLibraryStatus() (tail-of-background-task, extracted
from refreshLibraryStatus()) as the two reference call sites.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZpQK78WhohL1CqDip3PfM
Background tasks no longer share a docked column with the inspector.
TaskMonitorWidget's status-bar ring is now the permanent progress
signal; TaskPopover shows the per-task list and cancel buttons on
click; ToastLayer announces completion. InspectorDock and TaskPanel
are removed, and InspectorPanel now binds its visibility to
ViewportSelection, which ViewSwitcher updates on every mount/unmount
via the new SelectionSource contract.
Also fixes ToastLayer covering the whole window and blocking clicks:
an unconstrained VBox in a StackPane stretches to fill it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removes the IntelliJ-inspired dark/light CSS overrides in favor of
AtlantaFX's built-in theme catalogue. ThemeMode enum is replaced by
AppTheme, which wraps the full set of shipped themes (Primer, Nord,
Cupertino, Dracula, seasonal palettes, GitHub variants, and Autumn
through Yacht).
The theme preference now accepts any AppTheme value and renders the
theme name from Theme.getName() instead of translating it. Settings
view gains a ComboBox with all 29 themes; theme-intellij-immich.css
and theme-intellij-light.css are deleted. Application-specific CSS
classes (.navigation-drawer, .inspector-panel, .photo-card, etc.)
migrate to AtlantaFX style constants (Styles.TITLE_3, Styles.TEXT_MUTED,
Tweaks.*) where equivalents exist.
Updates PreferenceSchemaCouplingTest to verify theme options match the
AppTheme enum rather than the old two-value set. Adds test exclusion
for the theme preference in the translation coverage check since theme
names are proper nouns.
Adds jSystemThemeDetector library to monitor operating system dark/light
mode. ThemeManager now logs the initial OS theme at startup and registers
a listener for live theme changes. No automatic switching yet—manual toggle
remains the only way to change app appearance.
Introduces a combo box in the header bar for switching libraries
without restarting. Library selection is persisted through two new
preferences: `library.last-opened` holds the active database name,
`library.available` holds the discovered list. The selector writes only
`last-opened`; `LibraryService` observes it and redirects the
datasource off the JavaFX thread.
Adds `STRING_LIST` as the first non-scalar preference type. Values are
normalized to immutable `List<String>` with element coercion from YAML
sequences. AppPreferences gains `stringList()` and
`stringListProperty()` accessors. A blank scalar or `null` becomes the
empty list; a scalar becomes a one-element list.
Switches `spring-boot-starter-jdbc` to `spring-boot-starter-data-jdbc`
in preparation for JPA repositories. Adds mapstruct to annotation
processor paths and redirects test runs under `target/pholio-home` so
preference writes
Integrates DevToolsFX as a development-time debugging tool bound to F12.
The inspector shows the live node tree, attributes, CSS origins, and
highlights hovered elements—replacing code re-reading with browser-like
inspection. Disabled by default; enabled with --pholio.dev-tools.enabled.
Introduces a vertical drawer that appears to the right of the icon
rail when a destination is clicked. The drawer displays a title from
the destination's i18n key and hosts destination-specific content.
Core additions:
- NavigationDestination enum defining six sections (LIBRARY, ALBUMS,
TAGS, MAP, FAVORITES, REJECTED) with icon and message key
- NavigationDrawer component managing title binding, section registry,
and lifecycle delegation to sections
- NavigationDrawerSection SPI for pluggable drawer content
- LibraryFolderTree implementing lazy-loaded directory tree with
off-FX-thread listing via backgroundSyncExecutor
- FolderTreeItem providing on-demand child expansion without walking
the entire library upfront
The drawer's width is persisted to ui.left-panel-width preference
(default 240px, clamped 160-600) and controlled by a 5px resizer
handle. Width changes debounce writes via PreferenceService.
NavigationRail now builds entries from the enum, keeps buttons in
GRAPHIC_ONLY mode always, and positions the drawer in setCenter
rather than adding a fourth zone to AppShellView. The rail's width
becomes 44 + drawerWidth + 5 when open, 44 when closed.
Selection of library folders logs the path but does not filter the
grid yet; indexing must arrive first.
Adds 26 tests across NavigationRailTest (width follows open state,
drawer visibility, title binding), NavigationRailResizeTest (drag
handling, clamping, immediate unclamping, persistence restoration),
NavigationDrawerTest (section registry, duplicate rejection, title
resolution), and LibraryFolderTreeTest (lazy expansion, hidden file
exclusion, live root tracking).
Renames all packages across the codebase from io.pholio to
org.icroco.pholio. Changes affect:
- Production code: main classes, UI components, infrastructure layer,
CLI commands, and utility packages
- Test code: architecture rules, component tests, and fixtures
- Resources: logging configuration and application descriptors
- Build configuration: POM groupId and main class reference
Also fixes typo in application-cli.yaml logging config (double dot in
org..icroco.pholio) and updates import statements where fully
qualified names were used (e.g., GuiBootstrap, Disposable).
BREAKING CHANGE: Package structure has changed. Any external
references to io.pholio.* classes must be updated to
org.icroco.pholio.*.
Renames all packages across the codebase from io.pholio to
org.icroco.pholio. Changes affect:
- Production code: main classes, UI components, infrastructure layer,
CLI commands, and utility packages
- Test code: architecture rules, component tests, and fixtures
- Resources: logging configuration and application descriptors
- Build configuration: POM groupId and main class reference
Also fixes typo in application-cli.yaml logging config (double dot in
org..icroco.pholio) and updates import statements where fully
qualified names were used (e.g., GuiBootstrap, Disposable).
BREAKING CHANGE: Package structure has changed. Any external
references to io.pholio.* classes must be updated to
org.icroco.pholio.*.
Applies consistent code formatting across configuration classes, task
manager, and CLI command definitions. Formatting changes include:
- Aligns field declarations with consistent spacing
- Converts single-line to multi-line Javadoc comments
- Adjusts record parameter indentation
- Organizes imports with blank line separators
Adds utility classes:
- FileUtil for datetime extraction from filenames and directory checks
- LangUtils for null-safe collection handling and duration formatting
- Env for profile detection and desktop capability logging
Introduces dependency org.threeten:threeten-extra (1.10.0) for
duration formatting with AmountFormats.wordBased.
Configures sortpom-maven-plugin (4.0.0) to enforce POM element order
and adds versions-maven-plugin (2.21.0) for dependency management.
Removes the entire user preferences infrastructure including the YAML-
backed persistence layer, the reactive update mechanism, and the
settings form UI. All deleted components:
- Preferences domain model with nested records for library, UI, sync,
thumbnails, AI, and imports configuration
- YamlPreferencesService implementing debounced atomic writes via temp
files and copy-on-write updates with event publishing
- PreferencesChangedEvent for cross-component reactivity
- SettingsForm modal dialog with live binding to theme, locale, sync
interval, and library root path
The removal eliminates 630+ lines of configuration management code.
Applications must now provide alternative mechanisms for user
customization or rely on hardcoded defaults.
BREAKING CHANGE: User preferences are no longer persisted across
sessions. Theme, locale, sync settings, and window geometry are not
saved or restored.
Introduces Lombok as a provided-scope dependency to enable annotation-
based code generation without runtime overhead. The library is
explicitly excluded from the final packaged artifact to avoid bundling
2 MB of unused annotation processor.
Configuration ensures Lombok works with JDK 23+ by declaring it in
maven-compiler-plugin's annotationProcessorPaths, since javac no
longer discovers processors on the compile classpath automatically.
Version is managed by the Spring Boot 4.1.0 BOM (1.18.46) and
referenced via ${lombok.version} in the processor path to prevent
version drift between the dependency and the processor.
Also documents agent guidelines in CLAUDE.md to avoid AskUserQuestion
tool usage in favor of plain text questions.
Introduces native installer generation for Windows, macOS, and Linux
using jDeploy. The packaging is driven by package.json and a GitHub
Actions workflow that builds platform-specific bundles with embedded
Azul Zulu FX JRE 26.
Key additions:
- package.json descriptor with jDeploy configuration targeting 7
platforms (excludes windows-arm64 due to missing Azul FX build)
- jdeploy-maven-plugin behind a profile to sync descriptor fields
from POM without requiring Node in default builds
- GitHub workflow triggered by v* tags (versioned releases) or
*-snapshot branches (rolling prereleases)
- Version-free finalName (pholio.jar) so jar path stays stable
- Launcher reads package-info.json from jdeploy tag for auto-update
The spring-boot-maven-plugin's mainClass is the single source of
truth for the entry point: it lands in the repackaged jar's manifest
as Start-Class, which jDeploy's native launcher uses to bootstrap.
The application now launches exclusively in UI mode. The CLI/headless
execution path has been completely removed, including:
- CliBootstrap entry point and PicoCLI integration
- LaunchMode detection logic and associated tests
- Profile-based UI component gating (@Profile on UiView)
UI component eligibility is now controlled solely by component scan
rather than Spring profiles. The bootstrap always instantiates
UiConfiguration and starts the JavaFX runtime.
This simplifies the architecture by eliminating dual-mode complexity,
reducing the number of code paths, and removing the framework overhead
of profile-based conditional bean registration.
BREAKING CHANGE: Headless/CLI execution is no longer supported. The
application can only be launched as a desktop GUI.