Skip to content

Releases: muhammadmobi/SolarLens

SolarLens v3.0.1

Choose a tag to compare

@github-actions github-actions released this 29 Oct 09:34

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 to main in 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 changed scripts/setup-relay.ps1, so a relay laptop
    set up before it should run setup-solarlens-relay.cmd once more.

The full entry is in CHANGELOG.md.

SolarLens v3.0.0

Choose a tag to compare

@github-actions github-actions released this 29 Oct 08:51

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 b...

Read more

SolarLens v2.11.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 11:11

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

Choose a tag to compare

@github-actions github-actions released this 24 Sep 15:09

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/recent what 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 /auth cookie 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 secret VAPID_KEY without 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.mjs is now scripts/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

Choose a tag to compare

@github-actions github-actions released this 24 Sep 10:13

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 in docs/feature-gaps.md rather
    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 to coverage/page-coverage.json and 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,
    and scripts/backfill-reading-offsets.mjs gives 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

Choose a tag to compare

@github-actions github-actions released this 20 Sep 07:55

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. pick answers null for a key that is missing,
    and the code tested its result against undefined, 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 of src/. 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-rules and the earlier
    sparse - cover the shapes a device record arrives in, a Worker deployed
    without its ingest token, a curve nested inside data, 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

Choose a tag to compare

@github-actions github-actions released this 19 Sep 14:10

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 puts node:sqlite behind the D1 interface, applies the
    project's own migrations to it, and rewrites D1's ?1 placeholders 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 a D1_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_PLANTS filter, 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 as unknown rather 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.ts and poll.ts because 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.ts rather than in an environment variable, so npx vitest
    behaves the same as npm test on any shell. Node 24 and newer ignore the flag.


The full entry is in CHANGELOG.md.

SolarLens v2.6.0

Choose a tag to compare

@github-actions github-actions released this 19 Sep 12:58
e964e31

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 to main.

  • Four guards written for this project. check-privacy refuses 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-attribution refuses a commit
    credited to anyone else. check-headers refuses drift between the security
    headers in the Worker and the copy the asset handler serves, which exist twice
    because Cloudflare serves public/ without running the Worker.
    check-cmd-shape refuses a .cmd that 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 the security-extended rules, 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 on main, 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 but npm audit failing
    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, .ps1 and .vbs now 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 inside echo, 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

Choose a tag to compare

@muhammadmobi muhammadmobi released this 17 Sep 13:49

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.cmd and 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 .cmd windows close themselves when the run worked.
    renew-solis-login.cmd and setup-relay.cmd used to end on "press any key",
    and a personalised setup-solarlens-relay.cmd waited 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 when package-lock.json changed since the last install, and a relay task
    someone disabled is left disabled rather than switched back on. Its .dev.vars
    now says RELAY_HEADLESS=1 from 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): 0 a reading went through, 3 SolisCloud wants a login,
    1 anything 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.vbs forces RELAY_HEADLESS=1 for the relay it starts.
    Before, it followed .dev.vars, where a 0 left 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.cmd and setup-relay.cmd no longer trip over their own
    update.
    Both run a script that pulls new code, and that can replace the
    .cmd file 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

Choose a tag to compare

@muhammadmobi muhammadmobi released this 17 Sep 13:05

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.cmd already did. A personalised setup-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.