Repository navigation
SolarLens v3.0.0
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:remoteand thennpm 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 replacingAPI_TOKENresets nothing. Without it, replacing
API_TOKENmeans 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/apia 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 fromwrangler.jsonc, and readings keep their vendor payload for 30
days from now on: nothing to do for either. -
If the deploy fails on the
aibinding 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 - withAI_BASE_URL,AI_MODEL
andAI_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/askfor one question,GET /api/ask/status, andGET /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 fromSECRET_KEY(orAPI_TOKENwithout
one), never served and never logged; onlyhttps://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,PATCHandDELETE /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/summaryserves it as data. -
Home Assistant.
GET /api/sensorsanswers every figure a sensor wants,
flat and in fixed units, and Settings writes theconfiguration.yamlfor 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/seriesnow 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.GETandPUT /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,PUTand
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/weatherandGET /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/locationsanswers 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.
- Integration tests against the real Worker: wrangler starts it in workerd
-
A demo on GitHub Pages. The real dashboard, on made-up data, with no
server behind it:demo/demo.jsanswers 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 byscripts/build-demo.mjsand published
by.github/workflows/pages.ymlwhenever 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 newdevice_samplestable 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/historyserves 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.mdsets 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-storerather 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 answers410
with where to sign in instead: the same token typed as the setup code does
the same job without leaving it anywhere, androtate-tokens.ps1no 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
/apior/authanswer 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_TOKENnow keeps every signed-in device signed in - and,
withSECRET_KEYset, 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.tscounts 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
runsnpm ci, notnpm 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.mdhad 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 missingAPI_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.mdno longer promises the vendor payload is kept for
good. And a cron is run by hand withnpm run dev -- --test-scheduled: the
README saidnpm run devalone, under which wrangler serves no
/__scheduledat 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 sendschargePoweras 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, andtelemetry: 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.mjsran
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/ingestwith 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, andINCLUDE_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 adaysthat is not a number reads as
the default, whereNaNreached the query. -
npm run seed:localseeds the database the dashboard reads. It ran
npx wranglerwith 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, andnpm run devshowed
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_TOKENwas 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. WithSECRET_KEY
set the keys come from it instead, and it never travels. What was keyed
withAPI_TOKENmoves 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_TOKENlet 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_LIMITinwrangler.jsonc) holds
callers who are not signed in to a rate per address. -
Two more headers:
Strict-Transport-Securityfor a year, and a
Permissions-Policythat 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
frommain; 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 theundiciand Vite under
it, in the tools that build and test. -
Who may change what, tested:
tests/unit/access.test.tsreads 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.