Skip to content

SolarLens v3.0.0

Choose a tag to compare

@github-actions github-actions released this 29 Oct 08:51
· 4 commits to main since this release

SolarLens can now be yours alone: an owner login with passkeys, and a dashboard
that is private when the owner says so. Around it: any number of systems, the
weather and what it should have made, money and carbon, an Analysis tab, alerts
on ntfy, Telegram, Discord or a webhook, Home Assistant, an assistant that
answers from your own figures, and an MCP server for AI apps. A major version
because of the login - though nothing locks until the owner turns it on.

Upgrading from 2.x

  • Deploy as usual. The deploy takes a restore point, applies the nine new
    migrations (0014 to 0022) and deploys the Worker; by hand, npm run db:remote and then npm run deploy. The migrations add, 0021 turns round
    the SolarMan battery readings 2.x stored with charging as discharging, and
    0022 swaps an index the readings did not need for one the alerts do; no
    table or column is dropped, so a Worker rolled back to 2.x still runs
    against the 3.0 database. What 2.x stores meanwhile has charging the wrong
    way round again, and 0021 does not run twice: after a rollback, run it once
    more by hand when 3.0 is back (the README's upgrading guide has the command).

  • Set SECRET_KEY (recommended): npm run cf -- secret put SECRET_KEY,
    with a long random value. Every key is then derived from it rather than from
    API_TOKEN, so replacing API_TOKEN resets nothing. Without it, replacing
    API_TOKEN means setting the password again and adding each place under
    Alerts elsewhere again; signed-in devices, passkeys and connected apps
    carry on either way.

  • Nothing locks by itself. The dashboard stays open until the owner turns on
    Require sign-in to view. The relays, phone notifications and /api/status
    need no change either way. On Cloudflare's free plan, turning it on also
    keeps strangers from spending the database's daily allowance; a dashboard
    left open can have the optional rate limit instead (RATE_LIMIT).

  • Before turning it on, sign in once on each device that shows the
    dashboard - the TV included - or give it a share link, and give Home
    Assistant or any other reader of /api a share link's token.

  • The /auth?t=… link no longer signs a browser in. Sign in on the page -
    the same token is the setup code - or with a passkey or the password. A
    browser that used the link in 2.x signs in once: the cookie it left held the
    token itself, and is no longer accepted.

  • A second cron, once an hour at seven minutes past, is registered by the
    deploy from wrangler.jsonc, and readings keep their vendor payload for 30
    days from now on: nothing to do for either.

  • If the deploy fails on the ai binding with an authentication error,
    give the deploy's Cloudflare API token the Workers AI permission.

Added

  • SolarLens as an MCP server, at /mcp: AI apps outside SolarLens -
    Claude, an IDE, anything that speaks the Model Context Protocol - ask the
    same sixteen read-only questions as the assistant, from the same tools, whether
    the relay laptops are on or off, and so work as a chatbot over the owner's
    systems. Streamable HTTP with JSON answers; protocol
    versions 2025-06-18, 2025-03-26 and 2024-11-05. Protected by OAuth 2.1 tied to
    the owner's sign-in: discovery (RFC 9728, RFC 8414), dynamic registration of
    public clients (RFC 7591), an approval page only the owner's own browser can
    answer, PKCE with S256, single-use codes, rotating refresh tokens with a copy
    caught, revocation (RFC 7009), and only hashes stored. An app approved once
    stays connected until the owner disconnects it under Settings → Connected
    apps. Migration 0020. See docs/mcp.md.

  • An assistant, on the Ask tab. A conversation in plain words, with
    follow-ups - "How much did I export last week?" … "and the week before?" -
    about energy made, used, bought and sold, statistics from whole days (the average and median
    day, the best and worst, the spread, capacity factor, low days), systems
    compared per kWp, faults, why a day was low, a slow decline, a full report on
    any period, a day's power - between two times ("between 2pm and 4pm"), its
    peak, how steady it was - whether a system is offline, since when, when it
    last made power and why (its datalogger offline, or the relay silent or its
    login run out), the battery's charge and what it took in and gave out, the
    money saved, how much was used at home, how many days made more than an
    amount, a graph of power, load, grid, battery power or charge over up to
    three months - every 15 minutes, hour, day or week, a second measure beside
    the first ("PV against load"), or the average day hour by hour with the hours
    named ("which hours do I buy from the grid?") - anything the inverter shows
    (each string's voltage, current and power with a weak string named, the
    grid's voltage and frequency, the power factor, its temperature, the
    battery's own readings), what the house draws and which way the grid flows
    now, any other figure the records hold - a total, an average, the highest or
    lowest and when, how many days it was above or below an amount, the first or
    last time, by hour, weekday, day, week, month or year, and what each inverter
    has made since it was installed ("how many days was the battery full?",
    "when did I first buy more than 2 kW?") - what a word means (kWp,
    performance ratio, self-consumption, SOC, MPPT and more), and how to do
    something in the app. A question about tomorrow is told plainly that there
    is no forecast. Any question may be asked, in any words: read without a
    model, a question may name a time of day, a system by one word of its name,
    two periods to set side by side, or two or three questions at once, carry
    short forms and typing slips in the words it is routed on, and still be
    answered when no reading fits - with how the systems are doing. A model
    chooses up to three of sixteen read-only tools and fills in their arguments, which are checked like
    any input; the Worker computes every figure; the model words them, and words
    stating a number the figures do not hold are replaced by the tools' own. Each
    answer shows its tables, a chart where one helps - lines for a graph, bars
    for energy - the period, how fresh the data was and who worded it, and
    Download keeps it as a page of its own with its charts - a report prints
    to a PDF. The model is Workers AI by default (Llama 3.3 70B),
    or any server that speaks OpenAI's chat API - OpenAI, Groq, OpenRouter, or a
    model of the owner's own behind a tunnel - with AI_BASE_URL, AI_MODEL
    and AI_API_KEY. Anyone who can see the dashboard can ask: someone signed in
    gets the model, anyone else SolarLens's own reading of the question - as does
    everyone with no model, or none of the day's calls left. A daily cap
    (AI_DAILY_CALLS, 100) keeps inside the free allowance. The conversation
    stays in the browser tab; the Worker keeps none of it. POST /api/chat,
    POST /api/ask for one question, GET /api/ask/status, and GET /api/tools, /api/tools/<name>, which run a tool with no model. See
    docs/ask-solarlens.md.

  • An analysis, for any period. "Analyse the last 30 days", "how is my system
    doing?", "how can I improve?" read the figures together and say what stands
    out, why, and what to do: each system against the days before and for its
    sunlight - a fall the weather explains told from one the system does - the
    systems per kWp on an average whole day, the lowest day and its likely cause,
    how much of the power each house used and when it buys (with what a kWh
    moved to midday is worth by the tariff), the always-on load and the peak,
    whether a battery runs the house at night or is kept full while the nights
    are bought, whether a system is down now and why, days not wholly recorded,
    faults still active or coming back, and weak strings. Each finding needs a
    look, is worth knowing or is fine; the house's average day, energy day by day,
    the systems per kWp, the weeks for the sunlight, the battery and the faults
    follow as its evidence, and it downloads as a report. The Worker reads each
    kind of record once for all of it, so an analysis stays inside the free
    plan's limits. The weekly report now carries the week's analysis: what needs
    a look, and what to do, to the Ask tab and every place alerts go.

  • The agents. A weekly health check sets each system's output against the
    sunlight on its panels over twelve weeks and flags a fall of 8% or more, which
    becomes an alert told once. A weekly report, written by the model from the
    figures and held to them, goes out with the weekly summary. And "Why?" beside
    a low day on Historical Data explains it: less sun, a gap in the readings, a
    fault, or a system making less than its sunlight allowed. Migration 0019;
    GET /api/agents. See docs/ask-solarlens.md.

  • Alerts elsewhere: ntfy, Telegram, Discord and webhooks. Settings has a
    card for up to five places that get the same alerts as a phone - told once
    each, one post a run however many - and a summary every Monday. Each is
    tested the moment it is added, and Settings shows how its last post went, in
    the service's own words - with anything of the token, chat, topic or address
    it echoed back taken out. The token or address posting takes is encrypted
    with AES-GCM under a key derived from SECRET_KEY (or API_TOKEN without
    one), never served and never logged; only https:// to a named host is accepted; Discord is told to ping
    nobody and Telegram's HTML is escaped. A channel taking alerts counts as a
    listener like a signed-up phone, so the told-state is shared and primed the
    same way. No email. Migration 0018; GET, POST, PATCH and DELETE /api/settings/channels. See docs/alerts-elsewhere.md.

  • A weekly summary, from 08:00 each Monday on the first system's clock:
    last week's energy against the week before (only systems recorded in both),
    each system's share, best day and days reported, the money saved and CO₂
    avoided by the tariff, and the faults. Sent once, with the week's report -
    waiting up to three hours for it - caught up if a Monday was missed, and on
    request from Settings. GET /api/summary serves it as data.

  • Home Assistant. GET /api/sensors answers every figure a sensor wants,
    flat and in fixed units, and Settings writes the configuration.yaml for it
    with this dashboard's address and systems. A share link's token now works as
    a bearer token, as a viewer, so Home Assistant can read a private dashboard
    with a token that is taken back like any other link. Alerts can go into Home
    Assistant through the webhook channel. See docs/home-assistant.md.

  • An Analysis tab: any of the last seven days, one system or all of them,
    hour by hour - what the panels made (stacked by system) against what the
    house used, the best hour, the peak output and the peak draw, how much of
    the sun's output the house used itself and how much of the house's use the
    sun covered, and a table of every hour. Worked out from the power samples at
    each plant's own clock; an earlier day is fetched once when asked for.
    /api/series now carries each sample's house load (load_power_w).

  • Favourites and groups. A star on a system's page puts it first on the
    Overview, in that browser. The owner sorts the systems into groups in
    Settings ("House", "Outbuildings"), and the Overview shows each group under a
    heading with its own total, with a filter for favourites and for each group.
    Groups are kept against each system's real id, so a system added later
    cannot inherit one. GET and PUT /api/settings/groups.

  • Money saved and CO₂ avoided. A Tariff card in Settings, filled in by the
    owner when ready: the currency, what a kWh bought costs, what one sold
    earns, net metering, and how much CO₂ a kWh from the grid stands for. From
    it, each system's page has a Money and carbon card (today, this month,
    this year, lifetime), and Historical Data a Saved and a CO₂ avoided
    column, by day, month or year, with the totals. A kWh used at home is worth
    one bought; one exported earns the export price, or with net metering the
    import price. A period whose export the vendor does not give is marked as an
    upper bound. No tariff, no money anywhere: nothing is guessed. SolisCloud's
    month and year exports are now read for it. GET, PUT and
    DELETE /api/settings/tariff, and migration 0017.

  • Output measured against the weather. Each system's position is read
    from its vendor's plant record and kept for the owner alone, and every hour
    the Worker asks Open-Meteo - at the position rounded to about a kilometre -
    for the sunlight, temperature and cloud there: once for all the systems at
    one position, and at most six positions an hour, so a run stays inside the
    free plan's limits however many systems there are. Each system learns its own
    performance ratio from its last fortnight's sunny days (the median, so one
    odd day does not move it). The day curve carries a dashed line of what the
    sun allowed; the system's page has an Against the weather card; and an
    alert says when a system makes less than 60% of what its sunlight allowed.
    GET /api/weather and GET /api/expected.

  • A battery's usable capacity, over time, on the History tab: each day's
    charge over how far the charge rose, from days with a swing of 30 points or
    more, against the nameplate and against the oldest days in view. The daily
    history now carries each day's lowest and highest charge.

  • A site map, for the owner only: each system on OpenStreetMap, with
    Leaflet loaded from cdnjs behind an integrity hash, and each position listed
    with a link. GET /api/locations answers the owner and nobody else.

  • The CI/CD gaps, closed without a staging server.

    • Integration tests against the real Worker: wrangler starts it in workerd
      on a throwaway local database, and the tests check what only workerd can
      show - the page's headers from the assets binding, cookies through the
      header rebuild, the password and passkeys in workerd's WebCrypto - and run
      the deploy's own smoke test against it. npm run test:integration.
    • Screenshot comparison: each view of the demo at a fixed moment, compared
      with its recorded picture on Linux in CI.
    • Lighthouse on the demo: accessibility 0.95 and best practices 0.9 or the
      run fails.
    • A restore point before every migration: the deploy writes a D1 Time
      Travel bookmark, and the command to restore it, into its summary.
    • A production watcher: every half hour, an issue if the site is down or no
      feed has delivered for 90 minutes, closed again when readings return - and
      one when the database passes 400 MB of the free plan's 500 (DB_WARN_MB).
    • The owner's own pull requests approved by the Actions bot once every check
      has passed, Copilot has reviewed the newest commit and no thread is open.
  • A demo on GitHub Pages. The real dashboard, on made-up data, with no
    server behind it: demo/demo.js answers every request with three invented
    systems, placed wherever it is early afternoon so the day's curve is always
    there, and a banner says so. Built by scripts/build-demo.mjs and published
    by .github/workflows/pages.yml whenever the page changes. Its sample data is
    held to the real Worker's answers field by field, like the test fixtures.

  • Screenshots in the README, taken of the demo by npm run screenshots, so
    no real plant or figure can end up in a picture.

  • Any number of systems. Each system has a colour of its own, used
    wherever it appears, where colour used to come from the vendor - so two
    plants on SolarMan were both blue. A Compare tab, with two systems or
    more, sets them side by side on one scale: kWh per kWp today and this month,
    and a table of size, output, today, month, year and lifetime. With three
    systems or more the Overview adds a strip listing them all and scrolls
    instead of squeezing them onto one screen; TV mode shows three at a time and
    takes turns every twenty seconds; and a plant with more than one inverter
    shows each of them on its system page.

  • Polling that grows with the fleet. Past four plants a vendor, each
    five-minute run reads a group of four at most in turn, so up to twelve plants
    every plant is still read at least every fifteen minutes - inside the 25
    after which the page calls a system offline - without one run making every
    vendor call there is. Past twelve, a plant waits longer rather than a run
    outgrowing the free plan's fifty database queries. A run also stores four
    inverters and three hardware records at most, a plant's position in one
    write for all its inverters, and the slow extras for one plant of its group;
    a plant with more of either has them read in turns. SolisCloud's plant and
    inverter lists are read page by page, as SolarMan's are, so an account with
    more than fifty of either sees them all.

  • An owner login, and a dashboard that can be private. Set up once under
    Settings (the new gear in the header) with the setup code - API_TOKEN - and
    a password, and add passkeys on your phones and computers. Then turn on
    Require sign-in to view: the readings need a sign-in from then on. It is
    off until you turn it on, and cannot be turned on before there is a way to
    sign in.

  • Signed in once, signed in for good. A one-hour session and a year-long
    refresh token; the Worker renews both on the way through whenever the hour is
    up, so the dashboard, the TV and the service worker never see the sign-in
    page again. The refresh token rotates at every renewal and the one before is
    remembered: a copy used later signs the device out. Two tabs renewing at once
    do not. Only hashes are stored.

  • Passkeys, verified in the Worker itself (ES256 and RS256, no library),
    and never locked out.

  • Settings: every signed-in device with sign-out for one or all others,
    taking effect at once; the password changed or removed; and Share to
    view
    - a view-only link for a day, a week, a month or until taken back.

  • A lockout: five wrong tries from one address lock it for fifteen
    minutes; thirty from everywhere in an hour pause password sign-in for an
    hour. The setup code is held to the first alone, so no stranger can pause
    the owner's way back in.

  • GET /api/status: how fresh each feed is and how large the database
    is, and nothing else, open even on a private dashboard, for a monitor and the deploy's smoke test.

  • Everything each datalogger reports, and its week. The Devices tab opens
    with a card per datalogger: how it connects, its signal and signal bars, how
    often it uploads, its last contact, how long it has run since its last
    restart and in total, its model, firmware, make date, first connection and
    warranty - whatever its vendor reports, and nothing invented where it does
    not. Beneath, its last seven days: a strip of online, offline and not heard
    from, how often and how long it dropped, and its signal as a line on a fixed
    scale. The system page's link card names the link and the last restart.

  • Link history, kept small. Each device's status and signal are recorded
    in a new device_samples table when either changes, or hourly while
    nothing does - a steady logger writes 24 rows a day rather than 288 - and
    kept 90 days. GET /api/devices/history serves the last 1 to 30 days.

  • A datalogger's network handles are kept, and kept back. Its mobile
    operator and cell, or its MAC address, are stored for the owner and never
    part of a public answer: either can place a logger on a map. They live in a
    table of their own, so even a Worker rolled back to 2.10 - which serves
    every column of the devices table it does not know to strip - cannot serve
    them.

  • A roadmap to 3.0. docs/roadmap.md sets out the release phase by phase -
    the owner login, any number of inverters, a public demo,
    weather and expected output with a site map, savings, questions answered in
    the app and an MCP server - and what was left out on purpose.

Changed

  • Reads send Cache-Control: no-store rather than a minute of public
    caching: an answer now depends on who is asking, and no cache may go on
    showing it after a sign-out.

  • /auth?t=<API_TOKEN> no longer signs a browser in. The 2.x unlock link
    carried the key to everything in an address, where the browser's history,
    request logs and anything it was pasted into keep it. It now answers 410
    with where to sign in instead: the same token typed as the setup code does
    the same job without leaving it anywhere, and rotate-tokens.ps1 no longer
    prints such a link. The cookie the link left, which held the token itself,
    is no longer accepted, and is cleared when seen, since nothing could sign it
    out: a browser that had it signs in once.

  • The deploy's smoke test reads freshness from /api/status, and on a
    private dashboard checks that a signed-out caller is asked to sign in.

  • The security policy admits the site map's sources: Leaflet from cdnjs
    (scripts and styles) and tiles from OpenStreetMap (images). Only the owner's
    map view loads them.

  • The installed app keeps no API answers offline. Its service worker kept
    every same-origin answer for when the network was gone; an answer now
    depends on who is asking, so it keeps only the page and its icons, never an
    /api or /auth answer or anything marked no-store. The old cache is
    cleared on update.

  • A passkey's counter moves atomically, so two sign-ins racing with the
    same count - a copied key used at the same moment - cannot both succeed.

  • Replacing API_TOKEN now keeps every signed-in device signed in - and,
    with SECRET_KEY set, the password and the alert channels too. Without it
    the password must be set again and each channel added again, since both are
    keyed with the token.

  • The slower work has a cron of its own. The week's agents and summary,
    the weather and the housekeeping rode on the five-minute run, and on a Monday
    that run made some ninety-five database queries, where D1's free plan allows
    fifty in one invocation - past which every query fails. They now run once an
    hour, at seven minutes past, each step on its own, so a failure is logged and
    the next step still runs; in the hour the agents run, the rest waits for the
    next. The five-minute run keeps to the readings and the alerts.
    tests/unit/budgets.test.ts counts every statement on every run: 35 for a
    five-minute run, at most 41 for an hourly one. src/cron.ts.

  • The weekly agents are marked done only once they are done. The week was
    claimed before the work, so one failure lost its report and health check
    with no second try. A failed run is tried again the next hour, three times
    at most, so a fault that fails every time cannot spend the day's model calls.

  • A reading keeps its vendor payload for 30 days. The payload was some five
    kilobytes of each reading, against a few hundred bytes for everything else,
    and nothing ever deleted a reading, so the database grew by the better part
    of a gigabyte a year where the free plan stops at 500 MB. The hourly
    housekeeping clears the payload from readings older than a month, a day of
    them an hour; the figures stay. Expired kv entries are cleared too.

  • A relay's day curve costs a few queries, not hundreds. The relay sends
    the whole of today's curve every five minutes, and each point was stored one
    by one, three queries apiece: some twenty thousand rows written a day that
    changed nothing, and several hundred queries in one request. Only points
    newer than the last one stored are written now, in batches, at most 40 a
    post, oldest first - a day slept through is taken over a few posts.

  • The "polls failing" alert reads two hours of the poll log, not a week. It
    read every line of the week on every run with a listener - about a fifth of
    the free plan's daily reads on its own. Migration 0022 indexes the alarms by
    when each was first seen, for the alarms rule, and drops an index on the
    readings that repeated the table's own key and cost a row written with every
    reading.

  • Every vendor call gives up after 20 seconds, and a push service after
    10: a vendor that never answered held up every call queued behind it.

  • SolarMan's plant list is read page by page, fifty at a time, where only
    the first fifty plants were read.

  • The laptops install exactly what the lockfile says: setup-relay.ps1
    runs npm ci, not npm install, as does the README. The development tools
    want Node.js 22.13 or newer.

  • The handover covers 3.0 as a whole. docs/handoff.md had the sign-in's
    six tables missing from its data model, the 2.x views, twelve checks where
    fifteen run (the ruleset requires twelve), and nothing on what to do when
    the new parts go wrong. It
    now has every table, view and check; a line on each file 3.0 changed; what
    a full database, a missing API_TOKEN, a late Monday summary or the rate
    limit look like, and the fix; and a glossary of 3.0's words - owner, setup
    code, share link, key secret, channel, the hourly run, invocation, tool.
    docs/feature-gaps.md no longer promises the vendor payload is kept for
    good. And a cron is run by hand with npm run dev -- --test-scheduled: the
    README said npm run dev alone, under which wrangler serves no
    /__scheduled at all.

  • The README's pictures are of 3.0. Retaken of the demo, they show the
    tabs 3.0 added, the groups, the expected line and the Settings gear; two are
    new - the assistant analysing the week, and the Analysis tab - and the two
    that were taken but never shown, Historical Data and TV mode, are in the
    gallery now.

  • The README says who SolarLens is for, and what it cannot do. Badges for
    the checks, the release and the licence; a way to deploy your own beside the
    demo; every guide in its contents; its known limitations - the relay and its
    weekly login, the free plan's limits, read-only, no email or forecast; and
    the Home Assistant configuration described as Settings writes it. The
    feature list's hardware rows are back in their own table, from the Access
    table they had strayed into.

Fixed

  • A SolarMan battery charging now reads as charging. SolarMan's live
    snapshot sends chargePower as a negative number while the battery charges,
    and 2.x took it as it came: every charging reading was stored as
    discharging, so the dashboard showed the battery giving out while its charge
    climbed. The normaliser now takes the size of each and the direction from
    which one is non-zero. Migration 0021 turns round the readings already
    stored - only those whose own payload says charging - and changes nothing
    when run again.

  • Produced today and Consumed today no longer count a system silent since an
    earlier day.
    Its newest reading still carries that day's counters, which
    the header added to today's; with a relay off since Thursday, Thursday's
    energy read as today's. They now read as not reported, as the Worker's own
    answers already had it.

  • The Raw telemetry table, the vendor-flag alerts and a device's own alert
    count now appear on the live site.
    All three read the vendor's raw payload,
    which the public view has never sent, so none of them had ever shown outside
    the tests (gap 5). The Worker now sends the fields the alerts need as named
    columns, and telemetry: the payload's fields that are on a reviewed list
    of measurements, and nothing else - so the ids, the owner's notes, the
    platform codes and the zone name a live plant record carries stay out, and so
    does any field nobody has reviewed. A SolisCloud plant reporting zero alarms
    stays silent; a vendor that sends no counter, or a blank one, is no longer
    read as zero.

  • Each device has a public name of its own. Every device went out as
    "unknown", since its real id carries its serial; each is now named by its
    system and place ("s1-datalogger-1"), so the page can tell them apart.

  • The end-to-end fixtures can no longer drift from the Worker. They moved to
    tests/fixtures/dashboard-api.ts, and a unit test runs the real Worker on
    the vendor fixtures and fails if a fixture row carries a field the Worker does
    not send - the raw payload was one - or lacks one it does. The rows also use
    the aliases the Worker really publishes (s1, s2) rather than vendor ids.

  • The footer's feed lines fit a phone. As long as a relay's really are, they
    made the page wider than the screen too; their detail now gives way, cut
    short, to fit.

  • The deploy's automatic rollback works. scripts/wrangler.mjs ran
    wrangler through a shell, which split the rollback's message - "Smoke test
    failed for " - into separate words; wrangler refuses arguments it
    does not know, so the rollback would have failed exactly when it was needed.
    The wrapper now runs wrangler's own entry with Node, every argument whole.

  • A reading pushed to /api/ingest with fields missing is stored, with
    them as not reported, where D1 refused it with a 500 - found by the new
    tests. Every field is checked as a normaliser would, and INCLUDE_PLANTS
    applies as on every other route.

  • A body that is not JSON is answered 400 on every ingest route, not 500; a
    device list is 50 records at most; and a days that is not a number reads as
    the default, where NaN reached the query.

  • npm run seed:local seeds the database the dashboard reads. It ran
    npx wrangler with the committed config, whose database id is a
    placeholder, and wrangler keeps a local database per id - so the day of
    made-up readings went into a database of its own, and npm run dev showed
    nothing. It now goes through the same filled-in config as every other
    wrangler command.

  • A poll that fails no longer takes the rest of the run with it: the alerts
    are judged regardless, and an alarm or totals read that fails is logged - it
    was dropped without a word, so fault history could stop with nobody the
    wiser.

Security

From a review of the whole repository, before the release:

  • SECRET_KEY, a secret that only ever derives keys. API_TOKEN was the
    bearer token for writes, the setup code, and the root of every key - the
    session signature, the password's pepper, the seal on the alert channels -
    so replacing a token that had leaked reset all of those. With SECRET_KEY
    set the keys come from it instead, and it never travels. What was keyed
    with API_TOKEN moves over as it is used: a password at its next sign-in,
    the channels at the next hourly housekeeping, a device's session at its next
    renewal. The vendors' cached login tokens are sealed under it too.

  • Writes fail closed. A Worker deployed without API_TOKEN let every write
    through, with a line in the log; it now refuses every one (503) and says
    what to set.

  • A passkey must be unlocked - a fingerprint, a face or the PIN - not only
    touched. A sign-in challenge is signed rather than stored, so asking for one
    writes nothing, and it is spent once it works, so a copied answer cannot be
    sent twice.

  • Strangers cannot spend the database's day. On the free plan a spent
    daily allowance fails every query until midnight UTC, the poller's
    included. A question from anyone not signed in covers a year at most; an
    app that registers and is never approved is cleared after an hour, not a
    week, so a stranger who fills the list of a hundred holds it that long at
    most; and an optional rate limit (RATE_LIMIT in wrangler.jsonc) holds
    callers who are not signed in to a rate per address.

  • Two more headers: Strict-Transport-Security for a year, and a
    Permissions-Policy that turns off the device features the page never
    uses. The security policy's own comment no longer claims more than it does:
    it stops a script posting a reading elsewhere, not navigating the page away.

  • No preview addresses (preview_urls: false): each uploaded version had a
    public address of its own, with the production database behind it.

  • The workflows: the release's version reaches its script through the
    environment rather than pasted into it; a deploy started by hand runs only
    from main; the PowerShell analyser the checks install is pinned to a
    version.

  • The page escapes the apostrophe too, so text is safe in an attribute
    quoted either way. The link in a Telegram alert escapes the double quote.

  • Dependencies brought up to date. Hono 4.13.14, past an advisory on its
    serveStatic (GHSA-5r4p-p66f-jhc7), which SolarLens does not use - but what
    ships is clean again; and wrangler 4.155, with the undici and Vite under
    it, in the tools that build and test.

  • Who may change what, tested: tests/unit/access.test.ts reads every
    write route from the source and fails if one is not placed - the owner's, a
    signed-in browser's, an approved app's, the relays', or open for a stated
    reason - then asks each as nobody, a share link, a relay and the owner, and
    fails if it answers anyone it is not for.


The full entry is in CHANGELOG.md.