Skip to content

Releases: Springdale-Robotics/basis

v0.1.42-alpha

v0.1.42-alpha Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Oct 06:31
06069ad

v0.1.42-alpha — recipe import: amount field fixes, fractions, "or" alternatives

Recipe import (PR #127)

  • The Qty field in the import review no longer breaks while you type.
    It used to turn every keystroke into a number and show that number back,
    so "0." collapsed to "0" (the decimal disappeared and the cursor jumped)
    and a lone "." became "NaN" and stayed stuck. The field now keeps exactly
    what you type and only saves it once it reads as a number.
  • The Qty field accepts fractions: 1/4, 1 1/2, ¼, 1¼. Before, "1/4" was
    saved as 1. The field now opens the regular keyboard on phones, because
    the number pad has no "/".
  • Ingredient lines that offer alternatives keep their wording. "1/2 cup
    half and half (or heavy cream)" used to import as "half and half, heavy
    cream", which reads as two required ingredients. It now imports as
    written. Lines like "salt and pepper" also stay as one name, since the
    parser can't tell "or" from "and".

Installer (PR #122, unreleased until now)

  • A headless box comes back by itself after an unclean shutdown (GRUB
    recordfail timeout and automatic fsck repair). Like all unit and
    installer changes, this reaches an existing box only through
    sudo backend/deploy/native/install.sh --units-only --source <checkout>,
    not through the Update button.

Deploying: the Update button carries the recipe changes, including the
ingredient-parser service, which the post-update watchdog restarts. No
migrations.

Install

curl -fL https://github.com/Springdale-Robotics/basis/releases/download/v0.1.42-alpha/basis-0.1.42-alpha.tar.gz | tar -xz
sudo bash basis-0.1.42-alpha/backend/deploy/native/install.sh --source basis-0.1.42-alpha

Basis installs natively (Node/Postgres/Redis + systemd); there
is no Docker image. Local development still uses Docker for
Postgres/Redis/Ollama via backend/docker-compose.dev.yml.

v0.1.41-alpha

v0.1.41-alpha Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 12 Sep 18:36
287ba21

v0.1.41-alpha — Basis services now run pinned to UTC

Deploy-only release. The two systemd units, basis.service and
basis-worker.service, now set TZ=UTC explicitly instead of inheriting the
host's timezone.

Why it matters: all-day events are stored as a wall-clock instant and
converted to Google's exclusive end date at the sync boundary. That
conversion tells a Basis-authored end (noon) apart from a provider-supplied
one (exact UTC midnight). On a host at an offset of exactly +/-12, local noon
IS UTC midnight and the two collapse — a one-day all-day event then pushes to
Google with start date equal to end date, which Google rejects. At an offset
of 13 or more the date arithmetic shifts by a day silently. Self-hosted boxes
are usually set to local time, so this cannot be left to the host.

Found while reviewing the outbound sync worker. Nothing has gone wrong on the
production box because it happens to run Etc/UTC already.

How it lands: systemd units do not travel through the in-app Update button.
The change takes effect on an existing box only after a one-time

sudo backend/deploy/native/install.sh --units-only --source <checkout>

run from the box's checkout. That reloads the units but restarts nothing;
the pin takes effect at the next service restart, which the next in-app
update performs. Fresh installs pick it up automatically. Until then the box
keeps whatever timezone it had, which on the production box is already UTC.

No migrations. No application code changes.

Install

curl -fL https://github.com/Springdale-Robotics/basis/releases/download/v0.1.41-alpha/basis-0.1.41-alpha.tar.gz | tar -xz
sudo bash basis-0.1.41-alpha/backend/deploy/native/install.sh --source basis-0.1.41-alpha

Basis installs natively (Node/Postgres/Redis + systemd); there
is no Docker image. Local development still uses Docker for
Postgres/Redis/Ollama via backend/docker-compose.dev.yml.

v0.1.40-alpha

v0.1.40-alpha Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 05 Sep 09:04
3f5d3b1

v0.1.40-alpha — Google Calendar edits now reach Google

Phase 2, task 5. Everything built in tasks 1-4 has been inert until now:
the outbound sweep required a writable calendar and none existed. This
unlocks Google-synced calendars, and edits start pushing.

What a household can now do: create, edit and delete whole events on a
Google calendar in Basis, and see them in Google within about five minutes.
Changes made in Google still arrive on the hourly pull.

What it still refuses, deliberately: editing or deleting a SINGLE occurrence
of a repeating series. That is not an oversight — deleting one occurrence
adds an EXDATE to the master row, and pushing that would replace Google's
whole recurrence array and wipe exclusions it held, silently and
permanently. The spec always deferred per-occurrence editing to v2; what
went unnoticed until now is that EXDATE-on-master IS per-occurrence editing.
Basis says so at the point of action rather than accepting an edit it cannot
deliver.

Connecting a Google calendar now reads its actual access role and stays
read-only unless the household owns it or has writer access — a shared,
holiday or school calendar connects pull-only instead of failing every push
forever. ICS import into a synced calendar is refused, since it would stamp
ICS UIDs into the column that tracks Google's own event ids.

Outlook is unchanged: still pull-only, still read-only, no outbound path.

Known limitation: migration 0019 unlocks existing Google calendars without
being able to check access role — a migration cannot call the Google API. A
reader-only calendar connected before this release should be disconnected
and reconnected.

Install

curl -fL https://github.com/Springdale-Robotics/basis/releases/download/v0.1.40-alpha/basis-0.1.40-alpha.tar.gz | tar -xz
sudo bash basis-0.1.40-alpha/backend/deploy/native/install.sh --source basis-0.1.40-alpha

Basis installs natively (Node/Postgres/Redis + systemd); there
is no Docker image. Local development still uses Docker for
Postgres/Redis/Ollama via backend/docker-compose.dev.yml.

v0.1.39-alpha

v0.1.39-alpha Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 05 Sep 08:02
ab80871

v0.1.39-alpha — outbound discovery and sweep worker

Phase 2, tasks 3 and 4. Still INERT: isOutboundCalendar requires
isReadOnly === false and nothing is unlocked, so the sweep finds nothing and
no edit reaches Google. Task 5 is what turns it on.

What DOES change for a household in this release: Basis now refuses to edit
or delete a single occurrence of a repeating event on a calendar synced with
an external provider, and says why.

That refusal is the point. Deleting one occurrence adds an EXDATE to the
master row rather than creating an exception row, so it would have reached
the outbound path looking like an ordinary master edit — and pushing it
would have replaced Google's whole recurrence array, wiping exclusions it
held, then marked the row clean. The occurrence would have vanished in Basis,
stayed on every device reading Google, and previously-deleted occurrences
there would have come back, silently and permanently.

The spec always deferred per-occurrence editing of a synced series to v2.
What nobody had noticed is that EXDATE-on-master IS per-occurrence editing.
Refusing at the point of action is that decision, honestly applied: better to
tell a household what Basis cannot do than to accept an edit it cannot deliver.

Also fixed: all-day events converted correctly at the Google boundary in both
the create and update paths (Basis stores an inclusive end, Google's end.date
is exclusive), and an edit-form path that grew a synced all-day event by a day
on resave.

Local, unsynced calendars are untouched — every single-occurrence path still
behaves exactly as before.

Install

curl -fL https://github.com/Springdale-Robotics/basis/releases/download/v0.1.39-alpha/basis-0.1.39-alpha.tar.gz | tar -xz
sudo bash basis-0.1.39-alpha/backend/deploy/native/install.sh --source basis-0.1.39-alpha

Basis installs natively (Node/Postgres/Redis + systemd); there
is no Docker image. Local development still uses Docker for
Postgres/Redis/Ollama via backend/docker-compose.dev.yml.

v0.1.38-alpha

v0.1.38-alpha Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 30 Aug 21:47
d02a7b2

v0.1.38-alpha — echo suppression on the calendar pull

Phase 2, task 2. This is the release that makes v0.1.37-alpha's trigger
guard live: every Google Calendar pull now writes calendar_events.remote_updated,
so a pull that only learns 'Google's timestamp moved' no longer bumps the
event's revision, the calendar's sync_token or ctag, or appends a journal row.

revision is what CalDAV ETags are built from, so this is the first change in
phase 2 where a mistake would be visible to devices — either as every event
re-downloading, or as a real change being missed.

It establishes the invariant three later tasks depend on:

updated_at > remote_updated <=> this row has local edits Google has not seen

Pull inserts pin updated_at to the provider's timestamp; pull updates leave it
alone. Both master events and recurrence exceptions.

Nothing a household can see changes yet. Calendars remain read-only and
nothing is pushed to Google.

Known and disclosed: CalDAV LAST-MODIFIED can now trail the ETag on a genuine
content change. DTSTAMP is unaffected. Clients sync on ctag/sync-token/ETag,
so the effect is a possibly-stale 'last edited' in a UI.

Install

curl -fL https://github.com/Springdale-Robotics/basis/releases/download/v0.1.38-alpha/basis-0.1.38-alpha.tar.gz | tar -xz
sudo bash basis-0.1.38-alpha/backend/deploy/native/install.sh --source basis-0.1.38-alpha

Basis installs natively (Node/Postgres/Redis + systemd); there
is no Docker image. Local development still uses Docker for
Postgres/Redis/Ollama via backend/docker-compose.dev.yml.

v0.1.37-alpha

v0.1.37-alpha Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Aug 06:42
cc6cc50

v0.1.37-alpha — schema and triggers for two-way calendar sync

Phase 2, task 1. Deliberately inert: it adds columns and changes trigger
behaviour, and unlocks nothing. No calendar becomes writable, nothing syncs
to Google, no behaviour a household can see changes.

It ships on its own because there is no database rollback — the box's
post-update watchdog reverts the code symlink, not the schema — and because
the risk that matters is not a migration that fails loudly but the trigger
rewrite succeeding while subtly wrong. Those triggers fire on every write to
calendar_events for every household, and CalDAV clients depend on them for
sync tokens and ETags.

  • calendar_events.remote_updated, the provider's own timestamp
  • calendar_changes.external_id, captured on journal rows and load-bearing on
    deletes, where the event row is already gone
  • calendars.outbound_cursor and calendars.last_outbound_at
  • both trigger functions rewritten so a write touching only remote_updated is
    invisible: no revision bump, no sync_token bump, no ctag reroll, no journal
    row — otherwise every future outbound push would make every subscribed
    CalDAV client re-download the event

What to watch after deploying is an absence, not a feature: CalDAV keeps
syncing, devices do not re-download everything, calendar_changes does not
grow.

Install

curl -fL https://github.com/Springdale-Robotics/basis/releases/download/v0.1.37-alpha/basis-0.1.37-alpha.tar.gz | tar -xz
sudo bash basis-0.1.37-alpha/backend/deploy/native/install.sh --source basis-0.1.37-alpha

Basis installs natively (Node/Postgres/Redis + systemd); there
is no Docker image. Local development still uses Docker for
Postgres/Redis/Ollama via backend/docker-compose.dev.yml.

v0.1.36-alpha

v0.1.36-alpha Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Aug 05:52
8665c81

v0.1.36-alpha — the Google Calendar connect flow actually completes

Two defects that only appeared once a real household completed consent for
the first time. Both were reached by fixing the OAuth flow in v0.1.35-alpha;
neither was reachable before it.

  • The callback redirected to /settings/calendars/google/select, a path no
    route serves, while the settings page opens the picker from a ?select=
    query parameter. Consent succeeded, tokens were stored, and the household
    landed on a page that did nothing. Outlook had the same bug.

  • When Google refused a request during connect, the route did not catch it,
    so a clear 403 became a generic 500 and the picker rendered an empty
    dropdown. Observed with the Calendar API not enabled on a household's
    Cloud project — Google named the problem and gave a one-click fix, and
    nobody ever saw it. The route now maps through describeGoogleSyncError
    and the picker shows the message.

Install

curl -fL https://github.com/Springdale-Robotics/basis/releases/download/v0.1.36-alpha/basis-0.1.36-alpha.tar.gz | tar -xz
sudo bash basis-0.1.36-alpha/backend/deploy/native/install.sh --source basis-0.1.36-alpha

Basis installs natively (Node/Postgres/Redis + systemd); there
is no Docker image. Local development still uses Docker for
Postgres/Redis/Ollama via backend/docker-compose.dev.yml.

v0.1.35-alpha

v0.1.35-alpha Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Aug 05:05
5dd4e00

v0.1.35-alpha — connect Google Calendar from any box address

Phase 1 of two-way Google Calendar sync. OAuth now routes through a single
static relay at connect.home-basis.com, so a household can connect from any
box address rather than only from one pre-registered in a Google console.

  • redirect_uri is one fixed relay URI at both the authorize step and the
    token exchange, so they cannot diverge
  • the frontend pre-flights through the relay, leaving the box's own origin
    in the URL fragment — never sent to a server, so the cloud host does not
    learn a household's address
  • warns about the Testing consent screen twice: on the connect screen, and
    in the sync error a week later when refresh tokens expire

Calendars remain read-only; two-way sync is phase 2.
Outlook is unchanged and keeps its Host-derived redirect.

Install

curl -fL https://github.com/Springdale-Robotics/basis/releases/download/v0.1.35-alpha/basis-0.1.35-alpha.tar.gz | tar -xz
sudo bash basis-0.1.35-alpha/backend/deploy/native/install.sh --source basis-0.1.35-alpha

Basis installs natively (Node/Postgres/Redis + systemd); there
is no Docker image. Local development still uses Docker for
Postgres/Redis/Ollama via backend/docker-compose.dev.yml.

cloud-v0.1.4

Choose a tag to compare

@github-actions github-actions released this 29 Aug 05:24
35250ef

feat(cloud): privacy policy page (#107)

Google requires a privacy policy URL before an OAuth consent screen can be
published to production, so Option A is blocked without one — a gap the
design spec never mentioned. It is also a mandatory, human-reviewed input
to phase 3's sensitive-scope verification, so this is written for that
reader rather than to satisfy a form.

Every claim is checked against the code rather than boilerplate: the cloud
holds account email, argon2 hash, subdomain, billing state, hashed tunnel
tokens, bandwidth totals and heartbeat times; Stripe takes payment and
Resend delivers account mail; there is no analytics anywhere in the cloud
frontend; server error reports carry message, stack and route only, and are
off unless an address is configured.

The two honest disclosures matter most. Bug reports include a screenshot
and console log and land in a private repo — so the page says to check what
is on screen first. And the relay is described as what it is: a static page
with no server, holding the box address only in the visitor's own browser,
keyed to the sign-in attempt and cleared after use.

Marks phase 3's owner step O2 done.

Co-authored-by: dashboard-bot dashboard-bot@local

Deploy

On the provisioned VPS (see cloud/README.md for first-time provisioning):

sudo bash /opt/basis-cloud/current/deploy/update.sh --version cloud-v0.1.4

update.sh downloads this tarball, verifies its .sha256, migrates,
smoke-tests, swaps the current symlink, and auto-rolls-back if the
new version doesn't become healthy.

cloud-v0.1.3

Choose a tag to compare

@github-actions github-actions released this 29 Aug 04:20
a98ca7c

feat(calendars): connect Google Calendar from any box address (phase 1) (#104)

  • docs: design for two-way Google Calendar sync on self-hosted boxes

Why sync only pulls today, and only from a pre-registered address: Google's
redirect URIs are exact-match, so a box at an arbitrary LAN or tunnel address
cannot complete OAuth at all, and the outbound functions in
google-sync.service.ts were never wired.

Design: one static relay page on the cloud host as the only redirect URI, a
client-side bounce (return URL kept in the browser, never sent to the relay),
and a single sync engine. Two ways in: bring-your-own Google project for
everyone, and a verified Basis client behind the paid tier, where the tunnel's
public HTTPS also makes push notifications possible.

Every constraint is cited to Google's own docs or the Home Assistant source;
three earlier claims are corrected in the doc itself. Two things are left as
spikes rather than assumed.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FR1wtK6j19oYx3NFtoKGMa

  • docs: fold review findings into the Google Calendar sync design

Reviewed 378e828 against the repo. The shape holds; seven findings and the
two sign-off items are folded in.

Owner decisions (2026-08-28):

  • Option B's client config comes from a separate cloud endpoint the box calls
    on demand at connect time, not on the claim/heartbeat payload. Only boxes
    that use Google hold the secret, entitlement is checked at the moment of
    use, and the recurring contract in basis-cloud.ts stays as it is.
  • The client_secret enforcement spike is approved and goes first in phase 3 —
    a PKCE-only exchange would delete the delivery question entirely.

Findings:

  • Reserve connect before registering the redirect URI with Google. It is not
    in the cloud's reserved-subdomain list, so a customer could claim the host
    that receives every household's authorization code.
  • connect.home-basis.com needs its own Caddy site block; the wildcard sends it
    into frps today.
  • The access-log requirement is a constraint to preserve, not a change: the
    Caddyfile has no log directive at all.
  • Outbound work is discovered from row state plus journal deletes, not from
    per-route hooks. CalDAV writes and never checked isReadOnly (basis#101), and
    the REST routes write at ~20 sites with no shared helper, so per-site hooks
    would recreate that bug class. Needs external_id captured on the journal's
    delete rows.
  • remote_updated-only writes must be invisible to both calendar triggers, or
    every push churns CalDAV ETags and grows the journal.
  • Dropped the paid-tier relay fallback: it forces state to gain a structure the
    box must parse, to recover a case that already recovers by retrying.
  • Leave Outlook read-only; it has no outbound path.
  • New claim/heartbeat fields must be optional — basis-cloud.ts validates
    strictly in both directions.

Google allows two simultaneous client secrets, so rotation has an overlap
window and the refresh-token question is informational, not a blocker.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FR1wtK6j19oYx3NFtoKGMa

  • docs: spike answered — Google enforces client_secret for Web clients

A PKCE-only authorization-code exchange against a Web application client is
rejected with invalid_request / "client_secret is missing"; the same exchange
with the secret attached succeeds. Google's "Optional" does not describe this
client type.

So there is no version of Option B where a box holds only a client id. The
on-demand client-config endpoint stands as the delivery mechanism, and the
fleet-wide-secret consequences that follow from it are real rather than
contingent.

Still open, but informational: whether a refresh token survives its issuing
secret being deleted. Two secrets can be live at once, so rotation has an
overlap window and the answer only bites at the final delete, where it is
recoverable.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FR1wtK6j19oYx3NFtoKGMa

  • docs: implementation plans for two-way Google Calendar sync

One plan per phase, matching the spec's claim that each ships on its own.

Phase 1 (relay): reserve connect before the URI is registered with
Google, the Caddy site block, the relay's URL logic as a tested pure
module plus two static pages, the redirect-URI change at all four call
sites, the pre-flight bounce, and the Testing-consent-screen warning in
both the connect flow and the error path.

Phase 2 (engine): the schema and trigger work, echo suppression,
state-derived outbound discovery, the sweep worker, the unlock, LWW
conflicts, tighter polling, and a write-path test per path. The unlock
migration is deliberately last — unlocking before the worker exists would
be basis#101 across every household at once.

Phase 3 (Option B): owner steps for verification split out from executor
tasks, the on-demand client-config endpoint, the box side that calls it,
connect-without-a-project, and push channels. Tasks 4 and 5 are marked as
lower resolution because they depend on a client that does not exist yet.

Deviations recorded rather than papered over: the spec's Playwright relay
test is not buildable (no browser test infrastructure in this repo), so
the logic is unit-tested and the browser behaviour is checked by hand;
and the spec's "sync log" is satisfied by structured logger output, since
no such table exists and these plans do not add one.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FR1wtK6j19oYx3NFtoKGMa

  • docs: fix six defects found reviewing the calendar sync plans

Phase 1:

  • Drop Outlook from the relay. The plan had moved its redirect URI to
    connect.home-basis.com, but nothing serves /oauth/outlook, lib.js
    forwards only to accounts.google.com, and the pre-flight was wired
    only at the Google call sites — every Outlook connect would have 404ed,
    strictly worse than the Host-derived flow it replaced. Recorded as a
    deviation; giving Outlook the relay is its own task.
  • Import /lib.js absolutely. Caddy rewrites the pages to nested paths
    while the module sits at the relay root, so './lib.js' resolved to
    /oauth/google/lib.js and 404ed. The local http.server check masked it
    by serving the pages from the root; the deploy step now checks it.

Phase 2:

  • Put the working trigger guard in the step that writes the migration,
    instead of pseudo-SQL followed by a step that replaces it. Uses
    to_jsonb(NEW), not to_jsonb(NEW.) — the . expansion is not valid in
    a PL/pgSQL expression.
  • Clamp remote_updated past updated_at on the create path as well as the
    update path, so clock skew cannot leave a just-created event looking
    locally edited.
  • Add calendars.last_outbound_at and drive the five-minute tick from it.
    The derived signal collapsed the spec's one-hour active window to a
    single sweep, since "has outstanding work" goes false as soon as the
    push succeeds.
  • Fix the write-path test's create route: events are created at
    POST /:calendarId/events, not /events.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FR1wtK6j19oYx3NFtoKGMa

  • chore: ignore .superpowers scratch directory

Ledgers, task briefs, and review packages for subagent-driven plan
execution. Scratch state, not repository content.

  • feat(cloud): reserve connect subdomain for the OAuth relay

The relay page will live at connect.home-basis.com and become the single
registered redirect URI on the Google client. A customer holding that
hostname would receive an authorization code for every household that
connects a calendar.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FR1wtK6j19oYx3NFtoKGMa

  • docs: CalDAV mounts at /dav, not /caldav, in the phase 2 plan

The write-paths test would have 404ed on every CalDAV case. Caught from
the baseline test run's request log; app.ts:272 registers caldavRoutes
with prefix '/dav'. Test directory paths (test/caldav/) are unaffected.

  • feat(relay): URL logic for the OAuth relay page

Pure module so the bounce logic is testable without a browser. Rejects
relative, protocol-relative and javascript: return URLs, and refuses to
forward anywhere but accounts.google.com.

  • feat(relay): static pages for the Google OAuth bounce

start.html stores the box address from the URL fragment and leaves for
Google; index.html reads it back and forwards the code to the box. The
fragment is never sent to a server, so the relay host never learns a
household's address.

  • fix(relay): scrub authorization code and fix back-button trap
  • Use location.replace() instead of assign() on both pages to avoid
    leaving the code/box address in browser history
  • Capture window.location.search once before any page mutation, and
    use the captured value for both classifyCallback and buildCallbackUrl
  • Scrub the authorization code from the address bar with
    history.replaceState on error or after success
  • Move localStorage.getItem into the try block so site storage
    access errors are caught and displayed rather than hanging the page
  • docs: amend the relay pages in the phase 1 plan

Review of Tasks 2-3 found the plan's own page code leaves the Google
authorization code live in the URL and in history on every error branch,
and location.assign leaves it in history on the success branch too.
Ruling on that surfaced a second defect: assign() also makes Back from
Google's consent screen bounce the user forward again.

Both pages now use location.replace, the error branches scrub the query
with history.replaceState, and the localStorage read moved inside the try
so blocked site storage shows a message instead of hanging the page.

  • feat(cloud): serve the OAuth relay at connect.hom...
Read more