Skip to main content

Overview

The mobile app now ships first-party workspace surfaces for:
  • Drive at Routes.drive
  • CRM at Routes.crm
  • Education at Routes.education
These modules are registered in the Apps hub and available through the mini-app navigation system. The mobile Apps hub uses the v0.9.2 colored cards with consistent icons and clear descriptions. Keep module ordering explicit in the hub view rather than relying on registry insertion order. For workspace modules that need dense controls, prefer the same shell-owned chrome used by Calendar:
  • ShellMiniNav for mode or section switching
  • ShellChromeActions for quick actions such as filters, create, sort, and view toggles
  • A compact overview with a small number of relevant metrics
  • The v0.9.2 AppCardPalette gradient cards, consistent icon blocks, readable labels, and shared theme colors
The Apps screen defaults to the compact grid. A List/Grid switch in the top shell header sits beside Customize and changes the same visible, ordered modules between icon-and-name tiles and descriptive cards. Grid tiles have more space below the shell header and can be held and dragged to reorder without entering Customize. Holding an icon enters persistent ordering mode: icons shift during a drag and continue to wiggle after release until an empty area is tapped. Compact hide controls sit over icon corners only in grid ordering mode; list cards never show hide controls. The choice stays in the mounted Apps screen while navigating within the mobile shell; search and workspace access rules apply to both layouts. Descriptions remain searchable in grid mode even though they are hidden from the tiles. Holding a tile produces a pickup haptic, moving onto a new navigation target produces a selection tick, and completing a drop produces a light confirmation. The floating dock uses the same semantic feedback. Haptics are enabled by default on iOS and Android, can be disabled in Preferences, follow system haptic policy, and throttle repeated feedback during a continuous gesture. Shell actions and standalone primary actions give a selection tick, and a speed dial distinguishes opening from choosing an action. Finance transaction actions give a light completion tap after the local edit is accepted; this tap does not claim that a queued offline edit has synchronized. Background warmups for optional products should also respect access state before fetching. Do not let Apps hub or boot-time cache warmups call product endpoints blindly; skip the warmup when the corresponding access cubit is not loaded and enabled, and fail soft on opportunistic 404 responses. Tasks, Calendar, and Finance are the default launcher modules. Meet, Mail, Chat, and Notes join the existing opt-in apps in Settings → Experiments; all four are off until the user enables them. The choice is cached on the device and restored at launch. Turning a module on exposes it in Apps and its eligible Home card; turning it off removes those launch surfaces. Mail still requires its account access rule, and workspace visibility flags still apply. Deep links retain the existing route access rules. Meet includes meeting search, scheduling, editing, and deletion when enabled. Every launcher module has an English and Vietnamese description. Phones and tablets share a compact floating icon dock. Leaving a mini-app returns to its Home or Apps launch screen. Root screens remain mounted with hidden animations paused, preserving search and scroll state. The brand control opens the Apps screen within the normal shell, with compact search and arrange actions. Tapping a dock item glides the inner highlight to the new position without a button splash. During a hold or drag, the highlight follows the finger freely; it snaps to the selected item only on release. Reduced-motion settings shorten these transitions to an immediate state change. Switching between peer sections in any mini-app replaces the previous section in navigation history. Back follows the current context’s outer layer: a detail returns to its parent section, a peer section returns to the mini-app root, and the mini-app root returns to the Home or Apps launch screen. This also applies to deep links, so Back never visits a sibling section by accident. Personal Tasks boards are the Tasks entry surface. Their dock and Android system Back return to the root tab that launched Tasks, rather than the board directory that automatically opens the default board. Workspace board details retain their board-directory parent; task overlays return to their owning board. Apps is always available in the root dock. The Apps grid supports long-press reordering: nearby icons shift across rows during the drag, with a subtle motion cue, and the new order is saved on release. Tapping an app’s icon or label exits reorder and opens that app. Empty cell margins, row gaps, and unused grid space never open an app; tapping them exits reorder. Leaving Apps through any route also exits reorder, while preserving its search and scroll state. Long-press pickup and ordinary scrolling retain their existing gesture thresholds. App taps provide selection haptics. The list editor provides drag handles and visibility controls. Hiding an app requires confirmation. Hidden apps remain accessible at the bottom of Apps, separated from shown apps by a divider, and can be shown again without a confirmation. The editor places hidden experiments below Hidden Apps; enabled experiments appear in the regular shown order, while disabled or locally hidden experiments can be enabled or shown from Hidden Experiments. App order and hidden choices persist on the device. The editor disables Show when current account or workspace access would still prevent an experiment from appearing; access changes can make that control available later. Workspace permissions and experimental-app visibility still take precedence; a locally hidden app is only moved out of the shown launcher section, not disabled or removed from navigation. Home’s task, calendar, Finance, daily summary, and eligible Mail, Meet, and Notes cards can be reordered or hidden through the compact top-bar Customize action. Available cards are shown in their current order by default, and hidden cards remain in the editor for recovery. Unavailable modules do not appear as Home cards or editor choices; their saved layout preferences remain intact if access returns. The workspace picker and security prompts remain outside customization. The shell provides bottom scroll clearance, including for explicit-padding lists; pages must not add another dock inset. Downward scrolling hides the dock; reverse scrolling or idle restores it. Profile/Agenda reclaim hidden clearance; other pages keep it stable. Primary actions register with ShellActionSpec.inDock. ExtendedFab uses that registration inside the shell and remains standalone elsewhere. Actions use icons on phones and labels on tablets. Do not duplicate these actions in page-level floating buttons or top-bar actions. Nova spins once when entering Assistant and remains static afterward. Loading indicators continue animating, while pull-to-refresh uses an opaque circular surface. Reduced-motion preferences disable spinning. Calendar forms and event details use readable, blurred root-navigator sheets. High-contrast mode uses an opaque surface. Failed event edits or deletions restore the previous in-memory and cached list and expose the server error. Verify the next reload as well as the immediate rollback. Dock action handoffs retain presentation only, never callbacks or permission state. Destination actions remain disabled until their mounted page registers current handlers; missing destinations discard previews after a short handoff. Long-press navigation supports freeform dragging between destinations, including when the pointer leaves the dock. The pill stays under the pointer within the capsule; crossing the nearest enabled tab gives selection feedback, and release snaps to and selects that tab. The floating surface blurs content behind it, uses one translucent outline, and contains the moving glow inside the capsule. Pressing gently scales the island and smoothly brightens that internal glow, which fades on release. Dock controls use accessibility labels without hover tooltips. The Profile dock reserves its avatar space with a default placeholder, shows a skeleton while an image downloads, and displays the cached image when ready. Back from a peer navigation section exits to the root tab that opened that context; back from a detail page returns to its parent section. Shell search actions across Apps, Chat, Finance, Habits, Mail, Meet, Notes, task boards, and Timer focus the top search input on entry. The search icon, input, and close control share one vertical center; submitting an empty or whitespace-only query closes search mode except in Apps, which keeps its current search. Chat shows message matches in the page, while task boards and Timer keep filtering their current lists. The App, Workspace, and You dock destinations consolidate into one Settings screen at /settings, with Workspace and You as nested navigation rows. App preferences use compact grouped items, retaining two columns at widths of at least 840dp. Root rows show the current value and open nested choices; Finance visibility, haptics, and Task board behavior never switch directly from a root tap. Opening, canceling, dismissing, or pressing Back in a child does not write a preference. An explicit choice is required, and an account/workspace switch cancels a pending editor, including a switch away and back. This replaces the earlier inline-root-toggle design. Existing theme, language, calendar, finance, and haptics sources remain the owners; personal timezone still overrides workspace, then device timezone. Timezone loading, unknown, retry, same-scope failure retention, and workspace permission states stay visible. Permission reloads fail closed and stale results cannot expose an editor or administrative destination. Production native timezone reads use the Infrastructure Calendar gateway. Verified Bearer sessions use the same hashed session trust key as server authentication; header presence alone never grants a trusted bucket. A 429 response retains its Retry-After cooldown across workspace changes for the same account. Settings show the wait state and suppress repeated reads until that time; the previous account’s cooldown and preferences do not apply to a different account. A successfully read personal or workspace preference stays visible if the other read fails, while effective timezone can remain unresolved; an independently loaded preference remains editable after any 429 cooldown ends, while the failed read retains its retry state. Diagnosis must distinguish gateway status, preferences reads and native plugin resolution; an Unknown label alone does not establish which failed. Concurrent loads for the same account and workspace share one pending resolution. The repository also shares pending personal reads across that account’s workspaces, while workspace reads and all account data stay separate. Completed reads are not cached by this mechanism; the next explicit refresh reads again. After a pending read’s existing load deadline expires, a retry starts a new read; late transport results cannot overwrite it. A resume refresh for the same scope does not invalidate an in-flight preference save. Any 429 without a positive parsed Retry-After uses a conservative 30-second cooldown. A failed save keeps its requested value for an explicit retry after the cooldown; no timer automatically retries reads or writes. Shared repository callers honor the account’s cooldown, and late results cannot overwrite the current account or workspace. timezone_load_coalescing_test.dart and timezone_gateway_retry_test.dart cover these contracts, including HTTP-date retry headers and partial preference recovery. The Infrastructure Calendar gateway forwards only bounded scalar rate-policy, caller-class, window, limit, remaining, reset, block-reason and Turnstile challenge headers plus Retry-After. Cookies, credentials, redirects and arbitrary upstream debug headers are excluded; JSON responses remain private and uncached. Gateway tests cover the allowlist and malformed values. These diagnostics help identify a blocking layer; source checks do not identify the cause of a production 429. Settings choice sheets scroll the heading and choices together in one bounded viewport, so a long heading at large text sizes cannot consume the entire choice area. Cancel stays outside that viewport. Other non-sliver dialog callers retain their fixed heading by default; timezone choices keep their existing lazy slivers. Mounted compact navigation coverage verifies visible choice space, real Finance scrolling, dismissal without a write, and isolation from the underlying dock. The app-wide Home/Apps dock is unchanged. Workspace is a child of Settings, so Back returns to Settings; deeper workspace pages return to their parent. You opens the existing Profile, preserving its navigation and account behavior. Legacy Settings URLs and the Preferences redirect remain supported. Session, About, and open-source licenses remain accessible. Root rows use existing theme tokens, quiet shared borders, concise labels, muted values, and at least 48dp touch targets; they grow with large text. Contextual Time Tracker, Mail, Meet, and Assistant settings intentionally remain with their own app and permission context. Settings tiles center a single title with the icon. For tiles with a value or supporting line, the first title line aligns with the icon. Workspace settings use the shell’s Workspace title and omit a second in-content heading. Settings subpages likewise use the shell title once, without a repeated introduction. Finance and Inventory read the workspace currency from the encrypted local snapshot before their first frame. Inventory product and sales lists likewise show cached rows and periods while background revalidation runs; a cold workspace still fetches its first snapshot normally. Notification Inbox and Archive actions share a bordered segmented control with primary selected colors and accessibility selection state. Use segmentGroup on related ShellActionSpec entries instead of unrelated highlighted icons. Mobile notifications use App: item title as the title and put the event detail or timing below it. This is the default for every module that sends mobile push, for the mobile notification inbox, and for locally scheduled Task and Calendar reminders. The app label follows the device language in the inbox and local reminders; server-sent push currently uses English module names. An unknown notification type uses Tuturuuu. Existing stored notification records keep their original fields; display formatting does not change routing or read state. Security approval pushes use Security while retaining their challenge data and approval safeguards. Mail owns folder, mailbox, and filter selectors through ShellMiniNav. Opening a message replaces the shell title with its subject and reuses the dock back action. Child composers and attachment previews suspend reader chrome. Background read-state changes and revalidation do not display a refresh indicator; explicit pull-to-refresh does. Reply-all and forwarding remain in the message actions sheet. Chat bubbles expose copy through long press or secondary click. Markdown in these bubbles must not install a competing native text-selection long-press recognizer. Other standalone markdown views remain selectable.

Responsive Loading And Cache Verification

Device task and calendar reminders

Settings → Notifications & interaction → Reminders controls task and calendar alerts separately. Both default on, with selectable 3-day, 1-day, 12-hour, 3-hour, and 1-hour lead times. The app refreshes the existing user/workspace-scoped task and calendar caches after sign-in and when it resumes after 15 minutes. It then schedules the nearest 60 future device alerts, replacing outdated entries and cancelling them on sign-out. An all-day event uses 9 a.m. local time as its reminder anchor. Android boot/package-replacement receivers restore pending alarms after a restart or update. Calendar alerts use a localized timing title (for example, “Calendar: In 3 days”) and the event name as the description, including for all-day events. Tapping one opens that event in the mobile Calendar detail sheet. If the event was removed or cannot be loaded, the app returns to Calendar and shows a brief message. This behavior applies to device-scheduled reminders on iOS and Android; task reminders keep their existing title and description. The Reminders page shows the last successful fetch, the number of alerts still pending in the operating system, the next scheduled alert, notification permission, and a manual refresh action. A scheduled count is local state, not proof that the device displayed an alert. If the app remains suspended, new items created on another device cannot enter the local schedule until the next app refresh. The deployed web cron checks watched task deadlines every five minutes across workspaces, queries only the active reminder windows, and creates notifications through each user’s channel preferences, including mobile push. Workspace task reminder settings can change or disable the lead times; without an override, the server uses the five device defaults. Its receipt includes the due date, so a rescheduled task is eligible again. Calendar event reminders still depend on the device schedule until the server calendar reminder flow is shipped. Verify this flow on physical iOS and Android devices with the app closed, notifications enabled and disabled, account switching, changed due dates, and completed tasks. Root tabs initialize on their first visit and retain their state afterward. Hidden tabs stop ticking animations. Page content and navigation no longer wait for staggered entrance animations. Opening Apps does not prefetch every product. Screen routes use subtle fade and horizontal motion on Android and desktop; iOS retains the native swipe-back transition. Reduced-motion settings skip the custom transition. The floating shell tabs animate their content in place. Home shows Finance wallets and recent Notes alongside its existing summary cards when those modules are available to the current account and workspace. Both cards use scoped snapshots while refreshing, and clear their displayed rows when the account or workspace changes. They link into their corresponding mobile screens. The Notes link picker uses scoped snapshots and the local entity index for Tasks, Calendar events, Finance wallets, Notes, and Meet rooms. It overlays queued edits and marks unsynchronized options. Reopening a cached category shows its rows while revalidating. A new category without any local entities still waits for its first response. CacheStore shares initialization and concurrent fetches for the same scoped key. Prefetch returns stored snapshots immediately and starts background revalidation, even past their expiry window. Explicit refresh waits for the network. On startup, the encrypted store consolidates legacy aliases into canonical scoped keys and retains the newest snapshot for each identity. Pending offline mutations remain separate and intact. An encrypted entity index now materializes rows with stable IDs from scoped snapshots. It is rebuilt automatically from existing snapshots when its schema version is absent, updates alongside new snapshots, and removes rows when their source is cleared or evicted. Local queries require a user ID, workspace scope, and namespace; they can overlay the matching queued edits without a network call. Storage limits include the indexed entity payloads. This is the database foundation for module read adapters, rather than evidence that every module is already locally readable or writable. Mail keeps an explicit request path for authorization cleanup on 401/403. Meet now returns the scoped meeting snapshot immediately, refreshes in the background, and purges denied data if the refresh returns 401/403. Meet room costs, ended-meeting reviews, and assistant review details use scoped snapshots. Assistant review decisions queue with revision conflicts surfaced for manual review; a queued review card uses the pending frame. Live call tokens and assistant generation still require a connection. Pending Notes, Mail, Finance, Calendar, and Inventory product edits are stored in the account-scoped encrypted mutation queue and replayed in order per module and workspace on reconnect. Every authenticated replay request remains bound to the queued edit’s account, including after asynchronous local-ID resolution and between requests in a multi-step operation. Switching accounts pauses that edit in the queue; it cannot send with the new account’s credentials. API account and offline-sync regression tests cover this boundary. A conflict holds dependent edits in that scope while other modules continue. A queued item uses a dashed border and lower opacity until the server confirms it; conflicts and failed writes remain visible in Settings > Offline > Offline changes for retry or removal. Removing a queued change requires confirmation and does not undo a write that may already have reached the server. A user-initiated retry gives a completion haptic only when that specific outbox entry is removed after replay. Finance creates and transfers, Mail sends and attachments, and Notes creates use stable client IDs so replay can recognize a prior successful write. The queue checks structured local ID references across modules and holds a dependent edit until its source create has synced. The replica stores account/workspace-scoped ID mappings across module namespaces, so a queued Note mention can resolve a Task created offline. Operational actions that require a live server calculation or fresh authentication remain explicit online paths. Calendar event lists use scoped snapshots and overlay pending edits. An uncertain Calendar create stays in manual review because it may have already reached an external calendar provider. Connected Calendar accounts and visibility settings also read encrypted snapshots while revalidating. Offline disconnects and visibility toggles remain queued and visibly marked until confirmed. Provider OAuth URLs still require a live request. Inventory product lists overlay queued creates and updates on their cached catalog; an uncertain product create also requires manual review. Inventory setup choices (owners, manufacturers, categories, units, and warehouses) use the same queue. Their local chips carry the dashed pending frame and stay available after an offline refresh. A failed authorization refresh removes the protected snapshot. Storefronts and their listings use the same scoped read and write path. A newly created offline storefront keeps a local ID so queued listings can reference it; replay maps that ID to the server storefront before sending dependent edits. Queued storefront and listing cards retain the dashed, dimmed treatment. Education course, quiz, and flashcard edits and basic CRM user and feedback edits use the same encrypted queue. Education course, quiz-set, quiz, flashcard, and attempt reads and CRM user, group, feedback, and audit reads use scoped snapshots while revalidating. Education authored cards and CRM user and feedback cards show queued edits with the pending treatment. Education attempts are read-only in the mobile app and use scoped snapshots. CRM user merges and bulk imports enter the outbox as ordered commands. A queued merge marks its source user pending; a bulk import reports queued instead of claiming users were imported. User avatars stage encrypted bytes, obtain a new upload token at replay, and map their local placeholder to the returned URL before the dependent user edit runs. Duplicate detection remains a live server calculation and does not represent a local edit. Workspace secret create, edit, and delete queue in the encrypted settings outbox; pending secret cards are dimmed and dashed. A newly created secret is matched to its server ID after replay before dependent edits run. Ambiguous secret writes stop for manual review. Storage migration remains an online operational command and is never represented as a completed offline change. Documents lists and details use scoped snapshots. Document create, update, and delete enter the encrypted outbox when offline, and list rows expose their pending state. A newly created offline document uses a local ID for navigation; its create must finish syncing before edits to that document can be replayed against a server ID. The outbox persists a scoped local-to-server ID mapping when the create response supplies an ID and rewrites dependent queued paths and structured payload fields before replay. Drive directory and analytics, CMS collections and entries, and Habit tracker lists and details also read scoped snapshots while revalidating. CMS collection and entry CRUD uses the outbox and marks pending tiles. CMS entries targeting a locally created collection use the persisted ID mapping during replay. Drive folder create, rename, folder and file delete use path-scoped queued edits with pending rows. Drive uploads also queue encrypted file bytes; replay requests a fresh signed URL, uploads the bytes, and finalizes the file. An uncertain upload remains in manual review to avoid overwriting a file twice. Habit trackers, entries, and streak actions also queue edits, with pending cards and activity rows. Create responses that omit a server ID still need dedicated replay support. Habits repository reads capture the original account before API/cache and pending-edit awaits; only that account’s queued records are overlaid. A member detail includes queued entries only for the selected target member. Writes require authentication before queue admission and invalidate the owning account/workspace. habit_tracker_actor_scope_test.dart verifies owner filtering, delayed reads, anonymous transitions, and member-target isolation. The native Habits screen recreates its cubit when the authenticated account or workspace changes. Screen snapshot keys and pending asynchronous publication use the initiating actor; closed or departed sessions cannot publish late list or mutation responses, and workspace switches reset mutation busy state and quick-log drafts. Authentication cache cleanup also invalidates work when the same account signs out and back in. Forms and details remove only their owned route on scope departure, including a route covered by another modal. These rules are covered by habits_actor_lifetime_test.dart, real encrypted storage in habits_cache_lifetime_test.dart, habits_host_scope_test.dart, and habits_owned_overlay_test.dart. A definitive 401/403 from a native Habits list, detail, or activity request clears visible rows and blocks every secondary snapshot query variant for that account/workspace. A separate durable denial marker prevents reopening private history when snapshot deletion fails. Purges and subsequent publication are serialized; only a fresh authorized list saved successfully can remove the marker. Draft edits and queued mutations cannot reauthorize it. MFA/verification challenges and temporary transport failures keep the last authorized screen snapshot. The native cubit requests fresh repository responses while supplying its own stale snapshot, so a lower-level cached response cannot masquerade as renewed authorization. Other repository callers retain the default read-through contract. habits_snapshot_access_test.dart covers durable revocation, storage failures, fresh-read provenance, ordered recovery, private member removal, and actor/workspace isolation. Habits overview search is hosted by the persistent shell dock; closing search or leaving the section clears its query. A tracker detail sheet opens immediately while its scoped request loads. Reopened details revalidate in the background while keeping their last authorized snapshot; cold loads and background refreshes reuse an already-running request for that tracker. Duplicate taps cannot open stacked detail sheets. The scope-owned route still closes on account/workspace departure. These native-only presentation rules are covered by habits_page_search_test.dart and habits_page_personal_detail_test.dart; the API remains an unpaginated list. Meet meeting create, edit, and delete also queue offline with a pending list treatment. Assistant model favorites and name changes now queue offline and overlay pending values on their scoped cached reads. Live room access, realtime tokens, and assistant generation remain network-dependent and must not be represented as queued calls. Task summaries, board lists, and board task pages read scoped snapshots while revalidating. Core task create, edit, description, delete and restore, plus board create and lifecycle edits, enter the outbox offline. Task and board cards expose pending status. Label, project, and initiative lists revalidate from scoped snapshots. Their create, edit, and delete actions queue offline with pending cards. Task relationships and task/project/initiative links queue as scoped edits. Bulk task actions queue as one ordered edit with every affected card marked pending; the bulk action reports a queued result without claiming server success. Partial bulk replay stays in manual conflict review so successful tasks are not silently retried with failed ones. Board task create, edit, and move and board list create, edit, and reorder also queue offline. Their list reads overlay pending edits; task cards and list headers distinguish unsynchronized rows. Project updates read scoped snapshots and queue create, edit, and delete with pending cards. Board estimation settings also queue offline, overlay pending choices on the cached board list, and mark the board tile until the edit is acknowledged. Task description images stage encrypted bytes and request a fresh signed upload URL during replay. Their unique local image markers are replaced only in queued task description fields after upload; arbitrary prose is never rewritten. An ambiguous upload remains in manual review before its dependent description edit. Converting a Note checklist item to a Task uses the cached board/list reads and Tasks outbox. The Note mention carries the local Task ID until replay maps it; tapping an unsynced mention shows its queued state, and a synced mention opens the resolved Task. Chat conversation and message lists also return scoped snapshots while revalidating. Conversation create, edit, and delete and user message sends enter the encrypted outbox offline. A queued message appears immediately with a dimmed dashed frame. Each send carries a stable client request ID so replay can use the server’s idempotency check. Message edit, delete, read acknowledgment, reactions, AI settings, and friend requests also queue offline. Attachments stage encrypted bytes in the outbox, request a fresh signed URL on replay, and rewrite dependent queued message paths after upload. An ambiguous upload remains in manual review. Assistant generation remains network-dependent. Notification feeds and unread counts also return scoped snapshots while revalidating. Read state, metadata, viewed Mail thread archiving, and workspace invite decisions queue offline. Pending read state is applied to the feed and unsynchronized notification tiles use the dashed treatment. Profile’s former preferences snapshot migrates into the encrypted account-scoped replica on first read. The visible cached profile stays in place during each refresh. Profile text edits and activity sharing queue offline; identity and sharing controls show the pending treatment until acknowledged. Avatar changes stage encrypted image bytes offline and request a fresh signed upload URL on replay. A lost upload connection remains reviewable because the server may already have accepted the image. Workspace member, role, and invite-link settings use scoped snapshots and queue edits while offline. Permission and invitation changes only become effective after server acknowledgment; uncertain writes require review in Offline changes. Workspace selection migrates from preferences into the encrypted replica, and a cached workspace list always triggers revalidation when loaded. Creation limits and module visibility metadata use the same read behavior. Finance exchange rates, wallet details, transaction pages, and stats retain snapshots during refresh, including the initial Finance screen load. Workspace creation, name and default changes, and avatar edits queue offline. New workspaces receive local IDs; replay maps those IDs before dependent edits. Workspace avatar bytes remain encrypted in the outbox until a fresh signed URL is requested. An uncertain creation or upload requires review to avoid a duplicate workspace or avatar operation. Timer sessions, history, categories, goals, requests, and summaries use scoped snapshots while revalidating. The native Timer root captures its actor lifetime, workspace, and request generation before loading. Account and workspace changes invalidate old cache, network, and disk publications; a same-account return after logout cannot revive the old request. Current-scope snapshots remain visible while revalidating. Deferred repository and encrypted-storage regressions in time_tracker_scope_test.dart cover these boundaries. Session start, stop, pause, resume, edit, missed entry creation, and deletion queue offline, with dimmed dashed session cards. Timer category creation, goal create, edit, and delete, request create and edit, approval changes, comments, and threshold updates queue too. Request images stage encrypted bytes, obtain fresh signed URLs during replay, then submit the request with the resulting storage paths. Category, goal, request, and comment cards show pending sync state. Local ID reconciliation supports edits made before a create syncs; ambiguous sends remain in manual review. Running session state applies queued lifecycle edits over the latest snapshot. Finance category and tag lists also read scoped snapshots while revalidating; their create, edit, and delete actions queue offline, and queued cards use the dimmed dashed treatment. An uncertain transaction update or delete enters manual review rather than automatic replay. Once a transaction or transfer update is acknowledged, failure to refresh its detail does not invite another money write. Wallet checkpoint create, batch create, edit, delete, and reconciliation also enter the finance outbox. The timeline overlays queued checkpoints while keeping server-confirmed latest balances and interval calculations separate; pending rows have the dashed treatment. Finance feedback says queued or needs review when the write has not been acknowledged. Transaction attachments stage encrypted bytes, request a new signed upload URL on replay, and show queued state until delivery. An ambiguous money write or upload waits for manual review. Inventory sales-period create and edit, sale create and edit, period assignment, and delete queue offline. Sales lists overlay local edits; pending sale cards do not show an unconfirmed amount. Their local details remain readable and editable. Invoice replay maps its returned ID before sending a queued period assignment. Pending period and sale cards use the dashed treatment, and unknown outcomes require manual review. Invalidation and logout prevent older in-flight requests from writing back into the cache. Keys must include user, workspace, and all query filters. Offline refresh consistency (cache-backed reads in all mobile products): a two-phase read and its nested repository reads share one refresh cycle. Reconnect first revalidates visited resources, then visible projections reuse those completed reads. A later explicit refresh starts a new cycle; scope changes and invalidation fence earlier results. Explicit setup/catalog refreshes report transport failure; ordinary offline reads retain confirmed rows and project queued edits. Checkout coalesces overlapping quote reads, while an explicit retry after a settled request remains available. Removing one retained resource invalidates only the corresponding downloaded product for its actor and workspace. Each product must retain its shared permission snapshot before download readiness is accepted; Finance also requires retained exchange rates. Eviction of either dependency prevents readiness even when the product’s own catalog is complete. Snapshot replacement, source indexes, authoritative download reconciliation, and pressure eviction share one resource ordering boundary. An encrypted durable journal restores the previous completed snapshot after interrupted publication or cleanup. A persisted clear intent takes priority over rollback on restart; logout, actor changes and explicit invalidation must not resurrect old data. Resource-only clearing preserves unsynchronized outbox edits and local-ID mapping provenance. Replica extraction accepts known response envelopes, not arbitrary nested relation or metadata objects. Canonical domain fields survive sparse responses, while explicit clearing values stay cleared. Regression evidence: cache_store_publication_test.dart and cache_replica_policy_test.dart. Connectivity fallback is restricted to typed transport failures (socket, timeout, or HTTP-client transport errors). Invalid JSON, unexpected response shapes, oversized bodies, and failed session refreshes remain visible failures rather than presenting a cached response as a successful refresh. Download pacing wait is outside the actual HTTP request timeout. Search patterns are compiled once per retained search pattern rather than once per candidate row. Mutation queue admission uses the same typed transport rule. The explicit OFFLINE_CONTRACT_UNAVAILABLE exception is admitted only for replay-safe writes; session, malformed-response, and unknown failures never become pending writes. Reopened sale details immediately project queued sales-period assignments, including explicit removal, over the scoped confirmed snapshot. They retain queued status and local IDs until acknowledgment; pending IDs never become server mutation targets. Definitive not-found responses, including a locally queued deletion, clear retained sale details and mutation controls. Transient refresh failures retain the authorized snapshot. Regression evidence: offline_mutation_admission_test.dart, cache_revalidation_test.dart, offline_download_manifest_test.dart, offline_preparation_coordinator_test.dart, api_client_transport_failure_test.dart, inventory_sale_period_projection_test.dart, inventory_sale_detail_refresh_test.dart, and season_pricing_controller_test.dart under apps/mobile/test. Failed disk writes do not hide successful network responses. Mail and Meet retain the original authorization error and disable their cache if denied-data cleanup fails. Keys longer than Hive’s 255-byte UTF-8 limit use a SHA-256 digest of the complete scoped identity. Keep short keys stable, and test persistence/reopening with real UUIDs and long search filters; short mock IDs can hide storage failures. Mail list snapshots use the existing encrypted Hive store, with a one-minute freshness window and a seven-day retention window. Cached rows appear while a background refresh runs, including after their freshness window. A background authorization denial revokes the affected workspace snapshot, clears the inbox, and closes an open reader. Pull-to-refresh forces a network read, and successful mutations invalidate the workspace’s Mail variants. Thread details use the same scoped cache; permissions and individual messages (including editable drafts) are fetched from the server. General attachment bytes, credentials, and active compose buffers are not cached. Authorization failures discard the displayed list. Search waits 300ms after typing, and list rows are built lazily. Folder metadata loads independently so it cannot hold up the inbox. Small inline images keep an encrypted thread-scoped copy. A cached image renders immediately while one shared background request refreshes it; a denied image request revokes the affected Mail workspace and clears its thread media. Inventory can synchronously restore an unexpired user/workspace-scoped snapshot before currency and permission requests finish. Editing remains disabled until current permissions are confirmed. A denied request removes displayed content; cached data must never grant mutation permissions.

Tablet layouts and shared controls

Use the available window width, including split-screen windows, when choosing columns. Home uses a full-width workspace selector and independently flowing card columns; the Apps hub uses content-sized rows so larger text can grow. Drive and CMS allow their grids to use the wider workspace. Kanban and timeline are work surfaces: do not wrap them in the reading-column width constraint. Tablet Kanban fills the available height and puts dock clearance inside each scrolling task list. It shows multiple independently scrolling lists; compact windows keep the swipe-between-lists interaction. Leave room for the shell dock and task actions. The shell’s Apps button opens a searchable picker using the same module visibility rules and descriptions as the hub. Primary top-bar actions remain visible; extra actions use the overflow menu. Tablets can display more actions. Register Mail workspace actions only while its nested workspace route is current, so a reader or composer cannot leave stale workspace actions active. Mail controls scroll with the message list, and formatted messages render inline in an isolated native WebView. JavaScript, file access, forms, and browser-session sharing remain disabled. Remote images require an explicit action. Its shared document builder sanitizes the original body HTML off the UI thread, preserves newsletter styles and email tables, and uses the web reader’s typography and overflow rules. Do not force every table into fixed equal-width columns. CID images load through the authenticated attachment API, limited to raster images, 12 requests and 8 MiB retained bytes per message. The WebView receives only prepared image data, never API credentials. Large attachments remain available through the attachment actions. Meet uses cached, account/workspace/query-scoped pages, explicit refresh, and mutation invalidation. Workspace changes must invalidate pending UI requests even when the new workspace is absent. A 401/403 clears the cached and displayed data. The native Meet room keeps the shell’s top bar and places its leave/back control in the room content, so the route title and dock follow the rest of the app. Verify at least 768×1024, 1024×768, and 1366×1024, plus a compact split window, larger text, and an open keyboard. Check dock height and taps, multiple Kanban columns, horizontal and vertical scrolling, task-detail return position, picker filtering, and the last visible row above the dock. Widget checks supplement authenticated simulator checks; native email rendering requires device verification. When changing this behavior, verify cold launch, returning to a visited tab, pull-to-refresh, slow/failed requests, rapid search changes, and switching accounts or workspaces. Run the focused cache, Mail workspace, lazy tab, and shell back navigation tests, followed by bun check:mobile. Use an iOS simulator to inspect light/dark appearance, larger text, keyboard interaction, and narrow layouts. Time Tracking and CRM user mobile calls stay on the live platform origin: Track and Contacts require app-session credentials, while mobile currently uses Supabase Bearer sessions. Do not move those paths to their satellites until mobile supports that authentication contract.

Native API requests and hosting challenges

An HTTP 429 HTML response with x-vercel-mitigated: challenge is a Vercel browser challenge, not the application’s JSON rate-limit response. Native HTTP clients cannot complete that challenge. Production mobile Calendar requests use the infrastructure app’s /api/v1/mobile-calendar/api/v1/… gateway. Development uses the local Calendar app directly. The gateway validates the Bearer session with Supabase before reading any forwarding credential. It forwards only event CRUD, connected-account list/disconnect, calendar connection list/toggle, and OAuth URL discovery to the fixed https://calendar.tuturuuu.com origin. Calendar validates the original session again and enforces workspace permissions. Incoming cookies, forwarding headers, and client-supplied gateway credentials are never relayed. Redirects and non-JSON upstream responses are rejected; mutations are never automatically retried. The server credential is stored as a versioned encrypted envelope in the root workspace’s workspace_secrets row named mobile_calendar_gateway:v1. Its data key is encrypted with the infrastructure app’s existing ENCRYPTION_MASTER_KEY. The envelope contains version: 1, encryptedDataKey, and encryptedSecret. Only the server decrypts it; it must never enter mobile build configuration, logs, source control, or the signing bundle. Configure Calendar’s custom WAF exception to require both its API path and an exact x-tuturuuu-calendar-gateway header value matching that private credential. Do not accept the mere presence of an Authorization header as a WAF bypass. The private credential allows verified gateway traffic through hosting challenges; it does not replace Calendar’s application authentication or authorization. For cutover, deploy and verify the gateway, then publish clients using it. Replace the old path-plus-Bearer exception with the server-credential rule once both store builds are available. Verify invalid sessions return JSON 401 from the gateway without an upstream request, a signed-in Calendar loads real events, and direct Calendar requests with a forged Bearer or gateway header still receive the hosting challenge. Older clients that call Calendar directly need the mobile update. Keep publication on hold if gateway verification fails. Inspect the exact project’s active rules and unpublished draft before changing or publishing its WAF config. For credential rotation, create a new encrypted envelope, temporarily allow both old and new server credentials at the WAF, update the secret row, verify a live Calendar read, then remove the old credential. Never restore the public Bearer exception as a fallback. Missing or invalid secret configuration fails closed with JSON 503. Responses use private, no-store; request bodies are limited to 512 KiB, response bodies to 8 MiB, and forwarding to 20 seconds.

Locked Notes

Mobile Notes defaults to a device-first encrypted lock. Where OS local authentication is available, Face ID or Touch ID gates a random note key in device-only secure storage; otherwise the lock sheet defaults to a six-digit app PIN. The lock encrypts the note body; the title and list metadata retain their existing visibility. The PIN gates that local key and is never used as the encryption password. Five incorrect PIN attempts pause retries for 30 seconds. The server also stores a wrapped copy of the random key in the note envelope using the configured encryption master key. Creating a device lock fails if wrapping is unavailable. Web releases that key only for the note creator after a verified passkey sign-in within five minutes, on web or mobile. Account sign-in alone does not release it. Native passkeys use webcredentials:tuturuuu.com on iOS and the get_login_creds Digital Asset Links relation for the production Android signing certificate. Keep the association files and mobile signing identities aligned when changing app IDs or certificates. The user can also show a two-minute QR code on web and scan it from the original phone’s note actions; the phone requires fresh local unlock and sends the key encrypted with a one-time QR secret. The QR secret stays on the two devices. The server relays only the sealed key. A custom passphrase remains available and existing passphrase-locked notes keep their current behavior. Server-wrapped recovery means this lock is not end-to-end encrypted against the server. Preserve the configured encryption master key when rotating deployment secrets; losing it disables passkey recovery for existing locked notes, though the original phone or its QR transfer can still open them. On compact layouts, Back closes any open Notes action sheet and returns from note detail to the Notes list.

Mobile App Lock And Web QR Approval

The mobile app supports a device-level app lock from Settings > Session. The setting is stored in secure storage and uses the OS local authentication prompt with device PIN/passcode fallback allowed. When enabled, the app locks on an authenticated cold launch and after at least 30 seconds in the background. The locked surface centers the existing transparent logo at the generated native artwork size: 104 points on iOS and 80 dp on Android. Its light background is white and its dark background is #09090B, matching the native launch canvas. Entry stays centered. With text scaled above 1.5×, recovery moves the decorative logo to the top with 24 pixels of clearance after the system inset, keeping it above the instructions. It does not expose protected content to touch, keyboard focus, or accessibility. Recovery controls appear five seconds after entering the lock, including when authentication fails or remains pending; retrying does not restart that deadline. Unlocking still uses the OS biometric/device-passcode prompt, not a separately stored application PIN. A failed or cancelled prompt leaves the app locked. Authentication completions from a reset account lifetime or disposed lock are ignored. A physical in-flight authentication guard also survives settings reloads until the OS prompt settles. Delay saves run sequentially; a completed current-lifetime save preserves the locked state, and only the latest intent updates the visible delay. Reset/disposal prevents queued stale writes from starting. An already-started secure-storage write cannot be cancelled by resetting the UI lifetime. Regression coverage is in app_lock_splash_timing_test.dart, app_lock_boundary_test.dart, app_lock_auth_lifetime_test.dart, and app_lock_review_races_test.dart. Widget checks cover entry timing, compact English/Vietnamese recovery, large text and reduced motion. app_lock_splash_visual_test.dart decodes native logo assets and compares rendered artwork bounds and canvas colors at matching viewports. Those source fixtures do not prove an installed launch, native biometric or TestFlight behavior. Android legacy and Android 12 logo inputs retain the canonical 320-pixel RGBA artwork inside transparent 768/1152-pixel canvases. Regenerate Android only from apps/mobile with dart run tool/generate_android_splash.dart; it preserves the canonical logo and excludes iOS/web generation. Pixel tests verify transparent native padding and 80 dp artwork across all densities. Installed OS launch scaling and release artifact qualification remain separate.

Device authenticators and trusted registrations

Home suggests registering the current device when local authentication is available, the account has no verified authenticator on that device, and new registrations are unlocked. “Not now” suppresses this invitation for seven days per account. A failed policy lookup never produces a misleading invitation. Settings > Session always provides the explicit management entry point. Registration creates a Supabase TOTP factor and a separate random device proof. The mobile app saves both in account-scoped secure storage before verifying the factor. iOS uses a non-synchronizing, this-device-only Keychain item. Each code reveal, approval and management change requires fresh OS authentication; PIN or passcode fallback is allowed. Codes disappear when the app backgrounds. /api/v1/auth/mfa/devices uses request-scoped authentication for web cookies and mobile Bearer tokens. Server-owned app_metadata.tuturuuu_device_authenticators contains the registration policy and proof hashes, never TOTP secrets. The registry API serializes writes with a bounded Upstash lease and fails closed if UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN is unavailable. Do not log registration response bodies, proofs, TOTP secrets, or session tokens. The trusted-device list supports revocation and locking new registrations. Enabling the lock requires two currently verified trusted factors. Unlocking requires a current trusted-device proof and AAL2; the last trusted device cannot remove itself while locked. Keep a backup trusted device: there is no automatic lost-all-devices recovery bypass. The lock governs Tuturuuu device registrations; it does not disable third-party enrollment through Supabase’s own MFA APIs. Only canonical, verified registry entries with valid proofs can approve Tuturuuu sign-ins. A client-supplied device identifier or editable profile cannot grant trust. Provider-level MFA enrollment can invalidate other sessions, so setup explains that other devices may need to sign in again.

QR and number-matching approvals

The scanner at Routes.settingsQrLoginScan accepts Tuturuuu QR payloads from allowlisted origins; manual link entry supports devices without a camera. Approval requires fresh local authentication, a trusted-device proof and a verified mobile MFA session. The server checks the challenge secret, origin, protocol kind, expiry and one-time consumption. QR handoff creates a new session; Supabase MFA requirements still apply to that new session. The web MFA screen creates a session-bound /api/v1/auth/mfa/mobile/challenges request and shows a six-digit number. The mobile user must enter that number. The pending-approval API never returns it to the approving device. Approvals are rate-limited, expire after their short challenge lifetime, and cannot be approved by the requesting session itself. Users can explicitly deny an unexpected request. MobileMfaApprovalListener polls while authenticated, foregrounded and unlocked. Push receipt triggers an immediate refresh. A notification contains only account and challenge identifiers plus expiry, never a code or credential. Tapping it opens a page that re-fetches the authorized request. Notifications do not approve from the lock screen. Polling remains the fallback when notification permission or provider delivery is unavailable. After consumption, the web poller sets the HTTP-only ttr_mfa_mobile_approval cookie, bound to the same user, web session, hidden challenge secret and unexpired canonical approval. This is an application-level web approval; it does not mint a Supabase AAL2 token. Mobile AAL2 still comes from Supabase MFA verification.

Sessions and account connections

Settings > Session > Devices and connections lists active sessions and linked sign-in identities. Users can revoke another session, revoke other sessions, connect a supported OAuth provider, or disconnect an identity while retaining another sign-in method. Destructive actions require confirmation and local verification. Account changes during an asynchronous operation fail closed. Multi-account persistence snapshots the current session after profile loading, so a refresh during that request cannot save an older token. Authenticated API retries are account-scoped and refuse to replay after an account switch.
  • Multi-account removal and all-account logout must treat Supabase auth.signOut() as required before clearing the local account store or emitting an unauthenticated Cubit state. Google/provider logout and stored refresh-token revocation can be best-effort, but a failed Supabase logout must leave the account visible with an error so the persisted session cannot silently reappear later.
  • Auth account models may serialize refresh tokens and session JSON only for the encrypted secure-storage store. Do not include those raw secrets in Equatable.props, toString, or other Cubit/Bloc state debug output because the global Bloc observer logs state changes during development.

Mobile Drive Coverage

The Drive screen in apps/mobile supports the main dashboard Drive flow:
  • Workspace permission-gated loading
  • Storage analytics summary
  • Folder navigation and breadcrumb-style path movement
  • Search, sort, grid/list toggle, and pagination
  • Folder creation, rename, delete, bulk delete, and upload
  • File preview, external open, share, and path copy
  • Folder export-link generation and sharing
The mobile implementation talks to the same authenticated workspace storage routes used by the web product:
  • /api/v1/workspaces/:wsId/storage/list
  • /api/v1/workspaces/:wsId/storage/analytics
  • /api/v1/workspaces/:wsId/storage/folders
  • /api/v1/workspaces/:wsId/storage/rename
  • /api/v1/workspaces/:wsId/storage/object
  • /api/v1/workspaces/:wsId/storage/share
  • /api/v1/workspaces/:wsId/storage/export-links
  • /api/v1/workspaces/:wsId/storage/upload-url
  • /api/v1/workspaces/:wsId/storage/finalize-upload

Mobile CRM Coverage

The CRM screen in apps/mobile supports:
  • Workspace permission-gated users and audit tabs
  • Search, filters, pagination, and audit range filters
  • Create, edit, and delete user records
  • Avatar upload during create/edit flows
  • Feedback create/update/delete flows
  • Duplicate detection and merge flows
  • CSV import with preview and deduping by email
  • CSV export of the currently filtered users dataset
CRM mounts a separate native screen session for each signed-in account and workspace. The default users tab reads only that tab, renders the scoped stored snapshot, and publishes the refreshed result; pull-to-refresh awaits revalidation. Temporary transport failures and explicit MFA/abuse-verification challenges retain authorized rows. Optional group reads do not delay customer rendering. Row fields are intersected with current workspace permissions even when a refresh falls back to older cached data. Cold loads reserve row geometry and announce loading. Definitive access denial clears visible customer and audit data. Changing scope closes that session’s forms and sheets and rejects late responses and queued writes. Search lives in the persistent shell dock. Customer rows open a compact detail view using the permission-redacted /users/database result; private contact fields require its private-information permission flag. Pending local edits are redacted again using that metadata, so they cannot restore newly denied fields. Detail viewing does not call the individual-user GET endpoint. The shell owns the title and Back action. The existing create/edit/avatar, feedback, filtering, import/export, and permission-gated duplicate/merge tools remain available. Lists build rows lazily, load another page near the viewport end, deduplicate overlapping IDs, and stop when an empty page disproves a stale total. A failed page offers an explicit retry. Regression coverage: crm_workspace_test.dart covers scoped late responses, permission-redacted detail, enlarged text, automatic paging, empty-page termination, and retained-versus-denied refreshes. offline_mutation_admission_test.dart ensures model-returning CRM commands cannot queue for a departed account. These checks do not establish authenticated device or production behavior. The mobile CRM uses the existing authenticated routes:
  • /api/v1/workspaces/:wsId/users/database
  • /api/v1/workspaces/:wsId/users
  • /api/v1/workspaces/:wsId/users/:userId
  • /api/v1/workspaces/:wsId/users/groups
  • /api/v1/workspaces/:wsId/users/feedbacks
  • /api/v1/workspaces/:wsId/users/audit-logs
  • /api/v1/workspaces/:wsId/users/duplicates/detect
  • /api/v1/workspaces/:wsId/users/merge
  • /api/v1/workspaces/:wsId/users/bulk-import
  • /api/v1/workspaces/:wsId/users/avatar
For CRM list and duplicate/merge routes, keep workspace resolution request-aware on the web side. Mobile calls authenticate with Bearer tokens rather than dashboard cookies, so routes that resolve the workspace with cookie-only helpers can fail as Workspace not found even for valid workspace members.

Mobile Education Coverage

The Education screen in apps/mobile supports:
  • An overview dashboard with counts for courses, quizzes, quiz sets, flashcards, and attempts
  • Course search, create, edit, delete, and paginated browsing
  • Library management for quizzes, quiz sets, and flashcards
  • Quiz authoring with answer options, correctness flags, and explanations
  • Attempt browsing with status and quiz-set filters plus attempt-detail review
Education’s native session is keyed to the current account and workspace. Its repository binds API reads, cache keys, pending-edit overlays, mutation admission, and tag invalidation to that actor. Legacy callers without an explicitly bound account capture the active actor before each request and retain that identity through cache reads, pending-edit overlays, and write admission. Anonymous reads cannot publish after sign-in; writes require an account before queue admission. education_actor_scope_test.dart covers delayed responses/queue reads and exact actor filtering across all four collections. Changing either scope removes the old list, search, and owned modal routes immediately; late responses cannot repopulate it. Course/library search uses the persistent shell dock and resets when the section changes. Collection rows build lazily, page ahead when the viewport is within 400 pixels of the end, deduplicate stable IDs, and stop when the server returns an empty page even if its count is stale. Temporary failures and verification challenges retain authorized rows while offering explicit retry; definitive authentication/permission denial clears visible collections. Normal entry has a cached phase followed by revalidation, while pull-to-refresh awaits a fresh phase. Attempt details likewise retain their authorized snapshot during revalidation and clear on definitive denial. Authoring forms keep server failures in the form for retry instead of letting an exception escape. No new teaching or assessment API behavior is implied by this presentation change; existing endpoint permissions remain authoritative. Widget regressions in education_workspace_test.dart cover account/workspace transitions, paging, lazy rendering, challenges/denial, and dock search reset. Owned creation routes also close when another modal covers them: session cleanup requests a frame and removes only the departed route, preserving the unrelated modal above it. The mobile Education module uses authenticated workspace routes from apps/web:
  • /api/v1/workspaces/:wsId/courses
  • /api/v1/workspaces/:wsId/courses/:courseId
  • /api/v1/workspaces/:wsId/quizzes
  • /api/v1/workspaces/:wsId/quizzes/:quizId
  • /api/v1/workspaces/:wsId/quiz-sets
  • /api/v1/workspaces/:wsId/quiz-sets/:setId
  • /api/v1/workspaces/:wsId/flashcards
  • /api/v1/workspaces/:wsId/flashcards/:flashcardId
  • /api/v1/workspaces/:wsId/education/attempts
  • /api/v1/workspaces/:wsId/education/attempts/:attemptId
For mobile-facing collection APIs, keep list routes request-aware and list-shaped. A cookie-only helper or .single() response on a collection endpoint can work in the web dashboard but break mobile Bearer-token callers or pagination assumptions. The mobile endpoint registry is guarded by bun check:mobile-api-mappings. The check extracts Flutter /api/* templates from api_config.dart and the production endpoint catalog used by the ttr CLI SDK. It verifies that both legacy clients still resolve to a canonical owner in a checked-in Next satellite, the legacy route wrappers, or the Rust/TanStack migration manifest. The Mobile API Mappings workflow runs whenever either client catalog, an app API route, or the migration manifest changes. If a route is migrated or removed, update Flutter and the CLI SDK in the same change instead of leaving a legacy client to discover the break in production.

Mobile Inventory And Storefront Coverage

Inventory is a native mobile workspace with overview, product CRUD, checkout, sales, setup, audit-log access, and storefront management. Storefronts support create/edit/delete, status and visibility controls, public preview, product listing CRUD, stock-row selection, pricing, and per-order limits through the same authenticated inventory satellite APIs: Mobile sales also support workspace sales periods. Operators can create a season or event such as Summer 2026 or TuCon 2026, filter the mixed invoice/checkout history by that period, and assign the period during sale creation or editing. Archiving preserves historical reporting.
  • /api/v1/workspaces/:wsId/inventory/storefronts
  • /api/v1/workspaces/:wsId/inventory/storefronts/:storefrontId
  • /api/v1/workspaces/:wsId/inventory/storefronts/:storefrontId/listings
  • /api/v1/workspaces/:wsId/inventory/storefronts/:storefrontId/listings/:listingId
Keep Storefront in the five-item Inventory mini navigation. Audit logs remain discoverable from Inventory Manage so compact phones retain a stable native navigation bar. When an Education mobile list needs derived counts from workspace-protected join tables such as course_module_quiz_sets, verify workspace membership first with the request-scoped client, then run the joined read on an admin-backed client. Also select real join columns such as module_id; do not assume those bridge tables expose a synthetic id.

Import And Export Format Notes

  • The web CRM page still has richer spreadsheet tooling, including XLSX-oriented flows.
  • Mobile import and export currently use CSV plus native share sheets.
  • Keep the server contract for bulk import as a raw JSON array of { email, fullName } rows.
This format choice is intentional for now: the current Flutter dependency graph in apps/mobile does not accept a stable XLSX package cleanly without conflicts, so mobile should prefer CSV/share flows unless spreadsheet generation is moved server-side later.

Verification

For Drive, CRM, or Education mobile changes:
  1. Update apps/mobile/lib/l10n/arb/*.arb for every new user-facing string.
  2. Run flutter gen-l10n.
  3. Run bun check:mobile.
  4. Run the applicable mobile test and native build workflows in CI on the exact authored commit. Use CI for routine branch verification; if its queue is demonstrably stuck, record the pending jobs and local evidence, then resume exact-commit CI before claiming release readiness.

Offline preparation and synchronization

Finance, Inventory, Tasks, and Calendar keep visited responses in the encrypted, user/workspace-scoped cache. Reopening uses local snapshots immediately while revalidating. Loaded list pages and detail responses also materialize local rows; searching a new query offline does not require that exact query to have been requested online. Reconnect, app resume, and refreshed authentication replay the outbox before refreshing visited resources. Visible lists reload after replay. Only one replay runs at a time. Cached screens publish their first snapshot, then await fresh data without starting a second cold fetch or a cache-notifier loop. Transport failures retain the original scoped snapshot and its timestamp. Settings and pending rows distinguish queued, syncing, failed, and conflicting writes. An uncertain money operation stays in manual review unless its endpoint guarantees idempotency; replay must never turn a cache refresh failure after acknowledgment into a second write. Settings → Offline → Download all is an explicit download for the current account and workspace. Products settle sequentially, with per-product state, historical completion time, and targeted retry. Inventory downloads catalog pages and each product detail; sales, audit, and finance transaction history are paged. Audit pages are indexed by their server audit record IDs and reused across UI page sizes, ordered by occurrence time descending. Finance also prepares checkpoint summaries, wallet-specific statistics, and account-scoped global exchange rates, so these reads work on a first offline visit. Tasks include board collections, task details, relationships, and supported history. Calendar cursor pages cover the API-supported year range 0001–9999 and connection metadata. Its paginated requests cap each page at 500 rows; normal unpaginated requests accept at most 400 days. Pagination has finite safety bounds and fails rather than silently claiming completion when a cursor does not advance. Wallet checkpoint preparation currently follows the mobile endpoint’s latest-50 window; it does not promise a lifetime checkpoint archive. Completed full collections reconcile obsolete list/detail snapshots so removed server rows do not survive in local search. Failed downloads retain older snapshots and pending writes. Each explicit download requires successful persistent writes and a final check that its requested snapshots survived storage limits. A full run checks earlier products again for eviction. A restored completion timestamp is historical and shows Needs refresh, never proof that all rows remain stored. Cache eviction, clearing cache, changing quota, or changing account/workspace invalidates readiness. A scope switch stops subsequent requests; an already-started request settles before another bulk run starts. Normal visits fill a bounded older history window with 15-minute throttling, rather than rerunning Download all automatically. Bulk GETs are globally serialized on the device and spaced by at least 750 ms at the request boundary, using a monotonic clock after queued authentication preparation. Early timer wakeups are rechecked; regression coverage lives in offline_api_pacer_test.dart and api_client_offline_preparation_test.dart. This stays below the hourly session budget even when the gateway and owning app both enforce the guard. Ordinary unmarked foreground reads do not consume these additional bulk-download budgets. The additional server download protection applies only to eligible GET/HEAD requests explicitly marked x-tuturuuu-offline-download: 1. Ordinary daily enterprise operations in Finance, Inventory, Contacts, Tasks and other apps always continue through existing authentication, authorization, MFA, abuse and API protections without accessing this additional guard’s Redis clients, quotas or CAPTCHA, even when it is activated. This scope applies to the web API and every registered satellite proxy. OFFLINE_DOWNLOAD_PROTECTION_ENABLED defaults to inactive and activates only for the exact value true; while inactive, marked bulk downloads return 503 with Retry-After: 30. Omitting the bulk marker never bypasses baseline protections or grants access. Before enabling offline prefetch, provision UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN on every gateway and owning-app replica that can handle these reads. Verify distributed limiter operations, shared clearance storage, Turnstile configuration, and authenticated ordinary-read and marked-bulk canaries on the exact deployment. Enable the activation flag only after those checks pass, consistently across the request path; available Redis or elevated account trust never implicitly activates the feature. Keep the flag inactive during initial provisioning. Once activated, Redis/configuration/runtime failures still return 503; there is no automatic outage bypass or process-local substitute. When activated, the shared proxy enforces explicitly marked eligible bulk reads: 1,200/minute and 30,000/hour per IP, plus 600/minute and 12,000/hour per session when present. Gateway and owning-app enforcement may both count a marked request. Bulk requests require Turnstile clearance unless the server has verified elevated trust; clearance lasts 15 minutes and never bypasses authentication, permissions, MFA, blocks, or quotas. Ordinary unmarked requests do not spend these additional offline-download allowances. Regular reconnect refreshes are paced without the bulk marker; optional history fills use silent bulk mode. Background work never opens a verification modal and pauses when challenged. Verification-required 403 responses preserve existing authorized snapshots; authorization-denied responses purge the denied resource. A 429 stops downloads and delays queued replay according to Retry-After, with at least a minute’s wait. Regression coverage in offline-download-guard.test.ts proves unmarked ordinary reads avoid Redis, offline quotas and CAPTCHA with activation on or off, inactive bulk remains blocked, and activated bulk storage failures, quotas and CAPTCHA remain enforced. Local Inventory search mirrors name-only SQL ILIKE, trimmed whole query, case insensitivity, preserved accents, %/_ wildcards, and backslash escaping. Catalog ordering is creation time descending, then stable ID. Finance matches the untrimmed query against descriptions and orders by taken time, then creation time, both descending. Cached [CONFIDENTIAL] descriptions are not searchable: offline search cannot reconstruct hidden server fields or newly granted permissions. The cache represents the last authorized download; changes made elsewhere, permission revocations, and authoritative stock/balance calculations require a server refresh. Sales can be drafted offline from cached catalog and season prices, but remain queued until the server validates price IDs and commits them. Mobile applies configured current-period, revenue-wallet, and finance-category defaults while preserving explicit choices. Legacy period sales also use the atomic invoice request with a stable request ID; period rejection must not leave an already-created invoice. Reconcile cart refreshes current metadata/prices and removes incompatible or unavailable lines, then asks the operator to review the total. It applies only to the editable current cart, never historical sales or a frozen uncertain checkout journal. Stock health remains the last confirmed snapshot and excludes local drafts. Workspace permissions are refreshed online and persisted as an encrypted account/workspace-scoped UI hint. A missing network interface or transport failure may reuse that hint; authorization failures remove it, and switching accounts cannot reuse grants. Download all also prepares this metadata. Local permissions never authorize server writes: the server checks current permissions when replaying each mutation. Product images are cached as encrypted bytes when displayed and during Inventory Download all. Cards, checkout rows, and editor previews use the same scoped cache. Image bytes share the storage quota and clearing/eviction behavior with product records. Downloads accept HTTPS media with at most three HTTPS redirects, never send app credentials, and enforce an 8 MiB file and 16 megapixel image limit. A required image that cannot be retained makes the product download fail rather than claiming full offline readiness. Cache keys digest image URLs so signed read tokens are not exposed in plaintext keys. Offline inventory acceptance is scoped to the current account and workspace. A queued checkout receives a local pending identifier, never a committed invoice receipt; pending local catalog/setup edits must synchronize before checkout. Read-through helpers retain their default compatibility behavior while pinning an available account at entry, checking it at request and cache publication boundaries, and rejecting late responses after account changes. Request generations also prevent older workspace loads from replacing the active screen. For marked bulk reads, the authenticated gateway uses a session budget when the client IP is unknown; an anonymous marked request without an attributable IP fails closed. Known-IP bulk requests retain their IP budget. These controls do not grant offline clients server authorization. Product images share one 60-second deadline across redirects and body streaming, and unrelated resource eviction does not cancel their fetch. Deferred cache transaction/refresh sharing, transport draining, search performance, and cosmetic improvements are tracked in #5737. App disposal invalidates preparation readiness immediately; an already-started read may fail when its HTTP client closes. Graceful draining remains a followup and does not permit a disposed preparation to publish readiness or replay writes. Regression coverage includes inventory_product_image_cache_test.dart, inventory_product_editor_page_test.dart, api_client_account_test.dart, and packages/utils/src/__tests__/api-proxy-offline-response.test.ts. Provider authorization, live calls, AI execution, and uncached attachment bytes still require connectivity; Download all is scoped to these four product data collections, not every external service or file. Regression evidence: offline_sync_test.dart, local_search_test.dart, offline_preparation_coordinator_test.dart, offline_download_manifest_test.dart, offline_preparation_repository_test.dart, finance_inventory_offline_test.dart, inventory_audit_offline_test.dart, season_pricing_controller_test.dart, and packages/utils/src/__tests__/offline-download-guard.test.ts cover replay, scope changes, strict persistence, search rules, offline draft state, and server rate/challenge enforcement. cache_revalidation_test.dart, workspace_permissions_offline_test.dart, and finance_scope_races_test.dart cover fresh visible data, permission hints, scoped invalidation, and late-response rejection. Native airplane-mode and real Turnstile verification remain release checks beyond unit tests. Synthetic render evidence for the offline status and targeted retry surface: Offline cache with targeted retry, synthetic Flutter fixture These images come from Flutter widget fixtures, not an installed-device build.

Floating navigation and compact Profile

The first floating navigation tap opens its destination. Reselecting the active destination scrolls its content to the top. Reselecting again resets its top section to the default; if already at the top, reset happens immediately. Rapid third taps during scrolling must also reset. Notifications returns to Inbox and Profile to Overview within at most three taps; scrolling manually restarts the sequence. Apps resets to Grid, Finance taxonomy to Categories, and Calendar to Three days on phones or Week on tablets. Notes returns to Inbox and Assistant to Chat through their existing guarded handlers. Time Tracker sections remain separate routed destinations. Home defaults to Home; its second tab, Personal Agenda, uses only the signed-in user’s personal membership, with no shared fallback or global scope change. Agenda resolves user, personal workspace, then device timezone and reloads after preference changes; Home retains its mounted dashboard state across tabs. Agenda retains cached events during retry and offers read-only details; Calendar remains the editing surface. Apps defaults to Grid before List. Shared search replaces the top navbar; empty submission dismisses and unfocuses. IME visibility and hardware search focus animate the whole dock and its actions out together; route changes clear focus. Workspace visibility has a subtle icon, a named tooltip, and a 44-pixel target. Regression coverage: personal_agenda_test.dart, personal_agenda_scope_test.dart, shell_keyboard_chrome_test.dart, and apps_search_submit_test.dart. Apps opens with one tap and keeps its picker on long press. Profile uses the shell title for Overview and Timeline, with no duplicate body heading. Place the day-trail action before the rightmost Overview/Timeline switch, including compact/overflow layouts. Identity and metric layouts adapt to available width and text scale; avoid nested bordered panels and fixed three-column cards on small screens. Timeline mounts activity rows lazily and keeps transient provider failures in a bounded toast, without a persistent retry/status row. Preserve privacy and do not announce a partial response as complete history. These are defaults for the mobile Profile and comparable shell section switches; semantic borders for unsynchronized drafts and destructive confirmation dialogs remain intentional. Regression evidence: shell_dock_reselection_checks.dart, shell_chrome_actions_test.dart, profile_overview_shell_test.dart, profile_timeline_section_test.dart, and profile_timeline_races_test.dart. Synthetic Profile renders use placeholder identity and activity data: Compact Profile Overview, synthetic Flutter fixture Profile Timeline at double text scale, synthetic Flutter fixture

Profile activity privacy

The Profile timeline shows the signed-in user’s task creations, transaction creations, and notes from the verified personal workspace. Calendar rows are workspace events, shown only with manage_calendar permission. The API first checks actor membership and Calendar permission before its audited Calendar read; missing permission omits that source, while lookup failures mark the result partial. Finance keeps its permission-aware RPC. Calendar titles are not returned. Overview is the default view and has no edit shortcut. Profile editing and full banner controls live in consolidated account settings. Avatar/banner uploads are optimized off the UI isolate: input is bounded to 20 MiB/40 MP, orientation is applied, and JPEG output is limited to 1024/2048 pixels and the existing 2/5 MiB bucket ceilings. Animated images become a static first frame; re-encoding strips source metadata. Banner tickets reserve the existing actor-derived upload budget; failed tickets retain their reservation. Only public first-party actor-owned avatars/banners enter the scoped disk cache. Signed, private and foreign URLs do not. Account changes fence asynchronous profile loads and media dispatch. Timeline groups records by local day, links to native views, and reveals days near the bottom, including an initially short viewport. Day headings and activity rows are mounted lazily, with row extents adapted to the effective text scale. Further API pages contain at most 200 rows per source, with a pinned upper time boundary and a maximum of 1000 pages per browsing session. Offset continuation can omit or repeat records during concurrent inserts/deletes; client deduplication does not guarantee snapshot isolation. Refresh starts a new session. A partial or failed page pauses automatic continuation and keeps loaded rows. No persistent warning, retry row or completion footer occupies the Timeline; a failure notice automatically dismisses after five seconds. A capped or partial result must not claim an empty complete history. The loaded snapshot is encrypted and actor/personal-workspace scoped, shown before revalidation. Successful pages persist merged, deduplicated history. Partial refreshes retain stored rows from failed providers while replacing successful providers, with truthful partial flags; they do not retain newly permission-denied Calendar rows. Older partial responses without source metadata retain their stored rows as explicitly incomplete. Missing, ambiguous or unverified personal membership fails closed; neither Overview nor Timeline follows the selected team or tracked-time sharing preference. An unreadable local snapshot does not block refresh. Regression coverage includes profile_timeline_incremental_test.dart, profile_timeline_races_test.dart, profile_media_optimization_test.dart, profile_media_image_test.dart, and the first-class mobile-activity/banner-ticket API tests. Profile Overview and Timeline show personal data only and expose no team-sharing controls or opt-in team aggregates. Nothing is shared by default. Existing team sharing consent still gates workspace-scoped tracked-time totals and daily activity, excluding raw session titles, email addresses, personal aggregates and other workspaces. Revoking consent or membership blocks future shared reads. The API uses the existing owner-scoped user_workspace_configs preference PROFILE_ACTIVITY_VISIBILITY=workspace; missing or other values mean private. It checks viewer membership before privileged reads and subject membership and consent before and after aggregation. Shared activity is never stored in the mobile persistent cache. Test private defaults, confirmation cancellation, revocation, former members, and account/workspace changes before release.

Assistant and task verification recovery

A task write returning HTTP 403 with X-Abuse-Challenge: turnstile requires verification, not broader workspace permissions. Mobile opens the existing security dialog and retries the same request once using the returned token. Cancellation sends no retry; an account change aborts the request. Other 403 responses remain permission failures. Never bypass the server authorization or abuse check to repair this flow. Log status and error codes, not response bodies or verification tokens. Every satellite enforcing this challenge needs its own production TURNSTILE_SECRET_KEY configuration matching the mobile TURNSTILE_SITE_KEY. The mobile release vault also supplies TURNSTILE_BASE_URL. A successful client challenge followed by another 403 requires checking the server challenge audit: not_configured means the satellite secret is missing. Configure the existing matching secret through the provider vault and redeploy the current production source; never disable verification. Check names and readiness without logging secret values. Assistant credits must load independently of optional task, Calendar, soul, and model-catalog insights. Calendar insights use the authenticated workspace /api/v1/workspaces/:wsId/calendar/events gateway. A failed credit read offers a retry in the credit-source sheet rather than displaying a fabricated Free tier. Chat errors remain visible after stream completion, with the user’s message retained for retry. Older Assistant UUIDs are not native Chat conversation IDs: the specific Conversation is not an AI chat settings rejection must use the legacy Assistant stream through the shared authenticated API client, including challenge and refresh handling. Other malformed-settings errors stay errors. Read-only ai-agent-thread- mirrors belong in Chat, not Mira’s editable history. Do not restore them as Assistant conversations or send their synthetic identifiers to Live’s UUID-backed chat endpoint; Live starts a new owned session when no resumable UUID is available. Live initializes PCM playback before starting microphone capture on iOS. Its serialized audio queue preserves chunk order and discards queued chunks when playback is interrupted or disposed. The vendored PCM plugin preserves existing AVAudioSession category options when the category is already correct; resetting those options can disturb an active microphone route. Native microphone failures are recoverable UI states. A simulator without a valid input format cannot prove physical-device audio quality; verify recording, playback, interruption, and headset reconnection on hardware before claiming that a reported device crash or stutter is resolved.

Apps search submission

Submitting a nonempty Apps query with exactly one current matching app clears the field, closes search mode, and opens that app. Zero or multiple matches keep the query and results visible while unfocusing the keyboard. Empty queries close search mode. Submission uses the same current localized results as the hub, and repeated submissions cannot launch the app twice. Apps discovery retains its case-insensitive substring matching over the localized label, description, and module ID; this submission change adds no plural stemming. A queued selection is canceled if workspace, account, experiment, access, or locale inputs change after its match was resolved and before navigation.

Workspace picker search selection

The mobile current-workspace and launch-default pickers keep search text and filtered results when the search field loses focus, including keyboard dismissal before a result tap completes. Clearing search is an explicit action. Each result uses its workspace ID for identity and selects its rendered workspace object, including after sorting, duplicate display names, and list refreshes. This rule applies to the sheet opened from the navbar, dashboard and Workspace Settings; it does not change membership discovery, permissions, or default persistence.

Private Hidden workspace recovery

The compact Settings root places Hidden workspaces immediately after Workspace, before You. Its restore editor is account-scoped and available for guests, without an active workspace, and when every workspace is Hidden. Opening, searching and dismissing perform no preference write; explicit Restore changes only that user’s preference and does not select a scope or change its default. The all-Hidden workspace selection screen also offers recovery. See Hidden workspaces for canonical membership separation, actor-scoped caching and web/satellite selector behavior. Regression coverage is in settings_hidden_recovery_checks.dart (actual Settings/Shell root, one mini navigation item, guest/no-active/all-Hidden, Back and 320px/2× text) and workspace_visibility_test.dart (rollback, logout/account replacement, deferred module flags and explicit deep links before preference discovery).

Shared navbar text scaling

The compact mobile navbar keeps its normal size at the default text scale. At larger accessibility text sizes, its height grows to fit the title line without shrinking that text. The floating content inset grows with the same measurement, while icon switches stay compact in the top navbar. Read-only titles normally use one line with an ellipsis and retain their full accessibility label. When ellipsis would remove every title character, a clipped prefix remains visible. Editable titles scroll horizontally under a fade instead of ellipsizing. Mounted Assistant chat/live coverage confirms that its existing inner Row allows its scaled title to paint within the taller outer navbar: 56px at 2× and 84px at 3×. That inner Row does not clip the text vertically. Horizontal space remains limited by the compact mode controls; narrow layouts can show only a clipped prefix rather than the complete title. Its full semantics label remains available. The shared-navbar regression verifies actual ink in the first title glyph for both modes at both widths and all three text scales. Scroll threshold geometry is cached and refreshed for theme, text-scale and safe-area changes; drag notifications do not perform title layout. Title measurements follow inherited text style and platform bold, line-height, letter-spacing and word-spacing overrides. The painted-prefix check selects a complete first grapheme, so emoji and joined characters retain ordinary ellipsis when they fit. Full title semantics remain available in either overflow mode.

Profile Timeline date navigation

Overview and Timeline read only the signed-in account’s verified personal membership, even with a team selected. Missing or ambiguous personal memberships, authentication mismatch and account transitions clear data and make no fallback request. Cached and delayed responses stay scoped; opening records selects their personal workspace. Timeline has one primary scroll: optional week controls scroll with activity, rather than pinning a second header. Closed shell-controlled dates add no month row or empty refresh slot. Header clearance is inside the scroll; it leaves the viewport as the shell hides. Returned days and further provider pages append near the bottom, including a short viewport, with stable item positions and type/id deduplication. Date navigation expands returned days as needed. No completion label implies that the user’s complete history has been loaded. Partial responses retain existing rows and pause continuation; refresh starts a new paging session. Calendar attribution, device-local grouping, reduced motion and DST stay intact. Home’s personal calendar tab is titled Agenda in English and Lịch trình in Vietnamese. Its header clearance scrolls away; all-day progress is clipped by the card’s exact edge. Regression targets: profile_personal_scope_test.dart, profile_timeline_incremental_test.dart, existing Timeline race/date tests, personal_agenda_test.dart and agenda_view_test.dart; device proof is separate. The week strip reveals the selected date when opened or when the selected date, week, width or text scale changes; ordinary snapshot rebuilds retain the user’s manual horizontal scroll. Browsing another week begins at its first day when that week does not contain the selection. Calendar-day comparison uses UTC date carriers rather than elapsed DST-length days. Unknown cold failures and empty partial/capped snapshots show unavailable/partial state, never a confirmed absence of activity. A successful complete empty response can confirm the empty state. Actual padded Profile/Shell regressions cover the single activity viewport, selected-date bounds and existing compact navbar at 320px/3×. Opening date controls before the first activity response does not lock the default date: the newest returned day is revealed unless the user explicitly chooses a date. Empty stored snapshots stay provisional while revalidation is pending, using the fixed skeleton viewport rather than announcing absence. The section owns one bounded failure toast per failure streak; the embedded browser does not add a persistent provider-status banner, refresh icon, retry row, or completion label. Every entry shows the last authorized in-memory snapshot immediately and revalidates in the background, including snapshots marked fresh. Temporary network failures and verification challenges retain those rows; definitive authentication or permission denial clears both visible rows and the stored snapshot. A scoped secure-storage revocation marker is written before cache removal, so cache initialization or deletion failure cannot reopen that history after a restart. Until a new repository checks that marker, synchronous peek stays empty; unreadable markers fail closed. A successful actor-fenced refresh confirms a new snapshot was persisted before clearing the marker. If that write fails, fresh online rows may display but the stored denied history remains blocked. If both marker persistence and cache deletion fail, the client reports TIMELINE_CACHE_REVOCATION_FAILED and blocks the current session; durable erasure cannot be guaranteed during combined storage failure. Verification challenges, including MFA_REQUIRED, preserve history and continuation. Cold skeletons announce Loading as a live region without adding a visible refresh control. An account or personal-workspace change fences delayed responses. These defaults apply to mobile Profile Timeline; partial provider failures retain only previously loaded rows from the failed sources and pause provider continuation until a successful complete refresh. Viewport-near-end loading still expands returned days and complete provider pages automatically, including short lists. Regression evidence: profile_timeline_repository_test.dart, profile_timeline_races_test.dart, profile_timeline_revocation_test.dart, profile_timeline_browser_test.dart, and profile_timeline_paging_test.dart. Mounted Profile regressions locate the lazy activity section by its rendered position and wait within a bounded interval for fetched statistics. Fixed pixel scroll distances depend on the test font and can leave the section unmounted at large text sizes. Restore a hidden shell header with reverse user scrolling before tapping its selector; resetting content offset alone does not reveal the header. Validate both the default CI font and the optional capture font.

Offline settings and stored-data inventory

Mobile Settings groups Preferences into Appearance & language, Calendar & timezone, Notifications & interaction, and App preferences. Downloads, cache limits, cache removal, and unsynchronized changes belong to a dedicated Offline section. Account, workspace, experiments, and help retain their existing access rules. Each stored namespace expands to a local-only item list, searchable by name or ID. Rows render lazily in a bounded viewport; search covers the complete scoped local collection and survives unrelated coordinator updates. Namespace source-content identity triggers rereads only when that namespace changes. Payload sizing is deferred until visible item details open and is memoized for that content version. Item details show its stored identity, logical payload bytes and last fetch time; these persisted rows exclude queued-only edits. Account/workspace changes and loss of module access immediately remove the list and its expanded details. Regression coverage: offline_module_page_test.dart verifies item search, scoped isolation and permission redaction. Offline module detail pages offer stored-collection search, per-namespace inventory, and the applicable existing Finance, Tasks, or Calendar preference. Inventory has download, refresh, and eviction controls; it does not invent a new module preference. Available-item counts deduplicate persisted replica IDs within each namespace across overlapping pages and snapshots. Ordinary successful reads count as well as explicit downloads. Valid retained expired snapshots remain counted as available offline, with expiry reported separately from freshness. Counts never sum unrelated namespaces into a supposedly complete catalog. Server-reported query totals are displayed only when retained pages agree on the query and total; otherwise expected totals are unknown. These totals never establish complete download coverage. Pending-write coverage of server totals is unknown; the pending count is shown separately. Sizes measure logical encrypted-store payloads including snapshot and replica copies, excluding filesystem overhead. Expected download sizes and transferred network bytes are unknown. Module data is scoped to the selected account and workspace. Inventory details require current workspace access, and unavailable modules show no stored-data inventory. Removing a module’s data clears only its scoped namespace resources, preserving every queued write and other module/account/workspace snapshot. Cache removal in Offline preferences also uses the selected scope; the cache quota remains device-wide and can be changed by a signed-in user without a selected workspace. Scoped usage and clearing require a workspace; no cross-workspace usage is shown when none is selected. Scope changes cancel open cache preference sheets. Eviction and quota controls disable reactively while an explicit download is running. Stored corrupt JSON payloads still count toward storage and can be cleared, while they never count as available offline items. Pause cancels subsequent download steps. Resume retries unfinished modules while skipping modules verified ready in the current session. It restarts a module’s supported collection download; it does not promise byte-level continuation. Across launches, retained timestamps are historical evidence and never alone establish readiness. Cache removals during a download or its final retention checks are retained until completion: affected products return to queued status, while unrelated products can finish. Successful product downloads supersede their own reconciliation removals; final retention checks and later removals still revoke readiness. The next explicit retry must verify removed dependencies again. Cache removal notifications retain only scoped identity with an event-only opaque key digest, never the removed response payload or literal query/search parameters. Persisted cache keys remain unchanged. A corrupt stored response is treated as missing and still attempts network revalidation; account switches and explicit cache clearing continue to fence that response. Inventory product details permit pending-item fallback only for typed transport failures. Invalid response bodies, session failures, and unknown status-zero errors surface instead. Regression coverage: offline_preparation_coordinator_test.dart, cache_revalidation_corruption_test.dart, and inventory_repository_test.dart. Explicit downloads continue to use the existing bounded Finance, Inventory, Tasks, and Calendar collections; neither these pages nor a Ready label promise all server data or every attachment. Timezone reads and writes both pin their authenticated account. Automatic timezone preferences persist even if native timezone lookup fails; unresolved effective timezone and actual load/save errors remain visible, with wrapping text and retry. These settings use the production mobile-calendar gateway and existing Bearer renewal/retry behavior; no backend route or production state changes are included. Regression evidence: test/core/cache/offline_cache_inventory_test.dart, test/core/cache/offline_preparation_coordinator_test.dart, test/features/settings/cubit/timezone_settings_cubit_test.dart, and test/data/repositories/timezone_settings_http_contract_test.dart under apps/mobile. Widget tests are source-level evidence; native airplane-mode and installed-client behavior require a new exact-source CI artifact and separate runtime verification.

Native Assistant prompt and navigation

On the mobile Assistant text-chat surface, opening the prompt collapses the actual floating shell navigation into the right-hand hamburger toggle. The single-row prompt occupies the remaining width: plus menu, chat field, and microphone action. Text or attachments replace the microphone with Send. The plus menu holds attachment previews and removal, model selection, thinking mode, and the credit-source sheet, including its existing balance/error/retry behavior. Model selection remains constrained by the active credit source’s allowlist. The hamburger expands the existing navigation above the prompt and dismisses the keyboard. Closing the prompt restores normal navigation. Android back first collapses expanded navigation, then closes the prompt. Tab changes hide the prompt while retaining its unsent text and attachments; workspace changes and the existing account session boundary reset the draft to prevent scope leakage. The legacy fullscreen button is removed. Explicit Live entry, voice recording, camera controls, permissions, and session persistence keep their existing rules. Other shell screens retain their shared keyboard-hide navigation policy. Regression coverage: assistant_composer_dock_test.dart checks the 320px layout, large text, single-row actions, attachment removal, restoring state and blank submission; assistant_composer_options_test.dart checks adaptive model selection and credit allowlists; assistant_chrome_cubit_test.dart covers presentation reset; shell_assistant_composer_test.dart checks actual dock collapse, restoration, back handling, mounted shell continuity, retained tab drafts and workspace reset across the active floating dock at phone/tablet/desktop widths (the legacy compact footer is disabled). assistant_composer_geometry_test.dart checks the rendered scroll action clears the composer with safe-area insets, large text and keyboard visibility; both exclude physical bottom padding while the keyboard is visible. Widget coverage does not establish physical-device microphone, provider, camera, or production behavior.

Native onboarding and brand introduction

The first-run and replay guide introduces Tuturuuu and Mira alongside Tasks, Calendar, Finance, and Inventory. Native gradient surfaces and brand assets provide visual context; tool labels describe existing surfaces rather than enabling access or promising automatic actions. Role and goal selection remain optional local recommendation preferences, not permissions or subscription changes. The four-page guide keeps the existing completion flag, role and goal preferences, login destination, and replay return behavior. Skip completes the same guide. Content scrolls independently of its primary action, including on small screens and with enlarged text. Light and dark themes retain solid readable controls. Reduced-motion settings remove animated page, progress, and background transitions; there is no continuous animation. This introduction is a full-screen flow outside the consolidated shell and does not add a nested shell navbar. Regression evidence: apps/mobile/test/features/onboarding/onboarding_page_test.dart covers progression, preferences, skip, replay, and compact enlarged-text layouts.

Cache recovery and explicit quote refresh

Mobile cache publication journals retain prior snapshots for stale-while-revalidate rollback. Eviction and authoritative completed-download reconciliation instead persist a scoped deletion intent without copying removed payloads or replica rows. After interruption, recovery completes those deletions and preserves unrelated scopes and pending writes; it does not restore evicted or obsolete data. Journal completion batches the entity-log flush after affected snapshot/outbox flushes. Regression evidence: cache_store_publication_test.dart. Inventory manual season-quote refresh waits for an automatic refresh, then performs an unthrottled fresh request in the same actor/workspace/period scope. Other concurrent callers still coalesce; a scope change cancels the queued refresh. Regression evidence: season_pricing_controller_test.dart.

Challenges and shared cached reads

Shared mobile JSON object/list reads retain the last encrypted authorized snapshot when revalidation requires MFA_REQUIRED or a typed verification challenge. The request still rejects with the challenge, allowing the owning screen to request verification; retained cache does not authorize a write or bypass server checks. Rate limits and server availability failures likewise leave the stored snapshot intact. Definitive same-account 401/403 responses erase the snapshot and replica projection durably. Purge admission rechecks the request actor, so a departed account’s delayed denial cannot erase a newer authorized publication. Regression evidence: offline_read_through_access_test.dart verifies both payload shapes, reopening encrypted storage after retention/denial, and a delayed denial across an account change. A concurrent tag invalidation rejects the new response but retains the previous authorized snapshot as stale, including its replica rows. Account/workspace clears and actor changes still erase it. Offline search patterns are compiled per call once per Finance/Inventory collection scan and retained only by that caller; no application-owned global raw query cache survives logout. A queued sale-period assignment retains its period ID when period metadata is evicted, with unknown metadata distinct from an explicit null removal. Regression coverage: cache_store_publication_test.dart, local_search_test.dart, and inventory_sale_period_projection_test.dart. Finance replica merging treats a newer server [CONFIDENTIAL] description as an authorization barrier regardless of detail authority: older plaintext cannot appear in offline lists/search or merged JSON. A newer authorized response may restore that field even from a list after an older redacted detail. Amount and category permission nulls (with their server confidentiality flags) likewise yield to newer explicit authorized values, while ordinary detail clears and sparse-field authority remain unchanged. Newer explicit null still clears the field. This projection rule is covered by cache_replica_policy_test.dart and finance_inventory_offline_test.dart. Direct transaction detail refresh also routes transport fallback through merged replica rows rather than an older raw detail snapshot, even during awaited revalidation. Authentication/MFA errors still propagate. Other read-through callers retain their existing awaited transport fallback default. Covered by finance_review_regressions_test.dart. Transaction detail fallback reads merged replicas after the failed request or offline connectivity decision, rather than returning rows captured before it. A redaction arriving while a request waits therefore takes effect immediately; the captured actor is checked after the fallback read. Concurrency regressions live in finance_review_regressions_test.dart. Pending Mail and workspace-secret reads fall back only for typed transport failures; local response/session errors remain visible even with queued edits. Tag invalidation advances stored resource revisions as well as active flights, so completed futures in the same refresh cycle cannot replay a pre-mutation response. Queued manual season refresh is also bound to the generation, preventing A-to-B-to-A scope changes from resuming an old continuation. Regression coverage: mail_repository_test.dart, workspace_secrets_repository_test.dart, cache_revalidation_test.dart, and season_pricing_controller_test.dart.

CMS actor-bound repository reads and edits

CMS repositories created for a signed-in actor bind cached summary, collection, and entry reads to that actor. Pending edits are overlaid only for their owner; an account change during queue access rejects the old read. CMS writes carry the same actor into offline admission, and invalidation targets that actor’s workspace cache. Unbound legacy callers resolve the current actor for pending overlays and never display anonymous or another account’s pending edits. This foundation does not grant CMS permissions or replace API authorization. Regression evidence: cms_actor_scope_test.dart exercises collection and entry overlays, delayed queue completion after account switching, and obsolete reads and writes. The native CMS screen and device verification remain separate work.

CMS native library and editing

The mobile CMS surface owns one signed-in actor and workspace. Its library uses lazy rows, collection/status filters, and local search published into the single shell dock. Search covers the loaded CMS collection/entry response; it does not claim server pagination or full-text search beyond that response. Re-entry publishes stored scoped content and awaits background revalidation; explicit refresh waits for the current cycle. Same-actor temporary failures and MFA challenges retain content, while definitive denial clears it. Switching actor or workspace discards the previous surface and closes its owned editing routes. Editors keep drafts and inline errors until a save succeeds, permitting explicit retry without silently replaying an uncertain write. Regression coverage: cms_workspace_test.dart exercises lazy 500-row rendering, shared dock search, stale-scope responses, denied versus challenged refreshes, retained failed-save drafts, and actor-owned editor dismissal beneath an unrelated modal. Cleanup schedules a frame even when the covered route is settled; it removes that editor alone and preserves the covering modal. Timer also remounts its native host for each authentication transition, including batched logout and login to the same account. A preference read owned by the disposed host cannot prevent the new host from loading or resume its previous request. The actual-host deferred preference regression covers this liveness boundary independently of Timer load-publication fences.

Native Chat account lifetime

The native Chat root owns a new host and cubit for each signed-in actor, even when the workspace ID stays unchanged. An actor-transition generation also replaces the host when logout and same-account login occur before a frame renders. Search, selected conversations, drafts, and retained messages belong to that host. Closing invalidates asynchronous callbacks before subscription cancellation finishes; actor and cache-resource lifetime checks also fence delayed results. This preserves the existing shared shell dock and API-backed workspace chat behavior. The actual host account-switch and delayed-cancellation regressions are in chat_actor_lifetime_test.dart. Chat rejects retained-cubit API dispatch after its actor lifetime ends. A send waiting for previous stream cancellation rechecks actor, workspace and selected conversation before starting a new request; stale conversation creation results do not trigger selection reads or return a navigable result. Already dispatched server writes are not cancelled by this client boundary. Deferred cancellation and creation regressions cover these rules.

Delivered inbox identity foundation

Standard persisted notification pushes carry a versioned inboxIdentity capsule from the recipient account, actual inbox workspace column (explicit null for personal), and notification row ID. Android receives the same full identity as its notification tag. This is an encoded identity, not encryption or an action authorization token. Display/navigation metadata cannot change its owner or scope; a personal inbox invite can still navigate to a shared workspace. Custom/MFA pushes strip the reserved inboxIdentity data field and receive no owned inbox tag; other custom metadata remains unchanged. Legacy rows without valid recipient/workspace/notification identity remain unqualified; they must not be bulk dismissed by guessing their owner. Existing Mail navigation is preserved without promoting its legacy metadata into trusted inbox identity. push-inbox-identity.test.ts covers the actual payload constructor and outgoing transport with a fake Firebase client, including same-ID foreign scopes, personal/navigation separation and legacy/custom exclusions. This packet provides identity only. Mobile delivered-only removal and read-action integration remain separate; it does not clear an OS tray or cancel future task and calendar reminders. Source tests are not device proof. A future delivery receipt must record the actual TestFlight version/build and verify foreground and background single/all-read dismissal while foreign scopes, later arrivals, and pending reminders survive. The reported 0.23.4 build number remains unknown.

Delivered-only native bridge

The mobile bridge binds to the current account session, snapshots delivered inbox notifications, and removes only still-matching captured deliveries. A null inbox workspace selector means all inbox scopes available to that actor, matching the existing bulk-read API; an explicit workspace selector is narrower. Missing or malformed ownership capsules remain untouched. Foreground and data-only background local notifications preserve the same capsule as standard remote pushes. Android uses app-owned active notification tag/ID/post time; iOS uses delivered request identifier/delivery date and remote or local capsule metadata. Account session changes invalidate outstanding snapshots, including logout/relogin ABA. Enumeration beyond 512 delivered records or eight retained snapshots fails closed. A discard operation releases a snapshot without removing notifications. Neither platform bridge calls pending-notification or alarm cancellation APIs. The native platforms offer no atomic timestamp-conditioned removal. Rechecking captured identity and delivery time protects replacements already visible during the recheck, but a replacement racing between recheck and OS removal remains a platform limitation. Returned counts mean matched removal requests submitted, not verified tray disappearance. Read-action integration is a separate packet; this bridge alone does not dismiss notifications when marking them read. Dart MethodChannel tests cover actor/session ABA, held callbacks, strict receipts, scope selection, malformed capsules and metadata round trips using fake native transport. Native compilation and actual TestFlight foreground/background tray behavior remain separate required evidence. Future scheduled task/calendar reminders must survive single/all-read cleanup. Record the actual version and build number for device feedback; source tests do not identify a delivered build. Push-service session start/stop/dispose also invalidate a sticky lifetime epoch. A start held in native binding, initialization, token lookup or device-ID lookup cannot continue into registration or pending-navigation dispatch after departure, including a same-account logout/relogin. The registration helper rechecks its captured lifetime before writing, and disposal closes initialization/navigation admission. Already dispatched transport writes cannot be undone by this boundary. push_service_session_lifetime_test.dart exercises actual service methods and the actual registration helper with injected platform/token/device transport.

Calendar viewport and adjacent timed-card geometry

Mobile Day, Three-day and Week timelines keep isolated back-to-back events at full available width. Half-open timestamp components identify genuine overlap; an isolated card’s readable minimum is capped at the next disjoint start, and paint and column assignment use that same extent. Tiny capped cards clip their visual content, retain full title/time button semantics and do not extend their hit area into the next event. Genuine simultaneous/transitive overlap clusters retain readable minimum heights and physical conflict columns; a timestamp- disjoint successor can still share columns with that readable cluster. Shared Day, Three-day and Week strips size spans intersecting the viewport. Chip insets and offscreen events reserve no rows or expand control. Row and control heights transition together over 180ms and retarget from their current geometry. Whole rows and controls appear only when they fit, preserving scaled text and viewport-clamped labels. The header stays fixed; the timed grid moves with the strip while keeping its minute offset. Reduced motion places geometry immediately, including interruptions. A scope change resets the transition. Data refreshes retain current authorized content and date anchors. The all-day scroller retains its identity and attaches at the grid offset. Recentring updates dates and horizontal offsets before paint; scoped civil-date keys retain day cells. Pixel scroll rebuilds only the strip. Initial time positioning, week-start and date reselect keep their existing behavior. calendar_three_day_geometry_test.dart and calendar_all_day_transition_test.dart cover painted drags, data updates, retargets, text scale, themes, taps and scopes. Existing overlap tests remain. CI and installed-device checks remain separate; widget geometry does not establish TestFlight identity or frame-time impact.

Reading inbox notifications and the system tray

Mobile reads capture a bounded snapshot of actual delivered notifications before accepting the read. Single reads match the current actor and notification ID; workspace bulk reads match that workspace, while the personal/all-workspaces inbox follows the existing API’s actor-wide bulk scope. This is independent of the visible 20-row feed. Unread transitions never dismiss notifications. Only the captured, still-current session can request delivered-item removal. Mounted read controls suppress stale cleanup and refresh after account/workspace changes or route disposal. The opened-item helper follows its actor-bound ID across intentional navigation and rechecks that actor/native lifetime before publishing a refresh; logout/relogin invalidates both paths. Captured native tags/identifiers and delivery timestamps retain later arrivals and replacements. Pending scheduled reminders are never cancelled. Legacy deliveries without trusted identity remain untouched. A repository read may be a durable offline intent, rather than a confirmed server write. Cleanup failure preserves that acceptance and shows a localized warning; it does not retry the read. Failed reads discard their snapshot without removing OS items. A later inbox or badge refresh failure similarly preserves accepted read state and shows the localized refresh warning. Mutation refreshes consume explicit per-operation success receipts, including ordinary transport exceptions; background badge/feed helpers retain their nonthrowing behavior and badge calls still share one in-flight request. When cleanup and refresh both fail, the tray cleanup warning takes precedence; the accepted read is never resubmitted. The delivered-only bridge is applicable to native Android and iOS. Web, Windows, Linux and macOS retain actor/session fencing but do not attempt this unregistered channel or show an unavailable-cleanup warning solely for its absence. Failed binding or a missing channel on Android/iOS still produces that warning. Web capability-policy tests are not browser compilation or OS runtime evidence. The platform APIs still cannot atomically compare and delete an OS item; the previously documented last-moment replacement race remains. Regression evidence uses actual mounted inbox actions, the real Cubit and opened notification helper with fake repository/platform transports, including scope ABA and rejection controls. It is not device evidence. Delivery requires exact native CI and a recorded TestFlight version/build/source SHA: foreground and background single/bulk reads must clear only matching delivered tray items while other actors, out-of-scope workspaces, later deliveries and future reminders survive.

Notification post-write feed refresh

Notification read actions invalidate any pre-write feed response before awaiting its completion, then request a fresh feed after the accepted write. Loading is not treated as a failed refresh. Queued continuations retain actor/workspace lifecycle admission. Held pre-write pagination is fenced by the same tab generation on both success and error, so it cannot restore captured unread items. An abandoned scope cannot publish or start a replacement request. Ordinary background requests coalesce without throwing; a failed fresh request reports the existing safe refresh warning and can be retried without resubmitting the accepted read. Regression coverage: notification_read_held_feed_test.dart.

Assistant Settings Above the Shell

The central Assistant settings entry and Mira quick settings open the same framed editor above the shell. Its single heading and Back action cover the underlying shell chrome; other scoped settings routes retain their existing routing. Captured account/workspace providers and sticky scope invalidation remain in force, so a held preference load cannot restore an editor after an account/workspace switch, including switching away and back. Large-text content scrolls above the frame’s Back action. Real shell regressions cover English/Vietnamese, narrow screens, large text, Back restoration, actionable switch clearance and held-load scope transitions. Both current-route cancellation and removal below an unrelated covering route are exercised, with sticky rejection after account/workspace ABA and after the held preference completes. Regression coverage: settings_assistant_shell_checks.dart, registered by settings_compact_navigation_test.dart. This source fix does not claim native TestFlight delivery or establish the original screenshot’s exact installed build.

Workspace Secrets Settings Heading

The mobile workspace Secrets detail uses the shared shell title and Back action. Its summary panel retains the workspace description, icon, status counts and explicit create action without repeating the page heading. Permission checks and content/dock padding are unchanged. Actual-shell regression coverage in settings_secrets_shell_checks.dart checks English/Vietnamese, narrow layouts and large text, including denial visibility and dock Back. This scoped correction does not establish the cause of the reported Assistant Settings TestFlight issue.