Commit Graph

7 Commits (15b536f393977eaf212d5e5bf7c143875b08b9c8)

Author SHA1 Message Date
nexxo 15b536f393 Mint the archive collection key from the console instead of by hand
tests / pest (push) Failing after 7m49s Details
tests / assets (push) Successful in 22s Details
tests / release (push) Has been skipped Details
Setting a NAS up to collect the invoice archive meant three machines and a
dozen commands: generate a keypair there, carry the public half to the server,
write it into authorized_keys with a restriction, install rsync, and get every
path right on the first try. Every one of those steps was a place to be told
"permission denied" with no clue which of the three was wrong. It was, and
several times over.

One button now. The panel asks, the host does it — because the host is where
all of it lives: the home directory, ssh-keygen, rrsync, and the archive itself.
The panel is www-data in a container and owns none of that, so it uses the
mailbox it already uses for updates. The private half is shown exactly once,
alongside the finished rsync command, and is never written to the database: it
exists to be copied into a NAS, and storing it "for convenience" would put a
working credential in every backup of that database.

rrsync does the restricting, not a pinned rsync option string. That string
differs between rsync versions and fails silently — a refusal with no reason
given, which is the shape of the afternoon this replaces. If rrsync is missing
the agent refuses rather than issuing an unrestricted key while the panel says
it is restricted.

rsync now comes with install-agent.sh, which already runs as root once per
machine. It has to be on the HOST: a NAS connects by ssh and sshd starts
`rsync --server` here, so without it the pull fails with "command not found"
from a NAS whose own setup is perfectly correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:08:32 +02:00
nexxo 33e5b15099 Restart the workers automatically after saving .env, instead of handing the operator back to the shell
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 02:26:34 +02:00
nexxo 21b7aaca42 Update only to a released version, never to whatever landed on main
tests / pest (push) Failing after 7m21s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Has been skipped Details
Asked for directly: a commit on main must change nothing on a server, and only
a tag whose version is higher than the installed one may be installed.

The agent asked two different questions before this. A pinned server compared
tags; everything else counted commits on its branch — so a console following
main offered an update for every push, verified or not, and "Jetzt
aktualisieren" meant "take whatever is on the branch right now". Landing on main
and being released are different events, and only the second is a decision
somebody made.

It is one question now, regardless of what the checkout is attached to: is there
a tag whose version is higher than the deployed one? Compared with sort -V
against the version in the deployment manifest — not the VERSION file, which an
update moves before it migrates, so a run that failed in between would leave the
checkout claiming a version it never finished installing. A run always targets
that tag; the branch path is gone. A request that arrives with nothing to
install is answered rather than obeyed, before anything announces a run.

The console follows: the button is disabled with nothing released, says so in a
line rather than leaving a grey button to be guessed at, names the version it
would install rather than a count, and refuses the action server-side as well —
a Livewire action is a public endpoint, and one place a rule may not live is
only in the disabled attribute of a button.

VERSION becomes 1.1.0, to be tagged as the first release this mechanism can see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 22:32:44 +02:00
nexxo 07634c8a1d Answer a check at once, start an update at once, and count down to neither
Checking for updates was a request written into the same mailbox as a real
update, collected by the same agent on the same systemd timer — so asking what
was new was answered on the next tick, and the console, having nothing else to
show, counted down to it. An operator who asked a question was told an update
would start in 3:44.

A systemd path unit now wakes the agent the moment the request file is written,
for a check and for a run alike. PathChanged, not PathExists: PathExists re-arms
as soon as the service goes inactive, so a request the agent could not consume —
it holds a lock for the length of an update — would restart it in a tight loop.
The timer stays as the fallback, and a drop-in disables systemd's start rate
limit, because two triggers on one oneshot service can otherwise wedge the unit
and leave neither of them able to run.

The countdown is gone from both the settings card and the maintenance overlay.
It could not be made honest: its target was recomputed on every poll, and where
the interval the agent reports is shorter than the timer actually installed on
that host it had already gone by, so the fallback moved it forward again each
time. It ran backwards four seconds and snapped back to a full minute, forever —
reported exactly that way. A queued run says it is starting; a queued check says
nothing beyond its badge, because a check starts nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 22:25:07 +02:00
nexxo f05547921d Separate checking for updates from applying them, and show live progress
tests / pest (push) Failing after 7m11s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
2026-07-28 20:23:06 +02:00
nexxo 3699c7cdb9 Ask the tunnel gateway where it actually listens
tests / pest (push) Successful in 7m15s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Successful in 5s Details
The console has been unreachable over the VPN, and the cause was the readiness
probe rather than the tunnel. The gateway binds to the WireGuard hub address
alone; the probe asked 127.0.0.1, where nothing has ever listened. It could only
fail, so VPN_READY stayed false, and on that basis the application withheld the
`DNS = 10.66.0.1` line from every client config it issued. A phone then built
the tunnel, asked its normal resolver for the console hostname, got the public
address and had its connection closed — indistinguishable from "this site does
not exist", which is exactly what it looked like.

Probing over TLS on the bare address would not have worked either: the site
matches on the console's hostname, so a request without SNI is offered no
certificate. The gateway now answers a plain-HTTP health port on the hub
address, which removes TLS, SNI and name resolution from the question and
answers only what is being asked — is this gateway listening, in this network
namespace, right now. Caddy refuses to start when the certificate is unreadable,
so a health port that answers still proves the whole file loaded.

Also here, all found while looking:

- Icons pushed their label onto a second line and rendered a size larger than
  asked for. Tailwind's preflight makes an svg display:block, and `.size-4` and
  `.size-5` have equal specificity, so stylesheet order decided — and it emits
  size-4 first. Every icon written as 16px was silently 20px. Recorded as R18.
- Four error pages printed `errors.404.hint` in place of a sentence: the lang
  files give those codes a null hint and Laravel returns the key for a null line.
- The Developer role had no label in either language, so the dropdown showed
  `admin_settings.role_developer`.
- The secrets area held one key, which is not worth a password gate. It now
  carries the credentials that actually stop the business when they expire —
  DNS, monitoring, SMTP — and the test button appears only where a checker
  exists, instead of reporting on Stripe whatever was being looked at.
- The update button said nothing about when a queued run would start or where a
  running one had got to; both are now shown, and a failure names its step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 14:43:23 +02:00
Claude 276f926d57 Add the update button, and give it a host-side agent that can actually update
tests / pest (push) Successful in 7m41s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Successful in 4s Details
The panel had no way to update the installation, and could not have one on its
own: it runs as www-data inside a container, and the update restarts that
container. A button that shelled out would kill its own web server
mid-response, and would need the application to hold host-level credentials —
a far worse thing to own than an out-of-date checkout.

So the button is a mailbox. The panel writes a request into the checkout; a
systemd timer on the host, running as the service account, consumes it, runs
deploy/update.sh and writes back what happened. The same timer answers the
question the panel cannot answer alone — is there anything to update — which
needs a git fetch, and therefore credentials the application does not have.

The parts that were wrong before review, all of which would have shipped as a
button that looks fine and does nothing:

- The agent was only installed by install.sh, which the installed base never
  runs again. Every existing server would have shown the button permanently
  disabled. It has its own root entry point now, install.sh delegates to it,
  and update.sh says so when the unit is missing.
- On a release-pinned server the checkout is detached, so it followed
  "origin/HEAD" and then ran an update that deliberately stays on the pinned
  tag: updates advertised, nothing applied. It now compares TAGS in release
  mode and passes the target release through.
- On a branch other than main it advertised that branch's commits and then
  deployed main's, because update.sh defaults to main.
- A request written while the agent was down was executed whenever the agent
  next started, days later.
- A single status file meant the routine five-minute check overwrote a failed
  update with "idle" before anyone saw it.
- An agent that had been stopped left the button enabled forever, because a
  status file written once counted as an agent.
- The agent's error messages were German strings rendered into an English
  interface; it reports codes now and the panel translates them.

Also: the add-address button in the console-access panel was as tall as the
field plus its hint, because the hint was a sibling inside the flex row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:33:17 +02:00