Conflicts:
- README.md, docs/localization.md: language lists keep every language, Finnish appended
- assets/ATTRIBUTION.md: union of rows
- crates/ui-egui/src/i18n.rs, crates/ui-egui/src/menus.rs: language table and commands keep sv and fi
- crates/ui-egui/src/panels/keyword_list.rs: code list gains fi; Locale::ALL.len() is 16
- docs/localization-parity.md: header keeps the newer date and both changes; revision rows are the union
crates/ui-egui/src/widgets/tests_numeric_entry.rs: keep both sides' tests.
docs/parity-checklist.md: newer header (2026-10-21); KEY-SLIDER-NUDGE from #1402, KEY-SLIDER-TYPE from #1687; both revision rows.
apps/lightcraft/src/control_server.rs: serve() keeps #2027's idle read timeout and #1991's single-stream serve_lines (no try_clone); #1991's Windows test passes IDLE_TIMEOUT
crates/engine/src/camera_preview.rs: LOOK_VERSION keeps both history lines and the higher value.
crates/engine/src/media.rs: RENDER_CACHE_VERSION keeps the higher value.
Conflicts:
- README.md, docs/localization.md: language lists keep every language, Swedish appended
- assets/ATTRIBUTION.md: union of rows
- crates/ui-egui/src/i18n.rs: language table and command test list keep it, ka and sv; both tests kept
- crates/ui-egui/src/menus.rs: language commands keep it, ka and sv
- crates/ui-egui/src/panels/keyword_list.rs: code list gains sv; Locale::ALL.len() is 15
The two deep-chain tests now build their library with `deep_folders_app`, as #1243 does: every
level holds a photo, so it has a row of its own and starts closed, and the test opens it. They
pass the same whether or not the sidebar passes over folders that only lead on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/parity.md became docs/parity-checklist.md in #788, and ROADMAP.md no
longer has the "Where we stand" / "Where we're going" sections the
anchors pointed at. Link the checklist, the roadmap itself (as README.md
does) and its Upcoming section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The availability worker that answers "is this file there?" for the grid and the
Info panel can now also check a long list of files (every library photo's file,
for the sidebar's Missing Photos count), without that list getting in the way:
- Availability::sweep takes a list, checked one file at a time and only when
nothing is asked; files checked less than 15 s ago are skipped, and one sweep
runs at a time. The files found gone are kept (is_gone, gone_len), and
gone_generation, which replaces generation, changes only when a file is found
gone or back: a file found there at its first check changes nothing the
Missing Photos view shows, and wakes nobody.
- Availability::restart_sweeps / sweep_in drop what is left of a sweep when
another library is opened, and refuse a list that was being made for the old
one.
- Availability::sweep_yields_to takes a callback; while it says file work is
going on (an import, a scan, a folder listing), the worker starts no sweep
check. What is asked is still answered at once, and the sweep goes on where it
stopped, so a share is not asked for a stat of every file while a scan reads
it: 32 readers got 101 files/s alone and 65 files/s with one stat loop beside
them.
Nothing in the app starts a sweep yet; the worker tests exercise it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shift_iso writes back only a time it fully reads (a date alone, or HH:MM
with optional seconds and fraction, an optional zone, nothing after);
iso_seconds stays lenient for sorting and rules. photo.setCaptureTime
leaves such photos as they were and counts them as `unreadable`, and a
set that would move other photos by an unreadable time is refused; alone,
the photo gets the typed time. The dialog's preview says the photo's time
can't be read, OK says what was left, any error keeps the dialog open
(button and ui.dialog.confirm), and Set mode doesn't start from an
unreadable time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled Howard Hinnant civil/days-from-civil copies and the
leap-year/days-in-month table with chrono as a workspace dependency:
- apps/lightcraft/src/logging.rs: timestamp (log line time)
- crates/catalog/src/dates.rs: civil + weekday
- crates/catalog/src/stacks.rs: iso_seconds (date -> seconds)
- crates/engine/src/cmd/face_recognize.rs: capture_secs (native only)
- crates/meta/src/datetime.rs: DateTime::unix_seconds and the valid() day check
Output formats and semantics are unchanged (pre-epoch clamp, day rollover that
normalize_iso relies on, leap-second tolerance, EXIF/XMP parsing). chrono uses
only std, so wasm stays safe. AGENTS.md/CLAUDE.md now say time goes through chrono.
Follow-up to storytold/cadcraft#1785
* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed
chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. timestamp now uses the fallible DateTime::from_timestamp
and reads such a clock as the epoch, keeping the old pre-epoch clamp.
timestamp and catalog's civil write years past 9999 with {:04} like the old
{y:04} did, because %Y would add a '+'. Tests cover the out-of-range and
5-digit-year cases.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* time: preserve old out-of-range date conversion (no epoch fallback)
`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any instant
past chrono's year range to 1970-01-01, changing output the old hand-rolled
Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s was
318857-05-20 17:46:40, not the epoch). Restore the old conversion domain with a
cycle-based helper that routes the calendar through chrono (the calendar
repeats every 400 years = 146_097 days, so month/day/time come from chrono and
only the year is carried in i128, keeping every i64 exact).
- apps/lightcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/catalog/src/dates.rs: `civil` cycle-based (the callers stay inside
0000-9999, but a far clock no longer resets to the epoch).
Pinned old outputs in tests:
logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
civil 10_000_000_000_000 -> 318857-05-20T17:46:40,
-10_000_000_000_000 -> -314918-08-13T06:13:20, i64 extremes
* Don't build a year-318857 SystemTime unchecked in the logging test
Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
The E2E showed the previous fix missed the common case: enlarging a locked
crop until it stops leaves a frame that fills the photo's height, with its
top and bottom both at the edge of is_within_image's 1e-6 tolerance. No
translation keeps both there with room to spare, so the move stayed stuck.
When the frame can't move as it is, solve again with it shrunk about its
centre by the least that puts it strictly inside (at most 1e-5 of its size,
aspect unchanged). This also covers a frame at the edge on one side, so the
half-tolerance first pass is dropped and a click moves nothing again.
A handle drag stops where is_within_image's 1e-6 tolerance ends, so a
corner can sit right at it. The move solve then let that corner go 1e-9
further, the result failed the check, and the across-then-down fallback
left the frame stuck. Solve first with every corner kept within half the
tolerance, and only then where it is.
A handle drag can leave the frame up to is_within_image's tolerance past a
tilted edge. The move's bounds were tighter than that, so such a frame was
stuck again, and a click inside it moved it. The bounds are now never tighter
than where the frame is. A test pins how a smaller frame pushed past a tilted
edge slides along it, and the parity checklist's revision history notes the fix.
Retain both sets of parity revision notes after the upstream integration.
Keep the hierarchical picker and deleted-destination guard unchanged.
Co-Authored-By: Codex <noreply@openai.com>
On a straightened photo, a crop that fills the photo's height could not be
moved sideways: the move went across and then down, and no straight sideways
step stays inside the turned photo. The frame now goes to the place nearest
the pointer's that keeps it inside, so it slides along the tilted edges to the
photo's side. Unstraightened, that place is the old across-then-down clamp.
Fujifilm's nine-knot distortion and vignetting tables (knots at sqrt(k/8))
end at the corner on the eighth knot; the ninth, sqrt(9/8), lies past it
with the eighth's value repeated. Reading them at rho = knot_last * r took
every value from 6 % further out. On the X-T4 the warp polynomial then
failed its fit checks on 253 of 425 zoom-lens raws, which got no
distortion correction at all, and corners came out up to 6.5 % brighter
than the camera's. Read at rho = r, the tables match the camera's measured
geometry and corner gain (docs/fuji-lens-corrections.md, "Knot radius").
Nothing changes for the 11-knot and 8-knot tables, whose last knot is 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve and validate command-line path arguments before use, enforce HTTPS at
download sinks, and confirm path containment. Hardens local codegen/fetch tools
against path-traversal and command-argument-injection findings.
Review follow-up (CodeRabbit): zoomed in, the badge moves to the canvas'
top-left corner, where the info overlay draws its lines; with the overlay
on, the label was painted under its first line. The badge's top inset
now takes the overlay's extent as well as the filter pill's. The overlay
is split into its lines and an extent helper, laid out with the same
fonts, so the two can't disagree.
Test: zoomed_in_the_before_label_keeps_clear_of_the_info_overlay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
Calls storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with secrets: inherit. Adds SonarCloud badge to README where missing and
removes the redundant sonar-project.properties (config now passed by the
reusable workflow).
The disarm waited for the button to be released outside the row, so a
drag that went out and came back kept the wheel armed. Leaving disarms
now even mid-drag, and a drag that starts outside the row does not arm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups for the Reference lifecycle fix:
- photo.setReference refuses an id that is not a photo id instead of
pinning the active photo; only an absent or null id means the active one.
- A Recently Deleted photo is refused with a message that says to restore
it (setReference and view.reference); the grid menu item is disabled for
it and any refusal is shown as a toast, not dropped.
- Reference view with no live pin releases its panes (windows, region and
pane textures, full capacity) like other views.
- A successful library switch leaves Reference for the grid; the Compare
pair is now cleared only on success, like the reference pin.
- One compare::live helper replaces four copies of the "exists and not
deleted" check.
- whats-new entry moved to 2026-10-21; checklist and ui-parity dates bumped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>