Changelog¶
All notable changes to Prism are documented in this file.
Unreleased¶
Added¶
- Share a layout or a theme as a file, with no GitHub account. The Share dialogs for layouts and palettes have Download file, which saves the same checked
.jsona gallery submission would carry. A layout file loads through More → Import → Choose file…, and a theme file through the new Import button in Settings → Display → Palette. An imported theme gets the gallery's checks and never replaces a palette that is already there. Submitting to the gallery itself still needs a GitHub account. (#338)
[1.29.0] – 2026-10-05¶
Ships four database migrations, applied automatically on start: hidden events, hidden event series, notes from guests, and the chore approval default. The last one switches Requires approval on for every existing chore, because the switch now does something (see Changed); nothing else about approval changes on upgrade.
Added¶
- Hide an event in Prism without deleting it. A parent can tick Hide in Prism in an event's detail, and on a recurring event choose this event or every event in the series. Hidden events leave the calendar, the widgets, voice and birthday detection, sync does not bring them back, and nothing is changed in Google, iCal or CalDAV. Settings → Calendars lists what is hidden, with Unhide. (#592)
- The babysitter can leave a note for the family. While Babysitter Mode is on, its screen has a box for a note and an optional name, with no login needed. The note appears on the Messages board as from the babysitter, and only a parent can edit or delete it. The on-screen keyboard now opens above the Babysitter Mode screen and keeps the Send button in view. Thanks @yeojwal for the idea. (#497)
- Choose the on-screen keyboard per device. Settings → Input adds Use household setting, Always or Never for this device, so a tablet can use its own keyboard while a wall display keeps Prism's. (#525)
- Remove and restore birthdays. Settings → Calendars lists birthdays and milestones with Remove. A removed synced birthday is listed under Removed birthdays, where Restore brings it back. Removing one is now parents only. Thanks @zambonichief1721 for the report. (#605)
- Purge the demo family. Installs that started with the demo data get a Demo data card in Settings → Backups & Data. It lists what is left and deletes it all after a confirm, including tasks, chores, events and birthdays that removing the members left behind. Layouts and settings are kept. (#605)
- A parent can set a child's or guest's PIN without the old one. In Settings → Security → Change PIN and when changing a PIN's length. A parent's own PIN, and a co-parent's, still need the current one.
Changed¶
- Requires approval on a chore now decides whether a child's completion waits. Before, every child completion waited for a parent and the switch only showed a badge. A chore with it on still waits; a chore with it off is approved as soon as it is completed and its points count at once. New chores start with it on, and upgrading turns it on for every existing chore, so a parent turns it off for the chores they trust. By voice a chore with it on always waits, as before. (#597)
- A new Docker Compose install starts empty. The demo family is no longer loaded on first start. Set
PRISM_DEMO_SEED=trueto get it, as the demo compose file does. Existing installs are not affected. Thanks @zambonichief1721 for the report. (#605) - The shopping widget hides checked items on the dashboard and screensaver. An item ticked by mistake stays, struck through, for five seconds so it can be unticked. The Shopping page is unchanged.
Fixed¶
- Pages in portrait no longer run under the bottom navigation. Full-height pages such as Travel, Calendar and Chores now end at the top of the bar, so the globe's zoom buttons and the last rows of each page can be reached. (#594)
- A dashboard taller than the screen fits instead of being cut off. A layout in the old grid units letterboxed on a landscape screen and lost rows at the top and bottom once the gaps alone needed more height than the screen had. Cells, gaps and padding now shrink together until it fits; a layout that already fit is unchanged. Thanks @flightlinefoundation for the report. (#519)
- Edited occurrences of recurring CalDAV events show once, in their slot. The original time also showed next to the edit, every edit of a series shared one id, and deleting an edited occurrence in Prism deleted the whole series on the server. (#593)
- Edited occurrences in subscribed iCal feeds show as edited. They showed at their original time and title, and a cancelled occurrence still showed.
- Changing a PIN's length asks for the new PIN. It saved only the length, after which no PIN could match and the member was locked out until a server-side reset. Edit Member and the setup wizard now take the new PIN in the same step.
- The screensaver's Timing & motion panel stays inside the window when opened from the layout editor, instead of rising off the top.
[1.28.0] – 2026-10-01¶
Dates and times now follow the household's time zone wherever Prism decides what day or hour it is, instead of whichever zone the server or the viewing device happens to run in. Ships three database migrations, applied automatically on start: task due dates move to a date plus an optional time, older iCal and CalDAV all-day events are converted to the form the calendar reads, and timestamps a non-UTC database wrote in its own zone are converted to UTC. The last one only changes anything where the database's default zone is not UTC, as on some Home Assistant installs.
Changed¶
- A task's due is a date and an optional time. It used to be a single timestamp that each form and each sync provider wrote differently, so a task could move a day depending on who wrote it and where it was read. The API now takes
dueDateasYYYY-MM-DDanddueTimeasHH:mm; an ISO date-time from an older client is still accepted and read in the household zone. Existing tasks are converted on upgrade.
Fixed¶
- Only one keyboard appears on a touchscreen tablet. When Prism's on-screen keyboard opened for a field, nothing told the browser the page had its own, so on Android and Windows tablets the system keyboard opened as well. While Prism's keyboard serves a field, the field now asks the browser not to open one, and gets its original setting back when it lets go. Phones and mouse use are unaffected. Thanks @yeojwal for the report. (#498)
- The field you are typing in stays above the on-screen keyboard. The page assumed the keyboard was shorter than it is, scrolled only the nearest scrolling area, and did nothing for pop-up forms or for a field in the last row of a full-height page. The keyboard's height now comes from one place, every scrolling area between the field and the page moves as needed, and a pop-up form such as Add Item moves into the space above the keys while the keyboard is open. Thanks @yeojwal for the report. (#499)
- All-day events from iCal and CalDAV calendars cover the right days on a server outside UTC. Date-only events were stored at midnight in the server's time zone, while the calendar reads all-day dates at UTC midnight, so west of UTC a one-day event also covered the next day, and east of UTC it started a day early. They are now stored the way Google all-day events already were, and existing events correct themselves on the next sync. CalDAV all-day events were also being stored as timed midnight-to-midnight events, because the check for a date-only value never matched; they now show as all-day. Thanks @flightlinefoundation for the diagnosis. (#518)
- A fresh Docker install's first dashboard fits the screen. The database seed created the default dashboard in the old four-column grid units, which load as a tall portrait layout on the current grid, so on a landscape screen it was letterboxed, overflowed, and lost rows at both ends. The seed now uses the standard default layout in current units. Existing layouts are untouched. Thanks @flightlinefoundation for the measurements. (#519)
- A list on the Shopping page can be scrolled past again on a touchscreen. Every list card told the browser not to scroll for a gesture that began on it, and a finger landing anywhere on a card started a reorder, so on a display where the lists cover the width there was no background left to push: trying to scroll the page picked a list up and moved it. The card header, which is the part carrying the grip, is now the handle, and the rest of the card scrolls like any other part of the page. Thanks @yeojwal for the report. (#501)
- The German calendar's schedule view is now labelled "Terminplan". "Zeitplan" reads as a generic timetable rather than a list of appointments. Thanks to @Buanz.
- The PIN pad can no longer sign a person in as somebody else. The login pad is served the family as a numbered list rather than as user ids, and the server turns the number it is given back into a member by asking for that list again. The list was ordered by display position and creation time, neither of which is unique: members written in one statement, which is what a seed or a restore does, share both, and the database is free to return rows that tie in any order it likes. The two lists could then disagree, so the pad could show one member, send their position, and have the server resolve it to a different member and accept that member's PIN. The ordering now ends with the member's own id, which nothing else shares, so there is no tie left to resolve and both ends always see the same list. (#484)
- Weekday column headers are named correctly at UTC+13 and UTC+14. The names were taken from a date built at noon UTC, which is already the next day in Samoa, Tonga and Kiribati, so every header there read one day late. They are now taken from local noon.
- The setup wizard's household choices are saved. Before any parent account has signed in, the server refused every setting except the PIN length, and the wizard did not check the answer. The time zone, week start and location chosen on the Household step were lost while the screen showed them as set, and switching off the anonymous update check on the last step did not take effect. These settings are now accepted during setup and checked for a valid value, the Household step saves the time zone and week start it shows even when they are left unchanged, and the update-check switch reverts if its change was not saved. Installs that never stored a household time zone save one the next time a parent signs in or opens Prism, taken from that device; a device reporting UTC is skipped, and a stored zone is never replaced.
- Timestamps no longer read hours off on a database set to a local zone. Every database session now runs in UTC, so a message posted a moment ago no longer shows as hours old. Rows written in the database's own zone before this are converted once on upgrade. Thanks @flightlinefoundation for spotting it. (#526)
- Chores, points and birthdays count days in the household zone. On a default Docker install the server's clock is UTC, so a daily chore done in the evening in the Americas could skip a day, tomorrow's chores appeared the evening before, and points for a new week or month started arriving hours early. Scheduling, the due list, overdue colours, point periods, goal periods and upcoming birthdays all follow the household's calendar date now. Thanks @flightlinefoundation for the birthday report. (#521)
- Voice answers use the household's day and clock. The voice calendar, chores, tasks, meals, bus and messages routes asked about the server's "today" and spoke times in its zone, so a 9 AM event could be read out as 2 PM. They use the household zone now, and all-day events are dated by their own date. (#531)
- All-day events and drags land on the right day. Dragging an all-day event west of UTC dropped it a day late, the event detail labelled it with the day before, and the dashboard card dropped it in the evening or kept it into the next morning. All-day events are now read by their stored date, and a timed event keeps its wall-clock start when moved, across a DST change too. The calendar also moves to the new day at midnight on a display that is never reloaded.
- Older iCal and CalDAV all-day events show on the right day. Events stored before the earlier all-day fix, and no longer revisited by sync, are converted on upgrade.
- Synced calendar times and ids no longer depend on the server's zone. CalDAV times without a known zone are read in the household zone, recurring all-day events keep a stable id (existing ones carry over without being flagged for deletion), and all-day events sent to Google are dated correctly.
- "Today" follows the display zone, not the device's. On a device in a different zone from the one the display follows, the Today label, past-day shading, overdue colours, the meals page, the seasonal theme and the last-synced times could land on the wrong day for part of every day.
- Weather is dated in the weather location's zone. Pirate Weather and OpenWeather days and their morning, afternoon and evening periods followed the server's clock, the calendar placed each day's forecast a column early west of UTC, and moonrise, moonset and the sun arc could show the wrong day.
- Bus routes are tracked on the household clock. A route's scheduled time was placed on the server's clock, so its tracking window could miss the bus and it could be reported overdue hours early. Trip dates and weekdays are household ones now, and a Sunday route shows as active in the voice status.
- A visit on 1 January is filed under its own year in travel pins and trips, instead of the year before west of UTC.
- The same photo in OneDrive and Immich shows once. Immich capture times are now stored on the camera clock, the way OneDrive reports them, so the duplicate check matches them. Existing Immich photos are corrected on the next sync.
- Backup times show in the display zone and in the chosen 12- or 24-hour format.
- Time labels and grid heights are right on DST change days. Labels read the display zone's wall clock directly, and week and day views size an event by wall-clock time, so a night with a clock change no longer draws an extra or missing hour. The 12- or 24-hour setting also applies from the first paint, and message ages, tasks and chores refresh on their own.
- Task and chore due days follow the display zone. Overdue marks and the Today, Tomorrow and "in N days" labels on task and chore cards went by the device's clock, so on a device in a different zone from the one the display follows they could be a day off for part of every day.
- CalDAV recurring events that started long ago sync again. A repeating event whose series began years (or, for a daily event, months) before the sync range now shows its current occurrences, and a daily event is no longer cut off after 100 days. EXDATE, UNTIL and COUNT still apply, and synced ids do not change. (#533)
- Chores completed by voice follow the same approval rules as the app. A parent's own chore completed by voice is approved and counted at once. A child's waits for a parent to approve it, as it does in the app, and now keeps its points so approving it counts them. Chores that require approval always wait for approval in the app. (#530)
- MCP tools send what their API expects.
list_mealsfilters by its dates,create_mealsaves to the right day instead of failing, andcreate_maintenance_itemkeeps the due date and sends the category and schedule the API requires. Completing a chore, posting a message and creating a goal no longer fail on renamed fields, and updating a chore, event or shopping item uses the method the API accepts. (#532) - MCP tools read times in the household time zone. A time such as
2026-10-04T15:00is placed in the household zone, times with an offset are also accepted, and all-day events take plain dates. A newget_household_timetool reports the zone and today's date, and events come back with local start and end times. The zone comes from a new/api/household-timeendpoint, which other API clients can use too. - Voice, Home Assistant and MCP work with the authentication wall on. The wall only accepted a signed-in browser or a trusted device, so every API token caller was turned away while it was switched on. A valid API token now gets through; each route still checks what the token is allowed to do.
- Hidden action buttons show on touchscreens again. Since the 1.27.0 styling update, buttons revealed on hover (mark cooked, edit and delete on the meal plan, Undo on the Meals widget, and the chore, message and gift idea actions) stayed invisible on touchscreens wider than a phone. They now show dimmed on touch devices; hovering with a mouse works as before. Thanks @yeojwal for the report. (#523)
- The travel globe fits on portrait screens. It opened at a fixed size that was wider than a narrow portrait screen, so its sides were cut off, and short landscape windows lost the top and bottom. It now shrinks to fit when needed, including after the screen is rotated, and screens where it already fitted look the same. Thanks @yeojwal for the report. (#524)
- Performance mode removes the cards' blur again. Since 1.27.0 the cards and the screensaver kept their background blur with performance mode on. Thanks to @sanko-oz.
- The first Add dialog no longer blanks the dashboard. Opening Add task, chore, message or shopping item for the first time could clear the dashboard for a moment while the dialog loaded. Thanks to @sanko-oz.
- Reduced motion no longer animates every element when a dialog opens. Thanks to @sanko-oz.
- Text Size applies to the main dashboard. The setting saved and showed the new size, but the dashboard at
/kept rendering at 100% in the Docker and Home Assistant builds. Thanks to @sanko-oz.
Under the hood¶
- The unit tests run four times in CI, in UTC, America/Chicago, Asia/Tokyo and Pacific/Kiritimati, instead of once in UTC. Date bugs that only show away from UTC used to pass every check.
npm run test:tzruns the same set locally. Test fixtures that only held in UTC now use local wall times, or the UTC-midnight date the weather providers actually return.
[1.27.0] – 2026-09-19¶
Changed¶
- Widgets no longer refetch on every screensaver cycle, and stop polling while the screensaver covers them. Mounting a widget now reads the last value and the time it was fetched, and goes to the network only if that value is older than the widget's own refresh interval, so the copies of the widgets the screensaver draws come up with data instead of loading from cold every time the display goes idle. Polling pauses while the screensaver is up and does one catch-up refresh when the display is woken, rather than one per tick that was missed. The screensaver's own widgets keep polling, because they are the ones on screen. Away Mode and Babysitter Mode keep polling too, since either can be switched on from another device and decides what the display shows. Several live copies of the same endpoint now share one poll between them instead of each running its own timer. Measured on the demo instance: 6.5 minutes of screensaver went from 48 requests to 16, and the screensaver appearing went from 4 requests to 2, with no loading placeholders. (#336)
Under the hood¶
- Checks that could only ever pass in one environment no longer ship here. Two required checks were retired along with the workflows behind them, and one, "Repo hygiene", replaces them, so a contributor's pull request is now gated only by checks their own checkout can run. What stays is what a fork benefits from: the secret-shape scan, which fails on a committed API key or private-key block in any checkout, and the rule that screenshots under
docs/demos/may only arrive from the workflow that generates them against a seeded database. A checkout can add its own commit-time checks:.husky/pre-commit,commit-msgandpre-pushrun.husky/local/<hook>when it exists, and that directory is gitignored. - Every action in every workflow is pinned to a commit SHA rather than a tag that can be moved under it, and a maintained catalogue of secret patterns now runs alongside the project's own rules.
Fixed¶
- Skipping the optional PIN during setup no longer locks the household out of Settings. A parent PIN is optional at setup, but the settings gate asked for one regardless, and Settings is the only screen where a PIN can be set, so an instance created without one had no way back in. Settings now opens when no parent has a PIN, and is gated exactly as before as soon as any parent has one. Choosing a parent who has no PIN says so and points at Settings, Family Members, instead of showing a pad that nothing can complete, and the setup wizard now says what leaving the PIN blank means. (#481)
[1.26.0] – 2026-09-12¶
Added¶
- An optional authentication wall. Prism serves the family's calendar, messages, tasks and lists to anything that can reach it, which is exactly right for a screen on a kitchen wall and wrong for an instance reachable from anywhere else. A new setting in Settings → Security, off by default, requires a sign-in before anything is served. The screen in the kitchen is the exception: a parent marks that display trusted once, from the display itself, and it keeps working untouched while every other device has to sign in. Left off, nothing changes, guests can still read the board and still cannot alter it. Prism also gains a sign-in page, because signing in has always been a panel over a page that was already on screen, and with the wall on there is nothing underneath.
- Event notes now show in the calendar, rendered rather than dumped. An event carrying directions, a dial-in or a packing list showed nothing at all. Notes now appear in the detail view with their links live, and are sanitised on the way in, since the text arrives from whichever calendar you synced and is not yours.
- Themes now control the status colours too. Success, warning and error states were written as literal greens, ambers and reds in the markup, so a theme could not reach them: install a dark, muted palette and the "saved" ticks and "needs attention" banners stayed the same bright factory colours. Around 400 of those hardcoded colours across 66 files now read theme values instead. Two new colours,
successandwarning, join the palette a theme may set, alongside the error colour that already existed. Both are optional: a theme that says nothing about them keeps Prism's defaults, so every existing theme, including any installed from the gallery, looks exactly as it did. A theme that does set them is contrast-checked on them like any other pair. The per-widget icon colours on the dashboard are deliberately untouched, because those are a set of distinct hues doing a job that one shared colour cannot do.
Changed¶
- The month grid was rebuilt around one idea: a lane is a lane. Multi-day events, all-day events and a day's own entries were three kinds of thing drawn three ways, and every fix to one broke the alignment of another. They are now the same object on the same grid. A run that crosses a week joins into a single pill instead of repeating its title on every row, says how much of it is left, and keeps a rounded cap where it genuinely ends while a chevron still means it continues. A blank lane is a slice like any other, so a day a span misses no longer reserves space it never uses, and a day's own events can use the room above a bar. There is a browser-level geometry suite in CI now, because every bug in this area has been a box in the wrong place, which no unit test can see.
- Performance mode applies before the first paint. A display with it switched on still drew one frame of the expensive version, backdrop blur on every card, and then removed it, on exactly the thin clients that can least afford the frame, and on every page load rather than the first.
- A service worker install no longer downloads 3846 emoji. The emoji set was 91% of what each display cached on install, 18MB fetched as 3846 separate requests, and every update made it do so again. A page uses a few dozen of them; they are now fetched as they are needed. On a thin client this is the difference you feel after an update.
Fixed¶
- Calendar reassignment now follows the event to Google. Moving an event onto a Google calendar saved the change locally and made no API call at all, so it never appeared in Google and nothing said so. Moving one between two Google calendars now moves it there, and moving one away removes the upstream copy instead of leaving it to be re-imported as a duplicate. Moving a single occurrence of a repeating event is something Google refuses outright; that now says so plainly rather than reporting a general failure. Thanks to @sanko-oz.
- An event with a long venue address can be edited again. Calendars put the whole venue block in the location field (name, address, suite, parking notes), and anything past 255 characters made the event impossible to save, rejecting a field you had not touched and naming nothing. The form now also says which field it refused.
- Clearing a description, a location or a reminder now sticks. Emptying a field saved as empty and then came back on the next sync, because an absent value reads as "leave this alone" to the calendar it is being sent to.
- The travel globe survives its mapping library's next major. The upgrade would have left the globe drawing land shading with no water, no borders and no labels, with nothing logged and no error raised.
- Photos from a synced source now filter by orientation. Orientation was worked out only for photos uploaded by hand, so anything arriving from OneDrive or Immich was stored without one and the landscape/portrait filter on the Photos page returned nothing for it. That is the filter that keeps phone-shaped photos off a landscape display, and every photo joins the wallpaper rotation by default whatever its shape, so on a synced library there was no practical way to exclude them short of paging through the lot. Existing photos are fixed in place on update, using the dimensions already recorded, so nothing re-syncs and no photos are downloaded again.
- A birthday no longer appears twice for the same person when one source spells the name possessively and another does not.
- A CalDAV event with a stray carriage return in its notes no longer breaks the whole sync.
Under the hood¶
- Now runs on Next 16. The service worker, the offline behaviour and the dashboard are unchanged; this is the framework underneath.
- A local deploy no longer overwrites the container's compiled modules with ones built for a different C library, which took an instance down for twenty-two minutes and reported it as an unrelated start-up error.
- Local database backups now expire on the date in their filename rather than a timestamp that other tools reset, which had the nightly check reporting a failure on backups that were working.
[1.25.0] – 2026-09-07¶
Added¶
- A theme can now choose a typeface and how tightly things pack. Colour and three numbers were the whole vocabulary, and colour alone makes themes read as tints of each other. A theme can now name a typeface by role — sans, serif, rounded or mono — which is the largest identity lever left after density: a rounded geometric and a newspaper serif read as different products in a way no palette swap manages. It can also pick from a small set of display modes: whether calendar events pack comfortably or compact, and whether panels read as raised cards or flat regions of the page. A theme picks a name from a list; what the name means is decided in Prism, so themes stay pure data. Every one of these is optional, and a theme that sets none of them looks exactly as it did.
- A new built-in: Notice Board. Near-white, flat and packed tight, with a rounded face and no borders at all. It is built for the job a shared wall calendar actually does — the events supply the colour, one hue per person, so everything around them gets out of the way. Compact events fit a family's whole week into a month cell. It is also the worked example of the new controls, for anyone building a theme of their own.
- The community gallery takes themes, and Prism can install them. The gallery has held layouts since it opened; a theme had nowhere to go, even though a theme is the easier thing to share — it is pure data, nineteen colours per mode and three capped numbers, with no widget set to line up and no screen size to match. Two halves now exist. Share, next to the palettes in Settings → Appearance, takes the palette you are looking at, checks it, and hands you a prefilled submission form; nothing leaves the house until you submit it yourself, so a display with no GitHub session has published nothing. Browse, beside it, lists what other people have shared with both modes previewed on the card, and installs one in a tap. An installed theme is copied into the house rather than linked, so a display that boots without a network still comes up in the palette it was left on, and it appears in the palette picker beside the built-in five, marked Community, and can be removed again.
- A submitted theme is checked before a person ever reads it. Every colour has to be a bare HSL triple — which is what makes a theme unable to carry CSS into a page — and text has to clear 3:1 against its background in both modes, because Prism is read from across a room, often by somebody without their glasses. Below that it is refused with the failing pairs named; between 3:1 and 4.5:1 it is accepted and the card says so, so whoever installs it can judge for themselves. Subtle borders are counted separately and never held against a theme: a borderless look is a style, and Snow Day ships that way on purpose. What survives all that becomes a pull request a maintainer reads, because the part that actually matters — whether a theme belongs on a screen in a family kitchen — is not something a checker can decide.
- The calendar is translated into German. The calendar page, its widget and the add/edit event form now follow the selected language, including all views (agenda, week, multi-week, three-month, side-by-side day), the view and filter menus, the manage-calendars and pending-deletions dialogs, and the recurrence and reminder options. Date labels are built from the language rather than a fixed pattern, so a German calendar reads
4. September 2026and1. Sep. – 14. Sep. 2026rather than the English order. Event titles are left as typed. The German is a first draft and corrections are welcome: see the Languages guide and #289.
[1.24.0] – 2026-09-05¶
Added¶
- Themes now change the shape of the UI, not just its colour. Five palettes that differed only in hue read as one app with a filter over it — colour alone changes what something is coloured, not what it looks like. A theme can now also state three capped numbers: corner rounding (0 – 1.5rem), density (a 0.75 – 1.5 multiplier on card padding) and border weight (0 – 3px). Density does the most work of the three, because spacing is most of what separates one design language from another. Values outside the range are clamped rather than rejected, so a theme degrades to the nearest legal look instead of failing to load, and a theme only states what it wants to change. The built-ins now differ by more than hue: Arcade is square, tight and heavily outlined; Snow Day is round and airy with no borders at all; Clay is soft and roomy; Harvest sits between them; Prism is unchanged. Themes are still plain data — three numbers are not code — so they stay shareable through the gallery.
Fixed¶
- Cards now actually take their shape from the theme. They were hard-coded to a fixed rounding and border, so a theme could set those values and nothing on screen would move: the variables existed and nothing read them. Cards are the surface a dashboard is made of, so this is where shape has to land to be visible at all.
[1.23.0] – 2026-09-05¶
Added¶
- The screensaver can bring its widgets in and out, four different ways. It has always drawn every widget in its layout, all the time: busy to look at, and the worst case for a panel, since nothing on screen ever moves. It can now show about two thirds of them and rotate which ones — Settings → Appearance → Screensaver → Widget transition effect. Fade is opacity, eased at both ends. Smoke dissolves the widget's own pixels through a turbulence mask. Fill and drain runs a waterline across it, with one widget draining as another fills — the water leaving one is exactly the water arriving in the other, and the pair is chosen from opposite sides of the board so it reads as a transfer. Fireworks takes the widget apart into its own pixels: it is sampled pixel by pixel and each fragment carries that pixel's colour, so at the instant it comes apart you are looking at the widget itself. Off by default; no display changes character on an update without being asked.
- Widgets can drift, to spare the panel. Slow, out-of-step movement — Breathe, Ripple or Figure eight — over periods of twenty seconds to a minute and a half. Rotating which widgets are shown helps with burn-in; moving the ones that are showing helps more, because it shifts the edges rather than what is inside them. Breathe and Figure eight are meant to go unnoticed. Ripple is meant to be just noticeable.
- The screensaver's settings can be reached from the screensaver. Judging a transition means watching it, and every adjustment otherwise meant leaving the screensaver, finding the settings page, changing one value and waiting for it to come back. Switch on Show a settings shortcut on the screensaver and a Prism mark appears in the top-left — invisible until you hover it, then fading in over two seconds — which opens the same settings in place. The screensaver editor opens it too, since arranging a screensaver and deciding how it moves are two halves of the same job.
- Prism now tells you when a calendar has stopped syncing. When a calendar's connection expires or is revoked, syncing stops. The only sign of it was a banner on the calendar management panel, which nobody opens unless they already suspect something — so what a household actually saw was a calendar that quietly stopped changing, which looks exactly like a quiet week. The calendar page now carries a Sync paused badge next to the one for held-back removals, and the calendar widget carries a line saying the same; either one opens Manage Calendars, where the button to reconnect already was. It says how many calendars are affected, and which service when they are all the same one — never which calendar, because a wall display is read by whoever walks past it. The screensaver stays clear of it.
- Appearance uses the width of a landscape screen. Its sections now lay their cards out across the page instead of running down a single narrow column, so Seasonal Theme sits beside Color Scheme rather than below it. Only Appearance: widening the short sections would leave a thin strip of controls across a lot of nothing.
Fixed¶
- The screensaver's transitions start from an empty board again. With an effect switched on, the screensaver is meant to come up empty and bring its widgets in one at a time — that first second is when somebody is most likely to be watching, and it was being spent on the opposite. The board came up complete and then drained itself away all at once, because the per-display settings are read a moment after the first frame is drawn, and for that frame every display looks like one with the effects switched off. It now waits for the display's own settings before drawing anything. Screensavers with effects off are unchanged.
- A dashboard's text size now survives moving around the app. A dashboard can carry its own text size, and its subpages honoured it — but the navigation linked out of the dashboard rather than within it, so one tap on the sidebar landed on the unscoped page and the display dropped back to 100%. It looked like the setting not sticking; what actually happened is that the page it applies to was no longer the page you were on. Navigation now stays inside whichever dashboard you are in, and Travel and Weekend, which had no dashboard-scoped page at all, now have one. Settings is a deliberate exception: it is where the size is changed, and a settings page at 150% is the one place being oversized gets in the way.
- Away Mode no longer switches itself back on a minute after you switch it off. It decides whether the house is empty from how long that browser has been idle, but it turns the mode on for everyone — so any screen sitting untouched somewhere in the house re-asserted it within a minute of anyone dismissing it. Set to "1 day", it could come back over and over. A deliberate switch-off is now counted as activity by every screen, so it stays off until the house has genuinely been quiet again. This also explains a stranger symptom: because the screensaver yields to Away Mode, the screensaver button appeared to do nothing.
- Turning off widget outlines in the screensaver now turns off the outlines. It was removing the rules inside a widget — between rows of a table — and leaving the outline around it, which is precisely backwards.
- The unlock screen no longer shows the same person twice. Choosing who you are when leaving Away mode, leaving Babysitter mode, or opening Settings could draw one parent's picture next to another parent's name, so it looked as though somebody appeared twice and somebody else was missing. Signing in still worked; only the pictures were shuffled. Present since 1.10.0 and easy to miss on a household with one parent.
[1.22.0] – 2026-09-03¶
Added¶
- Chores, Messages, Meals and Wishes remember how you left them. Grouping, sorting and show/hide choices survive a reload, the way the Tasks page already did — set Chores to group by person once and it stays that way. Filters are treated differently on purpose: they are kept across a refresh but forgotten once the display has sat idle, so nobody walks up to a board that is silently hiding most of it with no memory of why.
- Prism now credits the work it is built on. The emoji artwork is licensed in a way that requires attribution from anyone who passes it on, and Prism was not giving any. There is now a NOTICE file and a Credits page covering the emoji graphics and both bundled typefaces, and they ship inside the container image rather than only living in the source repository.
Changed¶
- The per-display Font Scale setting is now Text Size: renamed, moved near the bottom of Settings, and collapsed by default. The old name described the mechanism rather than the decision, and the control sat open near the top of Settings for everyone in order to serve the few who read a dashboard from right across a room. It keeps its value, it now says out loud that larger text means fewer rows fit, and it stays expanded on any display where it has been changed from 100% — so a screen you have already tuned never hides that from you.
Fixed¶
- The screensaver no longer covers Away or Babysitter mode. Both put a full screen up deliberately, and both matter most when nobody is standing at the display — which is exactly when the screensaver used to slide over the top of them. A home left in Away mode showed photos instead of the away screen, and a babysitter's notes disappeared after a couple of minutes with nobody there to touch the screen and bring them back. Being idle now yields to both.
- A theme installed from the gallery no longer flashes the default palette on every load. Built-in palettes were drawn correctly from the first frame, but a theme you installed yourself was not: the page rendered in Prism's own colours and only corrected itself a moment later. The same fix that stopped built-in themes flashing now covers installed ones.
- Turning up the display text size no longer pushes the dashboard off the screen. The setting made text bigger and the board taller in equal measure, so at 150% the bottom widget was simply below the edge of the screen — on a display nobody can scroll. The dashboard now re-fits itself as the text grows: fewer rows, all of them on screen. Nobody's saved scale changes, and a display that was overflowing stops.
- Text is the right size again on a display with no mouse or touchscreen. Prism picks its text size from what it can tell about the screen it is on, and a display with no pointing device attached — a Pi or a streaming stick driving a wall panel — fell through to the size meant for a phone held at arm's length. That is the one kind of screen guaranteed to be read from across a room, and it was getting nearly the smallest text Prism has. It now scales up with the screen the way a touchscreen already did. If you turned the text size up under Settings → Text Size to work around this, you will want to bring it back down.
- The dashboard no longer shifts as it finishes loading. Prism ships a stand-in font whose letter widths are matched to the real one, so that text does not jump when the real font arrives. It was being built and then skipped over, so every cold start nudged the whole layout by a few percent as it settled. Most noticeable on a wall display, which reloads on its own.
- Pages that remember a view no longer redraw themselves on every load. Restoring a saved grouping or filter made the browser throw away the page it had just been sent and build it again, so a display briefly showed the ungrouped list before settling. Nothing looked broken, which is why it went unnoticed. Tasks was affected as well as the pages above.
- Prism no longer downloads ~4 MB of emoji font up front. The offline cache was told to fetch every font in the build the moment it installed, including all ten chunks of the colour-emoji fallback — undoing the careful arrangement that only fetches the emoji ranges a screen actually draws. Emoji are unchanged; they now arrive when something needs them. First load drops from about 4 MB of fonts to about 210 KB.
[1.21.0] – 2026-08-31¶
Added¶
- Colour themes. Prism has had light and dark; it now has palettes. Five to start — Prism, Clay, Harvest, Snow Day and Arcade — chosen under Settings → Appearance. The palette applies to every screen in the house, while light and dark stay per-screen, because one is a decision about how the home looks and the other is about the room a particular screen is in. Every palette is checked for readability before it can ship: Prism is read from across a kitchen, often by someone who is not wearing their glasses.
- Groundwork for sharing themes. Themes are plain colour values, which means they can be passed between households the way dashboard layouts already are. Two ways to share one, and they differ in what happens to your work: through the gallery it stays yours, or as a contribution it ships with Prism for everyone. Neither is better. The submission side of this is not finished yet; what is here is the part that has to be right first.
Fixed¶
- A security fix in the layout submission workflow. A submitted layout name was passed to a command line without being quoted, so a carefully chosen name could run commands on the machine that processes submissions. Anyone with a GitHub account more than a week old could have reached it. Nothing suggests this was used; it was found while reviewing whether the same workflow could handle themes. If you run a fork with community submissions enabled, take this update.
- The theme no longer flashes on load. Opening Prism showed the light palette for a moment before settling on your actual choice. Unnoticeable on a laptop, obvious on a wall display that reloads by itself. It also meant a saved "match my system" preference was ignored for the first moment of every page.
[1.20.1] – 2026-08-31¶
Fixed¶
- Immich photos display in Firefox and other browsers that cannot decode HEIC. Lightbox, screensaver, and wallpaper were proxying the iPhone original (
image/heic). Prism now asks Immich for a web-safe full-size JPEG/WebP (falling back to the preview), converts leftover HEIC with sharp, and ignores previously cached originals. Thanks to Brian Adams.
[1.20.0] – 2026-08-30¶
Added¶
- The display can now sign itself out after a stretch of inactivity. Once someone signs in at a wall display it has been staying signed in as them for weeks, so anyone who walked up afterwards could add, change or delete things as that person. It now signs out after 30 minutes untouched, which you can change or turn off under Settings → Appearance → Timers. Nothing disappears when it does: the calendar, tasks and messages stay on screen for anyone to read. What comes back is the PIN prompt on anything that changes something.
Fixed¶
- On-screen keyboard keys were blank in dark mode. The letters only appeared while a key was held down. Thanks to Brian Adams for finding and fixing this.
- Prism was fetching some things twice. Two parts of the app that ask for the same information at the same moment now share one request instead of making two, and the short-term cache no longer grows without limit on a display left running for weeks.
[1.19.1] – 2026-08-30¶
Fixed¶
- Prism no longer offers a Google sign-in button that cannot work. Google refuses a private address as a sign-in redirect, so on a home-network-only install the button sent you to Google and Google turned you away in its own words, with nothing on the Prism side explaining why. Where the address you are using cannot work, Prism now says so, names the ways round it — reopen Prism on a public https address or on localhost, or paste a token instead — and opens the paste-a-token section for you. On a public address nothing changes.
- The backups page put its most-used button last. Clear Cache & Reload now sits above the list of backups rather than below it, and the list shows the five most recent with the rest behind a Show older backups toggle. Nothing is deleted; the list simply no longer grows until it pushes everything else off the screen.
- A locked-out PIN no longer says the PIN is wrong. After five failed attempts Prism stops accepting a PIN for a while, and during that pause it was reporting even the correct PIN as incorrect — for up to four hours, with nothing to suggest waiting would help. It now says how many tries are left before the pause, and once paused, the time to try again. A PIN entered while Prism cannot be reached is no longer reported as wrong either.
- A screensaver you opened yourself can always be closed again. Opening it from the toolbar button, on an installed app or with the timeout set to Never, produced a screensaver that nothing could dismiss. It still will not switch itself on in those cases, which is deliberate.
- Your calendar no longer stops rolling over at midnight. A display left running for days kept asking for the range of days it worked out when it started, so "today" on screen quietly stopped being today. Nothing looked wrong, which is why it went unnoticed.
- The screensaver was quietly fetching everything. It loaded data for every widget in Prism while showing three of them, and asked your task provider to sync each time it appeared. It now loads only what it draws.
- Prism no longer tells the network who has a PIN. The member list shown before anyone signs in, so the login screen can draw it, also said which members had a PIN set. Names and faces are on the display anyway; which member is unprotected is not, so it has been removed.
[1.19.0] – 2026-08-29¶
Added¶
- Tasks removed in Google or Microsoft are now held for your review instead of disappearing. When a task you sync stops being listed by the other app, Prism no longer deletes it. It stays put and a Review button appears on the Tasks page, where you choose to delete it or keep it as a local task. If an unusual number vanish at once — the shape of an outage rather than someone ticking things off — nothing is flagged at all and the sync says why, so a bad connection cannot quietly empty your list. A task that comes back on its own clears itself with no action from you.
- Tasks can be deleted from the Tasks page. There was previously no delete anywhere in the task list or its edit window; you could only mark something complete. Both now have one.
- A read-only Google calendar can be connected on its own. See 1.18.3; this release extends the same idea to task sources.
Fixed¶
- Deleting a task in Prism now removes it from the app it syncs with. It previously vanished locally and came back within about five minutes, with nothing to explain why.
- Google Tasks could not be connected without a public web address. Two separate faults: the connection read its credentials from a place they are not stored when Prism is set up through the app rather than a configuration file, and after pasting a token there was no way to reach the screen that picks which lists to show. The first also meant that, where the connection did work, it stopped about an hour later and reported only that credentials were missing.
- Apple Reminders tasks removed on the server are held for review too, rather than deleted outright.
- The Tasks page kept its own settings. Grouping, sorting and show completed survive a refresh now. The list filter does too, but is forgotten once the display has been idle a while, so walking up to a sleeping screen gives you the whole list back rather than a filtered one you do not remember setting.
- The task list filter said "No List" where it meant "All". Choosing the obvious-looking option emptied the screen, and clearing the filter was only possible by unticking everything. There is now an explicit All, with the old option renamed Unassigned.
- The Tasks page stopped updating for child profiles. It was repeatedly attempting a sync only a parent is allowed to run, and failing silently.
- A child asked to review a removed task was refused without being told why.
- Weather and Kroger API keys could be overwritten without signing in. Both now require an authenticated parent, matching the equivalent Google and Microsoft settings.
[1.18.3] – 2026-08-28¶
Added¶
- A read-only Google calendar can now be connected on its own. Pasting a token that covers only
calendar.readonlyused to be refused. It now connects, and its calendars appear in Prism exactly like an iCal subscription you have subscribed to: the events show up, and Prism will not try to write to them. These calendars are not offered when you add an event, so you cannot fill in a form that was never going to save. To create events from Prism, include thecalendar.eventsline as well when you generate the token; the setup screen shows both.
Fixed¶
- A Google calendar Prism could not write to no longer offers to add events to it. Whether a calendar accepted new events was decided from your permissions on that calendar in Google, rather than from what the token itself was allowed to do. A calendar you own therefore looked writable even when the connection was read-only, and an event typed into it was saved locally with a vague warning that it "could not be synced". Now a read-only connection is treated as read-only everywhere, and a save that fails for this reason says so plainly instead of suggesting you try again.
[1.18.2] – 2026-08-28¶
Added¶
- Google Tasks and bus-tracking Gmail can now be connected without a public web address. Prism has supported Google Tasks as a task source for a while, but connecting it required Google's browser sign-in, which needs a public HTTPS address that a home-network-only install does not have. The same paste-a-token method already used for Google Calendar now covers Tasks and Gmail too, and one token can carry all three. You choose which: in Google's OAuth Playground you paste only the lines for the parts you want, so a token can cover your calendar and tasks while leaving your email alone. Prism tells you afterwards exactly what the token enabled. The exact lines to paste are shown in Settings → Integrations → Google. Note that a token cannot gain access later, so to add something you generate a fresh one with the extra line included.
[1.18.1] – 2026-08-28¶
Fixed¶
- A misconfigured encryption key is now reported clearly instead of surfacing later as a broken integration. If the
ENCRYPTION_KEYin your.envwas missing, left as the example placeholder, or otherwise malformed, Prism started and looked completely healthy — nothing encrypts until an integration first stores a credential. The problem only appeared later, as an unexplained failure when connecting Google Calendar, iCloud, bus tracking or photo sources, which pointed at those integrations rather than at the key. Prism now checks the key when it starts and prints a clear message saying what is wrong and how to generate a valid one, and connecting an account will tell you the key is at fault rather than blaming your credentials. The example.envno longer ships placeholder values for secrets, since a placeholder looks configured when it isn't.
[1.18.0] – 2026-08-28¶
Added¶
- The clock and weather widget now speak the language you picked, and the German wording has been corrected by a native speaker. Choosing Deutsch previously left the day of the week, the weather conditions and the "today" label in English. Those now follow the selected language, along with corrections to the existing German throughout, contributed off the back of #289. Catalogues are also checked automatically now, so a language can no longer drift out of step with English without the tests noticing.
- Birthdays are found on any calendar you use, not just two specific Google ones. Prism previously only picked up birthdays from Google's contacts calendar and from a calendar that happened to be named "Friends & Family", which worked if you kept one and did nothing at all otherwise. It now reads them from every calendar you've connected — Google, iCloud, an iCal subscription, or a calendar you created inside Prism. That last one is the answer to "how do I add a birthday Prism can't find": make an all-day event with the person's name and the word birthday in the title, and it appears on the next sync. Add the year in brackets, like
Grandma's Birthday (1948), to show their age. Anniversaries work the same way with the word anniversary, and anything else you want to remember is picked up if it's all-day, repeats yearly, and has a year in the title. Deleting a birthday now sticks too, rather than reappearing on the next sync. Read-only calendars you subscribe to (school terms, public holidays) are skipped on purpose, so "No School — Martin Luther King's Birthday" doesn't become a family birthday, and titles like "birthday party" or "prep for Sam's birthday" are ignored because they describe the celebration rather than the day. Titles are recognised in English and German. Full details in the Birthdays & Milestones guide.
Fixed¶
- Input and Bus Tracking settings now actually save. Turning off the on-screen keyboard, changing any of the barcode scanner options, or setting the bus Gmail label appeared to work and then quietly reverted, because those pages were sending their save in a form the settings API rejects. Nothing was written and no error was shown; the Bus Tracking page went as far as reporting "Gmail label saved" while saving nothing. Both pages now save correctly and will tell you if a save ever fails. Reported in #298. If you use a tablet where Prism's own keyboard appears on top of the system one, turning the keyboard off under Settings → Input now works and stays off.
- A failed Google token connection no longer blames your token when the problem is ours. When connecting Google Calendar with a pasted refresh token, anything unexpected going wrong on Prism's side reported the same message as a genuinely bad token, sending people off to generate a new one that was never the issue. Prism now says plainly when the failure is on its side and points at the log line to quote if you report it.
[1.17.2] – 2026-08-27¶
Fixed¶
- The weather widget now shows the location you actually chose. Setting a location in Settings → General updated the forecast but not the name above it, so the widget kept displaying whatever place was in the install's environment file — Springfield, on a default install. The forecast data was correct the whole time, which made it look like the setting had done nothing at all. The name and the forecast now always come from the same place. If you upgrade and still see the old name for a few minutes, that's the cached forecast expiring; changing your location clears it immediately.
[1.17.1] – 2026-08-25¶
Fixed¶
- A birthday on the day itself no longer disappears from the widget. The next occurrence was worked out against the current time rather than the start of the day, so a birthday falling today always compared as already past, rolled forward a year, and was then filtered out of the 30-day window — meaning the one day it mattered most was the one day it wasn't shown. Today's entries now appear at the top of the list, counted as 0 days, and are highlighted in green.
Added¶
- Choose the interface language. A new Language option under Settings → Appearance switches Prism's wording, starting with German (Deutsch) alongside English. Navigation is fully translated and the Birthdays widget is done; the rest of the app follows in stages, and anything not yet translated simply stays in English rather than breaking. Dates and numbers automatically follow the language you pick. Whether the clock shows 12- or 24-hour time stays a separate choice under Settings → General, so you can mix the two however you like. Translations are plain files anyone can correct or extend — see the Languages guide.
[1.17.0] – 2026-08-23¶
Integrations¶
- Connect Google Calendar on a Home Assistant / LAN-only install — no public URL required. If Prism only runs on your local network (the Home Assistant add-on, or bare Docker on a private IP), Google refuses to accept your address as an OAuth redirect, so the normal Connect button can't finish. There's now a "Connect without a public URL (advanced)" option under Settings → Integrations → Google: you generate a refresh token with Google's OAuth Playground and paste it in for full two-way calendar sync — the sign-in stays entirely on Google's own domain, with no reverse proxy or tunnel needed. The in-app instructions walk through the whole setup end-to-end — creating the project, publishing the consent screen to Production (so the connection doesn't expire after 7 days), and generating the token — all under a single Google account.
[1.16.2] – 2026-08-23¶
Integrations¶
- Connect Google Calendar without a public URL. For installs that only run on your local network (Home Assistant add-on, LAN-only Docker) — where Google refuses to register a private address as an OAuth redirect URI — you can now connect Google Calendar by pasting a refresh token you generate with Google's OAuth Playground, under Settings → Integrations → Google → "Connect without a public URL (advanced)." It's full two-way read/write, needs no public URL or reverse proxy, and the sign-in stays on Google's own domain.
[1.16.1] – 2026-08-22¶
Calendar¶
- Faster view switching and month navigation. Flipping between calendar views and advancing the month (especially 3-month) is much snappier, and switching no longer briefly freezes the display while the new view renders. Each view now scopes its work to just the dates on screen instead of scanning the whole event history every time.
- The "hide hours" setting now sticks. It is stored in the database instead of per-browser, so it survives updates and stays consistent across your devices.
- The Manage calendars window no longer scrolls sideways and now uses more of the screen width on large displays.
[1.16.0] – 2026-08-21¶
Display¶
- Choose 12-hour or 24-hour time, family-wide. A new time-format preference under Settings → Display applies everywhere times appear — clocks, weather, every calendar view, and the Away/Babysitter overlays — instead of each surface deciding on its own. Thanks to @m4rtski for the contribution.
- Pin a display timezone for the household. You can now set the timezone Prism renders times in (defaults to each device's own, so nothing changes unless you set it) — keeping calendar, reminder, weather, and clock times consistent across devices. Also from @m4rtski's contribution.
Calendar¶
- Multi-day events render as one continuous bar. Trips, holidays, and other events that span several days now draw as a single connected bar across the month, multi-week, and week views instead of repeating as a separate chip on each day, with clipped ends where the bar crosses a week or month boundary. Events already in the past are dimmed. Thanks to @m4rtski for the contribution.
- All-day events created in Prism sync to Google correctly. Creating or editing an all-day event now writes it to Google Calendar as a true date-only all-day event on the right day, rather than a timed event, and all-day events no longer shift by a day depending on your display timezone. Also from @m4rtski's contribution.
[1.15.5] – 2026-08-20¶
Integrations¶
- Set up Google Calendar entirely in the app — no
.envfile needed. You can now enter your Google OAuth credentials (Client ID, Secret, Redirect URI) directly in Settings → Integrations → Google, stored encrypted in Prism's database. This unblocks Home Assistant addon and other installs where you can't edit.env— previously the Google card only pointed you at.env, so there was no way to configure it. (Microsoft already worked this way; Google now matches.)
Security¶
- The OAuth credential-save endpoints now require an admin.
/api/setup/credentials/googleand/api/setup/credentials/microsoftwere unauthenticated; they now require a signed-in user with settings-management permission before storing app credentials.
[1.15.4] – 2026-08-20¶
Calendar¶
- The "Review removals" window now scrolls to show every item. When the sync holds a lot of removed events for review (Delete vs. Keep in your local calendar), the list is now natively scrollable — including drag-to-scroll on touch wall displays — and the Delete/Keep buttons stay pinned at the bottom, so a long list is fully reachable instead of clipped.
[1.15.3] – 2026-08-20¶
Calendar¶
- Deleting a calendar in Google no longer flashes a "Sync failed" error in Prism. When you remove a calendar from Google, Prism used to surface the resulting "not found" error as an alarming sync failure for a few cycles before auto-disabling the calendar. It now handles that quietly and, once confirmed, labels the calendar "Removed in Google — auto-disabled" in Manage calendars so you can see why it went inactive.
- Restore a Google calendar you removed by mistake. Deleting a Google calendar from Prism tombstones it so it won't reappear on your next sign-in — but until now there was no way to undo that. Manage calendars now has a Removed calendars section listing anything you've deleted, each with a Restore button that brings it back. (The tombstone now remembers the calendar's name, so the list is readable rather than a cryptic id.)
[1.15.2] – 2026-08-20¶
Calendar¶
- Google calendars you've hidden in your list are now discoverable, and hiding one in Google no longer removes it from Prism. Prism now sees all your Google calendars regardless of their visibility in your Google sidebar. Newly-discovered hidden calendars are added switched off, so they're available in Manage Calendars without cluttering your dashboard — and your on/off choices in Prism are now independent of Google's list, so tidying up your Google sidebar won't make calendars vanish from your board.
[1.15.1] – 2026-08-20¶
Calendar¶
- Re-authenticating Google now picks up newly-subscribed calendars. Once Google was connected, re-authenticating only refreshed the calendars Prism already knew about — so a calendar you subscribed to afterward never appeared, and the only workaround was to fully disconnect and reconnect. Re-auth now also discovers and adds any new calendars (skipping ones you've deleted from Prism before), so subscribing to a calendar and re-authenticating brings it in.
[1.15.0] – 2026-08-20¶
Privacy¶
- Anonymous update check (on by default, one switch to turn off). Once a week Prism now checks whether a newer version is available and, in the same request, adds one anonymous install to a count the maintainer uses to gauge real usage. Exactly four fields are sent — a random per-install id, the version, docker-vs-Home-Assistant, and CPU architecture — with no IP address, no personal data, and no usage tracking. See the exact payload any time under Settings → About, disable it there with one switch, or hard-disable it for the whole install with
PRISM_DISABLE_TELEMETRY=true. Update notices are quiet: they appear only in Settings (never on the dashboard) and only for minor/major releases, never patches. Full details in the Anonymous update check guide.
Calendar¶
- Fixed calendar sync failing for events with long locations. Events whose location lists several venues (e.g. a CalDAV event with three rooms joined by
;, ~300+ characters) exceeded the location field's 255-character limit, so every occurrence of that recurring series failed to sync and flooded the logs with errors. The location field is now unbounded (stored astext), matching the description field — nothing is truncated, and existing data is preserved.
[1.14.4] – 2026-08-19¶
Calendar¶
- Synced calendar events now populate the same multi-year range the calendar shows. Google, iCloud, and CalDAV events were only synced for a rolling ~90 days back / 1 year forward, so once the calendar's visible window widened (see 1.14.3), synced events beyond that horizon could be missing even though local events at the same dates appeared. Sync now covers about 1 year back and 2 years forward to match. (Recurring series still populate as far as each provider expands them at sync time.)
[1.14.3] – 2026-08-18¶
Calendar¶
- Events more than ~2 months out no longer disappear from the calendar. The calendar only loaded a rolling window around today (about 30 days back and 60 days forward), so events further out silently dropped from every view — Month, Agenda, and the rest — and paging ahead showed empty grids. This was purely a display limit: the events were always saved and simply reappear once you update. The calendar now loads a much wider window (Jan 1 of last year through the end of two years out), so future events stay visible while navigation stays fast. Thanks to @JoshuaPostema for the precise report (#250).
[1.14.2] – 2026-08-17¶
Integrations¶
- OAuth sign-in returns you to the right place behind a reverse proxy. After connecting Google or Microsoft (calendar, tasks, or the bus tracker), the post-connect redirect now uses your configured
APP_URLinstead of an undocumented variable that fell back tolocalhost:3000— so you land back on your dashboard's Integrations page instead of an unreachable address. The connection itself always worked; only the redirect afterward was wrong. Thanks to @c-jw for the precise report (#245).
[1.14.1] – 2026-08-17¶
Dashboard¶
- The dashboard no longer flashes and rebuilds every few minutes. Background data refreshes now happen silently — the dashboard stays on screen and updates in place, instead of every widget briefly blanking to a loading skeleton on each refresh.
Calendar¶
- The agenda view lists all of your events. It no longer caps each day at five and hides the rest behind a "+N more" line — since the agenda scrolls, every event across the next 30 days is shown.
- The "+N more" button on the month and multi-week views is an easier touch target. Tapping it still opens a popup listing that day's hidden events; the button is now bigger and simpler to hit on a wall display.
[1.14.0] – 2026-08-17¶
Dashboard¶
- The built-in templates were rebuilt around real composition principles. Each board now leads with one hero (usually the calendar), sizes every widget to its natural shape (birthdays runs tall rather than wide, the clock stays small, weather gets room for its sun/moon detail) and arranges them into a couple of balanced zones instead of an even grid packed with too many panels. The new lineup is Family Central, Calendar Focus, Command Center, Meal Planner, School Mornings, and a photo-forward Ambient whose glassy clock and weather float over your wallpaper.
Calendar¶
- Meals now sit at the bottom of each day cell in cards mode. The events lead the cell, and the day's chores, tasks, and meals are grouped in a delineated band pinned to the bottom (meals last), so the schedule and the day's plan read as separate zones.
- Day view is available at more widget sizes. The single-day timeline no longer needs a very wide calendar widget — it's offered wherever the week view is.
On-screen keyboard¶
- The touch keyboard now works inside pop-up dialogs (for example, posting a message). Previously the first key tap closed the dialog and lost the keystroke, Shift dismissed the keyboard, and dropdowns were hard to land on a touch display. Typing, Shift/capitals, and dropdowns are all reliable now.
Integrations¶
- Google and Microsoft sign-in work from any address. When Prism is reached directly on a LAN IP, the sign-in redirect now falls back to your configured public URL, so connecting a calendar no longer fails with a provider "invalid redirect" error.
Meals¶
- Meal-type icons render everywhere, including thin clients. The breakfast, lunch, and dinner icons now use Prism's self-hosted emoji images, so they display on kiosk and thin-client browsers that cannot render a color-emoji font, instead of showing blank.
Screensaver¶
- Refreshed screensaver templates. The calendar (or tonight's meals) is the hero; the clock, weather and messages are small, aligned accents floating over one clean photo region, calmer and less cluttered, with everything sitting fully on-screen.
- The calendar's controls now work on the screensaver. You can change the view, toggle hide-hours, and use the other calendar controls directly on the screensaver without it dismissing on the first tap — and the screensaver's calendar keeps its own view, independent of the dashboard calendar.
Weather¶
- Sunrise, sunset, moonrise and moonset now show their times along the sun/moon arc. The arc and the hourly timeline appear only when the widget is tall enough to draw them cleanly, so a short widget no longer clips them.
Community layouts¶
- Redesigned the community-layouts gallery. The preview boards now float on a wallpaper-style field with cleaner cards, a header with a live count, calmer search, and a Landscape / Portrait orientation filter (which defaults to the board you are editing), so browsing and applying a shared layout is easier to scan. The bundled community layouts were also regenerated to the current grid so they apply correctly. Sharing your own layout opens a pre-filled submission form.
Layout editor¶
- "Save As" confirmations are no longer trapped behind the editor overlay, when a dashboard name already exists you can now see and act on the confirm/cancel prompt.
Photos¶
- Bulk photo management on desktop. Select several photos at once, or Select all across your whole library, then act on the selection in one go: remove them from Prism (this only drops them from Prism, it does not delete the originals in OneDrive/Immich), or flip their Wallpaper / Gallery / Screensaver usage toggles together.
- Below-HD filter and resolution indicators. Each thumbnail carries a small resolution dot, and a Below-HD filter surfaces low-resolution images so you can keep them out of wallpaper and screensaver rotation.
- Sturdier wallpaper handling and OneDrive recovery. Wallpaper selection is more robust, and a OneDrive photo source that stopped syncing can be recovered without losing already-imported photos.
[1.13.1] – 2026-08-02¶
Dashboard¶
- Fixed the bottom row clipping on some kiosks. On touch displays where the top toolbar renders a little taller, the bottom row of widgets could be clipped while the toolbar was showing. The layout now measures the real toolbar height and fits cleanly whether the toolbar is shown or hidden.
- Weather no longer cuts a forecast day in half. The daily forecast shows only the whole day rows that fit, instead of clipping the last one mid-row.
Screensaver¶
- Screensaver scales to fit any screen. It shrinks to fit the display so nothing runs off the bottom edge, on any monitor size.
[1.13.0] – 2026-08-02¶
Dashboard¶
- Your layout now fills any screen. Design your dashboard once (on your kiosk or in a laptop browser) and it stretches to fit whatever screen it's shown on, edge to edge, without clipping or stopping awkwardly short of the bottom. It re-fits live when you resize the window, go full-screen, or zoom, and when the toolbars auto-hide the layout grows to fill the space they leave. A design whose orientation doesn't match the screen (a portrait layout on a landscape display) is neatly letterboxed rather than stretched out of shape.
- Preview shows exactly what the wall will show. Full-screen preview now renders the real, stretched dashboard, and a new device gallery shows how your one design looks on a 27″ display, an iPad, a Fire tablet and a phone, so you can check the fit before you deploy. The old per-resolution "screen zone" controls are retired in favor of this simpler model.
- Better starting templates and widget sizes. The built-in layout templates were rebuilt to fit the screen properly (several used to run off the page), the weather panel now has room to breathe, and a freshly-added widget arrives at a sensible size instead of nearly filling the screen.
- The weather widget fits its space. It shows as much as fits: current conditions, an hourly timeline, and up to a 7-day forecast, revealing more as you make it bigger, so it's never cut off.
Screensaver¶
- Refreshed, legible screensaver templates. The built-in screensaver layouts were rebuilt to sit fully on-screen (some used to run off the bottom) and kept clean and minimal, and screensaver text is now light and readable over the wallpaper. No more dark-on-dark widgets.
[1.12.0] – 2026-08-01¶
Setup & first-run¶
- A slimmer, keyless setup. First run is now just Welcome → Family → Household → Done: no API keys or accounts required to finish. Location uses a ZIP lookup with automatic time zone; calendars and other integrations connect later from their own pages. You can no longer accidentally finish setup with no family members (which used to lock you out of login with no way back), and a brand-new dashboard shows your calendar without anyone needing to sign in.
Calendar¶
- Assign events to a person with zero integrations. Every family member automatically gets their own personal calendar (plus a shared Family one), so an event you add is assigned to someone and color-coded on the dashboard: no third-party setup. The old anonymous "Local only" bucket is gone; existing installs pick the calendars up automatically. Connected accounts (Google) still layer on top, clearly marked as two-way syncing.
- Meals on the calendar are marked with a utensils icon, so a meal is instantly distinct from an ordinary event.
- Calendars refresh on their own after a sync, no manual page reload after adding a subscription or hitting "Sync Now".
- Clearer calendar connect: the dialog shows exactly where to find a Google Calendar's private iCal link, and rejects a Google web link (which used to sync nothing) with a helpful message.
Meals¶
- Correct weekday labels, and meal plans no longer vanish when you change "Week starts on." Meals now anchor to their real calendar date, so toggling the setting simply re-windows the week. Ships one automatic, additive database migration; existing meals keep their dates.
[1.11.0] – 2026-07-31¶
Calendar¶
- Deleting a synced Apple/CalDAV event in Prism now removes it from the source too. Previously only Google deletes propagated upstream. A CalDAV event you deleted in Prism was tombstoned locally but lingered on iCloud/Nextcloud/etc. Prism now captures each event's server address at sync time and sends the delete back to the source. Single (non-recurring) events only; recurring series still delete locally without touching the source (they need proper recurrence editing first). Ships one additive database migration, applied automatically on start.
Setup & first-run¶
- A much smoother first run for non-technical users. The setup wizard now lets you edit or remove family members as you go (fix a typo, change a color), remembers them if you step back or refresh, and no longer drops you on a blank screen when you finish. Setup dialogs fit the screen, member names must be unique, "Add member" no longer looks disabled, and Prism now starts in light mode.
Security¶
- Per-member PIN length. Each family member can pick their own 4- or 6-digit PIN, and every PIN pad now asks for exactly that member's length, so a longer PIN can no longer lock someone out (fixes login for 5/6-digit PINs). The confusing family-wide "default PIN length" setting is gone. Ships one additive, automatic database migration; existing PINs keep working. The admin/settings gate now correctly lists parents only.
Interface¶
- Leaner one-screen default dashboard (weather-forward, no off-screen overflow; existing custom layouts are untouched), consistent empty & loading states across every page each with a clear "Add your first…" button, list pages that start ungrouped, and a Wishes view that matches the rest of the app (person filter + group-by). Plus dozens of small first-impression fixes.
[1.10.0] – 2026-07-27¶
A feature release: recipe & meal-plan sync with Tandoor and Mealie, a calendar-sync overhaul that stops the sync from ever silently losing events, and a settings reshuffle so household basics live where you'd expect. Ships three database migrations (recipe sources, deleted-event tombstones, and a pending-deletion flag), all applied automatically on start.
Recipes & meals¶
- Sync recipes and meal plans from Tandoor and Mealie. Connect a server once (read-only API token) and pull recipes (ingredients, steps, times, tags, and photos) and your meal plan into Prism, from Recipes → Add ▾ → "Sync recipes…" and Meals → Add ▾ → "Sync meal plan…". It's review-and-approve: every sync shows exactly what would change and you pick what to apply: adds and updates are pre-selected, removals are opt-in, and a mass-delete guard keeps a source glitch from wiping your library. A planned meal automatically brings its recipe along if you haven't imported it yet. Re-syncing is idempotent (unchanged items show "up to date"). Both apps ride one reusable framework, so more integrations are straightforward from here.
- Fixed ingredient scaling. Scaling a recipe's servings multiplied every number in an ingredient line, so "1 8 oz can" doubled to "2 16 oz can". It now scales only the leading quantity (handling fractions and mixed numbers) and leaves pack/size numbers alone.
Calendar¶
- Sync never silently deletes anymore. When an event disappears from its source calendar, Prism no longer removes it on the next pull. It holds it and shows a "Review N" badge on the calendar. You review each removal and choose Delete (remove it) or Keep (turn it into a permanent local event). Adds and updates still apply automatically, so the dashboard stays current; only removals wait for you. Applying requires delete permission, so a shared display shows the badge but can't act on it without a parent.
- Deleting an event in Prism now sticks. Previously a synced event you deleted could reappear on the next sync; a tombstone now keeps it gone (and Google deletes also propagate back to Google).
- Calendar management moved onto the Calendar page. Connected calendars, groups, hours, and iCal subscriptions now live behind a Manage button on the calendar itself instead of buried in Settings.
- Honest sync counts + a smoother sync. The sync toast reports what actually changed ("2 added, 1 updated, 3 flagged for review") instead of the total re-pulled, a manual sync refreshes the calendar immediately (no page reload), and the Add-Event date field is now clickable to change the date, not just the time.
Settings¶
- New "General" section groups the household basics that were scattered before: Location, Time zone, and Week starts on.
- Household time zone setting: server-side scheduling and syncs (e.g. placing imported meal-plan times) now anchor to your zone; defaults to your browser's.
- Set your weather location by ZIP / postal code with a clean, single-result lookup (no API key required), instead of fiddly free text.
[1.9.0] – 2026-07-24¶
Security-hardening release from a full codebase audit. It closes a cluster of access-control gaps on the API, adds server-side-request-forgery guards to the calendar/photo integrations, hardens sessions and OAuth, and updates dependencies carrying published advisories. No changes to how Prism behaves for you day-to-day, and no database migration. But if you run Prism on a network-exposed Home Assistant ingress, this is a recommended upgrade.
Security: Access control¶
- Item-level edit/delete on events, chores, calendars, and photo sources now enforce the same permissions as the rest of the app. Several
PATCH/DELETEendpoints checked only that you were signed in, not which role you had, so a child, guest, or a narrowly-scoped API token could edit or delete family data (and, for calendars/photo sources, cascade-delete the linked source and its on-disk originals) by addressing it directly by id. Every one of these now applies the owner-or-role check the collection routes already used. Chore completion is now anchored to the signed-in caller, closing an approval bypass where a child could self-approve by supplying a parent's id. - API-token scopes are enforced on the admin routes. Bearer tokens were treated as full parent on the database and backup routes regardless of their scope; a
voice-scoped token could truncate/seed the database or download/restore/delete backups. Scopes are now honored. - Kiosk PIN lockout can no longer be brute-forced (the lockout counter is keyed on the resolved user, so failed attempts actually accumulate).
Security: Server-side request forgery (SSRF)¶
- CalDAV, CardDAV, and Immich outbound fetches now validate the target address. A signed-in parent could previously point one of these at a loopback / private / cloud-metadata address and use Prism to probe the internal network. All three now reject private targets before connecting, the CalDAV test endpoint no longer leaks whether an internal host exists, and Immich no longer follows a redirect to an internal host.
Security: Sessions, OAuth & storage¶
- Sessions now have an absolute lifetime (parent 30 days / child 7 / guest 1 hour) on top of the existing sliding window, so a stolen session cookie can't be kept alive indefinitely.
- Google and Microsoft OAuth now verify a single-use state nonce, matching the Kroger flow, closing a window where a valid
codecould bind an attacker's account (or an attacker-chosen owner) to the dashboard. The Microsoft photo-source callback, previously unauthenticated, now requires a signed-in parent. - The service worker no longer caches authenticated
/apiresponses to disk: previously messages, family, tokens, and other per-session data were written into on-disk Cache Storage and outlived the session on a shared kiosk. - Avatar and recipe-image URLs validate the id before reading from disk, rejecting path-traversal payloads.
Security: Dependencies¶
- Updated dependencies carrying published advisories: Next.js (
15.5.21), drizzle-orm (0.45.2), node-ical (0.27.1, which also drops a vulnerable bundledaxios), undici (6.27.0), and dompurify (3.4.x), plus dev tooling (postcss, next-tooling alignment). The abandonednext-pwa/Workbox build chain is a known remaining item slated for a separate migration.
Fixed: Correctness¶
- The API rate limiter can no longer permanently lock you out. If Redis dropped the window's expiry (e.g. a crash at the wrong moment), the counter never reset and you'd stay blocked; the window now self-heals.
- Weather "Morning / Afternoon / Evening" temperatures now bucket by the forecast location's timezone, not the server's UTC clock, so day-parts land on the right hours and don't roll to the wrong day.
- A Google calendar is only auto-disabled after 3 consecutive 404s, not three assorted failures. Transient network/5xx blips no longer count toward disabling a calendar that still exists.
- The travel globe re-highlights trip stops correctly when you select or deselect a trip.
Fixed: Photos¶
- Immich album photo sources now sync on Immich v3. An album-backed source synced zero photos on Immich v3: the old
GET /api/albums/{id}listing returns an empty asset array over a share key, so the sync finished without error and added nothing. Prism now lists album assets via the paginatedPOST /api/search/metadataendpoint (requesting EXIF so photo GPS still reaches the Travel Map); thumbnail and original downloads were already fine and are unchanged. Thanks @guylenical for the report and the suggested fix (verified against a live v3 instance). Closes #154.
[1.8.14] – 2026-06-29¶
Fixed: Display¶
- Emoji now render on devices without a system emoji font. Prism uses Unicode emoji throughout the UI (Goals 🎯, Birthdays 🎂, shopping categories 🛒, Points 🏆, the avatar picker, …); these render in the viewing browser using the device's emoji font, so on a minimal client with none installed (e.g. a bare Raspberry Pi OS / Chromium kiosk) they showed as empty "tofu" boxes (□). Prism now bundles the Noto Color Emoji webfont and adds it to the font stack after the system emoji fonts, so it serves the glyphs itself and emoji render on any client regardless of installed fonts. The font is subsetted by Unicode range, so a browser only downloads the small chunks for the emoji actually on screen. Devices that already have a native emoji font keep using it (no extra download). Thanks @theg00se1030 for the report. Closes #145.
[1.8.13] – 2026-06-29¶
Added: Integrations¶
- Each Integrations card now shows which account it's connected to: "Connected as
you@example.com". Previously the cards showed that a provider was connected but not which account, so anyone running split accounts under one provider (e.g. a personal Google for calendars plus a family Gmail for school-bus emails) couldn't tell from the card which account each feature used. Prism now captures the account's email during the OAuth login (Google, Microsoft, and Gmail) and shows it on the provider card and its Account sub-section; split-account setups show the primary plus a "+N more". Existing connections show no email until you click Re-authenticate on the card: the email is only captured on a fresh login, and there's intentionally no backfill of stored tokens. The new login adds read-only identity scopes (Googleopenid email, MicrosoftUser.Read); Gmail reuses its existing scope. Closes #100.
[1.8.12] – 2026-06-28¶
Fixed: Integrations¶
- Google & Microsoft OAuth redirect URIs are now derived from the request, fixing
redirect_uri_mismatch. Previously the redirect URI came from a static*_REDIRECT_URIenv var (defaulting tolocalhost), so anyone whose env var didn't byte-match what they'd registered in the provider console hitError 400: redirect_uri_mismatchon Connect. All Google (Calendar, Gmail/Bus, Tasks) and Microsoft (OneDrive, Tasks) flows now build the redirect URI from the incoming request's host/proto (honoringX-Forwarded-Host/-Protobehind a reverse proxy), the same approach Kroger already uses, so the URI always matches the host you started from, and/authorizeand/tokenstay in lockstep. The env vars still work as a fallback. Existing connections are unaffected (token refresh doesn't use the redirect URI). Closes #124.
[1.8.11] – 2026-06-27¶
Fixed: Home Assistant addon¶
- The bundled-database addon now actually starts: and survives restarts. With
bundled_db: true(the default), the addon failed to boot at all: Alpine's Postgres defaults its Unix socket to/run/postgresql, which doesn't exist in the container, sopg_ctldied withcould not create lock file "…/.s.PGSQL.5432.lock": No such file or directoryand the dashboard never came up. A second bug then crash-looped the addon on its first restart/update: the base schema (a fullpg_dump) was re-applied on every boot, and itsADD CONSTRAINTstatements aren't idempotent (relation "…" already exists). The entrypoint now creates the socket directory on every start, and applies the base schema only when the database is empty (mirroring the standalone deploy, where Postgres' init dir runs it once), letting the idempotent migration step handle every later boot. It also dumps the Postgres log on a startup failure instead of swallowing it behindpg_ctl's "Examine the log output". Thanks @joe-cole1 for the report and logs. Closes #81.
Changed: Backups¶
- Off-site sync and the dead-man healthcheck are now opt-in via
.env.RCLONE_REMOTEandHC_URLwere previously hardcoded indocker-compose.yml, so every deployment shared one set of endpoints. They now default to empty: set your ownRCLONE_REMOTE(an rclone remote for off-site copies) andHC_URL(your own healthchecks.io check) in.envto enable them, or leave them blank to keep backups local-only. The backup tunables (BACKUP_HOUR,RETENTION_DAYS,RCLONE_RETENTION_DAYS) are overridable the same way. See the new Backups section in.env.example.
Fixed: Integrations¶
- Clicking "Connect" before configuring OAuth now shows a setup prompt instead of a raw JSON error. If you skipped OAuth setup during onboarding and then hit Connect on the Google / Gmail / Microsoft cards, the browser landed on a bare
{"error":"Failed to initiate … authentication"}page. The init routes now detect the not-configured case and redirect back to the Integrations page with a clear banner, pointing to the Setup Wizard (where you enter your OAuth credentials) and naming the required env vars, instead of a dead JSON page. Thanks @joe-cole1 for the report and the "make this clearer for dummies who skipped onboarding" nudge. Closes #108.
[1.8.10] – 2026-06-19¶
Fixed: Tasks¶
- Newly-added tasks no longer vanish once a household has 100+ tasks. The Tasks list fetches a capped number of rows (100) ordered by due date; with many tasks (especially ones without a due date, which sort last) the newest tasks fell past the row limit and were silently dropped from the fetch. They saved to the database fine but never appeared in any view (touch display or PWA). The fetch now orders incomplete tasks first with a newest-first tiebreaker, so active tasks are never truncated out. Display order is unchanged (the client still sorts the visible list).
Fixed: Touch keyboard¶
- On-screen keyboard no longer dismisses itself when you tap Shift. On touch displays (e.g. Raspberry Pi kiosks), tapping Shift (or a symbol key) blurred the focused input, so the global focusout handler hid the keyboard. The keyboard container now keeps the input focused on tap: a
mousedownpreventDefault, since simple-keyboard swallows the pointerdown the existing handler relied on. Thanks @theg00se1030 for the precise root-cause analysis. Closes #125.
[1.8.9] – 2026-06-16¶
Fixed: Security / Login¶
- 5–6 digit PINs can now actually be used. PIN length is now a uniform family-wide setting (4–6 digits, like an iPhone passcode), chosen in the setup wizard and changeable in Settings → Security. Previously PIN creation allowed up to 6 digits while every login/unlock pad was hard-coded to 4 and auto-submitted at 4, so any 5–6 digit PIN could never be entered and the member was locked out. All five PIN surfaces (login, settings gate, quick-switch, away-exit, babysitter-exit),
PinEditModal, and the family API now read and enforce the configured length. Addedscripts/reset-pin.jsfor offline recovery of a locked-out member. Closes #123.
[1.8.8] – 2026-06-16¶
Changed: Mobile¶
- Per-person list views become a swipeable carousel on phones: Chores, Tasks (flat + nested), Wishes, and Gift Ideas all switch to a CSS scroll-snap carousel when viewed on mobile with more than one group: each profile takes the full viewport width and the user swipes left/right between people while still scrolling vertically inside the active profile's list. Desktop / tablet behavior unchanged (min-width columns + horizontal scroll). Cleaner than the previous "1.5 profiles visible at 220 px each" feel on phones.
Fixed: List views¶
- Chores person filter now actually filters the rendered columns:
ChoresView'schoresByUsermemo iterated every family member when building columns, so selecting a single-person filter under Group:Person still showed empty columns for everyone else. Tasks and Wishes already filtered correctly; Chores now matches. Unassigned column also hides when a person filter is active (the user explicitly asked to see only those people). - Per-person grids no longer squeeze when the family is large: Chores, Tasks (flat + nested), Wishes, and Gift Ideas all used
grid-cols-2 md:grid-cols-3which crammed 7 people (5 kids + 2 adults) into 3 narrow columns × 3 rows on a portrait tablet. Switched all five views togridTemplateColumns: repeat(N, minmax(220px, 1fr))+overflow-x-auto. Each column gets at least 220px; if the viewport can't fit every column comfortably, the grid scrolls horizontally, same shape across all list views. Closes #105. - Desktop carousel reveal: per-person carousels on Chores, Tasks (flat + nested), Wishes, and Gift Ideas now render left/right chevron buttons over the column area when more profiles exist than fit on screen. Mouse users were otherwise stuck with no obvious affordance (touch users already had the snap-swipe gesture). New
CarouselArrowscomponent scrolls 85 % of the visible width per click and fades at the edges. - Tasks PersonFilter now hides unselected columns: under Group:Person, picking a subset in the filter chip-bar dropped the unselected people's empty columns (and the Unassigned bucket) entirely, matching Chores/Wishes.
useTaskGroupingtakes a newfilterPersonparam and the column list is filtered before the grid renders. - Gift Ideas intermittent "Something went wrong" on refresh:
useReffor the carousel scroll container was placed after three conditional early returns (!activeUser/loading/error), so the hook count changed between renders depending on data state and React threw on roughly half of mounts. Moved the ref to the top of the component. - Wishes per-member cards now fill row height:
WishesViewwas missing theh-screen flex flex-colwrapper that Chores/Tasks/Shopping use, soflex-1on the content area never engaged and the columns collapsed to natural content height. Wrapper added andMemberWishCardroot now stretches withh-fullso the inner body'sflex-1 overflow-y-autoengages. - Gift Ideas tab shares the PersonFilter with Wishes: the filter chip-bar now also renders on the Ideas tab and the selection drives which member columns appear there. Previously Ideas always showed every member regardless of filter.
Changed: Shopping¶
- Mobile list switcher is now a swipe carousel: on phones with more than one shopping list (Groceries / Costco / Home Depot / …), the horizontal pill bar collapses into a compact prev / dots / next indicator and swiping left/right inside the content area switches between lists. Single-list households see no affordance. Desktop 3-column category grid is unchanged. Categories aren't peer entities (it's nice to see the whole list at once), but lists are, which is what makes the swipe metaphor fit only at the list level.
Added: Triage¶
needs-replyauto-label workflow: new.github/workflows/needs-reply.ymladds theneeds-replylabel to any issue/PR when a non-owner non-bot comments (or opens it), and removes the label when the owner replies. Triage view lives at /labels/needs-reply, one navigable place to see everything an external user is waiting on.
Fixed: Uploads¶
- Photo and avatar uploads no longer 500: the app container runs as uid 1001 (
nextjs) butscripts/install.shcreated the bind-mounteddata/directory owned by the host user, sofs.mkdirunder/app/data/photos(and/app/data/avatars) hitEACCESand every photo upload, 30-minute photo sync, and avatar upload failed with a 500.install.shnowchownsdata/anduploads/to1001:1001via a throwaway root container (no hostsudoneeded), with a fallback warning and a troubleshooting note in the install docs for existing deployments. #130
Fixed: Backups¶
- Photos, avatars and recipe images are now backed up off-site: the backup container synced an empty
uploads/directory while all user assets actually live underdata/(src/lib/config/runtime.ts), so they never reached cloud storage. It now syncsdata/(regenerablephotos/cacheexcluded). The app container also only bind-mounteddata/photos, leaving avatars and recipe images in the container's writable layer where they were lost on rebuild. The wholedata/directory is now persisted. #127 - Backups verify the off-site copy before reporting success:
rclone copy/synccan exit 0 on a partial or silently-dropped upload, so a green healthcheck didn't prove the data actually landed. Added anrclone check --one-waypass before the success ping; any mismatch pings/failinstead. Also consolidated the duplicate cron + in-container database-dump jobs into the single container job (porting over the cron's healthcheck ping and dump size check), and fixed an rclone OAuth token-refresh error caused by mountingrclone.confas a single file (which can't be rename-replaced, now seeded to a writable path at startup). #127, #128, #129
Fixed: Calendar¶
- Task-only CalDAV sources no longer show a false "stale / failed" status: iCloud reminder lists (no event component) never run the event-sync path, which was the only code that advanced
last_syncedand clearedsync_errors, so they appeared stuck on an old date with a stale error even while task sync ran cleanly every cycle.syncCalDAVTasksnow refreshes those fields itself (and records task-sync errors for task-only sources), leaving event-capable sources'sync_errorsowned by the event path. #131
[1.8.7] – 2026-06-01¶
Fixed: Distribution¶
- HA addon arm64 build now succeeds: v1.8.6's release workflow shipped amd64 cleanly but the aarch64 matrix job died with
qemu: uncaught target signal 4 (Illegal instruction)duringnpm run build: SWC (Next.js's Rust-based compiler) emits ARM NEON / SIMD instructions that QEMU TCG can't translate when running an arm64 binary on x86 host. Switched the arm64 matrix entry fromubuntu-latest + setup-qemu-actionto GitHub's free public-repo nativeubuntu-24.04-armrunner. Real ARM hardware, no emulation, no instruction-set gap. arm64 build time should now match amd64 (~5 min) instead of 30-60 min QEMU. Bothghcr.io/sandydargoport/prism-ha-amd64andghcr.io/sandydargoport/prism-ha-aarch64should publish on tag pushes from this version onward.
[1.8.6] – 2026-06-01¶
Fixed: Distribution¶
- HA addon release pipeline now builds successfully: v1.8.5's tagged release was the first run of the release pipeline and exposed two compounding bugs: the addon
Dockerfileusedapt-get(Debian) against HA's Alpine-based supervisor base images, and even withapk addHA's base ships Node 20 whilepackage.jsonrequires Node ≥24. Switching the addon base fromghcr.io/home-assistant/<arch>-basetonode:24-alpineresolves both: it's a published multi-arch image with the guaranteed Node version and lets HA Supervisor run the container as-is (Supervisor doesn't care which base the addon uses).apt-getblock rewritten asapk addwith Alpine package names;run.shswitched from Debian-style/usr/lib/postgresql/15/bin/paths to bareinitdb/pg_ctlon PATH. Surfaced by @joe-cole1 in #81.
Added: SEO / Docs¶
SoftwareApplicationJSON-LD in the docs site: structured-data entity card crawlers + LLM trainers can scrape without parsing prose:applicationCategory,offers.price: 0,operatingSystem,alternateName(de-disambiguates from GraphPad Prism / LaTeX Prism),featureListwith 13 capabilities. Plus a<meta name="keywords">cluster withglassmorphism dashboard,Skylight alternative,Dakboard alternative,MagicMirror alternative, etc. Hidden from human readers (lives in<head>+ the already-hiddenalternatives.md), respects the PR #87 walk-back of marketing-toned prose in user-facing surfaces.
[1.8.5] – 2026-06-01¶
Added: Distribution¶
- Home Assistant addon (
ha-app/): Prism can now be installed as a one-click HA addon via custom repository. Bundled Postgres + Redis run inside the addon container; all state lives under HA's/datavolume and survives addon updates. Newsrc/lib/config/runtime.tsaddsisHaMode()plusgetDataRoot()/getPhotosRoot()/getAvatarsRoot()helpers so photo and avatar storage automatically use/data/{photos,avatars}in HA mode;PRISM_HA_MODE,PRISM_DATA_ROOT,PRISM_PHOTO_ROOT, andPRISM_AVATAR_ROOTenv vars are respected. Standard tier (single all-in-one container, no nginx/cert work), matches the install-friction constraint surfaced in #81. - HA addon release pipeline (
.github/workflows/release.yml): tag-push (vX.Y.Z) triggers a multi-arch build (amd64 + aarch64 via QEMU) and publishes toghcr.io/sandydargoport/prism-ha-<arch>:<version>+:latest.ha-app/config.yamlnow referencesimage: ghcr.io/sandydargoport/prism-ha-{arch}so HA Supervisor pulls the pre-built image (~30 s install) instead of building from source on the user's host (~10 min).scripts/check-version-sync.shextended to fail ifha-app/config.yamldrifts frompackage.json;scripts/release.shbumps all three (package.json, CHANGELOG, ha-app/config.yaml) in lockstep. Closes #104. Also unblocks the install failure surfaced on issue #81: the original addon Dockerfile used multi-stage paths that don't work under HA Supervisor'sha-app/-scoped build context; the Dockerfile is now self-contained (clones source via git at build time) and only used as a fallback when the published image isn't available.
Added: Settings¶
- Consolidated Integrations page (
/settings?section=integrations): one card per provider brand: Google (Calendars + Tasks), Microsoft (incl. OneDrive), Bus tracking (Gmail), Apple/CalDAV, Kroger, plus a cross-provider Photo Sources card. Each card has a connection status badge and collapsible sub-sections for per-feature wiring; the Account row sits at the top of each card so disconnect / re-auth controls are reachable in one expand. URL anchors (#microsoft-onedrive,#gmail-bus,#caldav-calendars, etc.) deep-link straight to a sub-section. Ships alongside the legacy Connected Accounts / Task Sync / Shopping Sync / Wish List Sync / Photos sections; cleanup pass removing the legacy sections will follow once parity is verified on real accounts. Closes phase 1 of #52.
Fixed: Settings¶
- Sub-section links inside Integrations cards now navigate:
SettingsViewread the?section=query param only at mount, so when an Integrations card's "Open Calendars settings" link changed the URL the content panel stayed put. Added auseEffectthat syncsactiveSectionwith the live URL.
Changed: Integrations¶
- OAuth callbacks land on the Integrations page when initiated from it: Google, Google Tasks (errors), Microsoft (OneDrive), and Microsoft Tasks (errors) callbacks now honor a
returnSection=integrationsflag bubbled through OAuth state, redirecting to?section=integrations#<provider>with the right sub-section auto-expanded. Legacy callers (the still-mounted Connected Accounts section, etc.) keep their existing destinations. Step toward removing the legacy sections, phase 2A of #52.
Removed: Settings¶
- Connected Accounts section retired: fully replaced by the Integrations page in phase 1. The legacy
?section=connectionsURL still works:SettingsViewredirects it to?section=integrationsso OAuth callbacks in flight at deploy time, bookmarks, and the few remaining cross-links (Calendars page, Task Sync page) all land somewhere sensible. Sidebar nav entry gone; section file deleted. Phase 2B-1 of #52. - Task Sync / Shopping Sync / Wish List Sync nav entries retired: per-list wiring UI is now embedded directly inside the Microsoft and Google provider cards on the Integrations page, no more page-jump to manage which Prism list maps to which Microsoft To-Do / Google Tasks list. Sections still exist as embedded components, no longer reachable as standalone pages.
?section=tasks/shopping/wishURLs (bookmarks, in-flight OAuth callbacks) redirect to Integrations. OAuth callbacks update to land on?section=integrations#microsoft-tasksetc. when initiated from the new cards (legacy?section=tasks&selectMsList=trueflows still work via redirect). Phase 2B-2 of #52.
Fixed: Mobile¶
- /settings now reachable on iPhone PWA:
MobileNavhad no Settings entry, so a Prism installed as a home-screen PWA on iPhone had zero path to settings (the original "PWA can't reach Photos settings" report turned out to be the entire route being unreachable, not a Photos-specific link). The More menu now includes Settings, and the desktop sidebar collapses to a section selector on<mdviewports so every section remains reachable after landing.
Fixed: Dashboard¶
- Grid no longer locks to interim cold-boot viewport on slow-launching kiosks:
LayoutGridEditorcomputedvisibleRowsandcellSizefromwindow.innerHeightinside auseMemowith no resize listener, so the values were frozen at mount time. On a Wyse thin client booting before its window manager finalized the work area, the dashboard rendered against the smaller interim viewport and stayed that way until the user manually refreshed. NewuseViewportSizehook subscribes toresize+orientationchangeand feeds both memos so the grid re-measures when the viewport settles. Closes #73.
Added: Docs¶
- Apple iCloud integration overview (
docs/features/ICLOUD.md): single-page summary of which iCloud surfaces Prism can integrate and which it can't, with the structural rule (open IETF standards work, CloudKit dead-ends don't). Covers Calendars, Contacts, Reminders, Notes, Photos (shared + library), Find My, Health, iMessage, Apple Music. Cross-linked from Calendar and Photos guides. Saves prospective users from "wait, can't we just pull X from iCloud?" investigations that always hit the same wall.
Changed: Docs¶
- Photos guide drops the "iCloud Shared Album coming in a follow-up" hint: Phase B of the photo sources work was abandoned in late May after Apple migrated public share URLs to a CloudKit-only backend with no public API. The Photos doc now points at OneDrive + the iOS Shortcut as the canonical iPhone path and links to ICLOUD.md for the explanation.
[1.8.4] – 2026-05-23¶
Added: Integrations¶
- CalDAV / Apple iCloud (read-only): Connect any CalDAV server (Apple iCloud (
https://caldav.icloud.com), Nextcloud, Radicale, Baikal, Synology) from Settings → Connected Accounts → CalDAV. Username + app-specific password, encrypted at rest. Discovery picks calendars + Reminders lists; events sync into the sameeventstable as Google/iCal, VTODO items intotasks. New API surface:POST /api/caldav/{test,discover,connect}. Documented indocs/features/CALENDAR.md. Validated against a real iCloud account during the shakedown. Known Apple-side limitation: Reminders lists migrated to CloudKit (most modern iCloud accounts) return placeholder VTODOs only, not actual reminders. The integration filters those placeholders so they don't pollute Tasks, and doesn't materialize a Prism task list for any CalDAV source that returns only placeholders. - iCloud Contacts → birthdays via CardDAV: Optional checkbox on the CalDAV connect dialog ("Also import birthdays from contacts"). Same login, CardDAV protocol, auto-swaps the iCloud hostname from
caldav.icloud.comtocontacts.icloud.com. Every vCard with aBDAYfield feeds the birthdays table. Handles Apple's quirky "no year given" sentinel (literal year1604) by mapping it to the 1904 year-omitted convention. Manual re-sync viaPOST /api/caldav/sync-birthdays; otherwise rides the existing 10-minute calendar sync cron. - Cross-source birthday dedup: A calendar event titled "Alex's birthday" (regex-stripped to "Alex" by the Google sync) and an iCloud vCard with FN "Alex Doe" no longer create two birthday rows for the same person. The
upsertBirthdayhelper merges by token-prefix + same month/day, keeps the longer name, and prefers the non-1904 year when one source has a real birth year. Conservative: same first name + different last names ("Jordan Smith" vs "Jordan Doe") are treated as distinct people.
Added: Dashboard¶
- Save As → overwrite existing dashboard: The Save-As flow no longer just creates a new dashboard via a name prompt. The new dialog lists every existing dashboard with an "Overwrite [name]" button (with a confirm step) and a separate "Save as new" input. Overwrite preserves the target's name, slug, and default flag. Only widgets + screensaver + orientation are swapped.
Changed: Dashboard¶
- Default dashboard
/honors Display Settings → Font Scale: The zoom wrapper that scales a dashboard up or down lived only at/d/[slug]/layout.tsx, so the slider had no visible effect on the main dashboard.src/app/page.tsxnow fetches the default layout'sfontScaleand applies the same wrapper. - Dashboard-switch flash eliminated: When activating or switching dashboards, the brief loading window used to render
DEFAULT_TEMPLATE(weather UL, clock UR, meals bottom) under the page chrome before the saved layout fetched. Established users perceived this as "Prism flashed a different dashboard." During the API-fetch window the dashboard now renders empty for the same fraction of a second; theDEFAULT_TEMPLATEfallback fires only when no saved layout exists at all (genuine first-run). - Per-widget text scale (S/M/L/XL) works on the dashboard, not just the screensaver: The
zoom: textScaleCSS lived on the grid-cell wrapper, which interacts inconsistently with CSS grid layout. Moved onto the inner content wrapper so the dashboard renders apply it the same way screensaver does. - Widget picker alphabetized on both sides: The visible-widgets table in the Widgets popover followed
WIDGET_REGISTRYinsertion order while the "+ Add widget" picker was sorted alphabetically, so Birthdays and Bus Tracker showed up in arbitrary positions on the left and alpha-positioned on the right. Both views now sort by widget label. - Clock widget can shrink to a slim strip:
minHlowered from 8 to 4 grid rows. Defaults unchanged.
Changed: Widgets / Weather¶
- Forecast pill track stays visible under any custom text color: The 7-day forecast row uses a pill background that previously read
bg-muted-foreground/25.WidgetContaineroverrides--muted-foregroundto match the user-chosen text color (so headings inherit), which turned the pill into white-on-white when white text was picked and into a faded tint of any other chosen color. The pill bg + ring now usebg-black/10 dark:bg-white/15, decoupled from the text-color override.
Changed: Auth¶
- Settings PIN gate modal tightened: Mirrors the spacing pass applied to QuickPinModal: smaller padding, tighter avatars, smaller PIN dots and number-pad buttons. Touch targets stay at 48px (above the 44px Apple HIG minimum).
- Sign-in toast fires for every blocked mutation: When a signed-out viewer edits a field, ticks a chore, adds a shopping item, or otherwise attempts any
/api/*mutation, the call returned 401 and the UI silently failed to update, no signal that auth was the cause. A globalwindow.fetchinterceptor inAuthProvidernow catches mutation 401s and toasts "Sign in to make changes. Enter your PIN to save edits." Debounced 2.5s so a save burst fires one toast, not ten. Suppressed while the PIN modal is already open. Limited to/api/*paths and POST/PUT/PATCH/DELETE so third-party fetches and the initial session-check GET aren't affected.
Changed: Integrations¶
- Gmail/bus "Token expired" false alarm removed: The integration status surface used to warn whenever the stored
expires_atwas in the past. Gmail access tokens have a 1-hour TTL, so the warning fired for hours after every reconnect even thoughbus-tracking-sync.tswas auto-refreshing silently on the next tick via the stored refresh token. Genuine refresh failures (TokenRevokedError) delete the credential row outright, flipping the badge to "Not Connected". That's the only user-actionable failure. Stopped exposingexpiresAtfrom/api/integrations/statusand dropped the warning UI. - Task provider picker no longer lists Todoist + Apple Reminders as "Coming soon": Both were hardcoded
disabledProvidersentries with no roadmap behind them. Apple Reminders is permanently impossible (CloudKit-only). Todoist isn't on the build list.
Changed: Weather¶
- Sun + moon info now in the header row: Sunrise (lucide
Sunriseicon, amber) and sunset (Sunset, orange) times sit alongside Feels Like / Humidity / Wind in the upper-right of the weather widget. A moon-phase glyph + phase name (e.g. "Waning Gibbous") sits on its own line above the sun row. - Daylight arc still carries its anchor strip: sunrise / "Xh Ym" duration / sunset under the curve.
[1.8.3] – 2026-05-22¶
Added: Dashboard¶
- Double-tap any widget to magnify it (interactive Dashboard only). The widget snaps to a centered ~84vw × 84vh modal with the rest of the dashboard dimmed behind. Auto-collapses after 8 seconds of inactivity (timer resets on any tap or scroll inside the magnified widget), or immediately on Escape / backdrop tap. Re-renders the widget at the larger size so any compact-mode threshold (e.g., Weather widget's
gridW < 12mode) unwinds into the full layout. Gated to the interactive render path only: Screensaver, Away Mode, and Babysitter Mode don't wrap their widgets in the provider, so the handler isn't attached there.
[1.8.2] – 2026-05-22¶
Adds an MCP server (
.mcp/) that exposes Prism's REST API as Model Context Protocol tools, so AI clients (Claude Desktop, Claude Code, Cursor, Gemini CLI, Gemini Code Assist, VS Code Copilot Chat) can read and write family data through natural-language chat.
Added: Integrations¶
- MCP server for Prism (
.mcp/): Self-contained Model Context Protocol server that exposes the Prism REST API as MCP tools, so AI clients (Claude Desktop, Claude Code, Cursor, Gemini CLI, Gemini Code Assist, VS Code Copilot Chat) can read and write chores, tasks, events, shopping, messages, meals, goals, recipes, maintenance, points, weather, and family data directly from a chat window. Uses the existing API-token auth (Settings → Security → API Tokens) via env vars in the client config. Built on@modelcontextprotocol/sdkv1.29+ with stdio transport. Returns both legacycontenttext and modernstructuredContentobjects (2025-06-18 spec) so parsed-object-aware clients can skip a JSON parse step. Future remote/hosted variant would use Streamable HTTP + OAuth 2.1 per spec 2025-11-25. Current build is local-subprocess only. See the.mcp/README on GitHub for setup.
[1.8.1] – 2026-05-21¶
Weather widget overhaul (sun + moon on one altitude arc, red→orange→amber gradient by altitude, per-day moon phase glyphs, Apple-style temperature pill tracks), bus tracker fixes for the duplicate AM school stop and PM route ordering, and a round of layout editor polish (smarter widget placement, alphabetized add menu, sign-in gate, edit-mode click protection).
Added: Weather¶
- Sun + moon altitude arc: The daylight chart plots both bodies on the same 24-hour timeline, with peak heights driven by true celestial altitudes from
suncalc(zenith = full arc height, sub-zenith proportionally smaller). Summer sun visibly arcs higher than winter sun, and the moon arc varies with declination. Sun arc is colored by altitude via an SVGlinearGradient: red at the horizon, orange at low altitude (~25°), amber at zenith, matching the atmospheric-scattering color shift you'd see in the sky. Sun dot's fill follows the same altitude bucketing so a low sun glows red/orange. Moon arc is blue when above horizon, muted slate below; the moon glyph at the current position renders the actual phase shape (full = filled circle, new = outlined empty circle, crescents and gibbous show only the lit fraction).WeatherDatanow carriesmoonrise,moonset,moonPhase,moonIllumination,moonPhaseName,lat,lonfrom all three providers (Open-Meteo, OpenWeatherMap, Pirate Weather) via a sharedsrc/lib/integrations/moon.tshelper.suncalcis purely local, no API key or network call. - Per-day moon phase glyphs: A small phase glyph sits next to the weather icon on every multi-day forecast row. Phase is computed locally via
suncalc.getMoonIlluminationat the day's local noon (phase angle is global, no lat/lon needed). - Apple-style unified temperature pill track: Each forecast day's temperature range now sits inside a full-width pill track that's the same width across every row, so the colored bars align visually instead of starting at different X positions. The day's range within the week's min/max is shown by the position and width of the inner colored bar. Track background uses
bg-muted-foreground/25plus a thin 1px inset ring so the container reads as a defined edge. - Daylight duration tinted amber: The "Xh Ym" label between sunrise and sunset is now amber to match the sun arc instead of muted gray.
Added: Layout Editor¶
- Smarter widget placement: New widgets land in the first free slot (scan top-to-bottom, then left-to-right inside each row) instead of stacking at the bottom. Top-row gaps get filled first; if a row has horizontal room, the widget slots in next to existing ones. Six-case jest suite covers empty grid, side-by-side fit, full-row fallthrough, top-row-gap fill, hidden-widget skip, and custom grid width.
- Alphabetized Add Widget dropdown: Hidden widgets in the dropdown sort by display label so "Bus Tracker" lands under B rather than its internal
busTrackingid. - "Mini-map" rename: The left toolbar's popover button (mini-map thumbnail + screen-size toggles + validation issues) is now labeled "Mini-map", distinct from the right toolbar's "Preview" (which toggles the measure-mode render at a target screen's actual dimensions). Two buttons named "Preview" had been doing different things.
- Sign-in gate for edit mode: Clicking the Edit button while signed out now toasts "Sign in to edit. Log in as a parent to edit the dashboard layout" rather than silently doing nothing. The button stays hidden for children (signed in as a non-parent role). The sessionStorage edit-flag re-entry now requires
activeUser.role === 'parent'so a stale flag can't re-engage edit mode after a logout. - No accidental navigation in edit mode: Internal widget links and buttons (e.g. the Travel widget's "Open the map →") can't navigate away mid-edit and discard unsaved layout changes.
pointer-events: noneon the widget content swallows clicks while drag + widget select still work because both bubble up to the outer wrapper'sonClickand dnd-kit listeners.
Bug Fixes¶
- Bus tracker: duplicate school stop on AM, PM ending at school instead of home: The train map rendered both a "School" checkpoint (placeholder name from FirstView email ingestion) and a separate
schoolNamediamond, producing a duplicate node at the end of AM routes. PM routes ended visually at the school checkpoint instead of the family's home stop.buildNodesnow recognizes "Home"/"School" checkpoints (case-insensitive, and matches against the route'sstopName/schoolNameproper noun) as the same semantic terminals, then arranges them by direction: AM = [intermediates, home, school-diamond], PM = [school-diamond, intermediates, home]. The PM school diamond carries anisOriginflag so it lights up the moment any PM event has fired, guard for legacy routes where the school checkpoint sortOrder doesn't match the chronological start. - Weather widget Date hydration: Moonrise and moonset Date objects were lost in the JSON round-trip from
/api/weatherto the client;SunriseSunsetArcthen threw"moonrise.getTime is not a function"on the string forms.transformWeathernow hydrates moonrise/moonset alongside the existing sunrise/sunset, forecast.date, and hourly.time fields. - Mobile dashboard data never populated: Cards stayed on "No tasks" / "No upcoming events" / "Lists are clear" even though
/api/*returned real data. BothDashboard.tsxandMobileDashboard.tsxwere independently callinguseDashboardData(), causing every domain hook to fire twice in parallel against the same URL. The duplicate inside StrictMode's dev double-mount left MobileDashboard'suseFetchpermanently stuck atloading:true, data:[]. Fix: lift the data hook to a single call inDashboard.tsxand pass it toMobileDashboardas a prop. Halves the network traffic on dashboard load and eliminates the race.
GitHub / Docs¶
- Stale bot + issue templates: Added
.github/workflows/stale.ymlto auto-close inactive issues / PRs and.github/ISSUE_TEMPLATE/for structured bug and feature submissions. - README: install commands collapsed by default: Both Quick Start options are now wrapped in
<details>blocks so the README narrative ("Behind the project", contributing, roadmap voting) sits closer to the top of the page.
Internal¶
- Screenshot capture: detect Unicode ellipsis in loading states:
waitForContentReadyregex now matches both...and…so the weekend page (which renders "Loading…") no longer gets captured mid-fetch. BumpedweekendsettleMs1000 → 3000 as a safety margin.
[1.8.0] – 2026-05-17¶
Send-to-Kroger cart push (every Kroger banner, OAuth per-user, SKU picker with normalized unit prices and per-item caching), recipe import from pasted OCR text with section-aware ingredients and ½×–4× scaling pills, server-side calendar sync cron with a ±90/365-day window, mobile PWA becomes agenda-only, plus the foundation Voice API for the upcoming Alexa / Home Assistant integration. Same
git pull && docker-compose up -d --buildupgrade.
Added: Shopping¶
- Send to Kroger (and every Kroger banner: Mariano's, Ralphs, King Soopers, Fred Meyer, QFC, Smith's, Fry's, Harris Teeter, Pick 'n Save, Metro Market, Pay Less, Food 4 Less, Foods Co., Bakers' Plus, City Market, Copps, Dillons, Gerbes, Jay C, Ruler Foods): Push your Prism shopping list straight into your online Kroger cart for pickup or delivery. Per-user OAuth 2.0 with encrypted token storage in a new
user_kroger_connectionstable. A picker walks you through each item with up to 5 SKU candidates, image, price, and a normalized unit price (lb / fl oz / ct) so candidates within a page are directly comparable. Quantity controls let you bump cart count, "search again" lets you refine when the parser strips too much, and the chosen SKU is cached per shopping item (shopping_items.kroger_product_id) so weekly staples become one-tap after first pick. Per-user store picker via Kroger's Locations API binds location-aware pricing to your preferred Mariano's (or any banner). Settings → Shopping has the connect/disconnect flow plus inline credentials entry, no setup-wizard re-run needed. Dynamic OAuth redirect URI so a WAN https hostname and a LAN192.168.x.x:3000URL both work if both URIs are registered on the Kroger dev app. - Recipe ingredients can carry section headings: Lines like
Fries:orMeatballs:inside an ingredient list are now stored as{ heading }entries alongside the existing{ text }entries. Rendered bolded in the recipe detail; filtered out of the add-to-shopping-list payload (they're visual grouping, not items).
Added: Recipes¶
- Import recipe from pasted text: New "Paste recipe text" entry in the Recipes Add menu. Heuristic parser splits OCR'd / clipboard text into title (AP-style title-cased, with
a / an / the / and / or / for / of / withand friends staying lowercase mid-title), prep/cook/total time, servings, ingredients, and instructions. RecognisesIngredients:/Instructions:section headers, "Step N:" prefixes, comma modifiers ("seeded and sliced", "peeled and deveined")," or "alternatives, parentheticals, and inline step markers ("1. Preheat. 2. Mix." gets line-broken). Pre-fills the existing recipe form so the user reviews before saving. Designed for iOS Live Text from a photo of a recipe card. - Per-recipe photo upload: Each recipe can carry its own image. Phone camera or photo library (no
captureattribute, iOS shows the native sheet), saved atdata/recipe-images/<recipeId>.jpg, served viaGET /api/recipes/<id>/image, sharp pipeline (auto-rotate from EXIF, resize to ≤1200px, JPEG quality 85). Replace / Remove controls inline in the form. ≤10MB, magic-byte validated, rate-limited. - Recipe scaling: quick ½× / 1× / 2× / 3× / 4× pills: Detail modal shows pill buttons next to the +/- servings adjuster. Active multiplier highlights. ½× rounds up to the nearest whole serving so a 3-serving recipe lands on 2.
- Recipe shopping items scale on add:
scaleIngredientapplies the active multiplier to the ingredient text when sending to the shopping list, not just visually in the modal.
Added: Calendar¶
- Server-side calendar sync cron: A 10-minute
setIntervalininstrumentation.ts(delegated to a node-onlylib/server/calendarSyncCron.tsso the edge bundle isn't dragged into node-ical / node:crypto chains) keeps Google + iCal calendars in sync without depending on anyone having the dashboard or calendar page open. Sync window expanded from ±30 days to −90 / +365 so far-future school-year, sports-season, and holiday-card events actually show up. Events outside the window stay in the DB forever. The delete-on-remove pass only operates inside the window. Disable withPRISM_DISABLE_CALENDAR_CRON=true. - Mobile PWA: calendar is agenda-only: Mobile viewport no longer renders the Agenda/Day toggle, prev/next chevrons (no-op for agenda), or Today button. Header reads "Upcoming Events". useEffect forces agenda on mobile.
Added: Voice / API tokens (carried from earlier work in this Unreleased block)¶
- Voice API foundation (
/api/v1/voice/*): New versioned, token-authenticated API surface for voice and home-automation integrations. First endpoint:GET /api/v1/voice/calendar/todayreturns today's events with a pre-formatted natural-languagespokenfield ("Today you have Soccer Practice at 4 PM.") so callers don't need their own templating. Reuses the existingapiTokensBearer-token system; per-token rate-limited at 60 req/min. Documented indocs/voice-api.md. Phase 1 of the Alexa + HA distribution plan, additional intents (shopping/add, chore/complete, calendar/upcoming, etc.) coming in follow-ups. - Voice token scope (
voice):withAuthnow supports atokenScopeoption that rejects session-cookie callers and requires an API token whose scopes include either the named scope or*. The Voice API usestokenScope: 'voice'so a leaked browser session cannot reach it, and Voice tokens issued withscopes: ['voice']are confined to/api/v1/voice/*(vs. the legacy['*']default which grants full account access). Token-creation validator now restricts scopes to the known set['*', 'voice']. - Voice API endpoints: Six new endpoints filling out the
/api/v1/voice/*surface:GET /family,GET /calendar/upcoming,GET /tasks/today,POST /shopping/add,POST /chore/complete,POST /message/post. Each returns the shared{ ok, spoken, data }shape.chore/completeenforces the documented security rules: completions inherit the chore'sassignedTo, voice cannot bypassrequiresApproval(pending completions stay pending until a parent approves in-app), and ambiguous chore names (e.g. both children have "Feed the dog") return anok:falsedisambiguation prompt withdata.candidatesfor the caller to resend withassignee. Phrase-builder tests grew from 7 to 18 cases covering the new spoken templates. - Six more voice endpoints (Alexa Phase 2): A further round rounds out the read surface:
GET /weather/today,GET /bus/status,GET /birthdays/upcoming,GET /meals/today,GET /chores/today, andGET /message/recent, each returning the same{ ok, spoken, data }shape with a pre-formattedspokenline. All are documented indocs/voice-api.md. - API token scope picker in Settings: The Security section's token issuer now shows a scope dropdown ("Voice API only (recommended)" / "Full access (legacy)") and the issued-token list shows each token's scopes as a colored badge (Voice = blue,
*= amber). Voice is the default: picking the smallest scope that works means a leaked token can't reach data outside its surface.
Bug Fixes¶
- Shopping items: rate limit raised 30/min → 120/min: Recipe imports add ingredients one-by-one in a sequential loop; long lists were hitting the cap with "Failed to add item: too many requests". Proper fix (a batch endpoint) is a follow-up.
- Mobile calendar header overflow: Picker modal now fits an iPhone in portrait by trimming dialog padding, gap, image size, and price-column fixed width. Product names wrap to 2 lines instead of truncating. Review-screen items break-words.
- Calendar: recipe form / shopping list error visibility: "Failed to save recipe" / "Failed to add ingredients" / "Failed to remove photo" now propagate the actual server error so a stuck user can self-diagnose.
- Shopping list: assigning to a family member: Members whose
idwasn't exposed (unauthenticated/api/familyreturnsid='') are filtered out of the assign-to picker so selecting them can't silently setassignedTo=''and fail validation. - Recipe photo: iOS file input: Removed
capture="environment"so iOS shows Photo Library / Take Photo / Files instead of forcing the back camera. - Meal page order: Snack now appears between lunch and dinner (matching
CalendarView.sortMealsByType) instead of after dinner.
Internal¶
- Kroger picker: modifier-aware ingredient stripping for product search:
parseShoppingQuantitystrips leading quantity + unit, then drops everything from the first comma," or ", parenthetical, or" to taste"onwards, so"1 Fresno pepper, seeded and sliced, or ½ teaspoon crushed red pepper flakes"searches Kroger for"Fresno pepper". Original text stays visible in the picker title; "Searching Kroger for X" subtitle shows the cleaned query. Manual override input lets the user refine without skipping the item. - Kroger picker: canonical unit per page: For each item, detect the dominant dimension (weight/volume/count) across all candidates and show every same-dimension candidate's unit price in one canonical unit (lb / fl oz / ct) instead of mixing $/oz against $/lb. Mismatched-dimension outliers keep their native unit.
- Kroger: Cloudflare-tunnel-safe redirect: OAuth redirect URI is now derived from the incoming request's
X-Forwarded-Host/X-Forwarded-Protoand persisted in Redis with the state token so the callback's/tokenexchange uses the byte-exact URI Kroger requires.
[1.7.2] – 2026-05-02¶
Same-day patch follow-up: forecast past-day filter across all weather providers + a developer-experience fix for
npx jest.
Bug Fixes¶
- Weather: forecast skips stale past-day entries (OWM, Pirate, Open-Meteo): When a cached weather response was generated before local midnight, the 7-day forecast would open with the previous local day (e.g. "Thu" on a Friday) until the cache TTL expired. Affected all three providers via three different mechanisms: OWM 3-hour intervals starting on non-zero UTC boundaries, Pirate Weather's pre-aggregated
daily.data[0]carrying yesterday, and Open-Meteo'sdaily.time[0]doing the same. Each provider now filters past-day buckets server-side, andWeatherWidgetadds a defense-in-depth client-side filter so a still-warm cache can't leak yesterday into the UI. The "N-Day Forecast" heading now matches the actual visible day count. Thanks to @iann for the diagnosis and the OWM/Pirate fix in PR #27 (merged via #31).
Internal¶
- Local jest no longer fails on integration tests:
src/lib/db/__tests__/integration.test.tsnow self-skips whenE2E_HAS_TEST_DB !== '1'sonpx jestruns clean on a dev machine that doesn't expose Postgres on localhost:5433. CI keeps the same shape (no real DB → suite skips → unit-tests job stays green).
[1.7.1] – 2026-05-02¶
Patch follow-up to v1.7.0. Three small fixes surfaced by post-release verification: a stale-cache trap when switching weather providers, three tables missing from the fresh-install schema snapshot, and an e2e-test chicken-and-egg around the public
/api/familyresponse.
Bug Fixes¶
- Weather: provider in cache key: Switching
WEATHER_PROVIDER(e.g.openweather→meteo) used to serve a stale response shaped by the previous provider until the 10-minute TTL expired (manual mitigation:redis-cli DEL weather:<location>). Cache key now embeds the active provider, so a switch produces a non-colliding key automatically. - Fresh-install schema: travel_trips + weekend_*:
src/lib/db/init/02-schema.sqlwas 3 tables behind master.travel_trips(v1.4, Travel Map),weekend_placesandweekend_visits(v1.5, Weekend Ideas) are now created cleanly on first init.test-fresh-install.shreflects the full v1.7 surface.
Internal¶
- e2e helpers: test DB isolation + auth fallback:
helpers/reset.tsandvisual-regression.spec.tsnow respectE2E_DB_NAME(defaultprism) so suites can target a synthetic test database.helpers/auth.tsloginViaAPIfalls back tomemberIndexwhen/api/familyreturns the redacted public response (id'', loginIndex set), needed for any spec that logs in from a fresh page. - Weather: Open-Meteo provider, now the default (carried over from the v1.7.0 unreleased section, shipped here):
WEATHER_PROVIDER=meteois the new zero-config default. No API key required. Tests cover lat/lon plumbing, env fallback, TZ-aware day labels, network error wrapping, and the no-API-key path.
[1.7.0] – 2026-05-02¶
Major calendar refactor (widget toolbar parity with the subpage, drag-and-drop in cards mode, ten view modes including 1W–4W and Schedule, click-to-edit on widget items) and a multi-provider weather system. The
/weekpage is retired. The calendar subpage is now a strict superset.
Added¶
- Calendar: drag-and-drop everywhere: Drag meals, chores, tasks, and events between days in cards mode across all calendar views (Day, List, Week, 1W–4W, Month, 3 Months, Agenda) and inside the dashboard CalendarWidget. Uses a 5px PointerSensor activation distance so drag and click-to-edit coexist on the same card. Drop targets in every cell via
DroppableOverlayCell;moveErrorsurfaces inline if the API rejects the move. - Calendar: click-to-edit from the widget: Tasks, Chores, and Meals widget items open the same edit modals as the calendar subpage. Modals are lazy-loaded via
React.lazy+Suspenseso the dashboard's first paint isn't taxed. - Calendar: cards display mode: New per-day card view (alongside the existing inline list view) renders meals at top, events in the middle, chores+tasks at bottom, with a dynamic per-cell capacity probe (
useCardCapacity) that respects the current font scale and viewport. Overflow folds into a "+N more" popover so nothing is silently clipped. Toggle in the View Options gear; persists per surface (subpage and widget). - Calendar: view modes: Subpage and widget now expose Agenda, Day, List, Schedule (week vertical), 1W, 2W, 3W, 4W, Month, and 3 Months. View dropdown gains stacked ▲▼ triangles for one-click cycling. Multi-week navigation now advances/retreats by
weekCountweeks (was 1). - Calendar: view options: Hide weekends (multi-week views), merge calendars into one column, show notes column (Day/Schedule), and overlay toggles for events/meals/chores/tasks. Settings persist to localStorage and the View Options trigger shows a badge when any toggle is non-default.
- Calendar: meal/chore/task overlays: Cards mode renders these alongside events on every view; bucket data comes from a shared
useDayBucketsForRangeso the subpage and widget see the same data with the same TZ handling. - Weather: multi-provider system:
WEATHER_PROVIDERenv var (meteo|pirate|openweather) selects the active provider via a factory insrc/lib/integrations/weather.ts. Default ismeteo(Open-Meteo). No API key required.LocationParam(string display name OR{lat, lon}) is the provider-neutral input. - Weather: Open-Meteo (new default, zero config): WMO weather-code mapping,
timezone=autoso day-of-week labels respect the response's local timezone, °F + mph units. No API key required. Activated automatically whenWEATHER_PROVIDERis unset ormeteo. - Weather: Pirate Weather (Dark Sky-compatible, opt-in): sunrise/sunset arc, minutely precipitation forecast, hourly timeline rendered via
merry-timeline. NewPIRATE_WEATHER_API_KEYandWEATHER_LAT/WEATHER_LONenv vars (see.env.example). Thanks to @iann for the original PR.
Improved¶
- Calendar widget: toolbar parity: Layout now mirrors the subpage: Today | < > | View dropdown | View Options gear | Add Event. Today button gets explicit contrast classes for transparent vs normal mode (no more white-on-white). Calendar pill chips, view-mode persistence, and notes column all match.
- Calendar: TZ correctness: Forecast day-of-week labels now use
Intl.DateTimeFormatkeyed off the response's IANA timezone (wasgetUTCDay(), which rolled past midnight for late-evening users). Chore overdue stripes parsenextDue(a YYYY-MM-DD DATE column) as a local date instead of UTC, so today's chore isn't flagged overdue in negative-UTC zones. Same fix applied inDayColumn,useDayBucketsForRange, and the drag preview. - Calendar: month view bucket range: CalendarWidget month view now spans the full 6-week rendered grid (start-of-week containing month start through end-of-week containing month end), so overlay items on leading/trailing days from neighboring months are no longer missing.
- Calendar: meal sort order: Aligned across
useDayBucketsForRange,useWeekViewData, and CalendarView'ssortMealsByTypeto chronological order (breakfast → lunch → snack → dinner), matchingMEAL_TIME_DEFAULTSincells/itemTime.ts. Previously the widget rendered a 3pm snack below a 6pm dinner.
Bug Fixes¶
- Calendar drag: task time-of-day preserved:
moveTasknow accepts the originaldueDateand preserves its hour/minute on the target date instead of hardcoding 23:59:59 (a 9am task no longer becomes 11:59pm after drag). - Calendar drag: multi-week capacity:
MultiWeekViewcards-mode capacity now reserves space for the always-rendered overlay rows (meals at top, chores+tasks at bottom) in BOTH overflow branches viauseCardCapacity({ headerHeight, popoverHeight }). Previously the no-overflow branch ignored overlay rows and dense days silently clipped chores/tasks with no+N moreindicator. - ChoreModal / TaskModal: clearable due dates: Both modals now allow explicitly clearing a previously-set due date.
ChoreModalno longer falls back tochore?.nextDuewhen the input is empty;TaskModal'sonSavewidensdueDatetoDate | nulland the API consumers (TasksView, CalendarView, Dashboard) forwardnullso the server's clear branch fires. - POST /api/meals:
mealTimepersisted:mealTimefield was destructured-then-not-inserted on creation, so meal time-of-day silently dropped on first save. Now persisted on both insert and the response payload. - CalendarWidget DndContext:
onDragCancel: Escape during a drag now clearsactiveDragIdand any priormoveError(was leaving stale state). - CalendarWidget AgendaView: overlay props: Widget's agenda view now receives
bucketsByDate,displayMode, andenableDndlike the other six views: meals/chores/tasks no longer silently disappear in agenda mode. - WeekVerticalView merged-view: Recognizes the synthetic
allgroup the same way DayViewSideBySide does. Meals/chores/tasks no longer drop in the merged column. - MonthView popover height: Per-overlay-row reservation bumped from 20px to 26px (matches the actual sm-card + gap-1 height) so dense days don't push overlay items into clipped territory.
displayModedefault: Aligned across state initializers, ViewOptionsMenu's non-default-count badge, and both Reset-to-defaults handlers:inlineis the first-load default everywhere. Eliminates the spurious1badge on first load and the "Reset to defaults flips the calendar layout" surprise.hideWeekendstoggle scope: Tightened to multi-week only (the only view that honored it). Previously the toggle appeared in week / list / month view options and silently did nothing.- Cookie
Secureflag (logout):/api/auth/logoutnow derives HTTPS fromx-forwarded-protolike the rest of the auth path, so cookies cleared during logout match theSecureflag they were set with behind a TLS-terminating proxy.
Internal¶
- Code review modalities: multi-agent cloud review (
/ultrareview): This release was reviewed in three rounds (24 candidate findings → 16 confirmed → all addressed): caught wiring/units/TZ regressions on weather, drag-and-drop time-of-day loss, capacity miscalculations, default-state divergence, and several silent-clipping bugs that text-only review structurally misses. Seedocs/code-review-modalities.md. OverlayFlagsconsolidated: Single canonical definition inuseDayBucketsForRange; cells/DayColumnre-exports.- Dead code cleanup:
src/app/calendar/OverlaysToolbar.tsx(created but never imported) removed; duplicateformat as fmtimport in AgendaView removed. /weekpage retired:src/app/week/*deleted,useWeekMutationsmoved tosrc/lib/hooks/. The calendar subpage is a strict superset.
[1.6.0] – 2026-04-29¶
Consolidates the previously-prepared (but never tagged) v1.5.2 PWA fixes with substantial reverse-proxy / install-flow reliability work, Performance Mode polish, and new CI gating.
Improved¶
- Performance Mode: extended scope: Existing Performance Mode toggle now also stretches polling intervals (×2.5) and renders the Photo widget as a single static image instead of a slideshow. Auto-enabled on first load when the device reports ≤2 GB RAM or ≤4 CPU cores (
navigator.deviceMemory/hardwareConcurrency); your explicit choice in Settings is always respected on subsequent loads. A subtle lightning-bolt badge appears in the dashboard header while active so you know what you're seeing. Existing?perf=1URL param continues to work for kiosk URLs. - Performance pass: Polling now does a structural-shared compare on each fetch: when the new payload is byte-identical to current state (the common case), the existing reference is reused so React skips re-renders downstream. Wraps
useFetchso every consumer benefits without any callsite changes.prefers-reduced-motionis now honored site-wide via standard accessibility CSS; full-screen celebrations (plane fly-by, seasonal goal scenes) skip their motion entirely under either reduced-motion or Performance Mode and fire theironCompletecallback immediately. Lite-mode photo widget now requests the?thumb=1thumbnail variant instead of the full image. TravelWidget gained theReact.memowrapper its peers already had. - Default dashboard: set in app: The layout editor's More menu now exposes "Set as Default" (becomes "Default Dashboard ✓" when active). The
/api/layouts/[id]/defaultendpoint already existed; this wires the UI consumer. - Reverse-proxy install: fewer surprises: Fresh installs behind nginx / Cloudflare / Caddy now Just Work.
install.shgenerates a missingENCRYPTION_KEY(was a fresh-install setup-wizard failure);verify-pinandlogoutderive HTTPS from the per-requestx-forwarded-protoheader instead of a module-level constant, so the session cookie carries theSecureflag whether you're behind a TLS-terminating proxy or not. - Setup-wizard recovery:
/api/family POSTnow permits unauthenticated calls during setup (when nosetupCompleterow exists) and re-locks the moment setup finishes. Fixes the chicken-and-egg "log in to set up your account" trap on fresh installs. - Migration reliability:
scripts/migrate.jsnow detects first-run by checking whether0000_upgrade.sqlhas been applied (not by table existence) and wraps each migration in a transaction. Recovers cleanly from partially-applied migration state instead of throwing. - Crypto key compatibility: AES key derivation now falls back to
PIN_ENCRYPTION_KEYwhenENCRYPTION_KEYis unset: older installs that only had the PIN key continue to work without manual.envsurgery. - About-page version: Settings → About now shows the actual
package.jsonversion (was a hardcoded1.1.0for several releases). Health endpoints (/api/health,/api/health/deep) report the same source of truth.
Bug Fixes¶
- Performance Mode: light-mode widget white-out: An
opacity: 1 !importantoverride on translucent surfaces forced widget bodies to solidhsl(var(--card)), which resolves to white in light mode, making muted/secondary content blend into the background while only chrome (titles, icons) stayed visible. Dropped the override; surfaces now show their original 85–95% translucency over the wallpaper, which reads correctly with or without backdrop-blur. - PWA tiles: weather blank: Weather tile was reading a flat data shape; fixed to use the correct nested
WeatherData.current.temperature/condition/descriptionstructure. - PWA tiles: meals wrong: Meals tile (and rows-mode meals card) matched any meal with today's day name across all historical weeks. Now filters to the current week before looking up today's meal.
- PWA tiles: bus 404: Bus tile linked to
/buswhich does not exist. Removed the link; tile now shows status inline with no navigation chevron.
Internal¶
- CI gates: New
.github/workflows/ci.ymlruns type-check + lint + jest + a gated reverse-proxy e2e suite + migration-replay on every push and PR to master. Catches the bug classes that text-only review structurally misses (deployment-shape, schema idempotency, cookie handling behind a proxy). Seedocs/code-review-modalities.mdfor the rationale. - Test debt: Stale unit tests aligned with current code: session TTL constants moved to 7d/1d for the "stays logged in" UX; OneDrive test suite rewritten for the async credentialStore-based API.
- Pre-push content check (
scripts/scan-pii.sh): fails a push when tracked files match a list of disallowed values held outside the repository. Closes the gap that text-only LLM review can't cover.
[1.5.1] – 2026-04-19¶
Bug Fixes¶
- Shopping: category colors missing: Grocery categories (produce, bakery, meat, dairy, frozen, pantry) were silently stripped from saved settings, causing list cards to render grey with no color and no Add Item button. Restored full category list in DB and made the hook backfill any missing defaults on load so this can't recur.
- Shopping: category validation: API rejected items added to general lists (Target, etc.) with non-grocery categories (clothes, housewares, etc.) because the Zod schema used a closed enum. Changed to
z.string()to match the open-ended category system. - Shopping: duplicate New List button: Removed the redundant New List button from the list tabs toolbar; the one in the top-right header is sufficient.
[1.5.0] – 2026-04-19¶
Features¶
- Weekend Ideas: New
/weekendpage, a family activity board for local places to visit. Add places to a backlog, mark them visited with a 1–5 star rating, flag favorites, and tag them (outdoor, nature, hike, food, museum, farm, etc.). Visit frequency shown as pip dots grouped in 5s. Filters for status, favorites, tags, and search. Side-panel detail view with edit, mark-visited, and favorite actions. Phase 2 (POI search + map) coming next. - Weekend Ideas: group by tag: Place cards are grouped into tag-category sections (emoji header + count) so you can scan by activity type at a glance. Untagged items fall into an "Other" bucket.
- Travel Map: GPS photo linking: Geotagged OneDrive photos are automatically matched to nearby travel pins. The pin detail panel shows a photo strip of matching shots within a configurable radius (default 50 km). Photos can be browsed via a lightbox.
- Travel Map: re-locate pin: Pencil icon next to a pin's coordinates opens an inline geocode search: search for a new location and pick a result to update the pin's lat/lng and place name in place (fixes pins dropped in the wrong location).
Bug Fixes¶
- Travel Map: globe longitude drift: Rotation formula now normalizes center longitude to −180..180. Fixes pins appearing in wrong ocean locations (e.g., Gulf of Mexico instead of Sanibel Island) due to accumulated coordinate drift.
- Travel Map: far-side pin culling: Hidden pins now use a CSS class with
!importantflags so MapLibre styles can't override visibility: fixes pins remaining visible behind the Earth. - Weekend: side nav missing: WeekendView was missing its
<PageWrapper>wrapper, causing the side nav to disappear when navigating to the Weekend page.
Improved¶
- Nav icons: Chores icon changed to
ListChecks(multi-check) to better differentiate from Tasks (CheckSquare); Weekend icon changed toTrees. - Travel Map: globe initial zoom: Default zoom adjusted so the Earth nearly fills the screen on load.
[1.4.0] – 2026-04-18¶
Features¶
- Travel Map: Trips: Multi-stop trip system with three styles: Route (A→B→C polyline), Loop (closed polyline returning to start), and Hub (home base + day-trip spokes). Trips are a first-class object separate from standalone place pins.
- Travel Map: trip globe rendering: All trips are always visible on the globe. Inactive trips render as small faded colored dots + thin low-opacity connecting lines. The active (selected) trip shows full numbered markers and a bright dashed line. Clicking any faint dot selects that trip.
- Travel Map: national parks in trips: Trips support national park stops alongside regular stops. NP stops display a green tree icon in the stop list instead of a number badge.
- Travel Map: Place/Trip toggle: The slide-out panel now shows a segmented Place/Trip toggle when adding something new, so you can switch between adding a standalone place and creating a trip without leaving the panel.
- OneDrive photo sync: folder picker: Settings now includes a folder picker so you can select which OneDrive folder to sync photos from, rather than defaulting to the root.
- Photos: GPS backfill: New
/api/photos/backfill-gpsendpoint reads GPS EXIF from already-synced photos and writes coordinates back to the database without re-downloading files.
Bug Fixes¶
- OneDrive OAuth callback: Fixed "fetch failed" error caused by Docker's bridge network not routing IPv6. A
undiciglobal dispatcher now forces IPv4 for allfetch()calls inside the container.
[1.3.0] – 2026-04-16¶
Features¶
- Travel Map: Phase 1: Interactive globe (MapLibre GL + OpenFreeMap tiles, globe projection) for tracking family travel. Pins use a drop-pin SVG marker anchored at the coordinate tip; root locations use colored drop pins (green checkmark = visited, white dot = want-to-go, amber star badge = bucket list, green tree badge = has national parks); child stops use purple circles, national parks use green circles.
- Travel Map: pin management: Full inline editing in the detail panel: name, trip label, status toggle (auto-saves), bucket list star (auto-saves), visit dates, description, and tags. No separate edit modal.
- Travel Map: stops & parks: Add stops via Nominatim geocode search or add national parks from a curated NPS list; sub-locations displayed as a combined drag-to-reorder list; connecting lines drawn from parent pin to selected children on the globe.
- Travel Map: Places tab: Sortable list with stats bar, text search, filter pills (All / Been There / Want to Go / Bucket List / Has NP), and group-by (Year / Country / None) with country flag emoji. Selecting a place switches to the globe and opens its detail panel.
- Travel Map: dark map mode: Moon/sun toggle button on the globe applies a CSS filter (
brightness · saturate · contrast · hue-rotate) only to the map canvas: tiles darken while all markers stay at full brightness, and no tile reload is required. - Travel Map: geocoding: Nominatim proxy at
/api/travel/geocodewith Hawaiian island aliases, special-character normalization, and national park search scoring (boundary/park results ranked above natural features like volcano summits). - Tasks: Person→List and List→Person nested group modes: primary group cards with sub-group sections inside (colored left-border dividers, per-sub-group badge counts). Available in the Group dropdown when task lists exist.
- Undo Stack: Global undo across shopping, tasks, wishes, and chores: undo button in the nav bar reverses the most recent mutation.
- Dashboard Editor: Transparent background mode. Widget cards can render over the grid background image without a double-card effect.
- Dashboard Editor: Per-widget text color and opacity controls.
- Dashboard Editor: Custom color picker for theme palette swatches.
- Dashboard Editor: Grid line opacity and cell background color/opacity controls for calendar and weather widgets.
- Camera Scanner: Scan product barcodes with phone/tablet camera on the Shopping page: camera icon in header opens full-screen scanner overlay; automatically looks up product on Open Food Facts and adds it to the active list.
- Docker: Multi-arch builds (amd64 + arm64), Raspberry Pi support via pre-built GHCR image.
- Health check:
GET /api/healthnow probes PostgreSQL and Redis. Returns 503 withstatus: "degraded"if either is down (previously always returned 200). - Calendar: Profile columns now follow family member sort order from Settings; Family calendar group always sorts first before person columns.
Improved¶
- Tasks: Group control split into a "Group" primary select and a "Then by" secondary select. The nested
Person → List/List → Personarrow-notation options are replaced by two independent dropdowns. - Chores: "Group by Person" toggle replaced with a consistent "Group" dropdown (None / Person) matching Tasks' style.
- Calendar: Day view and week view hourly rows now expand to fill available widget/subpage height:
1frgrid rows scale proportionally instead of using a fixed minimum. - Bus Tracker: Train map switches to 2-row snake layout when 6+ nodes: top row left→right, bottom row right→left, connected by a right-side vertical segment.
- Bus Tracker: PM route at-school status now shows "Bus at school, en route" instead of a bogus 0-minute ETA.
- Bus Tracker: Route dialog "Scheduled" field renamed to "Home ETA" with helper text clarifying it is the expected arrival time at your stop.
- Bus Tracker: Large minute values now display as hours and minutes (e.g., "15h 25m" instead of "925m").
- README: Replaced GIF demos with static screenshots for faster loading.
Bug Fixes¶
- Auth cookie secure flag: Login route now detects HTTPS per-request via
X-Forwarded-Protoheader instead of a globalisSecureflag derived fromAPP_URL. Fixes "Log in to make changes" errors when accessing viahttp://localhost:3000whileAPP_URLpointed at the HTTPS public domain. - Travel Map: pin creation validation: Zod schema now accepts
null(not justundefined) for all optional fields: fixes "Something went wrong" when creating a new place. - Hawaii Volcanoes location: Nominatim search for national parks now prefers boundary/park-type results over natural features. Fixes Hawaii Volcanoes appearing at the volcano summit rather than the park centroid.
- Bucket list unstar persistence: Star and status toggles now auto-save immediately on click rather than requiring the Save button: fixes unstar/status changes being lost on navigation.
- Microsoft OAuth callback: Removed
requireAuthrequirement on the callback route. Microsoft redirects without a Prism session cookie, causing the connection flow to fail silently. Success/error toasts now display in Settings → Connected Accounts. - Performance mode: Removed animation stripping (transitions run on the compositor thread and don't cause CPU overhead); caps transitions at 150ms so the UI remains responsive without looking broken. Fixes washed-out surfaces when backdrop-blur is disabled.
- Virtual Keyboard: Tapping a key no longer dismisses the keyboard after one character:
preventDefaultonpointerDownkeeps focus in the active input field. - Virtual Keyboard: Toggle button now appears correctly on touchscreen laptops where Windows converts touch events to mouse events (uses
navigator.maxTouchPointsinstead of pointer type tracking). - Virtual Keyboard: Reduced height from 38vh to 32vh, less intrusive on 1080p displays.
- Virtual Keyboard: Scroll position no longer jumps after voice input adds a new list item. Scroll restore is skipped when text was injected while the keyboard was open.
- Camera Scanner: Overlay now self-dismisses immediately after a successful scan; haptic feedback on successful scan; iOS AudioContext unlocked synchronously on "Open Camera" tap.
- UI: Desktop/laptop font size reduced to 14px base (via
pointer: finemedia query). Previously used the same 16px as touch displays. - Calendar: Day name headers rotate correctly when week starts on Monday.
- Mobile: Navigation no longer causes flash/slide animation on page transitions.
- Docker: App health check uses node instead of curl; fresh install schema fixed;
VirtualKeyboardandCameraScannerOverlayloaded vianext/dynamicwithssr: false. - Database: Truncate operation now includes all tables (gift_ideas, calendar_notes, wish_items, bus_tracking, audit_logs).
- CI: GitHub Actions upgraded from Node.js 20 to 22; layout validation size constraints downgraded to warnings.
Infrastructure¶
- Automatic database migrations: Schema changes now apply automatically on container startup via
scripts/migrate.js. Users update by running./scripts/update.sh(orgit pull && docker-compose up -d --build), no manual database commands required.drizzle/0000_upgrade.sqlbrings any existing installation (regardless of age) to the current schema; future changes go in numbereddrizzle/NNNN_description.sqlfiles.
Security¶
- CSRF: Next.js middleware validates
Originheader on all API mutations: cross-origin requests blocked at the edge (away-mode auto-activation exempt). - WiFi config: Password now stored AES-256-GCM encrypted in the database; decrypted on read with backward-compat for existing plaintext rows.
- Backups:
PGPASSWORDmoved from inline shell string to process env. Prevents credential leakage in process listings. - Babysitter info: Sensitive section content now requires authentication (
includeSensitive=truerequests gated behindrequireAuth). - Paprika import: HTML payload capped at 5 MB: prevents memory exhaustion from oversized uploads.
- Centralized cache invalidation: New
invalidateEntity(entity)helper insrc/lib/cache/cacheKeys.tsreplaces 166 ad-hocinvalidateCache('entity:*')calls across 65 files; cross-entity dependency graph ensures chore completions also clear points/goals cache. - Redis-down 503:
validateSessionnow returns a discriminated union{ ok, reason }. Redis unavailability returns 503 ("service unavailable") instead of 401 ("please log in"), preventing confusing auth errors during infra outages. - Request ID middleware: All API responses include
x-request-idheader (24-char hex UUID); propagated intologError()for log correlation across distributed traces. /api/health/deep(parent-auth): Deep health check verifying DB, Redis, last backup recency, and OAuth token expiry; triggers optionalALERT_WEBHOOK_URLnotification on degradation.apiError()helper: Standardized error responses viasrc/lib/api/apiResponse.ts.{ error: { code, message } }shape with typed codes:UNAUTHORIZED,FORBIDDEN,NOT_FOUND,VALIDATION_ERROR,INTERNAL_ERROR,SERVICE_UNAVAILABLE.- withAuth migration: birthdays, calendar-notes routes migrated from raw
requireAuthboilerplate towithAuthwrapper. - API token scopes:
scopesJSONB column added toapi_tokenstable (default["*"]= full access);withAuthnow enforces scope on API token requests; existing tokens unaffected. - Rate limiting: In-memory fallback limiter: rate limits now enforced even when Redis is unavailable (previously all requests passed through).
- Backups API:
POST /api/admin/backupsrate-limited to 5 per hour per user. - API:
GET /api/settings,GET /api/settings/wifi, andPOST /api/shopping/scannow require display/full auth, previously exposed unauthenticated.
Performance¶
- FamilyProvider: Equality check on polling results before calling
setMembers. Prevents unnecessary re-renders across all consumers on every 10-minute poll when data is unchanged. - CLS fix:
AppShellno longer removesml-16from<main>when auto-hide fires: SideNav isposition:fixedso layout should never shift; eliminates CLS spike caused by the 10-second auto-hide timer (CLS 0.337 → 0.07). - Caching: Messages, Tasks, and Photos GET endpoints now cache responses in Redis (60s / 60s / 300s TTLs). Reduces DB load on frequently-polled dashboard data.
- Visibility polling:
useCalendarEventsandusePhotosnow useuseVisibilityPolling. Polling pauses when the browser tab is hidden. - Images: Replaced raw
<img>tags withnext/image(lazy loading, layout stability) in RecipeCard, RecipeDetailModal, and all four nav components.
Quality¶
- BabysitterModeOverlay:
useBabysitterInfoanduseWifiConfignow skip authenticated endpoints when babysitter mode is inactive: eliminates 401 console errors and network failures in Lighthouse best-practices audit (best-practices 96 → 100).
Tests¶
- API error shape (19 tests): every error code maps to correct HTTP status;
{ error: { code, message } }shape enforced;apiSuccesscoverage. - Cache cross-dependency (14 tests):
invalidateEntity('chores')cascades to points/goals; visited-set prevents double-invalidation;invalidateEntitiesshares visited set across entities. - Middleware request ID (8 tests):
x-request-idgenerated and propagated; CSRF blocks mismatched origin; exempt paths pass through; 403 responses still carry the ID. - Redis-down degradation (7 tests):
validateSessionreturnsunavailablevsinvalid;requireAuthreturns 503 not 401 on Redis outage;optionalAuthdegrades to null. - PinPad behavioral (21 tests): member selection, digit entry, backspace, auto-submit, wrong PIN error, keyboard input, cancel, demo mode.
- Shopping widget behavioral (10 tests): check/uncheck items, optimistic update, progress bar ratio, list switching, all-checked, empty state.
- OAuth token expiry (10 tests): calendar/photo tokens expiring soon → warn; stale/missing backup → warn; Redis/DB down → degraded 503; auth enforcement.
- Integration tests (8 tests,
jest.integration.config.js): events CRUD, chore completion flow with cascade, auth session create/validate/invalidate against realprism_testdatabase. - Auth enumeration: Jest tests verify requireAuth routes return 401 and getDisplayAuth routes are correctly guest-accessible (
src/app/api/__tests__/authEnumeration.test.ts). - Widget smoke tests: 14 render tests for ChoresWidget, TasksWidget, ShoppingWidget covering empty state, data display, and filtering (
src/components/widgets/__tests__/widgetRender.test.tsx). - Jest config: Added
.test.tsxto testMatch; ts-jest now overridesjsx: react-jsxfor component test files.
Refactored¶
- TasksView (943→235 lines): extracted
TaskRow,GroupedTaskGrid,NestedGroupedTaskGrid,TaskContentArea,useTaskGrouping,taskGroupTypes, all files under 250 lines. - ChoresView (632→213 lines): extracted
ChoreGroupCard,ChoreGroupGrid,ChoreCompletionsList,useChoreModals. - LayoutEditor (901→228 lines): extracted 10 sub-components and hooks: toolbar sections, dashboard manager, measure mode, popover wrapper, shared types.
- PinPad (648→164 lines): extracted
usePinPad,NumberPad,MemberSelection,PinDisplay. - Days: Consolidated 8 inline day-of-week arrays across calendar views, chore modals, WeatherWidget, and the OpenWeather integration into shared
DAYS_SHORT_ARRAY,DAYS_LONG_ARRAY, andDAYS_SINGLE_ARRAYconstants insrc/lib/constants/days.ts. - CalendarWidget: Extracted
useCalendarWidgetPrefshook (view state, navigation, localStorage persistence) andCalendarWidgetControlssub-component: main component reduced from ~357 to ~200 lines. - Widgets: Added
useMemofor filter/sort chains anduseCallbackfor event handlers in ChoresWidget, TasksWidget, ShoppingWidget, and MealsWidget.
Docs¶
- CLAUDE.md: Added API error standardization, cache invalidation, auth degradation, testing, and request ID guidelines.
- API auth levels: Added
docs/api-auth-levels.mddocumenting the auth requirement (Public / Display / Auth / Parent) for every API route.
[1.2.0] - 2026-03-29¶
Added¶
- Google Tasks: Bidirectional sync with Google Tasks: OAuth flow, list selection, task sync provider
- Google Tasks: Google Tasks option in Settings → Task Sync provider picker alongside Microsoft To-Do
- Google Tasks: Connected Accounts page dynamically shows "Used for: Calendars, Tasks" when Google Tasks connected
- Mobile PWA: Floating action button (FAB) replaces bottom nav bar: Home, Reorder, Settings, Login
- Mobile PWA: Dashboard card reorder mode (FAB → Reorder) with drag pills and amber indicator
- Mobile PWA: Card visibility settings (FAB → Settings) to show/hide dashboard cards
- Mobile PWA: All widget cards available: bus tracker, goals, wishes, photos, clock
- Mobile PWA: Screensaver and away mode auto-disabled on PWA (useIsPWA hook)
- Mobile PWA: Meals touch-drag between days on mobile
- Mobile PWA: Light/dark PWA icon variants, apple-touch-icon support
Improved¶
- Mobile PWA: All grouped list pages (Tasks, Chores, Shopping, Wishes, Gift Ideas) use single-column on mobile
- Mobile PWA: Calendar simplified: short date, Day/Agenda toggle, no filter pills, read-only
- Mobile PWA: Messages important/expires badges on own line, edit/delete buttons visible on mobile
- Mobile PWA: Shopping extra bottom padding so FAB doesn't overlap last item
- Mobile PWA: GripVertical drag icons hidden on mobile across all list pages
- Mobile PWA: Card-level drag disabled on mobile to prevent scroll interference
- Mobile PWA: Body overflow-hidden changed to md:overflow-hidden for mobile scrolling
[1.1.0] - 2026-03-16¶
Added¶
- Gift Ideas: New "Gift Ideas" tab on the Wishes page, private per-user gift tracking for other family members
- Gift Ideas: Per-person columns with quick-add, edit, delete, and purchased toggle
- Gift Ideas: Privacy-enforced: only the idea creator can see their ideas; recipients never see them
- Mobile PWA: Compact subpage headers on mobile (reduced height, smaller text, hidden icons)
- Mobile PWA: Collapsible filter bars on mobile. Tap "Filters" to expand/collapse
- Mobile PWA: Mobile dashboard: summary card layout with weather, calendar, chores, tasks, shopping, meals, messages, birthdays
- Settings: "Week Starts On" toggle (Sunday/Monday) in Settings → Display. Controls calendar week boundaries, weekly goal resets, point counters, and meal planning weeks
- Chores: Reset Day picker in Add/Edit Chore modals: set which day weekly chores reset (Sun-Sat), day-of-month for monthly, or MM-DD for annual
- Goals: Seasonal celebration animations when a goal is fully achieved. Week-based holidays: Valentine's, St. Patrick's, Easter, Spring, Memorial Day, July 4th, Halloween, Thanksgiving, Christmas, New Year's (plus default trophy)
- Messages: Inline edit support: pencil icon on hover, click to edit in place, Ctrl+Enter to save
- Calendar Notes: Day-tied notes panel on calendar widget list and day views. Click the sticky note icon to toggle
- Calendar Notes: Inline contentEditable editing with auto-save (2s debounce + save on blur)
- Calendar Notes: Formatting shortcuts: Ctrl+B bold, Ctrl+I italic, Ctrl+U underline, Ctrl+Shift+S strikethrough, Ctrl+Shift+L bullet list,
-auto-converts to list - Calendar Notes: Notes column aligns row-by-row with calendar day grid in list view
- Calendar Notes: Read-only when not logged in; shared across all family members
- Calendar Widget: Agenda view available on dashboard widget; List (vertical week) view also available on widget
- Calendar Widget: Merge/Split toggle for day and list views when multiple calendar groups exist
- Calendar Widget: Month view grid toggle (bordered/borderless cells)
- Calendar: Agenda view added to calendar subpage
- Dashboard Editor: Single-row properties toolbar (widget name + Fill/Outline/Text/Grid + close)
- Dashboard Editor: Text size (S/M/L/XL) moved inside the Text color popover alongside swatches
Changed¶
- Auto-Hide UI: Only wakes on mouse click, keyboard press, or touch. Mouse movement/drag no longer triggers reappear
Improved¶
- Away Mode: Header layout matches babysitter mode: clock top-left, weather top-right in a compact bar
- Calendar: Multi-week 3W/4W event text size increased to match month view
- Calendar: Today cell border in multi-week view uses standard grid line instead of separate white line
- Dashboard Editor: Color popover z-index raised above widgets so dropdowns appear on top
- Auth: Settings PIN login now carries over to main app session (eliminates double-login)
- SideNav: Logo background made transparent to match nav toolbar color in both themes
Fixed¶
- Calendar: Events from shared calendars (e.g. Family) no longer duplicate across person columns, matching by groupId instead of color
- Calendar: Day widget notes column no longer shows redundant date header when viewing a single day
- Calendar: Notes column integrated into DayViewSideBySide with matching header bar and grid line alignment
- Calendar: Week view fills available height on calendar subpage
- Calendar: Events now span their full duration in week and day views (previously showed as ~30min blocks)
- Calendar: Event text top-aligned with start–end time byline below title
- Calendar: All-day events fully opaque (no transparency)
- Calendar: Event backgrounds fully opaque in week/day views
- Chores: Grouped view now shows pending approval state (amber bg, hourglass icon, "Pending" badge)
- Chores: Recently completed chores remain visible for 24h after parent auto-approval
- Chores: Points input max raised from 100 to 1000
- Gift Ideas: Data refreshes immediately on user switch (no stale cache from previous user)
- Navigation: Removed border lines from nav, header, and editor toolbars for cleaner appearance
- Tasks: Scrolling works correctly on mobile PWA
[1.0.4] - 2026-03-09¶
Added¶
- Calendar: Multi-week view replaces the fixed 2-week view, configurable from 1 to 4 weeks on both the calendar page and dashboard widget
- Calendar: Bordered/borderless toggle for multi-week cell outlines; rows auto-size to content
- Dashboard Editor: Frosted glass background option with variable blur intensity (Light/Med/Heavy/Max)
- Dashboard Editor: Default swatch (reset icon) to return any color target to theme defaults
- Dashboard Editor: Harvey ball indicators on Fill/Outline/Text target buttons show color state at a glance
- Dashboard Editor: Two-mode touch editing: tap widget to select (move mode), tap again for resize mode, tap again to deselect
- Auto-Hide UI: Nav bar and toolbar auto-hide after 10 seconds of inactivity, reappear on mouse/touch (configurable in Settings)
- Auto-Hide UI: Staggered animation: header hides first, then nav; nav reappears first, then header
- Settings: Location card wired to weather API. Supports zip code or city/state, stored in database
- CONTRIBUTING.md: Quality standards requiring 95% minimum Lighthouse score across all categories
- Drag Reorder: Tasks, chores, goals, and family profile cards can now be reordered by drag-and-drop (touch + mouse supported)
- Drag Reorder: Family profile sort order persists to database via
/api/family/reorder; task/chore group order persists to localStorage - Undo: Tasks, chores, shopping items, and wish claims now show an "Undo" toast button when completed/checked off
- Wishes: Self-purchase: cross off items on your own wish list; if someone else already secretly bought it, shows "Someone already got this for you!"
- Wishes: Quick-add input moved to top of list (consistent with tasks/chores pattern)
- Messages: "Group by Person" toggle groups messages into person-colored cards
Improved¶
- Settings: Consolidated Screensaver Timeout, Auto-Hide Navigation, and Away Mode Auto-Activation into single "Timers & Auto-Activation" card
- Calendar: Multi-week toolbar no longer resizes when switching views. Grid icon doubles as border toggle
- Calendar: Multi-week today highlight preserved in screensaver mode (data-keep-bg attribute)
- SideNav: Tap-to-expand drawer replaces hover-based expansion: works reliably on touch devices, collapses on outside tap or navigation
- Weather: Location resolved from DB settings with fallback chain (query param → DB → env var → default)
- Screensaver: Fixed
--primaryCSS variable override that turned today highlight bar white - Accessibility: Dashboard editor uses dashed border for move mode, solid for resize, distinguishable without color
Fixed¶
- Navigation: Fixed nav bar appearing behind page content on iPad. Removed wrapper divs that created CSS containing blocks breaking
position: fixed - Navigation: Fixed auto-hide SSR hydration mismatch: localStorage read deferred to useEffect
- Navigation: Auto-hide now limited to dashboard pages only, no more jarring nav animations on subpages
- Google Calendar: Fixed events beyond 250-event page being silently dropped. Added pagination loop following
nextPageToken - Google Calendar: Cancelled recurring event instances now filtered out during sync instead of appearing as active events
- Bus Tracking: Fixed token mismatch between discover and sync. Stale Gmail credentials now deleted on
TokenRevokedError - Layout Editor: Added
busTrackingto widget validation constraints (fixes "unknown widget ID" error) - Layout Editor: Fixed dashboard save showing "Saved!" but not actually persisting: save button now awaits the API call and shows error on failure
- Safe Zones: Shortened default label from "Example safe zone (edit me)" to "1080p" to prevent preview cutoff
- Calendar: Multi-day all-day events now span all their days instead of only appearing on the start date (affected all calendar views + widget)
Improved¶
- Performance: Split RecipesView into RecipeCard, RecipeDetailModal, RecipeFormModal, ImportUrlModal, and ImportPaprikaModal sub-components
- Performance: Split ShoppingView into ShoppingCategoryCard and extracted useShoppingCelebration, useShoppingDragReorder, useShoppingInlineInput hooks
- Performance: Lazy-load layout editor and dnd-kit (only loaded in edit mode): LCP improved from 7.2s to 3.9s, TBT from 2.4s to 1.4s
- Performance: Bundle analyzer added to build config (
ANALYZE=true npx next build) - Bus Tracking: Sync lock changed from 60s cooldown to mutex (release on completion). Updates arrive within seconds
- Bus Tracking: Response cache reduced to 5s, polling ramps to 5s when ETA ≤ 3 min
Fixed¶
- Recipes: Fixed crash when opening "Add Recipe" form (missing optional chain on ingredients)
- Layout Editor (iPad): Fix scrolling stopping too early: grid now extends 20+ rows (or half a screen) below the last widget
- Layout Editor (iPad): Fix touch drag not working. Tap to select a widget, then drag to move (selected widgets disable browser scroll so dnd-kit receives the gesture)
- Layout Editor (iPad): Enforce minimum 16px cell size so grid remains usable on narrow screens
- Layout Editor: Add "Move" grip indicator on selected widgets for touch discoverability
- Bus Tracking: Fix arrival event timestamps off by 6 hours in UTC Docker containers: arrival parsers now use the email Date header (timezone-correct) instead of parsing body text times as naive UTC
Changed¶
- Dashboard Grid: Migrated from 12-column to 48-column grid for finer widget positioning (~20px increments vs ~80px)
- All existing layouts auto-migrate on load (coordinates scaled 4x)
- Shared
GRID_COLSconstant as single source of truth -
Widget constraints, templates, breakpoints, and validation all updated
-
Dashboard Grid: Replaced react-grid-layout with native CSS Grid + dnd-kit
- Display mode uses pure CSS Grid (SSR-safe, zero JS layout overhead)
- Edit mode uses dnd-kit for drag-to-move with grid snapping, pointer events for resize
- Custom snap modifier adapts to dynamic cell sizes across screen resolutions
- Touch support via dnd-kit TouchSensor
- Removes 5 packages from bundle (react-grid-layout and dependencies)
- Performance: Lighthouse optimization pass (desktop score: 52 → 96)
- Lazy-load overlays (Screensaver, AwayMode, BabysitterMode): broke transitive import chain that pulled entire widget registry into root layout
- Extract screensaver storage utilities to break circular dependency between Screensaver and useDashboardLayout
- Lazy-load Add modals (task, message, chore, shopping), deferred from critical path
- Add React.memo to eager-loaded widgets (Clock, Weather, Calendar) to prevent unnecessary re-renders
- Accessibility: Lighthouse accessibility score 92 → 100
- Fix WCAG color contrast: rewrite
isLightColorwith proper sRGB linearization and WCAG contrast ratio calculation - Fix calendar "Today" badge using white text on yellow seasonal highlight background
- Add
aria-labelto all sidebar nav links (text hidden when collapsed) - Add
aria-labelto logo home link - Bus Tracking: Auto-sync emails on status poll (60s Redis debounce lock)
- Bus Tracking: Switch from
is:unreadto label+date Gmail filtering for email sync - Supports Gmail filters that skip inbox and route to a label (e.g. "bus")
- Configurable Gmail label in Settings → Bus Tracking
- Uses DB dedup (gmailMessageId) instead of marking emails as read
- Date-windowed search (last 24h) keeps queries efficient
Added¶
- Bus Tracking: Track school bus arrivals via FirstView email notifications
- Gmail OAuth integration for polling FirstView geofence notification emails
- Email parser for 3 notification types: distance-based, arrived-at-stop, arrived-at-school
- Route discovery: auto-create routes by scanning existing emails in Gmail
- Bus routes configuration with ordered geofence checkpoints, stop, and school
- Historical arrival time prediction using rolling median transit times (30-day window)
- Dashboard widget with progress dots, status colors (gray/amber/green/red), and ETA display
- Screensaver widget support
- Settings UI for Gmail connection, route management, checkpoint editing, and auto-discovery
- Adaptive polling: scales from 60s down to 10s as bus approaches
- Active days awareness: no false "overdue" status on weekends/non-school days
- Fuzzy location matching for stop/school name abbreviations
- API routes for status, sync, routes CRUD, connection management, history, and discovery
[1.0.3] - 2026-03-01¶
Added¶
- Wish Lists: New wish list feature with per-family-member lists, UI page, dashboard widget, and bidirectional sync with Microsoft To-Do
- Feature Toggles: Hide/show individual pages from navigation via Settings
- Message Expiration: Preset duration options for auto-expiring messages
- Audit Log: Activity audit log with settings PIN gate for parent access
- Connected Accounts: New settings section showing integration status with disconnect capability
- Chore Management: Delete button and enabled toggle added to chore modal and list view
Changed¶
- Calendar Event Layout: Improved overlap handling and simplified AddEventModal
- Calendar Deduplication: Runtime deduplication for cross-calendar and widget events
- Code Quality: Deduplicated code, extracted shared utilities, and decomposed large components
- Shopping Categories: Moved to Shopping page; renamed Settings sections
Fixed¶
- CI Lint: Fixed unescaped apostrophe in WishListIntegrationsSection that broke the build-only CI job
- Calendar Sync: Fixed settings getting wiped during sync and re-auth; fixed multi-account sync
- Calendar Toggle Styling: Fixed toggle styling, screensaver interactivity, and shopping modal issues
- E2E Test Cleanup: Added global Playwright teardown to sweep stale test data; fixed per-test cleanup deleting only the first match instead of all duplicates
- Architecture Review: Fixed 5 bugs found during architecture review
[1.0.2] - 2026-02-26¶
Added¶
- Calendar Merge Toggle: List view now has a "Merge/Split" button to collapse multi-calendar columns into a single chronological stream
- Past Time Dimming: Day view dims past hour cells with grey background and highlights current hour in blue; List view dims past timed events with reduced opacity
- Per-List Category Visibility: Each shopping list can now show/hide categories independently via the category manager
- General List Type: New "General" shopping list type with preset categories (Clothes, Housewares, Gardening, Electronics, Office, Gifts)
- General Categories: Added 6 general-purpose shopping categories alongside grocery categories
- Shopping Categories Settings: New Settings section for global category management (add, remove, reorder, reset to defaults)
- Inline Category Editing in List Modal: Category chips in the create/edit list dialog are now interactive toggles: select a preset (Grocery, General, All, Custom) then fine-tune by toggling individual categories on/off. Replaces the separate "Categories" button.
- Tasks Group by List: Tasks view now supports grouping by Person, List, or None (flat list)
- Tasks Show/Hide Completed: Quick eye toggle in the Tasks header to show/hide completed tasks
- Tasks Click-to-Complete: All task view modes (flat list, grouped) now support clicking a row to toggle completion (like shopping)
- Tasks Inline Add with List: Inline task creation now auto-assigns the active list filter or group list
- Headless Browser Recipe Import: Recipes from Cloudflare-protected sites (AllRecipes, Serious Eats) now fetched via Puppeteer headless browser fallback
- Meal Type Multi-Select Filter: Meal type filter pills now support multi-select (like calendar profile pills)
- Recipe Link from Meals: Meals linked to a Prism recipe show a direct link to open the recipe modal
Fixed¶
- Session Expiry Ghost Avatar: Users no longer appear logged in after session expiry. Sliding window extends active sessions, 5-minute periodic checks detect stale sessions, and 401 responses immediately clear the avatar
- Shopping List/Item Creation: Fixed "Failed to add item" error caused by importing client-only module in server API route
- Recipe Modal Close Loop: Fixed recipe modal reopening immediately after closing when navigated via URL param
- Past Day Dimming: Increased opacity of past-day dimming across all calendar views for better contrast
[1.0.1] - 2026-02-25¶
Added¶
- Shopping Categories: Custom categories for all list types: add, remove, and reorder via "Manage Categories" modal. Stored in settings with auto-assigned emoji and color. Removed "hardware" list type
- Gallery Mode: Full-screen photo slideshow from the Photos page. Respects active filters (orientation, usage, favorites). Tap to exit
- Inline Task Add: Quick task creation via inline text input (type + ENTER) in Tasks view. Available in both grouped and flat list modes
- Babysitter Mode Toggle: Activate Babysitter Mode directly from the /babysitter page header
- Vertical Week View: New "List" calendar view: planner-style vertical layout with days as rows and color-coded events. Profile grouping columns when multiple calendars configured. Today highlighted, past days dimmed
- Calendar Re-auth Flow: Detect expired/revoked Google Calendar tokens, show warning in Settings with "Re-authenticate" button that updates existing calendar source tokens
Fixed¶
- Calendar Sync: Token refresh failures now detect
invalid_granterrors specifically and mark calendars as needing re-authentication instead of showing generic errors - Task Creation: Fixed "Failed to create task" error when using + button with list filter set to "none"
- Day View Hidden Hours: Hour rows now expand to fill available space when hidden hours are enabled, instead of leaving blank space at the bottom
[1.0.2] - 2026-02-22¶
Added¶
- Transparent widget background: New "Transparent" swatch (checkerboard icon) in Fill palette strips the Card background entirely, letting wallpaper show through
- Widget text color: New "Text" section in properties bar lets you override text/icon color per widget (Auto mode uses luminance detection or theme default)
- Calendar transparent mode: When calendar widget has custom/transparent background, day cell backgrounds are removed so wallpaper shows through the entire widget
Fixed¶
- Text color persistence: Widget text color now saves to database (was being stripped by API validation)
- Text color coverage: Overrides CSS custom properties (
--foreground,--card-foreground,--muted-foreground,--primary,--seasonal-accent) so all text, icons, and accents in the widget pick up the chosen color - Day view transparency: DayViewSideBySide calendar now strips
bg-card/85in transparent widget mode - Calendar dropdown: Select trigger and filter chips go transparent with the widget
- iPad properties bar: Added
onPointerDown+touch-manipulationto all swatch buttons for reliable iPad touch
Added¶
- Custom color picker: Rainbow swatch in Fill, Outline, and Text sections opens native color picker for full color gamut
Improved¶
- Calendar dark mode: Replaced hardcoded
bg-gray-200past-day backgrounds with theme-awarebg-mutedvariants that adapt to light/dark mode - Properties bar UX: Opacity buttons only appear when a color fill is selected (not for None or Transparent); Fill palette uses 9-column grid to accommodate Transparent swatch
[1.0.1] - 2026-02-22¶
Fixed¶
- Background opacity: Widget background opacity no longer makes text/icons transparent. Uses rgba background color instead of CSS opacity
- Color picker touch targets: Increased button sizes to meet 44px HIG minimum, prevented RGL drag from intercepting touch events on color picker
- Pencil icon: Edit icon in dashboard toolbar now opens rename dialog on click
- Rename dialog: Replaced browser
window.prompt()with styled modal dialog (consistent with v1.0 polish)
[1.0.0] - 2026-02-22¶
Changed¶
- Toast Notifications: Replaced all 55 browser
alert()calls with styled toast notifications (success/warning/destructive variants) using shadcn/Radix toast system - Confirm Dialogs: Replaced all 18 browser
confirm()calls with styled AlertDialog modals via reusableuseConfirmDialoghook - Optimistic UI: Task toggle, task delete, shopping item toggle, and shopping item delete now update instantly with automatic rollback on failure
Added¶
- Error Pages: App-level
error.tsxandnot-found.tsxwith route-level error boundaries for calendar and settings - Accessibility: Added ~60
aria-labelattributes to icon-only buttons across all views, widgets, modals, and settings sections - Stack trace protection: Error boundaries gate error details behind
NODE_ENV === 'development' - SSRF Protection: Recipe URL import validates against private IP ranges, localhost, and internal hostnames
- Rate Limiting: Recipe URL import limited to 10 requests per 60 seconds per user
- Docker Resource Limits: Container memory and CPU caps (app: 2GB/2CPU, db: 2GB/2CPU, redis: 512MB/1CPU) with Redis LRU eviction policy
Fixed¶
- Console cleanup: Removed 28 debug
console.logcalls from production code (birthday sync, calendar sync, calendar settings, backup utils) - TypeScript: Replaced
as anycast in maintenance route with proper type validation - Chore authorization: Added missing
requireRolecheck on POST /api/chores - Portrait grid overlap: Bottom widgets no longer render behind the portrait navigation bar on iPads and vertical monitors
[0.9.5] - 2026-02-21¶
Added¶
- Comprehensive Test Suite: 635 unit tests (39 suites) + 76 E2E tests
- Core utilities: cn, color, crypto, formatters, backup, security headers, recipeParser, paprikaParser, validateFileType, calculateNextDue, pointWaterfall
- Auth & cache: session management, requireAuth cascade, API tokens, rate limiting, Redis cache layer (all with graceful fallback testing)
- Hooks (renderHook): useIdleDetection, useAwayModeTimeout, useCalendarFilter, useVisibilityPolling, useHiddenHours, useSwipeNavigation, useScreenSafeZones
- Integrations: OpenWeather, OneDrive, Google Calendar, MS To-Do (tasks + shopping), calendar sync
- Services: photo-storage, photo-sync, avatar-storage
- API routes: chore complete/approve workflow, family member deletion, recipe URL import, withAuth middleware
- E2E (Playwright): auth flows, dashboard, tasks/chores/shopping/calendar/settings navigation, CRUD mutations for all 5 modules, away/babysitter mode
- Extracted calculateNextDue: DRY refactor, shared utility used by both chore complete and approve routes
- CI Type Check Fix: All test files pass strict
tsc --noEmit - API Tokens: Long-lived bearer tokens for machine-to-machine access
- Generate tokens in Settings → Security → API Tokens
- Tokens grant parent-level access to all API endpoints
- SHA-256 hashed storage, raw token shown only once at creation
- Revoke tokens at any time;
lastUsedAttracked per token - All existing API routes automatically support
Authorization: Bearer <token> - Iframe Embedding: Configurable
ALLOWED_FRAME_ANCESTORSenv var for embedding Prism in Home Assistant, Node-RED dashboards, or any iframe consumer - Defaults to
SAMEORIGINwhen unset; supports comma-separated origins or* - Security headers extracted to dedicated module with unit tests
- Home Assistant Integration Guide (
docs/home-assistant.md) - Iframe embedding via
ALLOWED_FRAME_ANCESTORS+panel_iframe - REST sensor examples for calendar events, chores, shopping, meals
- Automation examples for TTS announcements and notifications
[0.9.4] - 2026-02-21¶
Added¶
- Multi-Dashboard Support: Multiple named dashboards for different physical screens
- Each dashboard has its own widget layout, screensaver, and orientation
- URL routing via
/d/[slug](e.g./d/kitchen,/d/hallway) /continues to show the default dashboard- Devices bookmark their dashboard URL for persistent per-screen layouts
- Dashboard Management in layout designer toolbar:
- Dashboard name is now a dropdown listing all dashboards
- "New Dashboard..." creation dialog with Blank, Default Template, or Copy Current options
- "Rename Dashboard..." and "Delete Dashboard" in the More menu
- Switching dashboards navigates to
/d/[slug] - Per-Dashboard Screensaver: Each dashboard stores its own screensaver layout in the database
- Screensaver bridge writes active dashboard's screensaver to localStorage on mount
- Global screensaver component works without changes
- Per-Dashboard Orientation: Screen orientation (landscape/portrait) saved per-dashboard in DB instead of localStorage
Changed¶
- Away Mode Icon: Moon icon replaced with palm tree (
TreePalm): more intuitive "vacation/away" meaning, avoids confusion with dark mode - Screensaver Icon: Monitor-with-play icon replaced with lamp/nightlight. Better represents ambient display mode
Improved¶
- Auto-Slug Migration: Existing layouts automatically receive URL slugs on first API fetch
- Last Dashboard Protection: API prevents deleting the last remaining dashboard; default reassigned if the current default is deleted
[0.9.3] - 2026-02-11¶
Added¶
- Outline Color: Widget designer now supports border/outline color in addition to background color
- Same color palette as background picker
- Persists with layout save (stored in JSONB, no migration needed)
Improved¶
- Widget Designer Touch Support: All resize handles now meet Apple's 44px minimum touch target
- Edge handles: 20px → 44px thick; corner handles: 32px → 48px
- Visual indicators always visible in edit mode (not just on hover)
- Larger visual dots (18px with white ring) and bars (56×6px)
- Color Picker Touch Fix: Picker no longer closes immediately on touch devices
- Replaced
onMouseLeavewith click-outside-to-dismiss pattern - Larger color button (12px → 20px) and swatches (20px → 28px)
- Wider picker panel (180px → 200px)
[0.9.2] - 2026-02-10¶
Added¶
- Away Mode: Privacy screen that hides sensitive info (calendar, tasks, chores, messages)
- Shows only clock, weather, and photo slideshow
- Parent PIN required to exit
- Toggle via moon icon in dashboard header
- Auto-activation after extended inactivity (configurable: 4 hours to 1 week)
- Babysitter Mode: Full-screen overlay showing babysitter info
- Displays emergency contacts, house info, children details, house rules
- Clock and weather in header
- Blue/purple gradient background
- Parent PIN required to exit
- Toggle via baby icon in dashboard header
- Babysitter Info: Public info page for caregivers (
/babysitter) - Emergency contacts with call links
- House information (WiFi, address, etc.)
- Children details (allergies, bedtimes, medications)
- House rules with importance levels
- Sensitive items can be PIN-protected
- Print-friendly layout
- New nav item: "Babysitter" in sidebar and portrait nav
- New settings section: "Babysitter Info" for managing content
- New settings: "Away Mode Auto-Activation" timeout in Display settings
Database¶
- Added
babysitter_infotable with section, sortOrder, content (jsonb), isSensitive fields
Changed¶
- Plane celebration animation simplified: 5s duration, slows in middle for text visibility, no loop
Fixed¶
- Plane celebration no longer triggers when login is cancelled (only celebrates on successful completion)
- PIN modal z-index issue - now uses React portal to escape stacking contexts created by backdrop-blur
- "Add Childre" typo in babysitter info settings (now correctly shows "Add Child")
- Away Mode and Babysitter Mode now activate immediately (previously required page refresh)
- Babysitter nav item now visible in portrait mode on iPad
[0.9.1] - 2026-02-09¶
Added¶
- Calendar hidden hours: Configure time blocks to hide (e.g., 12am-6am) in Settings → Display
- Calendar toggle button: Clock icon in day/week views to show/hide configured time block
- Grocery category drag-to-reorder: Drag categories by grip icon to rearrange
- Non-grocery list layout: 2-column "List 1"/"List 2" layout matching grocery card style
- Dashboard swipe prevention: Prevents scrolling beyond screen bounds while allowing widget internal scroll
Fixed¶
- Shopping list type now persists correctly (grocery vs hardware)
- "All Done!" celebration animation properly auto-dismisses
- Two-week vertical view Saturday row no longer cut off
- Non-grocery lists now use consistent card styling
[0.9.0] - 2026-02-07¶
Added¶
- Mobile PWA: Installable app with service worker, manifest, and app icons
- Bottom navigation: Mobile and portrait tablet navigation bars
- Swipe navigation: Swipe left/right on calendar views to navigate
- Responsive font sizing: 16px phones, 18px desktop, 20-24px tablets
- Shopping celebration: Animation when all items checked off
- Shopping mode: Full-screen mobile shopping experience
- Calendar auto-scaling: Views fit available space without scrolling
Changed¶
- Removed Chores and Goals from mobile navigation (kiosk-focused)
- Calendar forced to day view on mobile devices
- SideNav hidden on mobile (bottom nav only)
[0.8.0] - 2026-02-06¶
Added¶
- Microsoft To-Do integration: Bidirectional task sync with OAuth
- Shopping list sync: MS To-Do integration for shopping items
- Recipe system: Full CRUD, URL import, Paprika import
- Recipe scaling: Adjust servings with smart fraction handling
- Add ingredients to shopping list: From recipe detail modal
- Meal-recipe linking: Select recipes when planning meals
- Background auto-sync: Tasks sync every 5 minutes on dashboard/screensaver
- SVG favicon: New prism icon design
- Task list management: Edit names, delete lists, change external connections
Changed¶
- Task integration UI redesigned with per-list connect buttons
- Recipe categories/cuisine filter dropdowns
- Ingredient strikethrough toggle in recipe modal
[0.7.0] - 2026-02-06¶
Added¶
- Calendar event colors: Color picker with user profile color default
- Hide calendars from Add Event: Configurable per calendar in settings
- Calendar alias/rename: Edit display names in settings
- 24-hour week view: Shows all hours instead of 6am-10pm
- Overlapping events: Cycle through horizontal positions
- Login prompts: All create actions now require authentication first
Changed¶
- Portrait navigation icons increased 1.4x
- Week view shows all-day events in scrollable header
- Removed "+n more" event truncation
[0.6.0] - 2026-02-06¶
Added¶
- Wallpaper rotation: Configurable interval with "never" option
- Screensaver photo interval: Configurable in settings
- Auto-sync re-enabled: Calendar syncs every 10 minutes
- Wallpaper fallback: Uses all photos if none tagged for wallpaper
Fixed¶
- Wallpaper only shows on dashboard and screensaver
- Dashboard wallpaper no longer blocked by solid background
- Shopping cache invalidation on item changes
- Tasks cache invalidation on changes
- Points cache invalidation on chore changes
[0.5.0] - 2026-02-05¶
Added¶
- Points & Goals system: Full implementation with waterfall allocation
- Goal redemption: Parents can redeem goals for children
- PointsWidget: Dashboard widget with per-child progress
- Goals page: View, create, edit, delete goals with progress tracking
- Chore completion history: View recent completions with approval status
- Layout import/export: Share layouts via clipboard JSON
Changed¶
- Logo in SideNav: Pixel dissolve design
- Screensaver templates repositioned to hug top borders
- Goals cache invalidation on chore complete/approve
Fixed¶
- Chore period boundaries (weekly resets on Sundays)
- Pending chores display in dashboard
- Widget color settings now persist
- Completed goals visibility in light/dark modes
[0.4.0] - 2026-02-05¶
Added¶
- Security hardening: Transactions on concurrent mutations
- Magic byte validation: JPEG/PNG/WebP verification on uploads
- Per-user rate limiting: Redis-based with graceful fallback
Fixed¶
requireRole()authorization gaps in chores/messages/tasks- Race condition in family member deletion
- Missing author ownership check in messages PATCH
[0.3.0] - 2026-02-05¶
Added¶
- Lazy-loaded widgets: 7 non-default widgets load on demand
- Conditional modal rendering: Modals only mount when open
Changed¶
- Split 6 oversized components into custom hooks
- All component functions now under 250 lines
- Removed dead
getDemoEvents()function
[0.2.0] - 2026-02-05¶
Added¶
- Database indexes: 7 new indexes for query performance
- Consolidated shopping API:
?includeItems=trueparameter - Unique birthday index: For batch upsert operations
Fixed¶
- N+1 query in calendar groups (batch insert)
- N+1 query in birthday sync (batch upsert)
- FK cascade rules on 16 nullable user columns
[0.1.0] - 2026-02-05¶
Added¶
- Redis caching: GET endpoints with mutation invalidation
- FamilyContext: Replaces 9 duplicate fetch calls
- Visibility-based polling: Pauses when tab hidden
Fixed¶
- COUNT query bugs in tasks and messages routes
- Polling intervals reduced (60s→300s/120s)
Changed¶
- Brand rename to "Prism"