The PackageInstaller session's final status now reaches UpdateRepository:
InstallResultReceiver hands it by session id to the waiting AndroidApkInstaller
call (ApkInstaller.install returns a PendingInstall). installOutcome() maps
STATUS_FAILURE_BLOCKED to UpdateFailure.Blocked, STATUS_FAILURE_ABORTED back to
Available without an error, and every other failure to UpdateFailure.Install
with the installer's EXTRA_STATUS_MESSAGE. The cached APK is deleted once the
installer answers, so "Try again" downloads again. UpdateBanner's Failed variant
shows the reason on S-4 and S-22; two core:design strings (en/sv) with their
docs/ux/strings.csv rows. Unit tests for the mapping, the repository outcomes
and the receiver; goldens: update_banner re-recorded, S-17-install-blocked,
S-17-install-failed and s22_about_install_blocked new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
InsightsRepository in :core:model (SpendTrends, SpendForecast and parts); :data DTOs for E-34/E-35
(round-trip the contract examples, fixing ExampleCoverageTest), InsightsRepositoryImpl through KvittApi and
OnlineGate with category/account names from Room (deleted rows included); FakeInsightsRepository with
scenarios (loaded, empty household, account without history, offline, unreachable, long); the Insights
section below S-11's charts: household trend and per-category trends on SpendLineChart with average and
moving average, a forecast card per account, "Needs connection" / "Could not reach the server" over the last
data of the session. Strings sv/en; 41 goldens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
UpdateRepository.checkNow() (default: check(), for throttle-free fakes) skips
the 6-hour throttle of the automatic check (AC-16.2) and restarts its window;
AboutViewModel calls it. Debug builds still never check (AC-16.5).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
A drill-down opened from a breakdown row now shows its own period chip and
Month/Week toggle (initialised from S-11, S-11 keeps its own filter), the
header total, a line chart of the group's spend per bucket with the dashed
range average, the trailing 3-bucket moving average and a legend, and a
caption comparing the latest full bucket with the average; the expense rows
follow. Bucket drill-downs are unchanged. The series is the group's column of
AnalyticsState.buckets from spendSeries, so no contract change.
:core:design chart: SpendLineChart and LineLegend beside StackedBarChart,
shared axis/slot helpers (bar chart pixels unchanged), AnalyticsFilterBar
measure and group-by toggles optional.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The recurring, category and account editors drew their scrolling body
inside KvittScaffold with only the scaffold padding, so with adjustResize
and edge-to-edge the keyboard overlaid the content and a focused field
(S-19 day of month, notes) stayed hidden. Each body now applies
padding(padding).consumeWindowInsets(padding).imePadding(), as S-7 does
with imePadding(): the viewport shrinks above the keyboard and Compose's
scroll container keeps the focused text field in view. Consuming the
scaffold padding first stops the navigation inset (or the S-19 keypad
bottom bar) from being counted twice.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
KvittPullToRefresh in :core:design wraps Material 3's PullToRefreshBox with
Kvitt's own indicator: a primary-container disc (the hero's first-sync tile
look) with the sync icon, which winds with the pull while a primary arc fills
to the threshold, pops at the threshold with a GestureThresholdActivate haptic,
and turns into an indeterminate ring while the sync runs. Material 3 1.4.0 has
no LoadingIndicator or LinearWavyProgressIndicator, so the circular indicator
stands in; the swap happens in Refresh.kt only.
Home (S-4) and account detail (S-5) call syncNow() of SyncRepository, the path
of S-16's "Synka nu"; the indicator stays until it returns, success or failure.
Offline the pull ends at once without a sync. TalkBack reads "Synkar…"
(sync_syncing) while refreshing; no new string keys.
New goldens S-4-home-refreshing and S-5-account-refreshing; ViewModel tests
with GatedSyncRepository (core:design test fixtures).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
D-124 serves Kvitt at https://<host> on 443. The README, the AppBuildInfo and
AboutRepositoryImpl comments, the S-21/S-22 fake scenarios and the :data test
fixtures drop the :8443 port. S-21's invite-created goldens and every S-22
golden re-recorded (link and server row now portless). The release-URL
normaliser test checks a portless URL and keeps one explicit-port case (a
typed port survives), on example.org instead of the owner's old host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- Pinned-root network security config (D-082) is now a template in
app/src/releasePinnedRoot/; with kvitt.tls.pinnedRoot=true the task
generatePinnedRootNetworkSecurityConfig writes it into the release
resources with the domain kvitt.publicHost (D-121), and stops with a
clear message while the root src/release/res/raw/kvitt_root_ca.crt is
not committed (fallback unprepared, SPEC 10.5.3). Comment: pinned NPM
root (D-120).
- Port 443 (D-124): RELEASE_SERVER_URL is https://<kvitt.publicHost>
without a port (RELEASE_SERVER_PORT removed); the AC-2.6 intent filter
has no android:port; InviteLink drops an explicit default port.
- S-1 fixture GOLDEN_SERVER https://kvitt.example.org; 28 S-1 goldens
re-recorded (only the server row changed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
AppBuildInfo gains releaseServerUrl, which :app fills from RELEASE_SERVER_URL (https://<kvitt.publicHost>:8443,
D-121). AboutRepositoryImpl shows it while no server is paired, in place of DataModule.DEFAULT_SERVER_URL, which
is deleted: :data no longer names a host. The fakes' AboutState uses the fixture https://kvitt.example.org:8443;
30 S-21/S-22 goldens re-recorded (only the host changed; oxipng -o 4 --strip safe). New test: unpaired About shows
the release server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The release server's host name comes from -Pkvitt.publicHost, else kvitt.publicHost in
android/local.properties, else the placeholder kvitt.example.invalid. :app exposes it as
BuildConfig.PUBLIC_HOST, the manifest placeholder publicHost (the https invite-link intent
filter, port 8443) and RELEASE_SERVER_URL, which :feature:onboarding receives through the
@ReleaseServerUrl Hilt qualifier for S-1's prefill. Tests and goldens use fixed hosts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
* Milestone and ownership notes that no longer hold ("owned by M5/M6/M8", "M8 replaces it", "(M8b)", "M7x add",
"proposed decision in the M5 report", which is D-027 and D-028) in build scripts, :core:design, :core:fakes, :data,
:app, :core:model and the settings string files.
* SyncEngine: the clock_skew KDoc sat orphaned above restampAfterClockSkew's; it now documents renewMutationId.
* libs.versions.toml header: no milestone, the drift date.
* README: the module table as built (no owners, no DebugFakesModule, which M8 removed), the test fixtures in the
architecture check and the shared screenshot harness.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
DESCRIPTION_MAX (S-7 and S-19 each had 100), NOTES_MAX (S-7's 1000 and NotesField's private copy), FIRST_DATE and
DAYS_AHEAD_MAX (the S-7 and S-9 date pickers each spelled 2000-01-01 and 365). Values unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
* format/Failures.kt: FAILURE_NETWORK and commonFailureText, the §14.3.2 code-to-text map that :feature:account
(Errors.kt) and :feature:settings (Messages.kt) each spelled out; both keep only their own codes. The "network"
literals in :feature:onboarding and the two NETWORK constants use FAILURE_NETWORK.
* component/Split.kt: savedSplitLabel, the S-8 and S-16 line ("Kontots standard 50/50", "Egen fördelning 70/30",
"Exakta belopp", "Allt på dig/Anna") that ExpenseDetailScreen.modeLabel and SyncStatusScreen.lostModeLabel
duplicated line for line.
* format/Limits.kt: BP_TOTAL (five copies in four features and a literal in :core:design and S-19) and
DISPLAY_NAME_MAX (three copies in :feature:onboarding and :feature:settings).
Behaviour and goldens unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Eight modules each had a copy of the same configuration-context harness (ScreenSnapshots/Snapshots); M12f had to fix
the same capture bug in two of them. :core:design's test fixtures (src/testFixtures, package
se.hagstedt.kvitt.design.testing) now hold Variant, TestClock, TestEnvironment, ScreenSnapshots with its three
capture modes (Root; Screen, with the hand-advanced frame clock; ScreenWhenIdle), MainDispatcherRule and withMain.
Test fixtures rather than a :core:testing module: :core:design's own tests can use them without a dependency on
itself, no module is added to the §6.2 graph, and lint treats them as test sources. The architecture check rejects
test fixtures outside test configurations. Modules keep only their own frame (analytics household, settle size and
inline overlays, settings avatar pictures). Every golden is unchanged.
kvitt.android.screenshots applies Roborazzi with outputDir src/test/screenshots and adds the screenshot test
libraries; the feature plugin applies it and adds the test fixtures, so the seven feature build scripts lose their
copied blocks. Removed: roborazzi-junit-rule and roborazzi-compose (never imported), the explicit
lifecycle-viewmodel-compose (feature plugin, :app) and hilt-lifecycle-viewmodel-compose in :app (both still arrive
through the features; release and debug runtime classpaths are identical), and the unused catalog entries
androidx-hilt-navigation-compose and androidx-lifecycle-viewmodel-compose. @Before/@After setMain pairs in nine test
classes use MainDispatcherRule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
The task class and its wiring were duplicated verbatim in both build scripts; bundleCategoryKeywords(resourcePath)
registers it once per module. Resource paths and inputs are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- Rejection.lost: RejectedVersion (the pushed entity from sync_rejections.rejected_payload as a ledger row, account,
payer, split, notes, deleted); S-16 "Öppna" shows it read-only in a dialog with "Visa nuvarande version" (S-8/S-10);
without one it opens the current version as before. Ledger rows take a null onClick (read-only).
- AboutState.schemaVersion; AboutRepositoryImpl calls E-1 /health once per collection when online; header fallback.
S-22 shows "1.1.0 (schema 3)".
- SettlementRepository.start/recompute return null without a session or partner; save is Invalid(payer); no
checkNotNull/requireNotNull in :data.
- :feature:home screenshot harness: one more frame and an idle looper before each capture; the committed sv_dark,
200 % and dialog goldens were one variant stale (light instead of dark, en instead of sv_200).
- Tests: lost version (stale, refused deletion, unreadable payload), /health online and offline, settle without a
partner, category reorder PATCHes, recurring toWire splits, §9.2 effect cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
S-19's account, payer and split chips (the wrapping EditorChipRow whose chips
moved when a value changed) become three fixed full-width rows in a stable
order: Konto, Betalt av, Delning, using M8d's EditorOptionRows with
accountOption, payerOption and splitOption (no date row; the schedule stays in
its own "Schema" section). TalkBack sentences unchanged. A Robolectric test
lays out S-19 with short and long values at 100 % and 200 % and asserts
identical row bounds.
The chip composables stay in :core:design: S-9 uses EditorChipRow and
DateChip, and the debug catalogue still shows AccountChip, PayerChip and
SplitChip.
Goldens re-recorded: every s19_recurring_editor_* variant; oxipng -o 4
--strip safe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
S-7's account, date, payer and split chips (a wrapping FlowRow whose chips
moved when a value changed) become four fixed full-width rows in a stable
order: Konto, Datum, Betalt av, Delning. `EditorOptionRows` in :core:design
(segmented-group shape, the chips' tonal surface-container-high,
secondary-container while the split section is open; leading 24 dp element,
label column as wide as the widest label, value one line with an ellipsis,
trailing chevron or swap). At >= 1.5x font scale the label sits over the value,
which takes up to two lines and whose two-line height every row keeps.
A Robolectric test lays out S-7 with short and long values at 100 % and 200 %
and asserts identical row bounds. The chip composables stay (S-9, S-19).
S-11: connected buttons get 8 dp side padding so "Household" fits half the
filter bar at 1x.
Goldens re-recorded: every s7_* variant, the S-11 en_light goldens, the
analytics_filter_and_total en_light sheet, and the new editor_option_rows
sheet; oxipng -o 4 --strip safe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
AboutScreen loses onOpenLicences, AboutActions its onOpenLicences, the
:app call site its no-op lambda. settings_about_licences was unused in
every module afterwards and is dropped from both strings.xml files and
docs/ux/strings.csv. The 24 S-22 goldens are re-recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Drop the en_dark variant from every screenshot helper and render every
golden at mdpi (Robolectric qualifier w411dp-h<height>dp-mdpi instead of
xhdpi/xxhdpi), so a 411 x 891 dp screen is 411 x 891 px. All goldens
deleted and re-recorded; 1143 -> 865 PNGs. README describes the layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
- Invite code field (S-1): the value stays the raw normalised code and the
grouping hyphens are a VisualTransformation with an exact OffsetMapping, so
the cursor stays after the last typed character when a hyphen appears and a
backspace after a hyphen removes the character before it. Unit tests.
- Category quick row and grid: the selected item's check badge is drawn in a
layer above the clipped item, so it is no longer cut at the tile corner.
- Edge to edge: KvittTopAppBar pads for the top safe-drawing inset (status
bar) with its fill behind it; an ancestor's inset is consumed, not doubled.
Catalogue sample with a 24 dp inset.
- Stacked bar chart corners 4 / 2 dp (owner override of 10 / 4, proposed D-097).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Server: E-3 invite_kind; E-12 link ?from=<admin name>, E-4 passes it into kvitt://invite;
X-Kvitt-App-Version-Name stored in devices.app_version_name (migration 0003, schema 3);
OpenAPI and examples follow. Phone: :core:model, :core:fakes, :data, features and :app
updated together; goldens re-recorded for S-20 (version name) and new S-1/S-22 states.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Resource-backed snackbars (showKvittSnackbar) in account, onboarding and
settings; SettingsScaffold replaced by KvittScaffold + SettingsMessageEffect.
KvittButton(loading) for S-6 saving and S-16 Synka nu. S-10 missing state
disables Synka nu offline (connectivity in the ViewModel, new
detail_missing_offline goldens). S-6 system back asks to discard unsaved
changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Bugs: viewerPercentOf reads basis points (Samples in bp); FullSplitPicker and the
split labels use editor_split_full_on_you ("Allt på dig"); AmountInput.of restores
money with both decimals (AmountInput.percent for ratio fields), replacing M7c's
moneyInput; quickRowCategories does the D-060 fill itself; KvittScaffold resolves
resource-backed snackbars (showKvittSnackbar) while shown.
Gaps: ConfirmDialog optional and styled body; KvittButton loading form (M7a uses it,
ProgressBottomButton removed); InviteCodeField wraps after a group, placeholder,
imeAction; EmptyState icon tile and actionEnabled; DateFormatter.monthName and
dayRangeLong; AnalyticsFilterBar periodOwnRow; segmentedItem builder; screen tokens
in KvittDimens (OnboardingDimens removed); common_ok, common_undo; module-proposed
keys recorded in strings.csv; quick-row labels end in an ellipsis on line two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Hub, profile (photo picker), categories (drag reorder, editor with D-059 delete reasons), recurring
expenses (list and S-7-style editor with schedule, AC-9.8 default start), devices, invites and about,
against the :core:model interfaces. Scenarios and scripted online failures in fakes/settings.
84 states screenshot-tested in sv/en x light/dark (336 goldens); state-holder unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Expense editor (S-7) and expense detail (S-8) in :feature:expense against the
:core:model interfaces and :core:design components: defaults (AC-8.6), keypad
input (§9.6), the four split modes with live preview (AC-8.5, AC-8.7, AC-8.8),
category suggestion with the picked-by-hand flag (CS-2, CS-3, CS-7), quick row
from frequentCategoryIds (D-059, D-060), inline errors, delete and discard
confirmations; detail with hero, facts, timeline, recurring, deleted and missing
states. Fake scenarios in fakes/expense, view-model tests and 127 goldens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
:feature:home: Home with balance hero, sync pill, accounts, archived section,
empty and first-sync states, update banner and "Tillåt installation" dialog;
sync status with pending rows, rejections of every §14.4.6 reason and the
clock-skew banner. :feature:account: account detail with header, ledger by
month, overflow menu, archive/unarchive/delete and their errors; account
editor with name, percent fields (two decimals), slider, presets, preview,
name_taken/validation errors and offline state. Fake scenarios in
fakes/home and fakes/account; full-screen Roborazzi goldens for every state
in sv/en × light/dark (200 % for long states); ViewModel unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
SettleScreen (global, per account, editing) and SettlementDetailScreen with
Hilt ViewModels over the :core:model interfaces; proposals, remaining,
warnings and enablement come from SettlementRepository. Fake scenarios in
fakes/settle/, 19 states x sv/en x light/dark goldens (+ 200 % for long),
unit tests for amount input and both state holders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
Gradle project in android/ with every module of SPEC §6.2 (build-logic convention plugins, version catalog,
architecture check in check), :core:model types and repository interfaces of §14.2.4 and §14.5, :domain
implementing §14.5.2 and §14.5.4 (125 vector cases), scenario-driven fakes with the empty scenario, Room 3
entities and KvittDatabase v1 with exported schema, :app shell with Hilt, en/sv strings with MissingTranslation
as a lint error, Roborazzi goldens of the empty screen in light and dark.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>