Skip to content

v2.65.0

Latest

Choose a tag to compare

@ulsklyc ulsklyc released this 08 Sep 00:37

Added

  • Medication reminders now reach the person's caregivers too, and say whose dose it is (D#1041,
    asked by @ElHado). The scheduler used to send a due-dose reminder to exactly one account, the
    person the medication belongs to - for a child without a device that reminder went quietly
    nowhere. It now also delivers to every caregiver an admin has granted for that person under
    Settings → Family (the same explicit grant that lets a caregiver record and correct doses), over
    Web Push and each caregiver's own notification channels. The person's copy is unchanged; a
    caregiver's copy carries the person's name in front of the medication ("Anna: Ibuprofen"), so it
    is clear at a glance who the reminder is about. Nothing is derived from a family role: no grant,
    no reminder.

  • The shopping list is more compact and its categories now fold away. Rows resolve to 48px
    on touch instead of 64px, category groups sit closer together, and each category header is now
    a disclosure button that remembers its collapsed state per household member and per list. A
    deep link to a highlighted item now expands its category first if it was collapsed. The
    "checked" action pill (move to Pantry / delete checked) now shows for five seconds after a check
    instead of staying up permanently, stays open while hovered or focused, and never appears for
    items that were already checked when the list loaded. These are Shopping-only changes; the
    shared .list-scroller styling that comes with them - a thin, module-tinted scrollbar in place
    of the platform default - is visible in Shopping, Pantry, and Recipes as well, since those
    modules use the same scroll container.

  • Destructive folder deletion can now be undone for five seconds. The folder subtree and its
    currently visible documents disappear immediately, while the server operation waits behind the
    standard Undo toast. Undo restores only the affected entries and preserves other navigation or
    data changes made during that window, including the folder tree's expanded state. Leaving the
    page safely flushes the pending delete. If that delayed request fails, the folder returns and the
    error is reported even after the user has left Documents; a changed folder shows a warning
    instead of reopening the deletion dialog after the delay. Partial server results are reconciled
    even after navigation, and a failed page-exit request restores the optimistic state if the
    browser later revives the page from its cache.

  • The month view on a phone can now show event titles instead of coloured dots. Below 640px
    every entry in a month cell was reduced to a 10px dot - a reliable "something is happening"
    signal, but one that makes reading the month itself a day-by-day affair. A new "Event titles"
    switch under Display in the calendar's filter sheet keeps the same chips and shrinks them to
    10px single-line rows with an ellipsis, up to four per day followed by the existing "+N".

    The dots stay the default: their contrast recipe is measured, and an update should not rebuild
    every household's month view unasked. The switch is per device (localStorage, next to the
    schedule display mode), because the question it answers - does a title fit on this screen - is
    the device's, and the same person reads the same calendar on a monitor in the evening. It
    appears only below 640px, where there is a second reading to choose from. Colour, contrast and the
    tap target are unchanged: the titles inherit the tinted surface the wider month view already
    uses, and a tap anywhere in the cell still opens the day.

  • The countdown tile's overdue grace period is now a household setting (Settings → Modules).
    An expired countdown used to always stay visible for a fixed 7 days before dropping off - since
    overdue items sort to the top of the tile, a household that wanted to see fewer of them had no way
    to shorten that window. It's now configurable (0-90 days, default 7 so existing installs see no
    change).

  • Deleting a document folder can now either keep its documents unfiled or delete the confirmed
    subtree together with its documents.
    The dialog previews exact folder and document counts and
    offers destructive deletion only when the user may delete every affected document. The server
    binds confirmation to the previewed identities, locks them against concurrent moves while
    external storage is changing, and preserves the folder structure when storage deletion fails.

  • Complete directory trees can now be uploaded in one operation. Selecting a directory keeps
    that directory as the new root below the chosen destination, recreates supported descendants and
    uploads their files sequentially. Before writing anything, the dialog previews the planned tree,
    conflicts and rejected files. Existing folders may be merged or duplicated with a timestamped
    suffix, while file conflicts can be skipped or uploaded under a timestamped name.

    Validation follows the server's file-size, MIME-type, path and depth limits. Safe folder paths are
    still created when their own files are rejected, while a failed parent blocks only its descendants
    and does not stop sibling branches. If the server's request limit is reached, the upload waits for
    its advertised retry window and resumes automatically. Cancelling stops queued work but keeps
    folders and files that were already created.

  • The Schedule module gained a quick-start for shift types, a range fill for overrides, grouped
    range display and editing, and a "who's working today" dashboard widget.
    A household with no
    shift types yet can create seven common presets (Early/Late/Night/Day/24-hour, plus Vacation and
    Sick - both without a start/end time, since an absence is already a valid "all day" shift type,
    not a new concept) in one click instead of one at a time. Marking a whole date range as free (or
    on a specific shift) - a vacation, a temporary reassignment - now takes one action instead of one
    per day, capped separately from the read-side range limit since a fill writes real rows rather
    than computing them on read. The Overrides tab groups consecutive same-type days into a single row
    instead of one per day, and editing a group's From/To reconciles the change automatically - no more
    deleting fourteen rows one at a time to adjust a two-week range. An opt-in dashboard widget shows
    who has a shift or is free today, off by default like the module itself.

  • A shift type can carry its own icon, and the module gained an overtime warning with print
    support, a personal calendar feed, and opt-in shift-start reminders.
    The same icon picker used
    elsewhere in the app now applies to shift types too, shown on shift-type cards, override rows, the
    dashboard widget, and the Today list. A configurable weekly-hours target flags overtime with a
    warning-styled metric card on the Statistics tab and adds a Print action. A personal, read-only ICS
    feed (Settings → Personal → Feed subscriptions) lets a member subscribe to their own resolved
    schedule from any calendar app, alongside the existing household calendar and inventory-deadline
    feeds already there. An opt-in push reminder can fire a configurable number of minutes before a
    shift starts, tracked by a small rolling 7-day sync rather than a stored row per occurrence, since a
    pattern-derived shift doesn't otherwise have one to hang a reminder off of.

  • A member can now be on-call alongside a regular shift, and shift planning consolidates into one
    tab.
    "Extra shifts" stack additively next to whatever a pattern or override already resolves for
    a day - unlike the primary slot, any number can be added per person per day, useful for on-call
    duty layered on top of a scheduled shift. The former Overrides tab is gone: Patterns, Overrides, and
    Extra shifts now live together under a renamed "Planning" tab, and one create dialog reaches all
    three through a single Pattern/Override/Extra picker. A cycle day can also carry more than one shift
    type now (several class periods in a row, not just one work shift) - the day editor grows add/remove
    rows instead of a single dropdown.

  • Which shift-type quick-start templates appear is now a household preference. An admin can hide
    the Work, School, or University quick-start button from Settings → Modules → Module options, for a
    household that only ever uses one of them. Hiding a template only removes its button; shift types
    already created from it stay exactly as they are.

  • Shift types can now carry custom fields, defined once and reused across as many shift types as
    want them
    (e.g. a "Room" field shared by every school-period shift type, or a "Client" field on a
    work shift type) - deliberately not fixed columns like a timetable app's room/instructor, which
    would be nonsensical on a work shift with no room or instructor at all. A new "Custom fields"
    section on the Schedule module's Shift types tab manages the registry itself (create, rename,
    delete - any member may add one, only its creator or an admin may change or remove it). Each shift
    type's own card gains a second, collapsed section for attaching any number of these fields,
    reordering them (drag or keyboard up/down buttons), and choosing whether each one's value shows on
    the calendar overlay - hidden entirely for a shift type with none attached, so a household that
    never touches this stays looking exactly as it did before. A pattern day, an override, or an extra
    shift using a shift type with attached fields can now record its own value for each one - the
    cycle-day editor and both the override and extra-shift create/edit modals grow a field-input block
    right after their shift-type picker, appearing only for a type that actually has fields and rebuilt
    whenever the picked type changes. Filling in a room for one Monday and a different one the next
    works exactly like giving them different notes already did. A field only reaches the calendar
    overlay, the Today card, and the subscribed ICS feed if it's explicitly flagged to show there - a
    field can be filled in and stay purely informational (visible in the editors, nowhere else) if
    that's all it's meant to be. "Room: 204" now shows up right alongside the shift name wherever a
    resolved entry is already summarized.

  • A new Overview tab compares several household members' schedules side by side, one column per
    person, for a whole week or a single day.
    Answers the same request as the "Timetables" proposal
    (three kids on the same school schedule, one glance instead of three) without a second data model:
    the picker offers only real household members (never staff or split-expense guests), lanes stay in
    a fixed left-to-right order regardless of who has entries that day, an overnight shift continues
    into the next visible day instead of vanishing at midnight, and the household's school/public
    holidays show as a banner above the grid since a holiday belongs to no one person.

  • A person can now have an optional name day beside their birthday. The advanced section of the
    birthday form stores a month and day without inventing a year; leaving it empty keeps the existing
    behaviour. A saved name day becomes its own yearly entry in the birthday calendar layer, uses the
    birthday's existing reminder lead time and appears as a separately labelled row in the dashboard
    widget. The navigation badge counts both kinds of upcoming occasion, while the main Birthdays list
    remains one row per person. Name-day labels, validation and calendar text are included in all 24
    supported interface languages.

  • Documents can be shared through the device's share sheet, straight from the viewer (D#1014,
    requested by @gdanthy). A passport scan used to take three steps to send: download, find the file,
    share it. The viewer now carries a Share button that opens the operating system's share sheet with
    the file - WhatsApp, Mail, AirDrop, Files, whatever is installed. It is built the way the reporter
    proposed: the file is fetched in the background when the viewer opens, the button stays busy until
    it is there, and the tap then goes straight into the share sheet, because a fetch between tap and
    share is exactly where iOS drops the gesture. Whether sharing is possible is decided once, before
    anything is loaded: the type has to be one the Web Share API accepts as a file (PDF, images, text,
    CSV - not Word or Excel), the connection has to be secure, and the browser has to say yes to a probe.
    Where the answer is no, there is no dead button; a line under the metadata says why, and Download
    remains the path that works everywhere. Rows are unchanged on purpose.

  • Decisions made once now have a page of their own. docs/DECISIONS.md
    is the counterpart to the scope page: not what Yuvomi will not become, but how something it
    does build was decided, so that the next thread reaching the same point gets the answer instead
    of the argument. The first entry is the one that was reached three times from three modules -
    privacy beats admin convenience: a member's private data is never opened by a role or by an
    update, only per person and on purpose (#584, #869, #989). The second is the pattern behind
    three findings of the same two days: a rule lives in one place, not at a call site (#989,
    #1013, #1007). The third is the head rule the calendar settled on 27 August and Tasks reached
    again in #1012: one head, one width. Each entry names the rule, the reason, the code path that enforces it and
    what would reopen or undo it, and points to the thread and the release rather than restating
    them. Linked from the README, the scope page and the
    contributing guide.

  • An older Yuvomi on a newer database now says so, and a backup from a newer version is
    refused.
    Migrations only run forward, but nothing checked the other direction: after an image
    rollback on Umbrel or Unraid the older version started silently on a database carrying
    migrations it did not know, and a restore accepted any file that had a schema_migrations
    table. Now an older version refuses to start on such a database and says which migration numbers
    it does not know, which version it knows, and the way out; DB_ALLOW_NEWER_SCHEMA=1 starts it
    anyway for the emergency case, with a warning on every start, because what an older version
    writes in the meantime can be lost on the next update. A restore of a newer backup is refused
    before anything is copied, with the message to update first. The three sentences operators asked for stand in the installation guide under Updates:
    migrations are one-way, any older backup restores into any newer version, and the way back is
    the backup from before the update, not an older image.

  • Tasks can be filtered by category, on the Board as well as in the List (D#1017, asked by
    @radicchiodev). The filter panel offered status, priority, person and tags, and the List could
    group by category, but nothing filtered by it - and the Board cannot group at all, because its
    columns are already the status. The server had accepted ?category= since #825; the panel simply
    never got the group. It has it now, in both views, with the same label the task form uses, and a
    chosen category shows in the chip row and in the remembered filter sets like every other axis.

  • Notes can be organized with multiple personal or household categories. Every member manages
    their own private category catalog, while household categories are shared and may be managed by
    admins or members who receive the dedicated permission. Notes may remain uncategorized. The
    Notes API now exposes category management, ordering and assignment through one canonical
    scope field, and the permission catalog includes the household-category capability.

  • The Health module's cycle tracker gains reminders and a more honest prediction. cycleStats()
    now requires at least three logged cycle gaps (four periods) before trusting a derived average
    over the 28-day default - with only one or two gaps a single atypical cycle could silently drive
    the prediction, and the UI had no way to tell a genuine derived average apart from a coincidental
    match with the default. A new insufficient_history source value, surfaced as a caption on the
    cycle stat card, makes the distinction visible. Two new opt-in reminders - an upcoming-period
    notice (configurable lead time) and a daily nudge to log today, suppressed once a log for the day
    exists - reuse the same push/notification-channel pipeline as every other reminder source
    (reminders.entity_type widened for cycle_period/cycle_log_nudge, migration 177). Neither a
    predicted date nor "not yet logged" is a stored row, so both anchor to a new
    cycle_reminder_anchors table, the same pattern Schedule uses for its own computed-on-read
    entries. server/services/cycle-reminders.js reuses predictCycle() from
    public/utils/health-cycle.js directly rather than a second copy of the prediction math - which
    required switching that file's one dependency (date.js) from a browser-root absolute import to
    a relative one, so it resolves in Node without a test loader.

  • Cycle day logs can now grade a symptom's severity, not just note its presence. The symptom
    picker grew from 10 to 20 presets, and each one accepts an optional mild/moderate/severe rating -
    one tap on a symptom chip cycles through off, mild, moderate, severe, and back to off, with a
    small dot indicator showing the current grade. Storage moved from a comma-separated column to a
    normalized cycle_day_log_symptoms table (migration 178, backfilled from the old column, which
    is now frozen and no longer read or written); normalizeSymptomEntries() in
    public/utils/health-cycle.js is the single normalizer for both the new {key, intensity}[]
    shape and, for backward compatibility, the old comma-string/string-array shape. Also fixed a
    latent bug this surfaced: the month calendar's "does this day have a log" check treated the
    symptoms field as a plain truthy value, which is correct for a string but wrong for an array -
    an empty array is truthy in JavaScript, so a day with no flow, mood, or note but an empty symptom
    list would have been misreported as logged.

  • Day logs can optionally track basal body temperature, and a sustained rise now confirms
    ovulation for the current cycle instead of only estimating it from the calendar.
    A day log
    accepts a temperature reading and unit (migration 179); detectTemperatureShift() in
    public/utils/health-cycle.js implements the standard "3-over-6" coverline method - the first
    reading at least 0.2°C above the mean of the 6 preceding readings, sustained for 3 readings in a
    row, confirms ovulation. It works over the sequence of logged readings rather than calendar days
    (missing days aren't a special case) and, deliberately, skips the single-outlier-day exception
    real fertility-awareness methods allow, favoring a rule that's simple to check over one that's
    more forgiving. When a shift is found in the current cycle, the prediction's ovulation date, the
    fertile window, the cycle ring's marker position, and the stat card's label all switch from
    "predicted" to "confirmed"; future cycles keep using the calendar method, since they have no
    readings yet.

  • The cycle tab gains a Trends section with a cycle-length chart, a basal-temperature chart,
    and a symptom-frequency breakdown, each appearing only once there's enough history to be worth
    showing. The two line charts reuse this app's existing shared chart geometry
    (public/utils/chart.js, already used by Vitals/Labs/Activity) rather than a new charting
    system. Symptom frequency groups every logged symptom into menstruation, luteal (PMS window), or
    other, based on each cycle's own actual length rather than a household average, and shows the top
    8 as stacked proportion bars - deliberately three buckets instead of the ring/calendar's five,
    since accurately reconstructing follicular/fertile/ovulation boundaries for every past cycle
    would need a second copy of the prediction logic running over history, for a distinction the
    questions this view answers ("period symptom" or "PMS symptom") don't need.

  • The cycle ring shows the current cycle day as its own badge, connected to the "today" marker
    by a short line, instead of packing the day number into the ring's small center alongside the
    phase label and status line. The center now holds just those two.

  • Symptom frequency in the Trends section now surfaces severity, not just count. Each entry
    gets an avgIntensity (the mean of its graded occurrences), shown as the same three-dot
    indicator used in the day-log editor - a symptom logged rarely but always severely no longer
    looks identical to one logged often but mildly. A symptom with at least two graded readings also
    gets an expandable severity-trend chart, reusing the Trends section's line-chart geometry.

  • The symptom-frequency list gains a second expandable view: which cycle day a symptom typically
    lands on.
    For up to the six most recent cycles, a plain-language sentence ("You logged Cramps
    during Menstruation in 3 of 4 recent cycles") plus a compact per-cycle grid, day cells colored by
    phase with an inset ring marking a day the symptom actually occurred - a ring rather than a second
    color, so a hit day stays distinguishable without relying on color perception.

  • The cycle-length trend is now a bar chart, colored by whether each cycle falls in the typical
    24-38 day range
    , against a shaded band for that range - the same visual language the lab-value
    chart already uses for a normal range. The average-cycle-length stat card gets a matching
    typical/atypical badge (shown only once there's a real basis, not on a bare default). Also fixes a
    real gap: the cycle-variation stat card previously only appeared when fertility tracking was off -
    with it on, the default, cycle variation was never shown anywhere. It's unconditional now.

  • The cycle tab can now predict which days a symptom is likely to occur on, based on its
    cycle-day pattern across recent cycles, and overlay that prediction on the existing month
    calendar. Pick a symptom (only ones with enough logged history appear) to see a "Cramps often
    occurs around this day in your cycle" note when today matches, and extra markers on the calendar
    itself - a filled dot for a day it was actually logged, a ring for a day it's predicted but not yet
    logged. No new calendar view; the same one the tab already shows just gains markers when a symptom
    is selected.

  • Cycle predictions can now be subscribed to as a read-only calendar feed, the same Lock-Screen/
    Calendar-app trick already used for the household calendar and inventory warranty dates. Unlike
    the inventory feed, this one's content - not just the access token - is personal: it reflects
    only the subscriber's own logged and predicted periods (plus ovulation and fertile window, if
    tracked), never a household-wide view, keeping cycle data out of the caregiver-sharing system the
    same way the rest of this module already does. Manage it from Settings → Personal → Feeds,
    alongside the other two feeds already there.

Changed

  • Recurring calendar events are easier to recognise before opening them. A small, accessible
    repeat icon now precedes their title in month, week, day and agenda views. For a monthly series
    on the last day of the month, the form now names both the entered start date and the first actual
    occurrence; if the start is already a month end, it confirms that date instead. The date is
    supplied explicitly by the calendar form and follows its active timed or all-day field. Imported
    rules with additional filters, and rules ending before that first occurrence, keep the generic
    explanation instead of promising a date that may not exist. Tasks keep their separate due-date
    explanation, and recurrence storage and sync behaviour are unchanged.

  • Security reports now come with response times, and a security fix ships as a patch release cut from the last tag. SECURITY.md commits to an acknowledgment within 7 days, a classification within 14 and, for a confirmed high-severity finding, a fix within 30 days; every published advisory gets a CVE and names the reporter and the fixed version. The fix travels on its own branch off the last tag, so an installation updating for it gets nothing else - the interface work waiting on main for its Tuesday stays there. The procedure, including the ordinary release, is now public in docs/RELEASING.md, so that the "if the maintainer stops" clause in CONTRIBUTING comes with the instructions a fork would need.

  • The container image is signed, and carries its provenance and an SBOM. Every image the publish workflow builds is signed with cosign under the workflow's own identity, by digest, for both ghcr.io/ulsklyc/yuvomi and the legacy oikos mirror; a build provenance attestation and an SBOM travel inside the image index. The installation guide shows the one command that checks an image came from a release tag of this repository. Tags published before this carry no signature.

  • The build is pinned to what it was tested against. CI now runs on Node 22 and Node 24 (the image has run on 24 for months while CI tested 22 alone), every GitHub Action is pinned to a commit SHA with its version as a comment, and the base image is pinned to a digest; guards refuse a floating tag and keep the CI matrix and the image's Node major together.

  • Two more decisions are written down, and the direction has a page. docs/DECISIONS.md gains entry 4, A household is people, not accounts (a person is a users row and whether they can sign in is a state of that row, from #1007, #913 and #787), and entry 5, One visibility vocabulary (private, a named set, all, normalised on read and never by migration, from #699 and PR #1019). docs/ROADMAP.md names the six themes the open threads add up to, what is decided in each and what is open, and lists what the maintainer has called worth doing without a ticket; it carries no dates, and SCOPE.md section 4 says why.

  • SCOPE.md now says who Yuvomi is for, and how the project is run. A paragraph before the first boundary names the household the design is shaped around (one household, up to six people, one trust boundary) and who it is not for; a new section 4 answers the six recurring questions about the project itself (LTS, four-eyes merge, translation platform, hosted demo, CLA, fuzzing), each with the rule, the reason and the condition under which it would change. The README carries the same number.

  • Two promises that existed only in threads are written where people look. The contributing
    guide says what happens if the maintainer stops: nobody inherits repository rights, MIT allows
    any fork at any time, and after a year without a release, a commit or a reply the fork may carry
    the name. MODULES.md says how long /api/v1 holds: an operation is named as deprecated in the
    CHANGELOG and keeps working for at least 90 days after that release, and a /api/v2 would keep
    /api/v1 served for twelve months.

Fixed

  • Calendar deletions no longer reappear while their five-second Undo action is pending.
    Moving between months, weeks or days now reapplies pending removals over each freshly loaded
    range. Undo restores the latest server row exactly once instead of duplicating it, and a completed
    delete reloads the visible range so an older in-flight response cannot bring the event back.
    Calendar loads now share one ordering guard, including quick out-and-back navigation, and
    concurrent deletes of one series reach the server in user-action order. This covers one event,
    one recurring occurrence, this-and-following, and whole-series deletion. A failed range load
    also clears schedule warnings from the previous range instead of leaving stale warnings visible.

  • Authorized caregivers can now correct a cared-for person's medication log entries (#999).
    The log row now follows the same explicit care grant as recording, taking and skipping a dose;
    other family members still see it read-only, and scheduled entries keep their existing
    pending-instead-of-delete rule.

  • Mobile Week no longer silently drops timed events on its boundary day (#1006). Mobile Week
    renders a 3-day window centered on the cursor, but the calendar only loaded and indexed the fixed
    7-day desktop week; whenever that 3-day window crossed the week boundary, the neighboring day's
    timed events were fetched but then clamped out of the day index, while calendar tasks (loaded
    separately) and Agenda (which reloads a 31-day range) kept showing normally - the mismatch read as
    an inconsistent Sunday. The loaded range now covers the union of the desktop week and the mobile
    window whenever they diverge; Month, Day and Agenda are unaffected.

  • The calendar's person filter and "assigned to me" filter now also apply to Schedule
    entries
    (#1018). Found by @matthiasNX while testing this module against his own timetable
    work, which he then withdrew in favour of it. Both filters already narrowed events and tasks
    to the chosen people; Schedule's
    shifts ignored them and kept showing every household member's entries regardless of who was
    selected, which read as the filter silently not working for a module that has a single, well-defined
    owner per entry. Month, week and day views are affected equally, since all three read from the same
    function.

  • A same-version deployment now invalidates the installed PWA shell. The served service worker
    receives a build-specific revision, so acceptance builds and rebuilt images no longer reuse an
    older cache merely because the application version has not changed.

  • A recurring budget entry keeps its account past the first month (#973). Only the first
    booking of a series is entered by hand; every later month is materialised from the original,
    and that copy inherited the title, amount, category, owner and visibility but not the account.
    From the second month on the entry therefore had none, and anyone filtering the transaction
    list by account stopped seeing the series there. The single repair available to the reporter
    did not work either: setting the account on a later occurrence and choosing change all future
    occurrences
    went to a route that ignored the field and then deleted that occurrence as part of
    its normal work, so the entry appeared to vanish. Both halves are fixed, and the series route
    now follows the same convention as the single entry - omitted leaves the account alone, null
    clears it. It does not reach back into past occurrences: unlike visibility, where a stale wider
    value is a leak, an account is a fact about a booking that already happened. Making that promise
    hold moved the cutoff for future occurrences from the first of the month to today. A weekly
    series has several bookings in one month, and the ones already behind us were being deleted and
    regenerated along with the rest - which, now that the account travels with them, would have moved
    a completed debit to another account and skewed that balance. Their attachments survive the edit
    as well. The cutoff reads the household timezone, the same day boundary the account balances use.
    A virtual (smoothed) series is the exception and keeps its instances free of an account: those
    are planning figures - 1200 a year stored as 100 a month - while the bank debits 1200 once, and an
    account balance counts every booked entry assigned to it. Giving them an account would have moved
    the reported balance every month for a payment that had not happened. Months you had already
    opened are repaired once
    (migration v181): their instances existed with no account, and
    materialisation skips rows that are already there, so without it the reported bug would have
    survived in exactly the data where it was noticed. The repair leaves alone what is a decision
    rather than a gap - originals, instances carrying a different account, and virtual series.

  • Editing a recurring entry for all future occurrences no longer rebuilds them (#973). Until
    now the only way to bring future instances in line with a changed series was to delete them and
    let the next read recreate them. That is right when the rhythm changes, because the dates move.
    For a plain value change it was too blunt: the rows lost their identity, their receipts went with
    the cascade, and they came back carrying everything from the original. Future instances are now
    updated in place, and deletion is reserved for a changed interval, count, rule or smoothing.

  • The desktop sidebar has a scrollbar again (#970). It was deliberately hidden at
    min-width: 1024px, with a soft fade at the edges standing in for it. The fade answers "is
    there more below?", which was the problem it was added for - it does not answer "what do I
    drag?". On a phone the gesture scrolls and nothing is missing; with a mouse the bar is the
    control, and it had been removed exactly where it is needed. The fade stays; the bar is back,
    thin and in the same token colour the module lists already use.

  • Inventory and Schedule speak all 24 languages, and a guard now notices when a module does
    not.
    Inventory shipped on 15 August and Schedule on 27 August with their texts copied from
    English into the other 22 locales - navigation labels, forms, presets, the deadline feed settings,
    everything - and the locale test stayed green, because it checks that every key exists, not that
    any value was ever translated. All 222 of those texts are translated now, with one vocabulary per
    language for each module: the shift presets say early, late and night shift the way that language
    says it, and the module has a name of its own in every navigation. To keep it from happening to the
    next module, a new suite counts, per locale, the texts that are still word-for-word English while
    German is not, and holds that number against a baseline that may only fall.

  • The add-subtask button stays on the task card after the first subtask (D#1017). It used to
    disappear as soon as a task had one, and the only other entry sat at the bottom of the subtask
    list, which is collapsed until the progress bar is clicked - so the module read as "one subtask
    per task" to someone who had just added one. There was never a limit; the way in was hidden. The
    button on the card now stays, and the one at the end of the open list remains as well.

  • The Tasks header holds one width across List, Board and History (#1012). It used to jump on
    every view switch: List and History narrowed the head to the 720px reading measure, the Board let it
    run the full content column, and the actions on the right moved 354px back and forth (measured at
    1358px). That was the coupling the calendar dropped on 27 August, and Tasks had not followed. Now
    the page is built the calendar's way - the root carries no measure, List and History take the
    reading lane back for their rows and filter row, the Board stays uncapped - and the head keeps the
    edge of its widest body in all three views. The bodies are unchanged: the task rows still end at
    720px, exactly as the reporter asked. The one-line version of this fix does not exist, because a
    page that caps something at a measure has to show that measure in its head; the page had to stop
    being a reading page first.

  • The cycle-length trend chart's bars sat flush against the plot edges, with no date under most of
    them.
    Both traced back to the same cause: bar centers reused the x-position formula the line
    charts use for their point positions, which places the first/last points exactly on the plot edge
    (correct for a zero-width point) and relies on "first/middle/last" axis labels (enough context for
    a continuous line, not for discrete bars - a "10" y-axis tick could even read as "1" behind the
    first bar). Bars now sit in their own equal-width band with padding to their neighbors and to both
    plot edges, and every bar gets its own date underneath (thinned to a fixed stride only once there
    are more bars than the chart can label without overlapping, always keeping the first and last).

  • The cycle-day pattern sentence now says how many days before your period a symptom typically
    shows up
    , when that's a real pattern (the same value recurring across at least two cycles),
    instead of only the coarser "occurs during your luteal phase."

  • The cycle-length trend chart's typical/atypical bar colors had no legend - the only place that
    distinction was spelled out was a hover tooltip, invisible on a touch device. Added the same
    legend component the calendar and symptom-frequency chart already use.

  • The cycle ring's phase colors (period/fertile/ovulation) also had no legend, same problem as
    the bar chart above. Added a compact legend below the ring with just the colors it actually
    renders - lighter than the calendar's full legend, which also covers "predicted" and "today" states
    that don't have a distinct color on the ring. The swatches themselves match the ring's own solid
    arcs and filled ovulation dot, not the calendar's paler washes/hatching/hollow-ring styling that the
    shared legend component uses elsewhere - the ring never renders those cues, so borrowing them made
    the legend describe a different picture than the one above it.

  • The shift-type colour picker no longer spans the full row on a phone. width: 100% stretched
    the native colour input to fill its grid cell; on the mobile layout, where the two-column form
    collapses to one, that cell is the whole form width. It now carries a fixed size, matching the
    compact colour swatches used elsewhere in the app.

  • Clearing a birthday reminder no longer leaves its reminder row behind. Setting
    reminder_offset to empty deleted the generated calendar event but not the reminder that
    hung off it, so the household kept getting notified for an event that no longer existed. The
    orphan is now removed with the event.

  • A dashboard layout containing a third-party widget can be saved again (#1013). With an
    extension module that declares a dashboard widget, moving a tile, hiding one or resizing one
    failed with 400, and so did publishing the household default - it was not the extension widget
    that got dropped, the whole request was refused. The storage check for a widget id lived in the
    preferences route and knew nothing of the colon that <module-id>:<widget-id> carries, the form
    this project documents for third-party widgets and builds itself. The check now lives beside the
    function that composes those ids, so that widening one without the other is no longer possible,
    and a guard asserts the round trip at the maximum lengths a module id and a widget id may reach.
    Reported after the fix above had already closed its thread.

  • An extension module's access level can be saved again (#1009). Changing the access level for
    a third-party module under Settings → Administration → Roles & permissions failed with
    Unknown module: ext, and extension widget permissions failed the same way. The server was
    right to refuse: the permissions page labels each control with module:<key>, and because a
    third-party module's key is itself ext:<moduleId>, splitting that label on every colon kept
    only ext. It now splits at the first colon, so the rest of the key survives - which also
    repairs the second, silent consequence, where the widget list of an extension module was rebuilt
    under a key that matched nothing. A shared helper carries the rule, and a guard keeps the call
    site from parsing the label itself again.