Commit Graph

66 Commits

Author SHA1 Message Date
Trey t dea8eed184 refactor: getTasksByResidence is now a thin filter over _allTasks
Was 3 fallback paths (per-residence cache → filter from allTasks →
network). Now: ensure _allTasks fresh, return filter. The per-residence
cache becomes write-only by this path, scheduled for deletion in the
next commit.

Eliminates a class of bugs where the per-residence cache slot could
be missing while _allTasks was stale — the old Path 1+2 would either
return stale data or skip and hit the API redundantly.
2026-05-01 18:30:58 -07:00
Trey t 4f9b910a94 fix: bulkCreateTasks force-refreshes _allTasks instead of merging task-by-task
Server is the authoritative kanban categorizer. After a bulk insert,
re-fetch /api/tasks/ so the kanban view reflects exactly what the
server sees, including any column re-categorizations the client's
in-memory upsert wouldn't compute. One extra round-trip per onboarding
submission, called once per session typically.

Eliminates the entire bug class where DataManager.updateTask had to
correctly compute kanban column placement from the response's
kanbanColumn field. With force-refresh, the server is the source of
truth — fewer ways for the client cache to drift.

Refs gitea#2
2026-05-01 18:30:58 -07:00
Trey t 5f7498b755 fix: DataManager.updateTask seeds _allTasks when cache is empty (gitea#2)
Closes the silent no-op when _allTasks is null on first launch (the
onboarding bulkCreateTasks path). The function now upserts: builds an
empty kanban shell with the standard column names if needed and places
the task in its target column. Unknown column names append a new
column at the end so the task is always reachable.

Also drops the second branch that conditionally wrote to
_tasksByResidence — that cache is being deleted in Phase 3 and
updateTask should not maintain it any more.

The Phase 1 unit tests now pass; the Phase 2 force-refresh in the
next commit replaces the placeholder column metadata (display names,
colors, icons) with authoritative server values.
2026-05-01 18:30:58 -07:00
Trey t b2d03ef8b2 refactor(uploads): drop legacy multipart helpers; route Android UI through presigned flow
Android UI Tests / ui-tests (pull_request) Has been cancelled
The KMP shared layer's task-completion-with-images path now exclusively
uses the presigned-URL flow: each image is compressed, uploaded directly
to B2 via APILayer.uploadImage, and the resulting upload_ids are passed
to /api/task-completions/ as JSON. Bytes never traverse our API server.

Changes:
  - TaskCompletionViewModel.createTaskCompletionWithImages now does the
    presign→POST→collect-ids dance internally. The signature stays the
    same so the three Android UI call sites (TasksScreen, AllTasksScreen,
    ResidenceDetailScreen, CompleteTaskDialog, CompleteTaskScreen) need
    no changes.
  - APILayer.createTaskCompletionWithImages removed (dead).
  - TaskCompletionApi.createCompletionWithImages removed (the multipart
    HTTP helper that posted to the legacy POST /api/task-completions/
    multipart endpoint).
  - TaskCompletionCreateRequest.imageUrls field removed.
  - Three Swift call sites (CompleteTaskView, WidgetActionProcessor,
    PushNotificationManager) updated to drop the imageUrls argument.
  - Two Kotlin call sites (CompleteTaskDialog, CompleteTaskScreen) updated.

Image uploads now match WhatsApp/Slack-class architecture: client-side
compression + direct-to-storage upload + lightweight JSON entity create.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:48:11 -07:00
Trey t fa0ce30257 feat(uploads): direct-to-B2 presigned image upload from iOS + Android
iOS (Swift) — primary path, since iOS is the live platform:
  - ImageDownsampler.swift: ImageIO/CGImageSourceCreateThumbnailAtIndex
    based resize. Pays only the cost of the resized bitmap rather than
    decoding the full source — a 12 MP iPhone photo previously
    materialized ~50 MB regardless of JPEG size. Profiles: completion
    (2048 px / quality 0.85), document_image (2560 px / 0.90).
  - PresignedUploader.swift: three-step orchestration (POST /uploads/presign
    → multipart POST direct to B2 with the signed policy fields → return
    upload_id). Maps HTTP errors to user-facing copy. Concurrent uploads
    via TaskGroup.
  - CompleteTaskView.swift: replaces the multipart-with-images path with
    downsample → upload-to-B2 → create-completion-with-upload_ids[]. The
    no-image branch unchanged.

Android (Kotlin) — parity:
  - composeApp/.../media/ImageDownsampler.kt: BitmapFactory inSampleSize
    + proportional scale + JPEG compress. Same profiles as iOS.
  - composeApp/.../network/UploadApi.kt: Ktor-based presign + direct-to-B2
    POST. Preserves form-field order so the S3 policy signature validates.
  - APILayer.uploadImage(category, contentType, bytes, fileName) → upload_id.
    UI integration to follow.

Shared (Kotlin):
  - models/TaskCompletion.kt: added uploadIds: List<Int>? to
    TaskCompletionCreateRequest and a new PresignUploadRequest /
    PresignUploadResponse pair matching the Go API DTOs.
  - Existing call sites (WidgetActionProcessor, PushNotificationManager)
    explicitly pass uploadIds: nil for backwards compatibility — Swift's
    bridge to Kotlin doesn't honor Kotlin defaults for required-positional
    parameters.

The legacy multipart path remains functional alongside the new one for
soak-test purposes; per-platform feature flags can flip between them at
any time. After zero multipart traffic in production for 7 consecutive
days, the legacy paths can be dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:42:41 -07:00
Trey T 9fa58352c0 Parity gallery: unify around canonical manifest, fix populated-state rendering
Single source of truth: `com.tt.honeyDue.testing.GalleryScreens` lists
every user-reachable screen with its category (DataCarrying / DataFree)
and per-platform reachability. Both platforms' test harnesses are
CI-gated against it — `GalleryManifestParityTest` on each side fails
if the surface list drifts from the manifest.

Variant matrix by category: DataCarrying captures 4 PNGs
(empty/populated × light/dark), DataFree captures 2 (light/dark only).
Empty variants for DataCarrying use `FixtureDataManager.empty(seedLookups = false)`
so form screens that only read DM lookups can diff against populated.

Detail-screen rendering fixed on both platforms. Root cause: VM
`stateIn(Eagerly, initialValue = …)` closures evaluated
`_selectedX.value` before screen-side `LaunchedEffect` / `.onAppear`
could set the id, leaving populated captures byte-identical to empty.

  Kotlin: `ContractorViewModel` + `DocumentViewModel` accept
  `initialSelectedX: Int? = null` so the id is set in the primary
  constructor before `stateIn` computes its seed.

  Swift: `ContractorViewModel`, `DocumentViewModelWrapper`,
  `ResidenceViewModel`, `OnboardingTasksViewModel` gained pre-seed
  init params. `ContractorDetailView`, `DocumentDetailView`,
  `ResidenceDetailView`, `OnboardingFirstTaskContent` gained
  test/preview init overloads that accept the pre-seeded VM.
  Corresponding view bodies prefer cached success state over
  loading/error — avoids a spinner flashing over already-visible
  content during background refreshes (production benefit too).

Real production bug fixed along the way: `DataManager.clear()` was
missing `_contractorDetail`, `_documentDetail`, `_contractorsByResidence`,
`_taskCompletions`, `_notificationPreferences`. On logout these maps
leaked across user sessions; in the gallery they leaked the previous
surface's populated state into the next surface's empty capture.

`ImagePicker.android.kt` guards `rememberCameraPicker` with
`LocalInspectionMode` — `FileProvider.getUriForFile` can't resolve the
Robolectric test-cache path, so `add_document` / `edit_document`
previously failed the entire capture.

Honest reclassifications: `complete_task`, `manage_users`, and
`task_suggestions` moved to DataFree. Their first-paint visible state
is driven by static props or APILayer calls, not by anything on
`IDataManager` — populated would be byte-identical to empty without
a significant production rewire. The manifest comments call this out.

Manifest counts after all moves: 43 screens = 12 DataCarrying + 31
DataFree, 37 on both platforms + 3 Android-only (home, documents,
biometric_lock) + 3 iOS-only (documents_warranties, add_task,
profile_edit).

Test results after full record:
  Android: 11/11 DataCarrying diff populated vs empty
  iOS:     12/12 DataCarrying diff populated vs empty

Also in this change:
- `scripts/build_parity_gallery.py` parses the Kotlin manifest
  directly, renders rows in product-flow order, shows explicit
  `[missing — <platform>]` placeholders for expected-but-absent
  captures and muted `not on <platform>` placeholders for
  platform-specific screens. Docs regenerated.
- `scripts/cleanup_orphan_goldens.sh` safely removes PNGs from prior
  test configurations (theme-named, compare artifacts, legacy
  empty/populated pairs for what is now DataFree). Dry-run by default.
- `docs/parity-gallery.md` rewritten: canonical-manifest workflow,
  adding-a-screen guide, variant matrix explained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:10:32 -05:00
Trey T f0f8dfb68b P1: All Kotlin VMs align with DataManager single-source-of-truth
Four broken VMs refactored to derive read-state from IDataManager, three
gaps closed:

1. TaskViewModel: tasksState / tasksByResidenceState / taskCompletionsState
   now derived via .map + .stateIn / combine. isLoading / loadError separated.
2. ResidenceViewModel: residencesState / myResidencesState / summaryState /
   residenceTasksState / residenceContractorsState all derived. 8 mutation
   states retained as independent (legit one-shot feedback).
3. ContractorViewModel: contractorsState / contractorDetailState derived.
   4 mutation states retained.
4. DocumentViewModel: documentsState / documentDetailState derived. 6
   mutation states retained.
5. AuthViewModel: currentUserState now derived from dataManager.currentUser.
   10 other states stay independent (one-shot mutation feedback by design).
6. LookupsViewModel: accepts IDataManager ctor param for test injection
   consistency. Direct-exposure pattern preserved. Legacy ApiResult-wrapped
   states now derived from DataManager instead of manual _xxxState.value =.
7. NotificationPreferencesViewModel: preferencesState derived from new
   IDataManager.notificationPreferences. APILayer writes through on both
   getNotificationPreferences and updateNotificationPreferences.

IDataManager also grew notificationPreferences: StateFlow<NotificationPreference?>.
DataManager, InMemoryDataManager updated. No screen edits needed — screens
consume viewModel.xxxState the same way; the source just switched.

Architecture enforcement test comes in P3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:42:40 -05:00
Trey T 2230cde071 P0: IDataManager coverage gaps — contractorDetail/documentDetail/taskCompletions/contractorsByResidence
Adds 4 new StateFlow members to IDataManager + DataManager + InMemoryDataManager + FixtureDataManager:
- contractorDetail: Map<Int, Contractor> — cached detail fetches
- documentDetail: Map<Int, Document>
- taskCompletions: Map<Int, List<TaskCompletionResponse>>
- contractorsByResidence: Map<Int, List<ContractorSummary>>

APILayer now writes to these on successful detail/per-residence fetches:
- getTaskCompletions -> setTaskCompletions
- getDocument -> setDocumentDetail
- getContractor -> setContractorDetail
- getContractorsByResidence -> setContractorsForResidence

Fixture populated() seeds contractorDetail + contractorsByResidence.
Populated taskCompletions is empty (Fixtures doesn't define any completions yet).

Foundation for P1 — VMs can now derive every read-state from DataManager
reactively instead of owning independent MutableStateFlow fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:31:06 -05:00
Trey T f83e89bee3 Parity gallery: honest populated-state coverage (10/34 surfaces differ)
Fixed & documented, not-just-marketed:
- HomeScreen now derives summary card from LocalDataManager.myResidences
  with VM fallback — populated PNG genuinely differs from empty.
- DocumentsScreen added same LocalDataManager fallback pattern + ambient
  subscription check (bypass SubscriptionHelper's singleton gate).
- ScreenshotTests.setUp seeds the global DataManager singleton from the
  fixture per variant (subscription/user/residences/tasks/docs/contractors/
  lookups). Unblocks screens that bypass LocalDataManager.

Honest coverage after all fixes: 10/34 surface-pairs genuinely differ
(home, profile, residences, contractors, all_tasks, task_templates_browser
in dark mode, etc.). The other 24 remain identical because their VMs
independently track state via APILayer.getXxx() calls that fail in
Robolectric — VM state stays Idle/Error, so gated "populated" branches
never render.

Root architectural fix needed (not landed here): every VM's xxxState
should mirror DataManager.xxx reactively instead of tracking API results
independently. That's a ~20-VM refactor tracked as follow-up in
docs/parity-gallery.md "Known limitations".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:31:52 -05:00
Trey T 47eaf5a0c0 P1: Shared FixtureDataManager (empty + populated) for cross-platform snapshots
InMemoryDataManager + Fixtures with deterministic data (fixed clock 2026-04-15,
2 residences, 8 tasks, 3 contractors, 5 documents). FixtureDataManager.empty()
and .populated() factories. Exposed to Swift via SKIE.

Expanded IDataManager surface (5 -> 22 members) so fixtures cover every
StateFlow and lookup helper screens read: myResidences, allTasks,
tasksByResidence, documents, documentsByResidence, contractors, residenceTypes,
taskFrequencies, taskPriorities, taskCategories, contractorSpecialties,
taskTemplates, taskTemplatesGrouped, residenceSummaries, upgradeTriggers,
promotions, plus get{ResidenceType,TaskFrequency,TaskPriority,TaskCategory,
ContractorSpecialty}(id) lookup helpers. DataManager implementation is a pure
override-keyword addition — no behavior change.

Enables P2 (Android gallery) + P3 (iOS gallery) to render real screens against
identical inputs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:22:41 -05:00
Trey T c57743dca0 Fix: expect/actual enableTestTagsAsResourceId() for iOS compile
testTagsAsResourceId is Android-only; its use in commonMain broke
compileKotlinIosSimulatorArm64. Wrap behind expect fun — Android impl
sets the semantic, other platforms return Modifier unchanged. Blocks
P3 iOS parity gallery otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:20:48 -05:00
Trey T 00e215920a P0.2: migrate screens to LocalDataManager.current
Swaps direct `DataManager.xxx` access for `LocalDataManager.current.xxx`
across every Compose screen under ui/screens/** that references the
singleton. Each composable resolves the ambient once at the top of its
body and reuses the local val for subsequent reads — keeping rewrites
minimal and predictable.

Screens touched:
  - HomeScreen                          (totalSummary)
  - ResidencesScreen                    (totalSummary)
  - ResidenceDetailScreen               (currentUser)
  - ResidenceFormScreen                 (currentUser)
  - ProfileScreen                       (currentUser + subscription)
  - ContractorDetailScreen              (residences)
  - subscription/FeatureComparisonScreen (featureBenefits)
  - onboarding/OnboardingFirstTaskContent (residences × 3 sites)

No behavior change — in production the ambient default resolves to the
same DataManager singleton. The change is purely so tests, previews, and
the parity-gallery can `CompositionLocalProvider(LocalDataManager provides fake)`
to substitute a fake without tearing screens apart.

Files under ui/subscription/** and ui/components/AddTaskDialog.kt also
reference DataManager but live outside ui/screens/** (plan's scope) —
flagged for a follow-up pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:08:58 -05:00
Trey T 98b775d335 P0.1: extract IDataManager interface + LocalDataManager ambient
Adds a narrow IDataManager contract covering the 5 DataManager members
referenced from ui/screens/** (currentUser, residences, totalSummary,
featureBenefits, subscription) and a staticCompositionLocalOf ambient
(LocalDataManager) that defaults to the DataManager singleton.

No screen call-sites change in this commit — screens migrate in P0.2.
ViewModels, APILayer, and PersistenceManager continue to depend on the
concrete DataManager singleton directly; the interface is deliberately
scoped to the screen surface the parity-gallery needs to substitute.

Includes IDataManagerTest (DataManager is IDataManager) and
LocalDataManagerTest (ambient val is exposed + default type-checks to the
real singleton). runComposeUiTest intentionally avoided — consistent with
ThemeSelectionScreenTest's convention, since commonTest composition
runtime is flaky on iosSimulator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 19:06:16 -05:00
Trey T bb4cbd58c3 Audit: form-error TalkBack + focus management + navigateUp polish
(a) liveRegion + error semantics on form error surfaces so TalkBack
    announces them when they appear:
    - Shared ErrorCard (used by LoginScreen, RegisterScreen,
      VerifyEmail/ResetCode, ForgotPassword, ResetPassword)
    - OnboardingCreateAccountContent inline error row
    - JoinResidenceScreen inline error row

(b) focusRequester + ImeAction.Next on multi-field forms:
    - LoginScreen: auto-focus username, Next→password, Done→submit
    - RegisterScreen: auto-focus username, Next chain through
      email/password/confirm, Done on last

(c) navigateUp() replaces navController.popBackStack() for simple back
    actions in App.kt (6 screens) and MainScreen.kt (3 screens), where
    the back behavior is purely navigation-controlled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:16:22 -05:00
Trey T a1f366cb30 Audit: hardcoded Color.* → MaterialTheme.colorScheme (dark-mode parity)
Replaces ~31 literal Color.White / Color.Red / Color.Gray / Color(0x…)
usages in production screens with theme-aware colorScheme roles. Fixes
dark-mode regressions on those surfaces.

Covers: DocumentCard, DocumentStates, DocumentDetailScreen,
ContractorDetailScreen (delete/favorite tints), AddContractorDialog
(FilterChip, Switch, dialog container).

Overlay-specific Color.Black/Color.White (e.g., photo-grid darkening
overlay) and theme-defined brand tokens (OrganicDesign, ThemeColors)
are left untouched per spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:11:59 -05:00
Trey T d49bc719b2 Audit: .dp → AppSpacing/AppRadius (tokenization, partial sweep)
Replaces ~163 raw .dp values with design-system tokens per CLAUDE.md rule.
Covers most visible screens (Tasks, Residences, Profile, Documents,
dialogs, kanban, forms). Adds AppSpacing/AppRadius imports where missing.

Remaining sites are geometric/canvas values (stroke widths, icon sizes,
non-standard values like 6.dp/14.dp/20.dp) or don't map to existing
tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:08:23 -05:00
Trey T 77f32befb8 Audit: meaningful contentDescription for actionable icons (sweep)
Upgrades ~31 sites previously annotated `// decorative` to meaningful
strings where the icon is actionable. Purely decorative leading icons
retain null contentDescription per Material 3 guidance.

Focus areas:
- Selection indicators (CheckCircle vs RadioButtonUnchecked, check marks)
- Status icons (Error, Warning, CheckCircle, ErrorOutline, CloudOff, Info)
- Expand/collapse toggles (ExpandLess/ExpandMore)
- Feature inclusion indicators (Check/Close in comparison tables)
- Requirement indicators (password strength satisfied/not)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:57:08 -05:00
Trey T d8569c7aed Audit: PullToRefreshBox on remaining list screens (iOS parity)
HomeScreen + AllTasksScreen + TasksScreen now support pull-to-refresh.
forceRefresh=true per CLAUDE.md mutation pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:50:27 -05:00
Trey T 95f7318ee6 Audit 9a.3: custom sheets → ModalBottomSheet (M3 parity)
CompletionHistorySheet + contractor-picker sheet now use
material3.ModalBottomSheet. Standard Material 3 dim-behind + swipe-down
dismiss + sheet state. Inner content unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:48:14 -05:00
Trey T 0015a5810f Audit 9a: Long-press context menus + swipe-to-dismiss on cards
Partial implementation (wave 9a agent was rate-limited mid-flight).
Delivered:
- TaskCard long-press context menu (Edit / Delete / Share)
- ResidencesScreen + ContractorsScreen swipe-to-dismiss wired for delete
- Confirm dialog before dismissal (non-destructive gesture)

Not yet delivered (follow-up):
- CompletionHistorySheet → ModalBottomSheet migration
- Contractor-picker sheet migration

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:39:29 -05:00
Trey T ba1ec2a69b Audit 9b: Dynamic color (Material You) + 48dp min touch target helpers
Dynamic color opt-in on Android 12+ via expect/actual DynamicColor:
- commonMain: expect isDynamicColorSupported() + rememberDynamicColorScheme()
- androidMain: SDK>=31 gate, dynamicLight/DarkColorScheme(context)
- iOS/JVM/JS/WASM: no-op actuals
- ThemeManager gains useDynamicColor StateFlow, persisted via ThemeStorage
- App.kt wires both currentTheme + useDynamicColor into HoneyDueTheme
- ThemeSelectionScreen exposes the "Use system colors" toggle

Touch target helpers:
- Modifier.minTouchTarget(48.dp) + Modifier.clickableWithRipple
- Applied at audit-flagged sites in CompleteTaskScreen

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:39:22 -05:00
Trey T 227c0a9240 UI Test Suite8: Document/Warranty tests (iOS parity)
Ports representative subset of Suite8_DocumentWarrantyTests.swift
(22 of 25 iOS tests). testTags on document screens via
AccessibilityIds.Document.*. Documented deliberate skips in the
class header (5/7/8/10/11/12/16) — each either relies on iOS-only
pickers/menus or is subsumed by another ported test.

No new AccessibilityIds added — Document group already has parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:54:24 -05:00
Trey T 840c35a7af P7 Stream Y: empty/error/loading state audit (iOS parity)
Audits every list + detail screen (non-document) for empty/error/loading
state parity with iOS. Reuses StandardEmptyState / StandardErrorState where
possible; adds missing states where screens currently show blank on error.

- Add StandardErrorState and CompactErrorState components under
  ui/components/common/ (mirrors iOS ErrorView pattern: icon + title +
  message + Retry).
- ManageUsersScreen: error state previously had no retry button; now uses
  StandardErrorState with a Retry CTA matching iOS ManageUsersView.
- ResidenceDetailScreen: task and contractor sub-section error cards now
  use CompactErrorState with inline retry (previously plain error text).

Other audited screens (ResidencesScreen, TasksScreen, AllTasksScreen,
ContractorsScreen, ContractorDetailScreen, EditTaskScreen,
CompleteTaskScreen, TaskTemplatesBrowserScreen, TaskSuggestionsScreen,
OnboardingFirstTaskContent) already had loading + error + empty parity
via ApiResultHandler / HandleErrors / inline state machines; no changes
needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:50:07 -05:00
Trey T 6980ed772b UI Test Suite4: Comprehensive residence tests (iOS parity)
Ports Suite4_ComprehensiveResidenceTests.swift. testTags on residence
screens via AccessibilityIds.Residence.*. CRUD + join + manage users +
multi-residence switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:43:08 -05:00
Trey T eedfac30c6 UI Test Suite5: Task tests (iOS parity)
Ports Suite5_TaskTests.swift. testTags on task screens via
AccessibilityIds.Task.*. CRUD + kanban + filter/sort + templates + suggestions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:40:38 -05:00
Trey T c772215c04 UI Test Suite7: Contractor tests (iOS parity)
Ports Suite7_ContractorTests.swift. testTags on contractor screens via
AccessibilityIds.Contractor.*. CRUD + sharing + link-to-task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:40:21 -05:00
Trey T 95dabf741f UI Test Suite1: Registration + SimpleLogin ports (iOS parity)
Ports iOS Suite1_RegistrationTests.swift + SimpleLoginTest.swift to
Android Compose UI Test. Adds testTag annotations on auth screens using
shared AccessibilityIds.Authentication constants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:38:56 -05:00
Trey T b97db89737 UI fix 5/5: contentDescription for actionable icons (partial audit)
Actionable IconButton/Icon instances now expose meaningful
contentDescription for TalkBack. Purely decorative icons retain
contentDescription = null with clarifying comments. Full audit of
remaining 130+ sites is follow-up work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:27:17 -05:00
Trey T 00a217e8c8 UI fix 4/5: Button variant migration — OutlinedButton/TextButton for secondary + tertiary
Primary confirmation buttons remain as Button (M3 filled default). Secondary
actions (Cancel, Dismiss) → OutlinedButton. Inline links → TextButton. Reduces
visual hierarchy ambiguity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:22:59 -05:00
Trey T 2d80ade6bc Test infra: shared accessibility IDs + PageObjects + AAA_SeedTests
Ports iOS HoneyDueUITests AccessibilityIdentifiers + PageObjects pattern
to Android Compose UI Test. Kotlin AccessibilityIds object mirrors Swift
verbatim so scripts/verify_test_tag_parity.sh can gate on divergence.

AAA_SeedTests bracketed first alphanumerically; SuiteZZ cleanup to follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:19:37 -05:00
Trey T 42c21bfca1 UI fix 3/5: imePadding on form screens
Soft keyboard no longer covers input fields. Applied to every screen
with text input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:18:46 -05:00
Trey T 0ec2ac7744 UI fix 2/5: lifecycle-aware StateFlow collection in screens
Replace collectAsState() with collectAsStateWithLifecycle() so StateFlows
stop collecting when the host is in background — prevents memory/CPU leaks
on lifecycle transitions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:15:03 -05:00
Trey T a78494c529 UI fix 1/5: mirror ArrowBack/ArrowForward/List icons for RTL locales
Material 3 AutoMirrored variants flip correctly in Arabic/Hebrew.
Previous Icons.Default.ArrowBack pointed wrong direction in RTL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:14:02 -05:00
Trey T 485f70dfa1 Integration: residence invite accept/decline APIs + wire notification actions
Adds acceptResidenceInvite / declineResidenceInvite to ResidenceApi
(POST /api/residences/{id}/invite/{accept|decline}) and exposes them via
APILayer. On accept success, myResidences is force-refreshed so the
newly-joined residence appears without a manual pull.

Wires NotificationActionReceiver's ACCEPT_INVITE / DECLINE_INVITE
handlers to the new APILayer calls, replacing the log-only TODOs left
behind by P4 Stream O. Notifications are now cleared only on API
success so a failed accept stays actionable.

Tests:
 - ResidenceApiInviteTest covers correct HTTP method/path + error surfacing.
 - NotificationActionReceiverTest invite cases updated to assert the new
   APILayer calls (were previously asserting the log-only path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:36:59 -05:00
Trey T 10b57aabaa P6 Stream V: ImageCompression (expect/actual) + CameraPicker polish
Android: BitmapFactory + ExifInterface + JPEG quality 0.7 + 1920px downscale.
iOS: UIImage.jpegData. JVM/JS/WASM: no-op. CameraPicker uses TakePicture
ActivityResult + permission rationale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:35:54 -05:00
Trey T 3069ec41de P5 Streams Q+R: TaskAnimations + AnimationTestingScreen
Port iOS TaskAnimations.swift specs (completion checkmark, card transitions,
priority pulse) + AnimationTestingView as dev screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:35:32 -05:00
Trey T cf2aca583b P7 Stream X: ResidenceFormState + validation (iOS parity)
Pure-function field validators matching iOS ResidenceFormView rules.
Mirrors Stream W's TaskFormState pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:34:49 -05:00
Trey T 1ba95db629 Integration: wire 3 new P2 screens into App.kt nav + HapticsInit bootstrap
Navigation wiring (per follow-ups flagged by Streams G/H/I):
- Add TaskTemplatesBrowserRoute (new) + App.kt composable<TaskTemplatesBrowserRoute>
- Wire composable<TaskSuggestionsRoute> (declared by Stream H but unwired)
- Wire composable<AddTaskWithResidenceRoute> (declared by Stream I but unwired)

MainActivity.onCreate now calls HapticsInit.install(applicationContext) so the
Vibrator fallback path works on non-View call-sites (flagged by Stream S).

Deferred cleanup (tracked, not done here):
- Port push-token registration from legacy MyFirebaseMessagingService.kt to
  new FcmService (Stream N TODO).
- Remove legacy WidgetTaskActionReceiver + manifest entry (Stream M flag).
- Residence invite accept/decline APILayer methods (Stream O TODO).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:28:06 -05:00
Trey T 65af40ed73 P4 Stream P: NotificationPreferencesScreen expansion
Per-category toggle + master toggle + system-settings shortcut matching
iOS NotificationPreferencesView depth. DataStore-backed prefs, channel
importance rewritten to NONE when category disabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:24:45 -05:00
Trey T 3700968d00 P7 Stream W: TaskFormState + validation (iOS parity)
Pure-function field validators matching iOS TaskFormStates.swift error
strings. TaskFormState container derives errors from fields, exposes
isValid and typed request builders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:22:53 -05:00
Trey T edc22c0d2b P2 Stream I: AddTaskWithResidenceScreen
Port iOS AddTaskWithResidenceView. Residence pre-selected via constructor
param, form validation, submit -> APILayer.createTask(residenceId attached).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:22:13 -05:00
Trey T 46db133458 P6 Stream T: finish BiometricLockScreen + BiometricManager
BiometricPrompt wrapper with 3-strike lockout + NO_HARDWARE bypass.
BiometricLockScreen with auto-prompt on mount + PIN fallback after 3 failures.
PIN wiring marked TODO for secure-storage follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:21:22 -05:00
Trey T 704c59e5cb P2 Stream F + Stream U fix: JoinResidenceScreen + Coil test compile fix
Stream F: Convert JoinResidenceDialog -> dedicated screen matching iOS
JoinResidenceView. Invite-code input + inline validation + API success
navigates to residence detail.

Stream U fix: coil3 3.0.4 doesn't ship ColorImage (added in 3.1.0). Use
a minimal FakeImage test-double so CoilAuthInterceptorTest compiles.

Also completes consolidation of wave-3 work: all 6 parallel streams
(D/E/F/H/O/S/U) now landed. Full unit suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:14:55 -05:00
Trey T 917c528f67 P5 Stream S: cross-platform Haptics (expect/actual)
Common API + platform actuals (Android HapticFeedbackConstants/Vibrator,
iOS UIImpact/NotificationFeedback, JVM/JS/WASM no-op). 5 call-sites wired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:12:24 -05:00
Trey T 944161f0d1 P2 Stream E: FeatureComparisonScreen (replaces FeatureComparisonDialog)
Full-screen feature comparison matching iOS FeatureComparisonView.
Two-column table, iOS-equivalent row set, CTA to upgrade flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:12:21 -05:00
Trey T 224f6643bf P6 Stream U: AuthenticatedImage composable + CoilAuthInterceptor
Token-aware image loading matching iOS AuthenticatedImage.swift.
Bearer header attachment, 401-triggered refresh+retry, placeholder on error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:10:59 -05:00
Trey T 19471d780d P2 Stream H: standalone TaskSuggestionsScreen
Port iOS TaskSuggestionsView as a standalone route reachable outside
onboarding. Uses shared suggestions API + accept/skip analytics in
non-onboarding variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:10:47 -05:00
Trey T ee135c4673 P2 Stream G: TaskTemplatesBrowserScreen (replaces dialog/sheet)
Full browse-and-select experience matching iOS TaskTemplatesBrowserView.
Category filter, multi-select, bulk-create with templateId backlink.
Analytics events wired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:56:01 -05:00
Trey T 6b3e64661f P2 Stream D: ThemeSelectionScreen (replaces ThemePickerDialog)
Full-screen theme picker with 11 theme previews matching iOS
ThemeSelectionView. Live preview on tap. Old dialog deleted and
call-sites migrated to the new route.

Tests use the state-logic fallback pattern (plain kotlin.test rather
than runComposeUiTest) because Compose UI testing in commonTest for
this KMP project is flaky — the existing ThemeManager uses mutableStateOf
plus platform-backed ThemeStorage, which doesn't play well with the
recomposer on iosSimulatorArm64. The behavior under test is identical:
helpers in ThemeSelectionScreenState drive the same code paths the
composable invokes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:50:00 -05:00
Trey T db181c0d6a P1 Stream C: organic design primitives (BlobShape, RadialGlow, HoneycombOverlay)
Ports iOS OrganicDesign.swift primitives to Compose Multiplatform:
- BlobShape: seeded deterministic irregular shape
- RadialGlow: radial-gradient backdrop composable
- HoneycombOverlay: tiled hex pattern modifier
- OrganicRadius constants matching iOS

Determinism guaranteed by parametrized tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:34:47 -05:00