Repository navigation
Releases: muhammadmobi/SolarLens
Release list
SolarLens v3.0.1
A fix to how a deploy tells the owner about the relay laptops. Nothing changes
on the dashboard; the guide's What is new card is now titled by the release
line, 3.0, rather than by this fix.
Fixed
- The deploy's relay note sees every commit a merge brings. It compared
the newest commit with the one before it, but "Rebase and merge" brings a
pull request's commits tomainin one push - so the 3.0.0 deploy,
twenty-seven commits, said nothing about the relay's setup changing in an
early one.
It now compares with the commit the version serving before the deploy was
built from - each deploy records its commit on the Worker version it makes,
so this holds after a rollback or a re-run too - or the last release tag for
a version from before that, and lists only the files that run on the relay
laptops. 3.0.0 changedscripts/setup-relay.ps1, so a relay laptop
set up before it should runsetup-solarlens-relay.cmdonce more.
The full entry is in CHANGELOG.md.
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 b...
SolarLens v2.11.0
SolisCloud readings again after the portal moved its plant page, a relay that
starts again and logs in by itself, Historical Data one system at a time, and a
notice when a newer version is out.
Added
- The relay logs in by itself. SolisCloud's web login lasts seven days,
and renewing it meant noticing the dashboard's warning, then finding and
running a file. With the SolisCloud username and password in.dev.vars
(SOLIS_USERNAME,SOLIS_PASSWORD), a hidden relay whose login has run
out logs in again on its own, still hidden - filling both in, ticking the
agreement as the owner asked, pressing Login. Only if SolisCloud asks for a
puzzle or a code does a window open, filled in, for a person to answer it
and press Login; the relay never answers those, and never sends the login a
second time. One attempt an hour at most, remembered across restarts, and a
new laptop with the login saved starts hidden too.make-laptop-installer.ps1
carries the login into the installer it writes, and setup writes it to
.dev.vars- plain text both, for the owner's own machines only. - A proper icon for the guide. The header's "?" is now an open book with
the word Guide beside it - drawn in the text colour, so it follows the theme
and inverts while the guide is open - and the book alone on a phone, where the
header has room for icons only. - Historical Data, one system at a time. A switch at the top of the tab -
All systems, or any one of them - narrows the charts, the tables, the row
count and the CSV to the system chosen, so looking after one system no longer
means scrolling past the other. All systems, one after the other, is still the
default, and the choice is remembered in each browser; a remembered system
that no longer exists falls back to all of them. A single system's CSV is
named for it. - "A new version of SolarLens is available." A dashboard tab refreshes its
figures but never the page itself, so a tab left open across a release kept
running the old one - which is how the 2.10 guide went unseen on a screen that
had been open since before it. The page now asks for its own address, headers
only, every quarter of an hour and on coming back to the tab, and compares the
ETag with the one it loaded with; when they differ it offers a Reload. TV mode
has nobody to press anything, so a wall display reloads itself. It costs one
tiny request each time and nothing on the database.
Changed
- The code explains itself. Every source file now opens with what it is for
and how it fits with the others - the Worker's routes and cron, the database
layer, each vendor's client, the relay agent, the scripts - and each function
worth explaining has a summary above it. The dashboard's script opens with a
map of its twenty sections, each marked so it can be found by searching. Test
files say what they hold the code to, and the shared test helpers - SQLite
behind D1, a Worker to send requests to, a stand-in push service and service
worker - are explained where they are defined. - Every CI job says what it proves, and what to do when it fails. Each job
that can run on your own machine now gives the command that does, so a red
cross comes with its next step. The three that exist only on GitHub - CodeQL,
dependency review and the reviewer request - say instead where their findings
appear, or how to ask again. The two tsconfig files say
what each type-checks and why there are two. Every test helper that builds
data or fakes an API has a line saying what it stands for, and one helper
nothing called is gone. - Debugging, written down. The README gains Debugging a failing test - one
test at a time, the browser visible, stepping through, and reading a trace -
and Debugging the live system: the Worker's live log, /api/health, a query
against production that reaches it in one piece, and watching a relay laptop
work, since a hidden relay writes no log. - Comments inside the long functions, too. Every function over thirty lines
now carries a comment at least every fifteen lines, stating each step and the
reason for it: how SolarMan's grid and battery figures are signed, how a
SolarMan timeline becomes occurrences, what a failed push does, how the relay's
chart tabs are walked back a year at a time. - A gap written down. Checking the comments against the live API showed that
three parts of the dashboard read a raw vendor payload the server deliberately
never sends: the Raw telemetry table, the alerts built from vendor flags, and a
device's own alert count. None of them shows on the live site, and the tests
missed it because their fixtures include the payload. It is recorded as gap 5
in docs/feature-gaps.md, with the ways to close it, rather than changed here. - The documents match the code again. Checked mechanically against it: the
README's data model now has all eleven tables (it was missing alarms, vendor
totals and relays), its end-to-end section describes all ten spec files rather
than one, and its coverage table has real figures and a row for push; the
handover used two wrong table names and missed a third, and its release steps
now include the guide. Both now say that an open Copilot conversation blocks a
merge, which one of them said it never did.
Fixed
- SolisCloud moved its plant page, and the relay finds it again. In October
2026 the portal dropped/station/stationDetails/generalSituation/<id>, the
page the relay opened to read a plant, for
/overview/plantStation/details/overview/<id>. A relay that knew only the old
address waited every cycle for figures that never came - with a fresh login
or without - so no Solis reading arrived from 15 October. It now opens the
new page, and the old one only if the new one sends nothing. - A relay that stops starts again. A page that failed to open - no network
yet, just after logon - left a wait nobody handled, and that ended the whole
relay about thirty seconds later. Windows' "restart on failure" covers only a
task that fails to start, so the relay stayed down until the next logon - once
for nine days. No wait is left unhandled now, a failure nothing was waiting
for is logged instead of ending the relay, and the installer adds a check
every ten minutes that starts the relay again if it has stopped: run it once
on each relay laptop. - The vendor's period totals arrive again. The redesign also dropped the
chart's "previous year" button. Looking for it threw away the month, the year
and the lifetime already read, and was tried again every cycle; the walk back
through earlier years now stops where the button is missing and keeps the
rest - the earlier years were sent long ago and do not change. - The page is no wider than a phone. The tab bar could not shrink, and on a
phone it made every page wider than the screen - "Devices" was cut off at the
edge and "Historical Data" broke over two lines - while the overview's
columns would not go below 430px. A page wider than the screen makes the
browser zoom the whole layout out, which is how the new-version notice first
landed below the bottom of a phone screen, where it could not be pressed.
The tabs now scroll sideways in their own strip, one line each, and the
overview's columns shrink to fit; a test walks every view at phone width and
fails if any is wider than the screen.
The full entry is in CHANGELOG.md.
SolarLens v2.10.0
Phone notifications, SolarMan faults that say when they cleared, and a guide to
the whole app.
Added
-
A guide, behind the ? at the top of every page. What SolarLens is, what
each tab holds, a "where do I find" table from everyday questions to the place
that answers them, what is new in this release, and the few things worth
knowing - that an on-grid system reads offline every night, that one relay
laptop is enough, that a SolisCloud login lasts seven days. An icon rather than
a tab, because a guide is read now and then and the tabs are used every day;
it opens even before the data has loaded, and its version is held equal to
the release by a test. -
Notifications that reach a closed browser. The Alerts tab gains Also when
this browser is closed: turn it on once per device and that device is told,
through its browser's own push service, when a system stops reporting while it
was producing, when a fault is recorded, when a SolisCloud login is two days
from running out or has run out, and when a vendor stops answering. Each rule
is written for a phone rather than a screen - the on-grid system goes quiet
every night because its inverter sleeps, so silence counts only after a reading
that showed it generating. Each event is told once, and again only if it clears
and returns.The push itself carries nothing: it wakes the device, whose service worker
asks/api/push/recentwhat to show, so what a notification says never
passes through the push services, and there is no payload encryption to get
wrong. Signing a device up needs the key - from the/authcookie or typed
once and not kept - while turning a device off needs none. A device gets one
message as soon as it signs up, and a Send a test button after that.Setting up is one command,
node scripts/make-vapid-key.mjs, which makes the
signing key and stores it as the Worker secretVAPID_KEYwithout printing or
saving the private half. Migration 0013 adds the two tables. -
SolarMan alarm detail. SolarMan's alert list says when a fault was raised
and nothing else. Its portal's detail panel makes two more calls - found in the
portal's own code - and SolarLens now makes them too, for the newest five
alerts each hour: the advice, where SolarMan has any, and the moments that day
the fault was active. Each run of five-minute samples becomes one occurrence
with an end, which also brings back the occurrences the list folds into one row
per day. On the plant observed here, two stored alerts became four occurrences,
each with an end. A run still going, or one that reaches midnight, is left
without an end rather than given a guessed one.
Fixed
- The reading-offset backfill works against the real database. Its first
run against production answered "undefined" for every plant: with--file,
a remote D1 database treats the statement as a bulk import and replies with
import statistics rather than rows, while a local one replies with rows - so
it passed every local test. Through the wrangler wrapper instead, a shell
splits the statement at every space. The script now runs wrangler's own entry
point with node, handing it the statement as one argument, and stops on rows
of an unexpected shape rather than printing "undefined" and carrying on. Found
on the dry run, before anything was written. - The config step of
scripts/wrangler.mjsis nowscripts/wrangler-config.mjs,
shared by the wrapper and the backfill, so the two cannot drift. The wrapper
behaves exactly as before.
The full entry is in CHANGELOG.md.
SolarLens v2.9.0
Daylight saving handled properly, history as a file, a dashboard that can speak
up, and accessibility checked rather than assumed.
Added
-
Download CSV, on the Historical Data tab. The rows on screen - day, month
or year - written out as a file, with the systems named, times in ISO, and a
figure the vendor never reported left empty rather than written as zero. A
value containing a comma is quoted the way every spreadsheet expects. Built in
the page from what is already fetched, so it costs no database read and adds
no route to guard. -
"Tell me when something changes", on the Alerts tab. The browser raises a
notification when a new alert appears - a system going offline, a fault, a
relay login about to expire - on whichever device has the dashboard open.
Permission is asked for only when the switch is turned on. Turning it on
records what is already wrong without announcing it: you turned it on to hear
what happens next, not to be told the last week at once. A fault that clears
and returns is announced again.Honest about what it is: the browser runs the page only while a tab holds it
open, so this is a louder dashboard rather than a pager. Reaching a closed
browser needs Web Push, which is written up indocs/feature-gaps.mdrather
than half-built here. -
Accessibility is checked on every view, desktop and mobile, with axe-core's
WCAG 2 A and AA rules, failing the run on anything serious or critical - plus
a keyboard walk-through and a check that every control shows its focus. -
The dashboard's own script is measured. V8 coverage during an end-to-end
walk-through, written tocoverage/page-coverage.jsonand attached to the
run: 67.3%, with a floor of 65% that fails the run if it drops. The figure
moves as the page grows - it was 69.6% before this release added the export and
notification code - so the floor is set below it rather than at it. Until now
only the Worker's half of the project had a number.
Fixed
-
Daylight saving no longer moves a day of history. The plant's UTC offset
was a number on the inverter row, refreshed on every poll, and every day of
history was cut with whatever that number said today: a summer evening read
back in winter landed in the wrong day, and for the five minutes around a
switch the current day was cut wrong too.The zone's own name is stored now, where the vendor states one, and each
reading carries the offset that was in force at its own timestamp - so a day
keeps the boundary it was recorded under, whatever the clocks do afterwards. A
plant whose vendor only ever sends a number still falls back to that number,
which is the best that can be said for it. Migration 0012 adds both columns,
andscripts/backfill-reading-offsets.mjsgives the readings already stored
the offset they were taken under - a dry run first, which says how many rows
it would touch before it touches any.Today's chart follows the same rule. It used to open at midnight computed
from the offset in force now, so on the one morning a year the clocks go
back there, the first hour of the day sat outside the window and off the
picture, and a time printed beside a sample was an hour out. The day is now
drawn between the instants it actually ran between - twenty-three or
twenty-five hours where that is what it was - and every time beside a reading
is the time that reading was taken. -
Four accessibility faults, found by the new checks on their first run:
muted text at 2.93:1 against the page's own ground where 4.5 is the bar; the
brand orange used for 16px type at 3.42:1; the normal and warning pills a
shade under; and tables that scroll sideways with no way to reach them from a
keyboard. The brand colours now have darker ink versions for type, while the
charts keep the brighter ones.
The full entry is in CHANGELOG.md.
SolarLens v2.8.0
Coverage above 90% on every measure, branches included - and a bug found on the
way there.
Fixed
- A device record with no station id was stored under the plant id
"null",
the four-character string.pickanswersnullfor a key that is missing,
and the code tested its result againstundefined, so the fallback was always
taken. It never showed, because every caller passes the plant in - which is
exactly the kind of wrong that only a test walking the arm can find.
Changed
-
Coverage thresholds rise to 97% statements, 90% branches, 97% functions and
99% lines, from 95/82/95/97, and the suite measures 98.0 / 90.5 / 98.5 /
99.4 over all ofsrc/. 314 unit tests, up from 275.Branches came up by walking the arms rather than by lowering the bar: each
fallback in the vendor normalisers was given a payload that takes it. Four new
suites -device-shapes,fallbacks,series-rulesand the earlier
sparse- cover the shapes a device record arrives in, a Worker deployed
without its ingest token, a curve nested insidedata, an inverter that
already knows its own name, the three ways a vendor states a timezone, a live
sample beating a backfilled one for the same instant, and where a plant's day
begins when the vendor never said where the plant is.
The full entry is in CHANGELOG.md.
SolarLens v2.7.0
The Worker's own routes, SQL and cron are tested for the first time, against a
real database.
Added
-
A database to test against, in
tests/helpers/d1.ts. D1 is SQLite, and
Node ships one: this putsnode:sqlitebehind the D1 interface, applies the
project's own migrations to it, and rewrites D1's?1placeholders to the
named form SQLite binds. The SQL a test exercises is therefore the SQL that
runs in production, and the queries had never been run by any test before.
It also refuses a bound value D1 would refuse, which turns aD1_TYPE_ERROR
in production into a failing test - it caught one such call while being
written. -
The Worker itself, in
tests/helpers/worker.ts: its exported fetch and
cron handlers, called with that database, a stub for the static-assets binding
and whichever tokens the case is about. Five new suites use it:worker-routes.test.ts- the security headers on an API response and on
the page the asset binding serves; reads open and writes refused, including
one token not opening the other's route;/auth's cookie; every ingest
route's contract and what each refuses; the identifiers proved absent from
what is served; the cron entry point.worker-edges.test.ts- the generic ingest route, a Worker deployed with no
API_TOKEN, the poll log's trimming and its newest line per provider, the
token store, and the hardware fan-out that fetches each inverter's own page.poll.test.ts- which providers are built from which secrets, the
INCLUDE_PLANTSfilter, a provider that throws not costing the other one, a
failing hardware list not costing the reading it came with, and the extras'
hourly and daily schedules including the first run's walk back through the
years until one comes back empty.readings.test.ts- SolisCloud's inverter page, with the portal's export
sign flipped to this project's "+ import"; SolarMan's per-device registers
layered over its station snapshot, including a register present but empty,
which must not be read as zero.sparse.test.ts- what happens when a vendor sends almost nothing: nulls
rather than zeros, an empty database answering every read, a device merged
rather than overwritten by a thinner second view, and an id with no alias
answered asunknownrather than echoed back.
Changed
-
Coverage is measured over all of
src/and enforced far higher. One
honest figure replaces two: 97.1% of statements, 84.7% of branches, 97.5% of
functions and 99.3% of lines, against an enforced scope that used to leave out
index.ts,db.tsandpoll.tsbecause nothing could reach them. Thresholds
rise from 80/63/80/80 to 95/82/95/97. 275 unit tests, up from 189.Branches stay below the rest deliberately, and the README says why: the vendor
payloads are long chains of optional fields and v8 counts every arm, so the
last few points would mean a fixture per arm for figures already proved. -
The unit suite runs in forked processes with
--experimental-sqlite, set in
vitest.config.tsrather than in an environment variable, sonpx vitest
behaves the same asnpm teston any shell. Node 24 and newer ignore the flag.
The full entry is in CHANGELOG.md.
SolarLens v2.6.0
Every change now has to pass twelve checks before it can merge, and reaching
the live dashboard takes one approval and proves itself afterwards.
Added
-
Checks on every pull request, in parallel, in about five minutes:
type check, unit tests with coverage, end-to-end tests, and a build dry run
that bundles the Worker without uploading it. None of them needs a secret, a
Cloudflare account or a SolisCloud login, so a fork gets the same checks.
All twelve are now required before anything can merge tomain. -
Four guards written for this project.
check-privacyrefuses an
identifier - the deployment address, an email that is neither a vendor's nor
plainly fake, a database id, a coordinate, or a long number that could be a
plant or station id - in any file, in every commit of the pull request,
patch and message, and in the pull request's own description. A value added in
one commit and removed in the next still lives in that pull request's ref for
good, which is how this repository's identifiers escaped in the first place.
Real values come from a masked secret, and a failure names the file, line and
rule but never prints what it matched.check-attributionrefuses a commit
credited to anyone else.check-headersrefuses drift between the security
headers in the Worker and the copy the asset handler serves, which exist twice
because Cloudflare servespublic/without running the Worker.
check-cmd-shaperefuses a.cmdthat is not the one shape that survives the
update it runs. -
A Windows job for the relay's own scripts: every PowerShell file must
parse, the analyser must find no errors, and the relay must refuse and exit
when its settings are missing rather than hang. Every bug fixed in 2.5.0 was
found by running those scripts by hand. -
Vulnerability and secret scanning, on every pull request, every push to
main, and again each Monday, because an advisory can be published against
code nobody has touched: CodeQL with thesecurity-extendedrules, dependency
review,npm audit, gitleaks over the whole history, actionlint, and a check
that every action names a commit rather than a movable tag. Downloaded tools
are pinned to a version and a SHA-256. -
Deployment, after an approval. A merge deploys nothing by itself: the run
starts only once the checks have passed onmain, then waits on a protected
environment until a person approves it. It then remembers the version serving,
applies migrations, deploys, and asks the live site whether it worked -
the page and its security policy, health and latest answering, a vendor feed
that is current, no identifiers in the public responses, and the
token-protected routes still refusing a caller without one. Any failure puts
the previous version back automatically. The summary says when the relay
computers need updating. -
A release button. It refuses unless
package.json, the changelog section
and its link reference all name the version and no such tag exists, scans the
notes for identifiers, tags the commit and publishes. It pushes the tag by its
full ref name, because a branch sharing a tag's name made that push fail by
hand on 17 September.
Changed
-
Dependencies are updated by hand, on purpose. Automatic update pull
requests were tried for a day and turned off; the config went with them. What
protects the project is not a queue of pull requests butnpm auditfailing
the build on an advisory in anything the Worker ships with, dependency review
refusing one that arrives with a new package, and Dependabot alerts warning
when an advisory is published against something already here. -
Node 22 is pinned for the checks, and the repository decides line endings
rather than each machine:.cmd,.ps1and.vbsnow arrive with Windows
line endings on any checkout. They were stored with Unix endings and only
became Windows ones through Git for Windows' own conversion setting, so a
machine without it handed cmd.exe batch files it can misparse. -
hono 4.13.8, plus the tooling: Wrangler 4.135.0, workers-types,
@types/node, Vitest and its coverage provider. Wrangler's update cleared the
three high advisories that stood against the development dependencies;
production had none.
Fixed
-
A deploy could have lost the ability to undo itself, quietly. The version
to roll back to was read with a command substitution insideecho, which
cannot fail: a failed lookup left it empty and the step went green. It is
checked now, and a run that cannot roll back says so. -
Dependabot signs every commit it makes as
support@github.com, which the
privacy guard read as a leaked address and refused; GitHub's own domains are
allowed now. The guard also read the digits inside a pinned commit hash as a
possible plant id, and so failed on its own workflow file.
The full entry is in CHANGELOG.md.
SolarLens v2.5.0
Renewing and updating a relay now shows nothing when nothing needs doing.
Changed
- The login check runs hidden, and a window opens only when a login is
needed. Since 2.4.0,renew-solis-login.cmdand every re-run of the
installer opened a visible Chrome window to check the saved SolisCloud login,
even when it worked, which is most weeks. On a machine that needed nothing,
a browser appearing and a terminal waiting looked like something going
wrong. Both now check in a hidden browser first. When the login works, a
reading goes through and nothing appears. When SolisCloud wants a login, a
window opens for it and closes itself once a reading has been sent. The check
also skips the alarm history and period totals, so it takes about twenty
seconds instead of a minute; the background relay started straight afterwards
reads them. - The
.cmdwindows close themselves when the run worked.
renew-solis-login.cmdandsetup-relay.cmdused to end on "press any key",
and a personalisedsetup-solarlens-relay.cmdwaited twenty seconds. All three
now close five seconds after success, and stay open only after a failure, so
its reason can still be read. - The installer skips what is already in place. Dependencies are installed
only whenpackage-lock.jsonchanged since the last install, and a relay task
someone disabled is left disabled rather than switched back on. Its.dev.vars
now saysRELAY_HEADLESS=1from the start.
Added
- The installer installs Google Chrome when it is missing, with winget, as
it already did for Node and Git. It used to warn and carry on, leaving a relay
that could not start a browser. RELAY_SKIP_EXTRAS=1, and distinct exit codes for a single run
(RELAY_ONCE=1):0a reading went through,3SolisCloud wants a login,
1anything else.
Fixed
- A relay with no working login could report success. It checked for the
login page once, three seconds after opening the portal. Measured with no
login at all, the portal stays on the plant page for about three seconds and
moves to its login page between three and five, so the check could miss it.
The cycle then sent no reading, logged the timeout as a per-plant error, and
reported success, and a single run told the installer a reading was through.
The relay now watches until the plant list loads or the login page appears,
and a cycle that sends no reading counts as failed. A login lost part-way
through a cycle is reported as a login to renew. - The background relay can no longer show a browser window.
scripts\relay-hidden.vbsforcesRELAY_HEADLESS=1for the relay it starts.
Before, it followed.dev.vars, where a0left by a login done by hand, or
by an installer run that stopped part-way, put a Chrome window on screen at
every logon. - A relay can no longer leave its browser open when it exits. If Chrome
does not close within fifteen seconds, the relay closes it the hard way. - A relay attached to your own Chrome (
RELAY_CDP) is no longer refused as
having no saved session when run hidden. renew-solis-login.cmdandsetup-relay.cmdno longer trip over their own
update. Both run a script that pulls new code, and that can replace the
.cmdfile itself while it is still running. cmd reads a batch file a line at
a time and carries on from the same byte offset in whatever the file now
says: in a test, a file rewritten under a running batch went on to run half a
line as a command. Each is now one parenthesised block, which cmd reads whole
before running any of it. This protects updates from 2.5.0 on; the files in a
2.4 checkout are still the old shape, so move a computer to 2.5.0 with
setup-solarlens-relay.cmd, which lives outside the repository.
Deploying: nothing to deploy; the Worker and the dashboard are unchanged. Move each relay computer to 2.5.0 by double-clicking setup-solarlens-relay.cmd, not renew-solis-login.cmd: the 2.4 copy of renew-solis-login.cmd is replaced by this update while it runs. After this update, either is safe. Regenerate setup-solarlens-relay.cmd with scripts\make-laptop-installer.ps1 for its five-second close. The full entry is in CHANGELOG.md.
SolarLens v2.4.1
Fixed
- Re-running the installer on a computer whose relay is already running no
longer races that relay for its browser. Since 2.4.0 a re-run checks the
SolisCloud login in a visible window, but it opened that window while the
hidden relay still held the same Chrome profile. Chrome lets one program use a
profile at a time, so the new window killed the hidden relay's browser to get
in, and the hidden relay could kill the new window's on its next cycle - in
the middle of a login. The installer now stops the hidden relay, and the
Chrome it drives, before the check, and starts it again afterwards, as
renew-solis-login.cmdalready did. A personalisedsetup-solarlens-relay.cmd
made earlier picks the fix up by itself: it updates the code before it runs
the installer.
Deploying: nothing to deploy; the Worker and the dashboard are unchanged from 2.4.0. On a relay computer, double-click setup-solarlens-relay.cmd or renew-solis-login.cmd; both fetch the latest code first. The full entry is in CHANGELOG.md.