The "For next month" amount in the summary and its tooltip already show what's held, so the extra line on desktop and in the mobile overbudgeted banner repeated it.
A rebuild whose input load fails part-way has already set some cells,
and the pass that saves them marked the cache clean without the rest.
With the old rows now cleared first, the next open trusted a cache
missing the stored buffers and frozen flags. A cache barrier now spans
`loadUserBudgets`: it marks the cache dirty on entry and only a load
that finishes releases it, so a failed open leaves the next one to
rebuild.
A runtime without the frozen cells can open the newer database: it keeps
the `frozen` cells in the cache but never updates them, while a peer's
freeze still lands in the database column. On upgrade the stale cells
read every month as unfrozen. The same holds for any cell an older
runtime doesn't know.
The cache key is now written to a second row as well. An older runtime
rewrites row 1 only, so a cache it marked clean has a mismatch and is
rebuilt from the database. This also covers a cache such a runtime built
from scratch, which replaces the frozen-cell presence check.
A client with the carryover change but not this one can open a
transferred database that already marks months frozen. It builds a valid
cache with no `frozen` cells, and a later upgrade would load it, read
every month as unfrozen and select the computed buffer over the stored
one. An envelope cache (`buffered` cells) without `frozen` cells is now
rebuilt from the database, like one without `carryover-stored` cells.
Tracking caches have neither cell and are unaffected.
Review of the simplification found three regressions:
- The budget range reaches back to the start month again, and the freeze
creates any missing months first, so a month managed before the first
budget month is still frozen and snapshotted when the mode turns off.
- The range clamp that keeps a created month from becoming the blank
sheet comes back with it. A transaction-free budget hits that whenever
the range's start moves past a created month.
- A sync counts for the freeze gate only if the spreadsheet it started
with is still the open one, so a sync in flight across a reload of the
same file doesn't authorise the new load.
The `buffered-managed` cell and the client clean-up stay.
A `buffered-managed` cell per month replaces three copies of the managed
rule: the `buffered-selected` formula, the server guard that queried the
preference and the `frozen` row, and the client's `isManagedMonth` with
its `month` prop plumbing and `monthFromSheet`. The hook now reads the
cell for the sheet it's rendered in, which also fixes frozen months never
reading as frozen on the client: the cell holds a boolean, not 1.
The freeze only covers months that have a sheet, so the budget range no
longer reaches back to the start month, and the blank-sheet guard that
reach-back needed goes with it. The sync handler keeps the file check
from the carryover seed and tracks only the synced spreadsheet for the
freeze gate.
The sheet answers after the modal's first render, so seeding the
selection from the mode picked Manual even while Automatic was on. A
Save without choosing then turned the mode off, which can't be undone.
The selection now follows the sheet until the user picks a choice.
The modal tests use the real sheet hooks with a fake server, so the
cells arrive late as they do in the app.
Desktop and mobile share one hook and one menu. `useFutureBufferMode`
reads `__global!future-buffer-start` and the month's `frozen` cell
through sheet bindings, so a mode change from another device shows up
without a reload, and `isManagedMonth` is a pure rule.
- To Budget menu item "For next month: Automatic…" / "Manual…" opening
the mode modal. Shown with the experimental flag, and always once the
mode is on.
- Mode modal with the Manual and Automatic choices, the start month, and
the consequences of switching either way.
- Managed months hide Hold, Reset, Disable current auto hold, the income
auto-hold toggle and the rollover arrow, explain "For next month" in a
tooltip, and say how much is held for future months under
"Overbudgeted". Frozen months keep Hold and Reset and lose the
auto-hold controls.
- The budget-type switch says automatic mode is turned off first.
- `useSheetValue` observes the sheet a binding names, which the global
cell needs.
- `month` reaches `ToBudgetAmount`, `TotalsList` and `BudgetSummary`,
`TotalsList` provides its own sheet, and `monthFromSheet` maps a sheet
back to its month.
Co-authored-by: Ngoc Pham <ngocphamm@fastmail.fm>
setFutureBufferMode writes the start month to turn the mode on. Turning
it off freezes closed months, keeps every other managed month's computed
buffer as a manual hold, clears income auto-hold flags in zero_budgets
and deletes the preference, in one batch outside undo. Both directions
clear the undo stack. Switching to Tracking runs the off steps first.
holdForNextMonth, resetHold and resetIncomeCarryover write nothing and
return false in managed months, and api/budget-month returns the
selected buffer as forNextMonth.
createAllBudgets passed its range to createBudget, and envelope budgets
use the month before the range's first month as a blank sheet. When the
range starts after months already created, as it does when automatic
"For next month" is turned off after reaching back to its start month,
a real month became the blank sheet and the next new month cleared its
cells. The range passed to createBudget now starts at the first month
already created.
A freeze queued for one load of a file could run after the same file was
reopened, before the new load's first full sync. Loads are now told
apart by their spreadsheet, which every load creates afresh, as well as
by file and group. Queued freezes and the full-sync callback drop work
from an earlier load, and the freeze writes nothing if the spreadsheet
changed while it waited for it.
The freeze skipped closed months without a sheet, which happens when
automatic mode started more than three months before the earliest
transaction. createAllBudgets now reaches back to the start month, and
the freeze calls it first, since the start month can arrive through
sync after the months were created.
In automatic mode, each month from the start month to last month that
isn't frozen yet gets its computed buffer written as the stored buffer,
with frozen set, in one batch. It reads the clock once per run, and
skips months that are already frozen, so running it twice or on two
devices gives the same result.
It runs in a mutator but never an undoable one, because an undo would
reopen the month on every device. It's queued after budget load,
get-budget-bounds and each full sync. A file with a sync server only
freezes once a full sync of it has completed, so a closed month uses
every device's rows.
Adds a frozen column to zero_budget_months and a frozen cell per month,
loaded with the stored buffer. buffered-computed holds what the months
after a month need from it: next month's budgeted amount, less its
income, plus overspending that reaches it, plus its own need.
__global!future-buffer-start holds the futureBufferMode preference. It's
read on every budget open, after the months are created, and updated
when the preference row changes locally or through sync.
buffered-selected uses the computed buffer in managed months, the stored
buffer in frozen months, and today's rule otherwise.
setBuffer takes an optional frozen flag and only writes the columns
that change. envelope-budget-month also emits frozen and
buffered-selected.
A deleted category keeps its budget rows, so an old persistent rollover
on one pulled the budget range back to that month even though the
category has no cells. The lookup now joins live categories, as the
latest-allocation lookup already does. Hidden categories still count.
A month's rollover reaches later months through the previous month's
chain cell, so a budget opened after its created months had moved past
the toggled month lost the flag. Also note in the docs that auto hold
enabled before this version still stops until it's enabled again.
Switching rollover or auto hold on now stores 2, which following months
without a flag continue. A 1 keeps its old meaning of on for that month
only, so runs written by earlier versions end where they always did and
no existing balance changes. That removes the need for the carryover
seed, its post-sync and load-time scheduling, the seeded preference and
the budget table filter it relied on.
The success event starts the carryover seed behind the mutator queue
without waiting for it, so `sync-budget` and `sync` could return while a
read still saw the pre-seed figures. Both now wait for the queue and for
the spreadsheet to settle before returning. The callers are plain
handlers, not mutators, so waiting on the queue can't deadlock.
Review showed what continuing costs: closed-month balances that change
on a later load, and in the stacked buffer PR a freeze that stores an
amount computed from unseeded flags, which no later seed repairs. The
load path refuses again, as it did before the simplification. The
shorter cache check stays.
The stale-cache test is two `some` calls instead of a helper with a flag.
A seed that fails at load is captured and retried on the next load, the
same as a seed that fails after a sync, rather than closing the budget.