Skip to content

Dev2026 07 07 - #106

Merged
koo5 merged 24 commits into
prodfrom
dev2026-07-07
Aug 26, 2026
Merged

Dev2026 07 07#106
koo5 merged 24 commits into
prodfrom
dev2026-07-07

Conversation

@koo5

@koo5 koo5 commented Aug 26, 2026

Copy link
Copy Markdown
Owner

No description provided.

Your Name and others added 24 commits August 21, 2026 15:53
"Map arrow still gets stuck on unbackgrounding, readouts are fine" —
plus, on asking, the map still pans on GPS. That combination rules out
everything the last few days were spent on: the sensors deliver, the
state updates, the canvas repaints.

What is left is the one path with none of those properties. The arrow is
drawn by osmdroid from a value the AndroidView update block copies out
of the bearing state, and that block runs on recomposition — while the
GPS follow is coroutine-driven (snapshotFlow) and needs none. A block
that stops re-running therefore looks precisely like this: readouts
track, map pans, arrow holds whatever it was last handed.

I could not reproduce it here. A HOME cycle and a 45-second
backgrounding behind another app with send-trim-memory RUNNING_CRITICAL
both left the arrow tracking (86k-92k pixels changed across a heading
change, against a 71k baseline), so the emulator has nothing more to
say and guessing further would be guessing.

So the readout gets the missing fact:

    🗺 arrow 24s @307.7° Δ0.0°

when the overlay was last handed a bearing, which one, and how far the
state has moved since. Δ is the tell rather than the age — a still phone
legitimately shows a climbing age at Δ0, because nothing recomposed. A
large Δ says the drawing stopped, not the sensing, and points at the
update block instead of the sensor stack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
"We really need a better way of visually indicating that the interval
mode is about to be started" — and the device showed why every way so
far had failed: the armed state lived only in the ladder's head label,
and at ordinary split positions the 280 dp track is taller than the
capture pane, so that label renders OFF-PANE. The user was not missing
the signal; the signal was not on screen.

Two changes. The fact itself was unrenderable: whether release starts a
run was a local variable inside the gesture loop (overSlider), invisible
to composition by construction. Hoisted into state (armedStop: what
release does now, null = cancel).

And the announcement moved to the one place that cannot be clipped: the
shutter button previews its own future. Armed for a run it wears the
run's green with "▶ Ns"; armed at the top stop, video's red with "⏺
REC"; over the button, blue with "release: cancel" under it. The verdict
line under the circle says it in words — release: start 4s run /
record / cancel. The ladder head keeps only the compact stop label.

Verified on the emulator mid-gesture (input motionevent DOWN/MOVE, no
release): all three states screenshot-confirmed, and the VIDEO stop
remains reachable while the track top is clipped — the gesture holds
pointer capture, so sliding to the screen's top edge still lands on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
Fast repeated swipes advanced zero photos. The original solved exactly
this, and the mechanism was sitting in swipe2d.ts:141-154: a successful
release parks its completion handler in pendingTransitionListener, and
startDrag calls it SYNCHRONOUSLY with a synthetic transitionend before
doing anything else — the turn the animation was carrying is applied
now, the grid rests, and the new drag starts clean on the new front
photo.

Our commit() was the opposite: animateTo → turnTo → snapTo in one
coroutine, so the second gesture's snapTo (Animatable's mutex: a new
caller cancels the current one) killed it INSIDE animateTo and turnTo
never ran. The first swipe was not delayed, it was destroyed — and the
second one then measured its drag against the un-turned neighbours, so
two swipes moved one photo at best.

Ported: commit() records what it is carrying (pending: turn + photo,
the original's listener made data), onDragStart and the chevrons fast-
forward it — apply the turn, rest the grid, then proceed. pending is
cleared between animateTo returning and the next suspension point, so
a touch landing in that window cannot apply the turn twice.

Also unkeyed the gesture handler from the neighbours:
pointerInput(state.left, ...) restarted detectDragGestures — killing
the live drag — every time a turn landed mid-gesture, which is
precisely when the fast-forward fires. The lambdas read `state` through
the State delegate, so they see current neighbours without a restart;
only the pane size remains a key.

Verified on the emulator via the round-trip invariant: two fast left
swipes (second landing inside the first's 300 ms animation), two slow
right swipes → back at the starting uid (c3b1dd9b → eb5197d3 →
c3b1dd9b), and the fast pair confirmed as two real steps (one slow
swipe from the same start lands elsewhere). A lost swipe cannot close
that loop.

The lesson is recorded in docs/tauri-viewer-ui-contract.md's swipe
section: the fast-forward IS part of the swipe contract, not an
optimisation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
The gutter goes 48 -> 64 dp and the track line 16 -> 22 dp. This
control is read mid-gesture, at arm's length, from the corner of the
eye — the defaults were sized for a control you look AT.

The track needed measuring, not eyeballing: a first pass set 12 dp,
which LOOKED like the improvement it claimed to be in the diff while
actually thinning the line — this Material3's default track is already
16 dp (41 px on the test device). Pixel-measured before committing:
41 px -> 57 px, matching 22 dp exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
"The whole problem was that there was no indication that it was the
area left of the button, i kept trying to target the track exactly."

The gesture has always accepted any point left of the shutter
(pos.x < circle.left) — the track is a picture, not the hit-box — and
the comment in the code even says why: "a mid-gesture thumb is not a
precision instrument". But the screen showed only the thin line, so the
user did precision work the code never asked for, and the widening of
the track two commits ago was treating the symptom.

While the slider is open, the entire catch zone — pane's left edge to
the button — now wears a wash, tinted by the armed state: faint white
while release would cancel, the run's green or video's red once armed.
The affordance is the hit-box.

The slider-visible/armed/circle state moved from the shutter cluster's
scope to the pane's, since the wash lives outside the cluster.

Verified on the emulator: a finger at (200,480) — nowhere near the
track — armed a 1 s run with the whole zone green; back over the
button, faint white and "release: cancel".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
Prior art, per review: the original's DebugOverlay is a mess, but it
has a close button in its header (closeDebug, DebugOverlay.svelte:50).
The readout here shipped without one, so making it go away meant a trip
through Settings.

The ✕ writes the same persisted setting the Settings switch reads —
one fact, two controls.

Placed at the START of the row, not the end, after the end placement
failed on device: the panel re-measures on every half-second tick (the
age strings change width), so a button positioned after the longest
line drifts as the text ticks — a moving target. The panel's left
edge is anchored; that corner stands still. (The isolated compose test
passes in either layout because its lines are static — it guards the
wiring and the hit region, not the drift.)

Verified on the emulator: tap on the ✕ dismisses the readout and
persists show_geo_debug=false. The verification script's dump had to
delete the stale file first — uiautomator dump can fail silently and
leave the previous dump to be re-read, which cost one round of tapping
phantom coordinates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
Field report: pitch-black room, Sports @ 1/2000 — "still says just
1/2000 and takes pitch black photos". Two separate things, and the
display gap made them inseparable.

The arithmetic is innocent, and now provably: DarkRoomExposureTest runs
the exact metering loop the phone runs — each dark frame credited to
the current plan's own exposure, plan recomputed from the meter — and
from either start (rule engaged in the dark, or lights going out after)
it slides to the Sports floor within ~30 frames and pins there at max
gain. A third test pins the contract itself: Sports NEVER goes slower
than 1/125, however dark — 1/125 @ ISO 3200 in a truly black room IS a
black photo, by design; the mode that photographs a dark room is Auto.

What was broken is that a working slide and a dead one looked
identical: the ⚡ button printed the RULE's target, always, and the
resolved plan lived only inside the open menu. The button now appends
where the plan landed when it differs — "🏃1/2000→1/125" — so the slide
is visible at a glance, and its absence in the dark now MEANS something
(a dead metering loop, worth a logcat).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
User-decided: an interval run is a walking or driving shoot, where
motion blur is the failure mode and Sports is the rule built for
exactly that — a fast target while the light allows it, a graceful
retreat to 1/125 when it does not, instead of Auto's smear or a pinned
target's black frames.

Scoped tightly to what "default" means. It engages only when the rule
is Auto at the moment the run starts — Pin, Floor or Sports picked by
hand is the user's choice and survives untouched. It uses the
remembered target and bias, not fresh defaults. And it lasts exactly
the run: engaged at the top of the run effect, stood down in its
finally (which the effect's cancellation routes both the stop-tap and
leaving the screen through), and if the user changed the rule mid-run
the stand-down keeps its hands off — identity comparison, so even
re-picking identical Sports values counts as an explicit choice.

Verified on the emulator through the full cycle: "⚡ Auto" before,
"⚡ 🏃1/500→1/1264" during the run (the auto-engaged rule and the
plan-divergence arrow from the previous commit, composing), "⚡ Auto"
after the stop tap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
The original has it in the stats header (RetryUploadsButton
global={true}, DevicePhotoStats.svelte:105); this port only had the
per-card retry, so "make everything go out, now" had no button.

Placed beside the stats line, and only when it can do something: shown
when photos are pending or failed AND the gate is open (auto-upload on,
logged in). With work waiting but the gate shut, the original's
sentence shows instead — a force button the gate would silently
swallow is a trap.

"Force" is real and was already plumbed: the retry_button trigger
bypasses the failed rows' exponential backoff (isEligibleNow) and the
wifi-only constraint (decideUploadSchedule's documented bypass), so it
uploads now, on whatever network is there.

Verified on the emulator end to end: network cut, photo captured (26
photos · 1 pending), button appeared; network restored — at which point
the parked NetworkType job fired on its own and drained the photo
before the tap, which is the wifi-return trigger doing its job — and
the tap's reconcile [retry_button] is in the log finding a clean queue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
"Pending" folded three different situations into one number — waiting
for a drain, bytes in flight, and uploaded-with-the-server-still-
processing — and the last reads as "stuck" precisely when everything is
working: the forced upload landed, the server said "authorized", and
the line went on saying "1 pending" (user-raised). A queue display is
only reassuring if it moves when the queue does.

StatusCounts now carries waiting / uploading / processing / done /
failed (the DAO always had the granular queries; the lump was ours),
and the line shows only the non-zero stages — "26 photos · 26 uploaded"
on the common all-done day, the full walk while a batch drains.

The Upload-now button's condition sharpens with it: actionable =
waiting + failed. It no longer offers to force what is already in
flight or already on the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
The per-card "Retry uploads" button was the global drain wearing a
per-photo costume — it sat on a card and drained the whole queue
(the original's does the same: RetryUploadsButton takes a photo prop,
logs its id, and invokes the global retry_uploads command). What the
user wants from a button on a photo is that photo, now.

The drain has carried a targeted mode all along (doWorkInternal's
photoId — fetch that row, bypass the auto-upload gate), but
startManualUpload was an empty stub, and finishing it surfaced why: the
targeted loop never ends. It refetches the SAME row each iteration, and
the claim only checks equality with the status it was selected under —
so after a successful upload it would claim the row again in
"processing", drain it in duplicate, reach "completed", claim THAT, and
so on forever. Two exits added: a row that is not waiting
(pending/failed) is nothing to force, and one pass over one photo ends
the loop, success or failure alike — failure especially, since
retry_button bypasses the backoff that would otherwise space the spins.

The gate is login only. The targeted path ignores the auto-upload
toggle by design — a manual "upload THIS" is not automatic uploading,
and someone who keeps auto-upload OFF and pushes photos out by hand is
exactly who a force button serves. (The original gated its button on
the toggle, correctly for ITS button: a global drain is what the toggle
governs. Ours diverges with the mechanism.)

Verified on the emulator, both halves: offline force = exactly one
attempt, row lands in failed, no spin; online force of that failed row
= targeted drain in the log with the row's id, retried 30 s after
failing (the 1-minute backoff provably bypassed), success, stats
walking waiting -> failed -> processing.

The header's global button stays where it is — it matches the
original's placement, and "everything, now" remains a real errand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjTMBKnJbfJ4cJmBitR3Sq
parseInstant handles all the app's instant shapes (ISO-Z, python
microseconds, offset-less-as-UTC, epoch ms, Date); formatters render
'2026-08-24 15:45[:12]' in the viewer's zone, plus UTC and zoned
variants and viewerZoneInfo for zone-transparency affordances.
formatUtcDateTime (admin surfaces) switches from DD-MM-YYYY to ISO
order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HA5gbvvUtDKBMKDQJ7cJQ
Captured now renders ISO 24-h in the viewer's zone; an inline ⓘ
(touch-friendly, title-attr mouseover on desktop) expands to the
corresponding UTC and a note naming the device zone — and that it is
not the capture location's local time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HA5gbvvUtDKBMKDQJ7cJQ
All absolute toLocale* renderings now go through dateUtils; the photo
detail page keeps its explicit zone suffix via formatDateTimeZoned,
and naive backend strings are read as UTC everywhere (parseInstant).
Humanized forms stay: activity day headers, users-list relative dates
(its >7d fallback is now ISO).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HA5gbvvUtDKBMKDQJ7cJQ
formatLocalDate/Time switch from locale DateFormat to fixed
yyyy-MM-dd / HH:mm:ss patterns (device-local zone), so both apps
render timestamps identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HA5gbvvUtDKBMKDQJ7cJQ
Copilot AI lite review requested due to automatic review settings August 26, 2026 09:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The PR spans multiple subsystems (web viewer rendering/printing, Android upload/metering logic, Docker/runtime changes, and dependency tooling), making it high-impact and better suited for final human validation.

Pull request overview

This PR is a cross-cutting UX + infrastructure update across the web zoom viewer, frontend2 capture/map UI, Android upload plumbing, and repo dependency/ops tooling.

Changes:

  • Adds a “print view” mode to the zoom viewer (frozen rendering + QR share link + print-specific overlay palettes/layering) with Playwright coverage and documentation.
  • Standardizes date/time display across the web UI via a new ISO-oriented dateUtils and updates multiple routes/components to use it.
  • Improves frontend2 capture/map behavior and debuggability (interval shutter UX, arrow-staleness debug line + close button, exposure metering credit fix with new tests) and introduces tooling/docs for dependency cooloff + a parked Postgres Alpine migration runbook.
File summaries
File Description
shared/zoomview/labelPaint.ts Adds label palettes and split-pass rendering (leaders vs pills) for print/layered canvases.
shared/terrain/labelPills.ts Adds palette/leader-width styling and two-pass paint ordering for sky pills (print support).
shared-kt/src/cz/hillview/plugin/PhotoUploadManager.kt Adds manual single-photo upload trigger on a foreground path.
shared-kt/src/cz/hillview/plugin/PhotoUploadLogic.kt Adds targeted-mode guards to prevent infinite retry loops / duplicate drains.
scripts/deps.py New repo-wide dependency audit/refresh helper with cooloff policy enforcement.
frontend2/shared/src/jvmTest/kotlin/cz/hillview/map/MapOverlayUiTest.kt Adds UI test ensuring geo debug overlay can be dismissed in-place.
frontend2/shared/src/jvmShared/kotlin/cz/hillview/devicephotos/Format.jvmShared.kt Switches JVM date/time formatting to fixed ISO patterns for consistency.
frontend2/shared/src/jvmMain/kotlin/cz/hillview/devicephotos/DevicePhotos.jvm.kt Updates desktop stub for expanded upload status counts and targeted retry API.
frontend2/shared/src/commonTest/kotlin/cz/hillview/geo/GeoDebugTextTest.kt Adds coverage for new “arrow last set” debug line behavior.
frontend2/shared/src/commonTest/kotlin/cz/hillview/capture/ShutterGateTest.kt Expands capture/exposure label tests and adds metering-credit precedence tests.
frontend2/shared/src/commonMain/kotlin/cz/hillview/viewer/ViewerPane.kt Improves gesture behavior (fast-forward in-flight transitions; pointer handler keying).
frontend2/shared/src/commonMain/kotlin/cz/hillview/map/MapOverlayUi.kt Adds dismiss button to geo debug panel and passes debug lines/close callback.
frontend2/shared/src/commonMain/kotlin/cz/hillview/geo/GeoDebugText.kt Adds staleness diagnostics for map arrow vs current bearing.
frontend2/shared/src/commonMain/kotlin/cz/hillview/devicephotos/DevicePhotosScreen.kt Refines upload status presentation and adds global/targeted “upload now” actions.
frontend2/shared/src/commonMain/kotlin/cz/hillview/devicephotos/DevicePhotos.kt Splits upload counts by stage and adds targeted retry API to browser interface.
frontend2/shared/src/commonMain/kotlin/cz/hillview/capture/PhotoCapture.kt Adds meterCreditedExposure and improves exposure label to show “landed” value.
frontend2/shared/src/commonMain/kotlin/cz/hillview/capture/CaptureScreen.kt Interval shutter UX improvements and auto-engage Sports for interval runs under Auto.
frontend2/shared/src/androidMain/kotlin/cz/hillview/map/MapScreen.android.kt Records when/what bearing the overlay arrow was last handed; wires close-debug setting.
frontend2/shared/src/androidMain/kotlin/cz/hillview/devicephotos/DevicePhotos.android.kt Implements targeted photo upload retry via PhotoUploadManager.startManualUpload.
frontend2/shared/src/androidMain/kotlin/cz/hillview/capture/PhotoCapture.android.kt Fixes SceneMeter credit precedence via meterCreditedExposure.
frontend2/shared/src/androidHostTest/kotlin/cz/hillview/capture/SceneMeterLoopTest.kt New end-to-end synthetic loop test guarding the “frozen harvest” precedence bug.
frontend2/shared/src/androidHostTest/kotlin/cz/hillview/capture/DarkRoomExposureTest.kt New numeric tests for Sports floor behavior in darkness scenarios.
frontend2/scripts/emulator.sh Documents watchdog coverage for emulators started outside the wrapper.
frontend2/scripts/emulator-watchdog.py New watchdog to clamp/stop forgotten emulators (systemd timer driven).
frontend/tests-playwright/zoomview-print.spec.ts New Playwright coverage for print view behavior and single-page PDF output.
frontend/tauri-plugin-hillview/Cargo.toml Updates Tauri dependency version.
frontend/src/routes/users/+page.svelte Switches date display to shared ISO/instant parsing utilities.
frontend/src/routes/photos/+page.svelte Switches log time formatting to shared formatter.
frontend/src/routes/hidden/+page.svelte Switches timestamp formatting to shared formatter.
frontend/src/routes/device-photos/+page.svelte Switches date/time display to shared ISO formatters.
frontend/src/routes/account/+page.svelte Switches date formatting to shared formatter.
frontend/src/lib/shareUtils.ts Factors share URL construction + adds QR-friendly slug compaction helper.
frontend/src/lib/shareUtils.test.ts Adds tests for QR slug compaction.
frontend/src/lib/photoUtils.ts Adds captured_at viewer-zone details + UTC companion details.
frontend/src/lib/photoUtils.test.ts Adds tests for captured_at details extraction (ISO-Z + epoch ms).
frontend/src/lib/photoDisplay.ts Replaces ad-hoc parsing/formatting with unified dateUtils helpers.
frontend/src/lib/dateUtils.ts Introduces site-wide ISO display convention + instant parsing utilities.
frontend/src/lib/dateUtils.test.ts Adds tests for instant parsing + ISO formatting shapes/UTC formatting.
frontend/src/lib/components/TimelinePanel.svelte Uses shared formatter for timeline timestamps.
frontend/src/lib/components/PhotoItem.svelte Removes local date formatting helpers in favor of shared formatter.
frontend/src/lib/components/PhotoInfoWindow.svelte Adds timezone transparency UI for captured time (viewer zone + UTC detail toggle).
frontend/src/lib/components/OpenSeadragonViewer.svelte Adds print view mode (freeze, QR, layer order, print CSS, fine zoom).
frontend/src/lib/components/DebugOverlay.svelte Uses shared date formatting for captured_at.
frontend/src/lib/components/DebugMode1.svelte Uses shared time formatting for “current time” and sensor timestamps.
frontend/src/lib/components/CameraCapture.svelte Uses shared date/time formatting for overlay timestamps.
frontend/src/lib/compass.svelte.ts Uses shared time formatting for sensor timestamp field.
frontend/src/lib/build-info.ts Uses shared formatter for build time display.
frontend/src-tauri/Cargo.toml Updates Tauri dependency version.
frontend/src-tauri/Cargo.lock Lockfile update reflecting dependency bumps (notably Tauri).
frontend/package.json Updates key frontend deps and expands overrides set; adds bun cooloff config file.
frontend/bunfig.toml Adds bun dependency cooloff policy configuration.
docs/zoomview-print.md Documents zoom viewer print view behavior, constraints, and verification steps.
docs/tauri-viewer-ui-contract.md Documents the “fast-forward interrupted animation” gesture contract.
docs/postgres-alpine-migration.md Adds parked runbook for migrating Postgres to Alpine + ICU collation rationale.
docs/frontend2-status.md Adds debug writeup for “map arrow freeze” investigation and interval ladder UX notes.
docs/frontend2-capture-backlog.md Records capture backlog fix details for exposure credit precedence bug.
docker-compose.yml Adds parked Postgres Alpine migration scaffolding + bumps Umami digest with security note.
CLAUDE.md Adds dependency tooling commands and links to new docs.
backend/worker/local_photos.py Expands symlink resolution to walk component-by-component with hop limits.
backend/worker/Dockerfile Trims system deps, switches to uv copy-in, and removes uv from runtime image.
backend/scripts/pg_rehearse_alpine.py Adds rehearsal script to validate dump/restore + collation parity before migration.
backend/scripts/fix_frontend2_captured_at_from_filename.sql Adds corrective script for mis-stamped captured_at based on filename epoch.
backend/pyproject.toml Adds uv cooloff config (exclude-newer) documentation and setting.
backend/.dockerignore Excludes local venv/ directories from Docker build contexts.
Review details
  • Files reviewed: 63/66 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +10 to +11
/** Colours of the pills and their leaders. The default is the on-screen
* look (white on translucent black); a host printing on paper flips it. */
Comment on lines +402 to +404
modifier = Modifier
.clickable(onClick = onCloseDebug)
.padding(horizontal = 6.dp, vertical = 2.dp)
@koo5
koo5 merged commit 40d1a2e into prod Aug 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants