Feature/aurora integration bugs - #388
Open
den3000 wants to merge 66 commits into
Open
Conversation
Preparing the Aurora port, the popup that holds the organizer list was rewritten to work in the fork, where a Popup is a scene of its own: the position provider was made to return 0,0, the layer was stretched to fill the window, and the list was moved by hand from positionInWindow(). All of that landed in commonMain, so Android and iOS got it too — and there the list falls through the layout, opens crooked and cannot be clicked. Split the two behaviours behind a new `popupIsSeparateScene` seam, next to `forceLandscape` and `uiScaleBaseline`. It is deliberately a separate flag: the popup problem is scene isolation, and rotation is only one of its consequences. The Android/iOS branch restores the position provider as it stood before the port (f0cfecb), including the `- 60` nudge — going to stock anchored positioning instead would be a second, unrelated behaviour change riding along with a regression fix. The list itself is shared; only the placement differs, which the branches pass in as a Modifier. The maliit key-event logging on the text field is now attached only where it is needed. Keyboard input on Aurora is still unverified, so the instrument stays — but it has no business running on every keystroke on every platform. Verified: core:ui compiles for Android, iOS simulator and the Aurora linux target. Behaviour on Aurora is unchanged by construction — that branch is the code that was there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TimeReceiver was constructed in exactly one place — AppActivity — so on iOS and on Aurora nothing ever drove CurrentTimeHolder. It was seeded once when the object initialised and stayed there, the collect in MainViewModel fired once, and the clock and the "N minutes left" countdown on the room card were frozen at whatever the time was when the app started. Wiring the expect class into Koin would have been the smaller diff and the wrong one. Its only public member, currentTime, has no readers anywhere — everything goes to CurrentTimeHolder directly — and the iOS and linux actuals were plain minute timers with nothing platform-specific in them. So the expect class goes, and a common CurrentTimeTicker takes its place, started from AppRoot, which is the one root all three platforms share. The tick is aligned to the wall clock, so the time flips at :00 instead of a minute after whenever the app happened to launch. Android keeps a TimeReceiver, because there it is not a timer: a broadcast is the only way to notice the clock being changed under the app. It is no longer an actual, it drops ACTION_TIME_TICK (the common ticker owns cadence now, the broadcast would only duplicate it) and it picks up ACTION_TIMEZONE_CHANGED, which was missing — a timezone change was invisible until now. Verified: composeApp compiles for Android, iOS simulator and the Aurora linux target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A booking made from a laptop calendar never reached the Aurora tablet: the room list only moved on FCM, and Aurora has none. Restarting the app was the only way to see it. The tablet does already tick — UpdateUseCase — but that ticking is not a refresh. It asks the screen to reload, the reload goes through GetRoomsInfoUseCase, and that serves the local cache and only reaches the network when the cache is empty. The one thing that actually re-reads the server is RefreshDataUseCase, and after startup nothing calls it unless a push arrives. So poll it, on the platforms that need polling: roomRefreshInterval is a minute on linux and null on Android and iOS, where push works and polling would be a cost with no benefit. Null means the coroutine is never launched, so this changes nothing for them. Nothing else moves. RefreshDataUseCase writes to the local repository's buffer, MainViewModel is already subscribed to it through RoomInfoUseCase.subscribe(), and the existing chain carries the update the rest of the way — so no ViewModel, no UpdateUseCase and no GetRoomsInfoUseCase changes. It hangs off ResourceDisposerUseCase, which already owns a scope, already has dispose(), and is already the single place background work starts from AppRoot. Verified: composeApp compiles for Android, iOS simulator and the Aurora linux target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A room tablet is supposed to fall back to the room it was set up with after a minute untouched. On Aurora it never did: the whole mechanism was Android — a Window.Callback wrapper installed over every activity, an ActivityLifecycleCallbacks to install it, and a Handler to time it. The common half was fine; nothing was calling it. Rewriting it for Compose turned up a second bug, this one on Android. A Dialog has a Window of its own and a Popup adds its own View to the WindowManager, so neither dispatches through the activity's Window.Callback. Sitting in the booking editor for a minute typing an organizer's name therefore reset the date and rebooted the screen under the user's hands. The Compose tracker sees those layers, so that stops happening too. The timer is the same idea without the Handler: a shared flow of restarts collected with collectLatest, which cancels the pending delay on every emission exactly the way removeCallbacks plus postDelayed did. The observer is a Box with pointerInput on PointerEventPass.Initial and onPreviewKeyEvent, consuming nothing — the initial pass is the point, on the main pass a clickable child would have taken the event first and the root would see nothing. It is installed in three layers, the same three that already re-apply ForcedLandscape and ScaledUiDensity, and for the same reason: separate windows on Android, separate scenes in the Aurora fork. Two notes. The timer sits in core:ui rather than next to DateResetManager in core:domain, because core:ui does not depend on core:domain and this is not worth a new module edge — the callback is handed in from composeApp, which sees both. And hardware keys with nothing focused inside Compose may not reach onPreviewKeyEvent, where dispatchKeyEvent saw everything; on a touch kiosk that is the acceptable half of the trade. InactivityManager.inactivityState is not carried over. It had no readers. Verified: composeApp compiles for Android, iOS simulator and the Aurora linux target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
enableUpdateButton was a stored flag written from four places that did not agree. updateEventDate recomputed it only inside `if (selectOrganizer != Organizer.default)`; updateEventDetails recomputed it unconditionally. So which rules applied depended on how you had got there. It also started out false and mapToUpdateBookingState never overrode it, which means the save button on an existing booking was dead from the moment the editor opened and only came to life if something happened to recompute it. On Android that something was closing the date picker. On Aurora the date picker dies on open, so it never happened at all — the button stayed enabled from a stale state, the request went out, and the backend rejected it. That is the reported difference in behaviour between the two platforms on an occupied slot; the platform was never the cause. So compute it. isFinishTimeExceeded joins the state — it was a local variable calculated identically in two places and folded into the flag, which is exactly where the two paths drifted apart. The organizer check reads organizers/selectOrganizer straight from the state, so the button also comes to life on its own once the organizer list has loaded, with nothing to remember to call. isInputError deliberately stays out of it: that flag is about painting the field red once the user has finished typing, not about whether the booking is valid. Two more things fall out. updateStateWithNewEventDetails now clears the error flags, so "Произошла ошибка" no longer sticks to the button until the editor is closed — nothing cleared it before. And the constructor primes the validation from the event it was opened with, instead of waiting for an edit. Behaviour on an occupied slot is unchanged and deliberate: the button is disabled, not enabled with an error. Verified: composeApp compiles for Android, iOS simulator and the Aurora linux target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tapping the date field on Aurora hung the app and then killed it. The bug is not ours: the fork ships androidx.compose.material3.internal.PlatformDateFormat as a stub — `// @todo feature linux`, `firstDayOfWeek = 0`, `weekdayNames = emptyList()`, `formatWithSkeleton` returning "". Material3's WeekDays then walks `firstDayOfWeek - 1 until weekdayNames.size`, which is `-1 until 0`, indexes an empty list and throws on the first frame. The fork swallows exceptions thrown from a composable and rolls the frame back, which is why it reads as a hang rather than a crash. There is nothing to patch around. Even with the index fixed the month headline would be empty (formatWithSkeleton) and the grid shifted by one (daysFromStartOfWeekToFirstOfMonth), so the date half is replaced outright with a 6x7 grid of Columns and Rows. The time half is untouched: it never reads weekdayNames, and it passes is24Hour explicitly so it never falls back to the stub either. The layout maths lives in shared:core/linuxMain as CalendarGrid, apart from the composable — always six rows padded with nulls, so the dialog does not change height when you page months, and the leading-blank calculation written the way the fork gets it wrong. Russian month and weekday names sit next to it; note that the genitive list the date formatter uses ("25 ноября") is not the one a calendar header needs ("Ноябрь 2026"), so there are two. LaunchedEffect(selected) is deliberate rather than emitting on click: the Material3 version fired once on first composition, and that first emission is what primes the picker's own confirm button. No LazyVerticalGrid and no FlowRow — both are SubcomposeLayout, which this dialog avoids on purpose. No year picker and no ban on past dates: the backend serves a 14-day window and Material3 did not restrict past dates either, so either would be a behaviour change riding along with a crash fix. Verified: compiles for the Aurora linux target, and for Android and iOS, which this does not touch (everything new is linuxMain). Not yet run on the device. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
800 dp laid Aurora out in 1333x800, about 15% more room than the reference Android tablet, which hid the fact that the fork draws Cyrillic wider — at the price of the whole UI being ~15% smaller than it was designed. Testing reported that as "formats, sizes and text differ from Android". 686 dp is parity, and it turns out to be exact rather than approximate. Both real devices are 1200 px on the short side — the Quadro's window is 1200x2000, the reference is 1920x1200 — so the substituted density is 1200/686 = 1.7493 against Android's own 1.75. The ~3% recorded in AURORA.md is the gap between the devices' system densities, which is not the number the app ends up laying out with. The measurements came from the metrics overlay in the screenshots attached to the bug report, so no separate measuring session was needed. Not verified on the device. The arithmetic is solid; what parity does to the text wrapping that 800 dp was papering over is not, and fixing that wrapping is a separate change once someone has looked at it on the Quadro. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VersionOverlay() was called unconditionally, so the diagnostic line with the window size and density shipped in release builds of Android and iOS as well as on Aurora, where it belongs. Gate it on a platform flag rather than on isDebug directly: on Aurora isDebug is Platform.isDebugBinary and the variant links a release binary, so isDebug would switch off exactly the platform the overlay exists for. Deleting the overlay outright is the eventual move, but not yet — it is still the only way to read what ScaledUiDensity computed on the device, and the new 686 dp baseline has not been looked at on the Quadro. Verified: compiles for Android, iOS simulator and the Aurora linux target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correction to "poll the server for room updates where there is no push". That commit left iOS at null on the assumption that FCM covers it. It does not: Collector.emit is called from exactly one place in the whole repo, ServerMessagingService in androidMain, and composeApp/iosMain contains no Firebase, no messaging service and no topic subscription at all. So on iOS the subscribeOnBookingsList chain never fires, RefreshDataUseCase is never reached after startup, and a booking made from a laptop calendar never arrives — the same defect as on Aurora, in a platform nobody had thought to check. One minute, matching Aurora. Android stays on push alone. Verified: compiles for Android, iOS simulator and the Aurora linux target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Revert this commit once push delivery is confirmed end to end. Android has real push in production and does not need to poll. But push is only worth anything for as long as it arrives, and when it does not, nothing else notices: the room list is served from the cache, the cache is only refreshed by a push, and the screen stays wrong until the app restarts — which on a wall-mounted tablet can be weeks. That is not hypothetical. During testing the Android emulator showed a slot as free for an hour after another client had booked it, and the backend had the booking all along. The local setup has no working Firebase, so nothing ever arrived; a broken push in production would look identical from the outside, and there would be no way to tell it from the tablet simply lying. One minute, same as iOS and Aurora. If this is kept rather than reverted, it should be dialled down to something deliberately low-frequency instead — a full minute is the test setting, not a production one. The AURORA.md push section is rewritten in the same commit because it still claimed the room list was never updated at all, which stopped being true when polling was added. Verified: composeApp compiles for Android. The Aurora and iOS targets were not built here on purpose — a device test run is in progress and I did not want to compete with it for the daemon. Neither is affected: androidMain is not part of the Aurora variant, and the commonMain change is a comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things in AURORA.md were wrong or stale, all found by actually running the plan on a TrustPhone T1 rather than by reading. The documented way to read logs does not work. `ssh defaultuser@<ip> journalctl -f` answers "No journal files were opened due to insufficient permissions" — defaultuser is not in the systemd-journal group and the device has no sudo. Replaced with the three routes that do work, starting with the one that is usually enough: the deploy task streams the app's stdout back, which gives one log per run. Keyboard input is no longer an open question. It types, and the organizer list filters as you go. That item had been sitting in "Not finished yet" since the port. The swipe-axis behaviour is now described rather than guessed at. Holding the phone sideways so the content reads horizontally, the close gesture fires from the physical side edge and swiping up from the bottom does nothing — which is exactly what a rotate(90f) that never touches window geometry predicts, and is the missing description for the bug report ticket that had an empty body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… start time The date/time picker filtered the overlapping events it had just found with `it.startTime != startDate`. The intent was "ignore the booking I am moving, it is allowed to overlap itself", but comparing start times is not that, and it went wrong in both directions. Found on an Aurora device with two bookings, 10:00-10:30 by one person and 11:00-11:30 by another, editing the second one: - setting 10:00 left the confirm button enabled. The other booking starts at 10:00 too, so it matched `startTime == startDate` and was quietly dropped from the check — the picker offered a slot that was taken. - setting 10:35 disabled it. 10:35-11:05 overlaps nothing but the booking's own old slot, which starts at 11:00 and therefore did *not* match — so the booking blocked itself and a legitimate move was refused. Compare by id, the way BookingEditorViewModel.checkForBusyEvents already does; a blank id means a booking that does not exist yet, so there is nothing to exclude. busyEvents itself already keeps only events that collide, so this filter has no other job. The first symptom never reached the backend — the editor's own check is id-based, so the save button would have refused it a step later. The second one cost the user a move they were entitled to. Verified on the Android emulator against the same two bookings: 10:00 now reads "This time is already booked" and 10:35 is accepted. Compiles for Android, iOS simulator and the Aurora linux target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kotlinx-datetime 0.7 renamed `dayOfMonth` to `day`, both on the property and in the LocalDateTime constructor, and the four warnings had been sitting in DateTimePickerComponent since the previous branch. The same cleanup does NOT apply to the Aurora calendar, and that turned out to be the interesting part. `monthNumber` is deprecated in favour of `month.number` on Android and iOS, but the Aurora variant builds against the fork's own kotlinx-datetime — 0.7.1-aurora — which has no `Month.number` at all. Following the warning there breaks the linux build, so it stays as it is with a comment saying why, or the next person to tidy warnings will make the same mistake. Verified: all three targets compile, and the warnings are gone from both files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverts the temporary polling backstop from c202888 now that the Aurora device testing it was covering is done. Not a git revert: that commit also rewrote the AURORA.md push section, which until then still claimed the room list was never updated at all — untrue since polling was added. That correction stays; only the Android interval and the notes about it being temporary go. The reason the backstop existed has not gone away, so it is written down rather than dropped: a push that fails to arrive is indistinguishable from nothing having changed, and the screen stays wrong until the app restarts. It was seen during testing — the emulator showed a slot as free for an hour after another client had booked it. Push delivery on real Firebase has still not been confirmed end to end; if that check ever fails, this is the first thing to reach for. Verified: composeApp compiles for Android. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
forceLandscape, popupIsSeparateScene and uiScaleBaseline were a file apiece per source set — twelve files holding one declaration each. The package is unchanged, so no call site moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
roomRefreshInterval is the only switch here so far; the file is named for what it holds rather than for its single occupant, the way core:ui now is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
statusBarInset and showDiagnosticsOverlay, eight files down to four. When the overlay goes — once the scale baseline is confirmed on the Quadro — that is an edit to this file rather than a file to delete. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two placements are platform facts, not a branch: Aurora's popup is a scene of its own and is positioned by hand, everywhere else an anchored position provider does it. Behind an internal expect/actual now. Android and iOS share one implementation and do not duplicate it — there is no source set common to just those two, so AnchoredOrganizerList stays internal in commonMain and both actuals are a single call to it. popupIsSeparateScene survives with one use, the maliit key-event log on the field: that diagnostic is about the fork's input path, not about placement. InactivityTracker is kept in both layers — without it, a minute and a half inside the Aurora dialog resets the screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The expect class was removed because on iOS and Aurora nothing ever constructed it — the only instance was built in AppActivity. That was a DI problem, and the common ticker fixed it by making all three platforms hold a timer, which is the expensive answer on a device that sits on a wall for days. So the three implementations come back, each on its system's own mechanism: - Android subscribes to ACTION_TIME_TICK again — the system wakes the app every whole minute by itself, so there is no timer at all, and the flip lands on :00 for free. ACTION_TIME_CHANGED and ACTION_TIMEZONE_CHANGED cover the clock or the zone moving under the app; the timezone action is new and worth keeping. - iOS is back on NSTimer, with the first fire pushed to the next whole minute, plus NSSystemClockDidChange so a clock change shows up at once rather than on the next tick. - Aurora has neither mechanism, so it keeps the coroutine — CurrentTimeTicker, which is where the wall-clock alignment already lives. DI is the actual fix: timeReceiverModule() is an expect module in the shape of settingsStoreModule(), and only the Android actual needs a Context, which only the Android graph has. Lifecycle moves off AppActivity and onto AppRoot, the root all three platforms share, as a DisposableEffect — the receiver is registered against the application context, so an activity is neither needed nor leaked. start() and stop() are idempotent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s back On iOS the date/time picker was inert: tapping a day in the calendar closed the modal without applying the date, and neither time wheel would scroll. Bisected to 5725761, the compose-navigation swap — 98a1d58 and its deps-only successor f9e95d7 are fine, 5725761 reproduces it exactly. calf's pickers are native UIKit views on iOS, and inside a Compose dialog window they never enter the hit test: the touch falls through to whatever Compose node is underneath. That was already diagnosed once, on 718cc49 of feature/replace-decompose-with-compose-navigation (Calf issue #115, "native picker draws on the wrong view inside a container"), and fixed there by rendering the modals as state-driven overlays. But that branch later went back to dialog<> destinations, and it is that end-state the Aurora port carried over — so the fix was lost on the way in. Restore the arrangement that was verified end to end on feature/calf-ui-with-compose-navigation-mvp @ 20e84fd: - The three modals (FreeRoom, BookingEditor, FastBooking) are state-driven overlays in the main composition again — ActiveModal + ModalHost — not dialog<> destinations. Only Settings and Main stay navigation destinations, so the modal routes and the serializable NavType go away with them. - The date/time picker gets its own Compose Dialog back, and it is now the only dialog window in the chain. Both halves are load-bearing: the overlay is what lets the native views receive touches at all, the Dialog's present animation is what masks the frame where calf has not applied our colors yet. The editor opens it through its own showSelectDate state, which the ViewModel never stopped carrying. - The Aurora wrappers move with the window that needs them: ForcedLandscape, ScaledUiDensity and InactivityTracker are re-applied inside the picker's Dialog (its own scene in the fork) and dropped from the modal host, which is part of the main scene and covered by AppRoot. Two things this does not fix and that were hiding behind it. The scrim's own clickable was swallowing the tap and popping the back stack, which is what made the failure look like "the modal closes" instead of "nothing happens"; it is harmless here because the picker's Dialog sits above the scrim. And the main screen's date field is still a no-op — MainScreen passes an empty onOpenDateTimePickerModalRequest. Verified on the iPad simulator against the local backend: the picker opens over the editor, a calendar day selects (Aug 4 -> Aug 6), both wheels scroll (08 -> 10, 00 -> 04) and the value lands back in the editor as "Book from 10:04 AM to 10:34 AM". Fast booking, its confirmation and Release all still open, act and dismiss. Android APK builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tapping outside the booking editor closes it; tapping outside the picker did nothing, so the two modals behaved differently for the same gesture. The picker's Dialog was relying on the platform's dismissOnClickOutside, which decides from the dialog content's bounds — and the wrappers this branch put inside that window (InactivityTracker, ForcedLandscape, ScaledUiDensity) all fill it, so there was no outside left to tap. Before those wrappers existed the content was just the card, which is why this worked on the pre-swap flow. Do it by hand instead, the way ModalHost already does: a full-size dismissing Box around the card and an absorbing one on it. Transparent, not dimmed — the modal host under this window is what draws the 0.9 black. Verified on the iPad simulator that the added click handling does not cost the native pickers their touches again: outside tap closes the picker and leaves the editor open, while a calendar day still selects (Aug 6) and the hour wheel still scrolls (08 -> 10). Android APK builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The overlay switch deleted DialogBackgroundDim and the dialog<> destinations, and the comments that named them are not something the compiler checks. The Aurora popup pointed at the deleted scrim for its "re-apply the wrappers here" precedent — the picker's own Dialog is that precedent now — FastBooking still claimed a dialog window it no longer gets, and composeApp/README.md still described the modals as destinations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An overlay is not on the back stack, so on Android back fell through to the NavHost, found nothing above the start destination and finished the activity — verified on the emulator: with the booking editor open, back left the launcher on top. Under lock task the app would come straight back, which hides it rather than making it any less wrong. Not androidx.compose.ui.backhandler.BackHandler: it ships in a separate ui-backhandler artifact this Compose version does not have, and adding a dependency would have to clear the Aurora fork's maven too. Android is the only one of the three with a back gesture, so the expect/actual is cheaper — iOS and Aurora get a no-op. Verified on the emulator: back now closes the editor and leaves the app on the main screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AURORA.md still described TimeReceiver as a coroutine on every platform and said nothing about where the instance comes from, which is the part that was actually broken. clients/README.md listed the tablet's platforms as Android and iOS while the same file has an Aurora build-variant section further down, and described the modals as navigation destinations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tablet goes back to the room it was set up with after a minute untouched, but a modal open on top of it stayed: the screen underneath returned to room A while the overlay went on saying "Book B". The next person walks up to a booking dialog for one room over a schedule for another. The timeout reached exactly one consumer, so there was nowhere to hang the second reaction. It is a SharedFlow now: AppRoot still resets the date and the room, and AppNavHost clears the active modal. Closing the overlay takes the date/time picker with it, since that lives inside the booking editor's composition. Verified on the Android emulator and the iPad simulator: with the editor open on a non-start room, after a minute the modal is gone and the screen is back on the start room; and eighty seconds of continuous tapping inside the modal still keeps it open, so the tracker has not started ignoring input. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four PNGs in composeApp/icons were placeholders of the right sizes but not the application's icon. They are now scaled from the iOS artwork, which is the largest source in the repo at 1024x1024 — centre-cropped to 764 first, because the artwork carries a purple border around the icon proper that a plain resize would have kept. Square to the edge rather than the rounded corners the placeholders had. That is also a guess at bug #8, the white border around the icon on Aurora: if the launcher paints a light backdrop under the transparent corners, this removes the transparency it shows through. Unverified — it needs the device. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The organizer field sits low in the booking card, and the keyboard covered it on both platforms: on Android the card stayed put and the keyboard came over it, on iOS the system shortened the scene and the card was squeezed until its own content clipped from the inside. Aim at the field rather than the card: it reports its bottom edge while focused, and the modal moves by however much it takes to leave FIELD_TO_KEYBOARD_GAP above the keyboard. The card keeps its full height — measured against the tallest the host has been, since the scrolling list inside it forbids an unbounded height — so what does not fit is clipped by the screen instead of squashed. The move is a draw-time translation, not a layout offset: the field reports its position from layout, so moving it there fed the shift back into the number the shift is derived from, and the card jittered between two positions. For the same reason the resting position is captured while the shift is still zero, and only retaken when the scene resizes. Only "how much of the content the keyboard covers" is per-platform. Android subtracts the navigation bar from the ime inset, since the content is padded away from it already; iOS answers zero, because the scene was shortened before any of this ran; Aurora also answers zero, with the maliit height left to wire up. The shift may go negative to bring the card back DOWN — that is what iOS needs, and it is capped at the card's overhang so a card that fits never drifts. While the keyboard is up, a tap on the dim now puts the keyboard away and keeps the modal; only a tap with no keyboard closes the modal. iOS has no dismiss key on its keyboard, so otherwise the only way out was to close the dialog. Calibrated against logged numbers on both platforms rather than screenshots: Android lands the field 16px above the keyboard at density 2.0, iOS 18px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The keyboard overlap is a platform switch like the other three, so it moves into Platform.kt next to them instead of keeping four files of its own — three of which existed to answer zero. LocalFocusedFieldBottom travels with it: it is the channel that makes the overlap useful. ModalHost gets its own file. It had grown past the navigation graph it was tacked onto: a dim, a ViewModel store, a back gesture and the keyboard geometry are not what a reader opens AppNavHost.kt for. The Aurora actual keeps answering zero but no longer says TODO, since nothing is pending in the code — the fork reports no keyboard insets at all, and the note now says which end would have to change. AURORA.md gains the switch in the layout table with the same caveat spelled out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The number moved to 686 dp when the baseline was derived from screen parity rather than from a familiar 10" tablet, and the linux actual and AURORA.md were updated at the time; the expect's own KDoc was not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The state event maliit sends on open carries height = 0; the real size comes in a follow-up event that never reaches the app. So the height is polled from Keyboard.height() rather than listened for, which also leaves no subscription for the fork's unpaired onWindowPause() to drop. Aimed at the tablet: there the keyboard rises from the bottom of the content, where the shared ModalHost geometry expects it. On the dev phone it comes up the right-hand side and a vertical shift does nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… what the modal did The press notice was timed against the phone, where a keyboard follows within a second or two. On the Quadro T a press has been seen to produce one six seconds later, with focus granted and taken away again in between — so the three-second promise expired mid-handshake, the modal dropped back, and the host read that drop as a keyboard that had gone away: it cleared the focus and closed the session while the keyboard was still on its way up. The app was fighting the keyboard it had asked for. The promise now runs for ten seconds, and is withdrawn on the two events that make it moot: a keyboard actually showing up, and editing ending. Withdrawing it on arrival also settles the bounce the phone showed — 535px to 324px to 0 as a spent promise lifted the card back for the remainder of its grace. The rest is diagnostics for a question a photograph cannot answer: whether the booking card is moving out of the keyboard's way or merely being squeezed into what is left. ModalHost reports the container, the card, the overlap and the shift; ScaledUiDensity reports the content size and the density it derives from it, because that density follows the scene's short side and a scene that shrinks for a keyboard would take the whole layout down with it — which from outside looks exactly like a squeezed dialog. Compiles for Android and Aurora. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tablet lifts the modal by nothing at all: overlap 540px, shift 0px, which happens for exactly one reason — the host never learns where the field is. The press handler was the culprit. It wrote whatever the row's coordinates gave it, null included, and nothing restores a null: the layout callback that would have reported a position fires when the layout changes, and by the time a field is pressed it long has. So the report is now made only when there is something to report, and the field says where it is on gaining focus as well, which on Aurora may be the only chance it gets — focus arrives seconds after the layout has settled. Whether the coordinates were the problem is now in the log too: "field pressed, bottom: …". A null there says positionInWindow() is unusable at press time under the forced rotation, and the fix is the coordinate frame itself — the field measured against the modal's container rather than the window, as fix/aurora-maliit-deadlock does through ModalHostState. Re-checked on the Android emulator: the press reports 1095px, focus follows 10ms later, the card travels 0 to 263px as the ime rises, Done drops both focus and keyboard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tablet reported a field bottom of 557px in a 1200px container with 540px of keyboard, so the host worked out that the field ended a hundred pixels above the keys and lifted the card by nothing. The number was the field's position across the content, not down it: under ForcedLandscape there is a 90° rotation between window and content, positionInWindow() goes through it, and the window-Y of a point inside rotated content is its content-X. By the layout's own arithmetic that field ends around 860px down, which is where the keyboard is. So positions are now taken between two nodes on the same side of the rotation, where it cancels: ModalHostState carries the container's coordinates and the field's bottom measured in them, replacing LocalFocusedFieldBottom, and the host computes the keyboard's top as containerHeight minus the overlap rather than reaching for window space at all. Taken from fix/aurora-maliit-deadlock (84778f6), which arrived at the same frame from the same evidence. Everything of ours around it stands: the height is still believed when it can be a keyboard and estimated when it cannot, the press still promises a keyboard for ten seconds and withdraws the promise when one arrives, the overlap is still clamped to the box, and the diagnostics that made this diagnosable are untouched. The Android emulator confirms the frames were equivalent there all along: the reported bottom moves from 1095px to 1047px — the container's own offset from the top of the window — and the shift comes out at the same 263px it did before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The claim was that ForcedLandscape is a drawing effect the coordinates never see, so an anchor read in window space could be used as-is inside a scene that lays out in content space. It came from the list appearing roughly beside the field, which turned out to be a weak thing to conclude from: the mapping walks up through every ancestor, the rotation layer included, so the Y reported for a node inside the rotated content is that node's content-X. That is what puts the organizer list off to the side, and it is the same confusion that had the modal deciding the keyboard was not in its way. So the popup section now says why the anchor is wrong and that it cannot be fixed inside a scene with no ancestor in common with the field, and the "Unexplained" line about the dropdown becomes an explanation with a fix attached: stop making it a popup. Also drops a guess of mine from the keyboard section — that the tablet's window was landscape while the phone's was not. The probe measured both portrait, 720x1600 and 1200x2000, scene equal to window on each. What differs is where maliit puts its keys: the window's bottom on the phone, which the rotation turns into the content's right-hand side, and the content's bottom on the tablet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Popup on this fork is a second scene: it takes a visible pause to come up, arrives without the rotation, density and inactivity tracking applied around everything else, and can only be aimed by carrying the field's coordinates across. That last part never worked — the anchor came from positionInWindow(), which maps up through the ForcedLandscape layer, so what it called Y was the field's content-X and the list landed off to the side. ModalHost now offers an overlay slot, composed inside the card's own box, and the linux actual writes the list into it. It appears on the frame it opens, inherits the rotation and density already applied around the card, and anchors against the field through their nearest shared ancestor, where the keyboard shift and the rotation sit on both sides of the measurement and cancel. Taken from fix/aurora-maliit-deadlock (84778f6), together with the cardCoords and overlay halves of ModalHostState it needs. The popup path is deleted rather than kept as a fallback: the list has one user, the booking editor, and the editor is only ever composed inside a modal, so the fallback would have been a path nobody runs — with a comment about coordinate frames that no longer hold. AURORA.md follows: two scenes left instead of three, and the dropdown moves out of "unexplained" into "should be fixed, unverified on a device". Android is unaffected — it keeps its own actual, and the smoke test on the emulator puts the list in the same place and the card at the same 263px shift as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The device block hardcoded port 22 and ~/.ssh/qtc_id, which is a real device on the LAN. The SDK emulator is neither: qemu forwards its ssh onto a host port, and the only key it authorises is the SDK's own. Both are now overridable the way the address already was, from local.properties or -P, and the key path resolves against $HOME. Defaults unchanged, so deploying to a real device is the same command it was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list took its width from the row and its x from the TextField inside it, and the row's 20.dp horizontal padding sits between them. On the emulator: card 1259 wide, row at x=61 and 1137 wide, field at x=96 and 854 wide — and a list 1137 wide drawn from x=96, so 96..1233 against the row's 61..1198. Off to the right, and hanging past the card's edge. So the anchor is the row now, which is where the width already came from; the parameter says anchorCoords rather than textFieldCoords because it is no longer the field. Android and iOS take x from the popup's own anchorBounds and only ever used this for the vertical anchor, and the row's top is the field's top, so nothing moves there. The list also opens from the press instead of from the focus callback. Aurora grants focus at the end of the maliit handshake, seconds later, so the list was arriving after the keyboard: press, jump, pause, keyboard, pause, list. Nothing about it needs focus. The gesture detector is remembered on Unit, hence rememberUpdatedState around the lambda it now calls. Diagnostics under a new OrganizerList tag, printing the anchor against all three boxes — twice now this list has been diagnosed off a screenshot, which cannot tell "offset" from "wrong width". Sizes are spaced (`0 x 0`) because the deploy plugin reads a bare `0x0` as an address in a native backtrace and fails the run over it. Verified on the Quadro T emulator: anchor 61,612, list 1137 x 262, and the list logged before the focus line rather than after it. Android and iOS compile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The emulator is a target now, so the setup section says which two properties it needs and why a real device needs neither. Its model has to be landscape or it reproduces the dev phone rather than the tablet — the keyboard comes up along the content's right edge — and the first deploy onto a cold emulator times out on the install, which is not a build error. The dropdown item moves out of "not finished": it was two bugs, and the second one, width and position taken from different nodes, outlived the move out of the popup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0.45 was the screenshot number rounded up: a 520px key strip across the Quadro T's 1200px short side is 0.433, and up seemed the safer direction. The emulator has now answered the same 520 against the same 1200 through Keyboard.height(), which it reports honestly there — so the fraction is two independent measurements agreeing, not one estimate, and there is nothing left for the rounding to protect against. Only reached where height() cannot be believed, which on the Quadro T is always: it answers 2000 there, the screen's whole long side. Visible on the emulator only in the first tick after a press, before the real number arrives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The script asked for :backend:app:bootRun with whatever buildVariant happened to be set, and the Aurora variant rewrites the module list down to clients:tablet. So with `buildVariant=aurora` active in gradle.properties — how anyone working on the Aurora client leaves it — the backend failed to start with "project 'backend' not found", which reads as the local stack being broken rather than as the flag it is. Nothing about the backend is ever an Aurora build, so the flag is pinned rather than passed through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
settingsStoreModule() handed out a mutableMapOf with a TODO over it, so the room picked on first run was forgotten with the process. On a wall-mounted tablet that is the difference between being set up once and being set up after every reboot. The fork's ak-shared-preferences backs it now. Its API matches SettingsStore one for one, but a put only reaches the process — save() is what writes to disk, and without it this would have been the same map through a C binding. Hence a save per write, which is affordable when the only setting is the room. Wrapped and logged under a Settings tag like every other call into the fork: a setting that cannot be stored is not worth an app, and the caller gets an answer that is merely not persisted. The tag stays silent unless something failed, and it did not. Verified on the Quadro T emulator: room picked, app killed, app came back up in that room. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The press detector sat on the row, and the row holds the arrow as well as the field. So a tap on the arrow ran the whole press path — the platform warned about a keyboard, the field measured, the list opened, the card lifted — and then no focus arrived, because the tap was never on the field. The grace timer put the card back down a moment later: a jump, a list, and a keyboard that never came. It moves to the field itself. The arrow has no clickable of its own on any platform, so with the row no longer listening it is inert everywhere, which is what Android and iOS already looked like. That is also a regression this branch had just introduced and nobody had seen yet: opening the list from the press is common code, so on Android the arrow would have started opening the list too. Verified on the Quadro T emulator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three names fill the list past its 150.dp cap by about nine, so the only thing the scroll could do was travel that nine and settle back — which is indistinguishable from a scrolling bug, and was reported as one. Twenty names, of varying length and script, so wrapping and clipping are visible here too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list is the entire staff — 46 people in production — and a Column inside a verticalScroll composes every one of them to show the three that fit under the 150.dp cap. LazyColumn is safe here despite the note in DatePickerView.linux.kt: that warning is about that dialog, not about the fork. LazyVerticalGrid already draws the room picker and LazyColumn the main screen's slot list, both on Aurora. Verified on the Quadro T emulator against twenty seeded organizers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A flick threw the organizer list back the other way on release, while a slow drag was fine throughout — and a slow drag is the one gesture that ends with no velocity, so it never flings at all. That put the fault in the velocity rather than in the drag, so the fling grew a seam, listFlingBehavior(), and the seam logged what it was handed. Three flicks in one direction on the dev phone: fling v=-5851.73, unconsumed=-5757.672 fling v=-1241.1434, unconsumed=-0.0 fling v=-900.90607, unconsumed=-0.0 Consistent and plausibly sized, so not a noisy tracker but one that disagrees with the drag about direction. The first line is the tell: nearly all of it went unspent, so the fling ran into an edge the list had just been dragged away from. The other two had room, which is when the snap-back was visible. The linux actual negates the velocity in and the remainder out; Android and iOS hand back the platform default. The log line stays, because this corrects someone else's defect rather than fixing it — a fork build that starts agreeing with the drag would need this taken out, and nothing else would announce that. Only the organizer list asks for it. The main screen's slot list and the room picker have the same defect underneath, and nobody has flicked them hard enough to mind. Verified on the dev phone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found while deploying a build before the local backend was up, which is a thing that happens often enough to have read as "the emulator cannot connect" twice. It is not: the first load fails, the screen latches that first error, and the once-a-minute refresh succeeding right afterwards never clears it. The evidence is in the note, including the timestamp showing the screen was displaying an error older than every failure in the log — first, not latest. It lives in feature/main's README because that is the screen it happens on and because it is not Aurora-specific — it reproduces on Android by starting the app with the backend stopped. AURORA.md gets a pointer only, since Aurora is where it keeps being met rather than where it lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things that were being carried between sessions in someone's notes instead of the repo. How rooms stay fresh, per platform: Android is pushed to and that is verified on hardware; iOS and Aurora have no Firebase at all, so both poll. Together with what push-only costs, which testing has actually seen — a slot shown free for an hour after another client booked it. Clock and timezone changes: what each TimeReceiver does, why Aurora notices them up to a minute late, and the one thing genuinely unverified — that Android reacts at once, which nobody has sat down and tried. And the organizer tags. The client asks for user_tag=employee and filters for employer; the backend ignores the parameter, so the filter is what decides and staff carry employer. This works in every environment it has run in and is written down because it reads like a bug, and because the two sides only stay compatible while the backend keeps ignoring the parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The polling section explains why the events are unusable but not the thing that actually stops anyone reverting it: a second listenState subscription alongside the fork's own kills maliit — after the first closed session the keyboard never opens again without a restart. An expensive negative result from origin/fix/aurora-maliit-deadlock, and until now it lived only in a handoff note, which is precisely where the next person to tidy this up would not look. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The emulator is documented as a target now, so the thing standing between a new M-series machine and that target belongs here too. It does not start, and the IDE blames a timeout: in fact libsfdk hardcodes -accel hvf, Hypervisor.framework on ARM cannot run an x86_64 guest, and qemu is dead inside a second. The wrapper that rewrites the accelerator is written out. Also why Docker is unaffected and why an empty buildengines.xml is not a broken install — both are things that look like faults on this platform and are not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Apple Silicon emulator workaround gets its own file. It is about one machine's SDK installation, not about the app — nothing in it changes when the tablet changes, and nobody on Intel or Linux needs it — so AURORA.md keeps a pointer and AURORA_QEMU.md keeps the detail. localQuickStart gains what was being carried around in notes: check for a backend already serving before starting another; run it in your own terminal, because a bootRun started as somebody's background job dies with them and the symptom looks like a broken client; the /v1/ in the workspaces path, whose absence answers "No static resource"; why the seed inserts twenty organizers rather than three; and the four Android-emulator traps — adb not on PATH, zsh not word-splitting an adb variable, -s when two devices are attached, and the soft keyboard staying hidden until show_ime_with_hard_keyboard is set. Plus the reminder to put api.url.debug back after building for iOS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing the whole overlay went too far. "Which build is this one?" is asked of a tablet on a wall, not of a debug build, so the version label stays and stays ungated — which is what it was before a debug gate was put on it to keep the metrics out of release. What comes off is the rest of that line: window size, system density and font scale, and the density ScaledUiDensity computed. That was scaffolding for measuring the 686 dp baseline, and UiScaleDiagnostics existed only to feed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> take down the scaffolding AuroraWindowProbe was installed to answer one question — does the window resize when the keyboard opens — and it answered it: no, it does not, which is why the keyboard height still comes from maliit. Its other half, the screen/scene/contentScale line, has served its purpose too; the port is on a device and the numbers are in AURORA.md. VersionOverlay and UiScaleDiagnostics go with it, and so does showDiagnosticsOverlay, which existed only to gate the overlay — on Aurora hardcoded to true, because the Aurora variant links a release binary and isDebug is false there. ScaledUiDensity keeps its own UiScale log line. It costs nothing, it says what the overlay's last useful field said, and the scale baseline on the Quadro is still unconfirmed — noted in AURORA.md, along with what to put back if it has to be measured again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ForcedLandscape, ScaledUiDensity and statusBarInset were spelled out twice — once in AppRoot and
once inside the date/time picker's Dialog, which the fork gives a scene of its own so nothing put
around the root reaches it. Two spellings of an ordering that is not free is one spelling too
many.
They are now one composable, AuroraWindowFrame, and it is called in exactly two places: the linux
application {} block, and the linux actual of a new DialogSceneFrame — the seam that re-applies it
around content that gets a scene of its own. Common code asks for DialogSceneFrame and knows
nothing else; Android and iOS get content() and start AppRoot with the theme alone, because all
three layers are no-ops there. statusBarInset moves to core:ui so the frame can reach it, which
empties composeApp's platform files.
The order inside the frame is the part worth keeping, and two thirds of it was got wrong while
doing this:
- the inset goes inside the rotation, or the padding lands in the window's portrait space and
shows as a stripe down the side;
- the inset goes inside the scale, not the other way round. Under the padding ScaledUiDensity
normalises 1157px instead of the window's 1200, and 1200/686 = 1.7493 against the reference
tablet's 1.75 is the entire point of the baseline — measured the other way the UI came out 3.6%
larger, which is what the emulator showed before this was fixed;
- the theme goes outside the frame. AppTheme paints the background through its own Surface, and
with the theme inside AppRoot the strip the inset leaves bare came out white. The bare Aurora
window is not dark.
AppRoot keeps the inactivity tracker, the effects and the version overlay, and nothing about the
window at all.
Also written down, because the history shows it has been undone twice: what the picker's Dialog is
for, and that the compose-navigation swap is how the nested-dialog regression got in last time.
Verified on the Quadro T emulator, the TrustPhone, the Android emulator and the iOS simulator:
status-bar strip dark, density back to 1.7492712, picker upright and themed, calf's hour wheel
still scrolling under a swipe on iOS, and the organizer field, keyboard, list and selection
unchanged on Android.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the keyboard Two comments that explained the wrong thing. ModalHost's said it "provides a modal-scoped ViewModelStoreOwner", which reads as though there would otherwise be none. There is one on every platform — the Activity on Android, the ComposeUIViewController on iOS, and on Aurora the fork's own scene-scoped default — and all three live as long as the app. What the modal needs is not an owner but a shorter-lived one: a ViewModel is cached in its store by class and parametersOf only runs on creation, so under a root owner the booking editor opened for a second booking is handed the instance built for the first. Keying the call per booking would answer that and leave an instance behind for each, in a store nothing ever clears, on a tablet that runs for weeks. The KDoc now says that, and that this is the scoping the modals had from their nested nav graph before they stopped being dialog<> destinations. The iOS entry point justified OnFocusBehavior.DoNothing by "the modal handles the keyboard itself with imePadding()". There is no imePadding in the project and there has not been since the modal started moving the card by a draw-time translation aimed at the focused field. The reason for the setting is unchanged — UIKit must not shove the content up on top of a shift the app has already applied — so only the explanation is replaced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ModalHost had grown into two files' worth of work: the dim, the ViewModel scope and the back gesture on one side, and on the other the arithmetic that keeps the field being typed into clear of the keyboard — the resting position captured while the shift is still zero, the retake when the scene resizes, the clamp of an overlap Aurora reports as the screen's whole long side. The second half is now ModalKeyboardShift.kt, and the host measures for it and applies what it answers. The watch that ends editing when the keyboard goes away behind the app's back went with it, and had to. It reads the same overlap the shift does, and on Aurora softKeyboardOverlapPx() is a produceState polling the maliit session every 100 ms — a second reader is a second poll of the one binding already known to be fragile. So the overlap is read once, and everything derived from it lives on that side of the call. The KDoc says so, since separating them looks like an improvement. The host's own layout is now two named composables, DimBox and KeyboardShiftableBox, which take the same pair of callbacks: a height and a position. Those are not two ways of asking the same thing — the height feeds composition and has to invalidate it, the position feeds measurement in the layout phase — and neither can stand in for the other, because the same LayoutCoordinates instance comes back every time and a state holding it never reports a change. That would have been a height that quietly stops updating exactly when the scene shortens for the keyboard on iOS, which is the one platform it matters on. The note is in the code now rather than in somebody's head. Smaller things along the way: the two nested CompositionLocalProviders are one call with two values, since nothing between them depended on the store owner; the card's height cap is a Dp carrying Dp.Unspecified rather than an Int carrying 0, which requiredHeightIn already reads as no constraint, so the then/if ladder is gone; and two imports that had been dead since the coordinate frames changed are dropped. One thing is not a pure move: the tallest-the-host-has-been height is now derived in composition from the container height rather than in the size callback beside it. The result converges to the same number, at the cost of one extra recomposition when the scene resizes. Compiles for Aurora linuxX64, Android and the iOS simulator. Not run on a device: what wants confirming is the iPad simulator, where the height cap is the half of this that does any work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ulator finds one The Aurora variant links a release executable, so the URL baked in is api.url.release and api.url.debug is never read. The logging section already says that flag is why the log needs installing by hand; the setup section, which is where somebody reads before a first run, did not mention it at all. It costs a session every time. A release URL left pointing at a stand on the office LAN is unreachable from anywhere else, and the app then starts, draws its whole layout and stays empty — which reads as an emulator that cannot reach the network rather than as an address nobody looked at. The number that was missing: on the SDK emulator the host is 10.0.2.2, qemu's user-mode networking putting it at the same address the Android emulator uses, so the localQuickStart backend needs no forwarding to be reachable. With the one-line curl over ssh that settles it from the guest before the app is blamed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tablet moved to another zone went on converting in the one it was started in. The clock still ticked, so nothing looked broken — but every answer derived from "what time is it now" was wrong by the offset until somebody restarted the app: which booking is the current one, where the day is cut into slots, how long is left before a room frees up. Testing hit it from the other end and reported the shape without the cause: the time was picked up, the zone was not, and afterwards the countdown to a room being freed stayed broken. The zone was captured in five places. `defaultTimeZone` in shared:core was the one that mattered — roughly forty reads of `currentLocalDateTime` and `currentLocalDate` go through it, which is most of the data layer. `SlotUseCase` and `SelectRoomUseCase` each held their own copy in a constructor default, and Koin builds both as singletons, so those were captured once per process too. `RoomInfoUseCase` held a third. `CurrentTimeTicker` held a fourth, and that one is Aurora's, the platform with no system notification to fall back on. All of them now read the zone instead of storing it, and the local copies are gone: one accessor, used everywhere. The remaining direct calls to `currentSystemDefault()` were behaviourally fine — a call inside a function is already fresh — but they were a sixth, seventh and eighth way to spell the same thing, so they go through the shared accessor as well. Where a computation converts more than once, the zone is taken into a local first. Not for the saved lookups: those conversions are one answer, and a zone read again halfway through it could be a different one. `RoomInfoUseCase` was the worst of these — the read sat inside a filter over every event of every room, next to a `now` that was already a snapshot. Injecting the zone was considered and dropped. The constructor parameters existed for tests that do not exist, nothing ever passed one, and the cost of having them was exactly the capture being fixed here. If they are wanted back, they should come back as `() -> TimeZone`, the way CurrentTimeTicker takes it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The time in the header was not coming from the ticker. It was formatted out of `selectedDate`, which carries a time only incidentally: nothing advances it except the date arrows and the inactivity reset. On a tablet nobody is touching, that reset is what moved it — once a minute, to whatever the time was at that moment — so the header advanced a minute at a time and looked like a working clock while sitting between zero and sixty seconds behind. Compared minute against minute it reads as exactly one minute slow, which is how it was filed. The countdown next to it is driven by CurrentTimeHolder and was always right, so the two numbers on the same screen disagreed. That is what made it findable: a probe on the tick showed the tick landing within thirty milliseconds of the minute and reaching the screen a tenth of a second later, on Aurora as on Android. Nothing was wrong with the clock — the header was reading a different value. So the day comes from `selectedDate`, which is what the arrows move and what the header has to follow, and the time comes from `currentDate`, which is the clock. Only while the selected day is today: on any other day the time on screen would be neither the selected one nor useful, and the future pattern drops it anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A room whose booking had just begun went on showing itself free. Which booking counts as the current one is decided in the repository, against the clock — but only while the repository is emitting, which it does on a refresh and not on a timer. So the room did not turn busy when its booking started; it turned busy the next time data happened to arrive. On the platforms that poll that is up to a minute late. On Android, which has no poll, it is whenever a push turns up — and a push that never turns up is indistinguishable from nothing having changed, on a tablet that runs for weeks. The tick already reached this ViewModel and already moved the countdown; it now re-reads the rooms as well. That read is the local one — the network is touched only when the cache has nothing, the same rule every other caller gets. Worth knowing for the next person watching a slot list: this also refreshes the slots once a minute on Android, where previously nothing happened between pushes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Under TCG the guest loses time against the host and there is no NTP to pull it back — a minute and a quarter after a couple of hours, measured. So the emulator's now is genuinely not the Mac's, and any judgement about the app's clock has to be made against the guest. This is worth a paragraph because the wrong comparison is the natural one and it is convincing: the app's header sits whole minutes behind the macOS menu bar, and the gap grows over a session, which reads exactly like a clock that runs slow. It came within one message of filing a working fix as a failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tick was re-reading rooms, and re-reading rooms asked the slot component to rebuild. That is worse than the churn it looks like. The list is sliced from *now*, so a minute later it is a different list — the first slot has a new start, a run of consecutive bookings is a different group — and the component, which tries to carry `isOpen` across an update, has nothing to carry it to. An expanded group collapses on its own once a minute. Under a finger it is not cosmetic. A rebuild landing mid-scroll leaves the list taking no touches at all: nothing on it responds until the room is switched away in the corner and switched back. Reported on the iPad and reproduced on Aurora. What the tick is for is the room — busy or free, and the countdown — and that needs no slot rebuild. So the tick reads rooms with `refreshSlots = false` and the list is left to the things that really change it: a poll, a push, or the user. This does not close the underlying defect, only stops this branch reaching it. The poll rebuilds the list once a minute on Aurora and iOS the same way, and has since before any of this — so the collapse, and the wedged list behind it, are still reachable there. Both want their own fix: identity for the groups that survives a re-slice, and whatever in the list stops answering when it is rebuilt mid-gesture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.