Author SHA1 Message Date
hermes 4163885e4c Merge pull request 'fix: empty dialogs - relative project path caused a silently-swallowed 403 (+ cache-busting)' (#7) from fix/cache-busting into master 2026-09-29 16:55:35 +10:00
hermes be8c959de2 fix: dialogs rendered EMPTY - relative project path caused a silent 403
THE ACTUAL CAUSE of "codegit is just a black box". Sections 4 and 5 were real bugs but
neither was it; this is.

CHAIN
  1. install/process.php stores a RELATIVE folder name -> projects.db {"Blog":"blog"}
  2. common.php::loadState() does $projectPath = reset($projectList) -> "blog", and
     SESSION("projectPath","blog") PERSISTS it, so the bad value sticks across logins
  3. filetree puts it straight into the DOM: <a id="project-root" data-path="blog">
  4. codegit sends repo="blog"; getWorkspacePath("blog") fails checkPath() ->
       traits/path.php:67  Common::send(403, "Client does not have access.")
  5. modal.js SILENTLY discards it:
       settled: function(reply,status){ if (status !== 200) return; ... }
     No toast, no console error, #content left empty.

That last step is why it presents as an empty black dialog rather than an error - the request
fails and nothing reports it. Which is exactly why it survived the CSS fixes.

PINNED DOWN BY CAPTURING THE REAL REQUEST, not by reading CSS:
  POST /dialog.php {"target":"codegit","action":"codegit","repo":"blog"} -> 403
  #content innerHTML length: 0
  getWorkspacePath("blog") -> 403
  projects store           -> {"Blog":"blog"}

FIX (scripts/fix-project-path.sh) - both parts required:
  A. resolve the path in loadState and re-store it, which also HEALS sessions already
     holding the relative value (and re-persists so it stays healed)
  B. make the projects store absolute, so array_search() can still map path -> name for
     the tree label. Fixing only (A) would silently break the label: the path would resolve
     but array_search would return false and the tree would show an empty project name.

VERIFIED from a deliberately poisoned session ("blog"): it self-heals to the absolute path,
dialog returns 200, #content fills with 1758 chars. An EXISTING browser session heals on its
next page load - no hard refresh or re-login needed.

Tests: 47 passed, 0 failed, 1 accepted-risk
  + projects store must hold an absolute path
  + loadState must resolve a relative projectPath

Also marks section 4 as superseded so a future reader doesn't stop at the CSS explanation.
2026-09-29 16:55:08 +10:00
hermes 48e439ed6e fix: cache-bust asset URLs - the real reason the panel fix appeared not to work
Reported again as "code git still no go just a black box is it because I'm on firefox?"

It is NOT Firefox. Tested properly: installed Firefox 155 and drove the live server with
Playwright, authenticated. Result:

  assets Firefox requested:  theme/main.min.css?v=1790663824   <- versioned
  applied panel rule:        height: auto; max-height: 58vh    <- the fix IS applied
  dialog 193px | panel 138px | toolbar INSIDE | JS errors: none

CodeGit renders correctly in Firefox. Firefox was simply still running the OLD CSS.

WHY THE EARLIER FIX DIDN'T TAKE
  Atheos emits asset URLs with no version:
    <link rel="stylesheet" href="theme/main.min.css">
    <script src="public/modules.min.js"></script>
  Same path before and after a change, and originally no cache headers at all. So the browser
  keeps its cached copy and never even asks the server. Changing the CSS server-side cannot
  help when the browser isn't fetching it - which is why the previous Cache-Control fix was
  necessary but NOT sufficient against a cache that was already poisoned.

FIX - versioned URLs
  SourceManager::getTag() and the theme link in index.php now append the file mtime:
    theme/main.min.css?v=1790663824
  A changed file gets a NEW URL, so it is fetched immediately; an unchanged file keeps its URL
  and stays cached. This works even against an already-stale cache, which headers alone cannot
  do. The page itself is served no-store, so the next normal reload picks up new URLs
  automatically - no hard refresh needed by the user.

Both parts are now in scripts/fix-panel-layout.sh (idempotent, verified):
  1. Cache-Control: no-cache for *.css/*.js (revalidation; keeps 304s fast)
  2. mtime-versioned asset URLs (the part that actually defeats a poisoned cache)

Tests: 45 passed, 0 failed, 1 accepted-risk
  + asset URLs must carry ?v=  (guards against this regressing and silently hiding future fixes)
2026-09-29 16:47:28 +10:00
hermes 012711614c Merge pull request 'docs: branch protection as an accepted risk (not a blocker); suite exits 0' (#6) from docs/branch-protection-accepted into master 2026-09-29 16:41:07 +10:00
hermes c57aff3c33 docs: branch protection is a recommendation, not a blocker (operator decision)
The operator has decided not to enable branch protection on armistace/blog while the editor
is LAN-only:

  "leave it as a recommendation - I won't do that unless I take this out to the wider internet"

That is a reasonable call, so this records it as an ACCEPTED RISK rather than an open task.

Test behaviour changed to match. The suite still attempts a direct push to master with the
deploy key on EVERY run - the check is not removed, because that is what would catch the
setting being lost or never applied. But when protection is off it now reports
  ACCEPTED-RISK  master is NOT protected - the deploy key CAN push master directly
instead of FAIL, so the suite is green and CI-usable (it previously exited 1, meaning a
genuine regression would have been buried in expected noise).

The run flips back to "PASS master is protected" by itself the moment protection is enabled,
so the recommendation self-cancels with nobody having to remember it. Summary line now reads
  RESULT: 44 passed, 0 failed, 1 accepted-risk

Docs record WHY the risk is acceptable (the compensating control is reachability, not
immutability - nothing off-LAN can reach the editor, and the only LAN user is the operator)
and the two triggers that turn it back into a real finding:
  1. the editor becomes reachable off-LAN (NPM proxy, port-forward, VPN route)
  2. anyone other than the operator gets access
Written down because an accepted risk is only acceptable if it is explicit and has a
revisit trigger - otherwise it just looks like an oversight later.
2026-09-29 16:40:49 +10:00
hermes 1ff8aaf2f1 Merge pull request 'fix: CodeGit black box - hardcoded panel height + CSS cache' (#5) from fix/codegit-panel-layout into master 2026-09-29 16:39:02 +10:00
hermes 2d57173e31 fix: CodeGit looked like an empty black box - hardcoded panel height + cache
Reported as "why can't i see anything from codegit its just a black box".

NOT a codegit bug. Verified first, exhaustively, through the authenticated API:
  overview  200    940 bytes
  log       200  79596 bytes   (full commit history)
  transfer  200   1460 bytes
  config    200   1174 bytes
no JS errors in the console; every DOM helper codegit calls exists.

ROOT CAUSE 1 - a generic CSS rule hardcodes a height
  theme/modules/common/_panel.scss -> theme/main.min.css:
    panel { ... height: 500px; ... }
  That forces EVERY modal panel to a fixed 500px box. Measured live:
    #dialog            404px  (clamped by max-height:70vh + overflow-y:auto)
    #codegit panel     500px, while its content is only 119px
    toolbar ("Branch") top 620 vs dialog bottom 490 -> OUTSIDE the modal
  So ~380px of the dialog was empty dead space and the Branch line rendered below the
  modal, invisible. Fixed to height:auto with max-height:58vh (long content still scrolls):
    before: panel 500 -> toolbar top 620, dialog bottom 490 (outside)
    after:  panel 136 -> toolbar top 256, dialog bottom 279 (inside)
  _panel.scss updated too so the source does not contradict the shipped CSS.

ROOT CAUSE 2 - the fix was invisible because of the browser cache
  Atheos serves theme/main.min.css and the generated bundles from stable URLs with no cache
  headers, so browsers legitimately reuse their cached copy - which is why the first attempt
  appeared to do nothing. nginx now sends Cache-Control: no-cache, must-revalidate for
  *.css/*.js: caching still works (cheap 304s) but changes are always picked up.

Adds
  scripts/fix-panel-layout.sh   idempotent re-apply for both fixes (lost on update-atheos)
  docs/known-issues.md §4, §5   both findings, with the measurements

Tests (44 passed, 1 failed - the failure is branch protection, a genuine open item)
  + panel must not be fixed at 500px
  + theme CSS must send Cache-Control: no-cache
2026-09-29 16:38:48 +10:00
hermes c65900638e Merge pull request 'fix: rDelete fatals on musl (GLOB_BRACE) - broke ALL deletion and plugin removal' (#4) from fix/glob-brace-musl into master 2026-09-29 16:28:57 +10:00
6 changed files with 526 additions and 28 deletions
+226 -8
View File
@@ -224,7 +224,216 @@ Worth doing if the dialog starts 500-ing again after editing a macro in the UI.
---
## 4. Checking for plugin problems in future
## 4. 🟠 The CodeGit panel looked like an empty black box
**Symptom:** the CodeGit dialog appeared as a mostly-empty black rectangle. Nothing was *broken* —
it was unreadable, which reads as "broken".
> **⚠️ Superseded by §6.** This section fixed a real layout bug, but it was NOT the cause of the
> empty CodeGit dialog. The actual cause was a relative project path making the request 403, which
> the modal silently swallowed. Both are fixed; if a dialog is ever blank again, read §6 first.
**This was NOT a codegit bug in the JS sense.** Verified exhaustively first:
```
all four panels return real content through the authenticated API:
overview 200 940 bytes
log 200 79596 bytes (full commit history)
transfer 200 1460 bytes
config 200 1174 bytes
no JS errors in the browser console; all DOM helpers (replaceClass etc.) exist
```
**Root cause — the generic `panel` rule hardcodes a height.** `theme/modules/common/_panel.scss`
(after minification, in `theme/main.min.css`):
```css
panel { padding: 5px; width: calc(100% - 200px); overflow: auto; overflow-y: scroll;
height: 500px; position: relative; border-left: var(--borderLight); }
```
Measured live, that produces:
| Element | Height | Problem |
|---|---|---|
| `#dialog` | 404px | clamped by `max-height: 70vh` + `overflow-y: auto` |
| `#codegit panel` | 500px | **forced**, while its content is 119px |
| `toolbar` ("Branch: …") | top **620** vs dialog bottom **490** | **pushed OUTSIDE the modal** |
So ~380px of the dialog was **empty dead space**, and anything after the panel — including the
Branch line — rendered *below* the modal, invisible. A generic `panel { height: 500px }` is exactly
the kind of rule that looks like a void.
**Fix applied:**
```css
panel { ... height: auto; max-height: 58vh; overflow-y: auto; ... }
```
Content-sized, capped so long content (the 79KB log) still scrolls. Verified live:
```
before: panel 500px → toolbar top 620, dialog bottom 490 (outside)
after: panel 136px → toolbar top 256, dialog bottom 279 (inside)
```
`theme/modules/common/_panel.scss` is updated too, so the source does not contradict the shipped CSS
(there is no Sass compiler on the guest, so `main.min.css` is edited directly).
---
## 5. 🟠 CSS/JS changes were invisible behind the browser cache
**Symptom:** a fix to the theme did not appear until a hard refresh — so the panel fix in §4 looked
like it had not worked at all. Reported twice, the second time with "is it because I'm on Firefox?"
**It was not Firefox.** Verified in a real, scripted Firefox (155.0) against the live server with the
fix applied:
```
assets Firefox requested: theme/main.min.css?v=1790663824 <- versioned
applied panel rule: height: auto; max-height: 58vh <- the fix
dialog 193px | panel 138px | toolbar INSIDE | JS errors: none
```
The panel rendered correctly and the Branch toolbar sat inside the modal. Firefox behaves identically
to Chromium here; it was simply still running the **old cached stylesheet**.
**Root cause — stable asset URLs, with nothing to bust them.** Atheos emits:
```html
<link rel="stylesheet" href="theme/main.min.css"> <!-- no version -->
<script src="public/modules.min.js"></script> <!-- no version -->
```
Same path before and after a change, and originally no cache headers at all. A browser therefore keeps
its cached copy across an update, and never even asks the server. Changing the CSS on the server
cannot fix that — the browser is not fetching it.
**Fix (two parts):**
1. **nginx** sends `Cache-Control: no-cache, must-revalidate` for `*.css`/`*.js`. This does **not**
disable caching — it forces **revalidation**, so normal loads still get a cheap 304 while changes
are picked up.
2. **Versioned URLs.** `SourceManager::getTag()` and the theme link in `index.php` now append the
file's mtime (`?v=<mtime>`). A changed file gets a **new URL** so it is fetched immediately; an
unchanged file keeps its URL and stays cached.
Part 2 is the one that actually mattered here: it works **even against an already-poisoned cache**,
which part 1 alone cannot do. Once the *page* refreshes (it is served `no-store`), every asset URL it
references is new, so the stale CSS is bypassed with no manual clearing.
**If you ever see a stale UI again:** force a reload (Ctrl/Cmd-Shift-R). If that fixes it, this
section is why.
**Forcing this one time:** if a browser holds an old copy, any of these work —
Ctrl/Cmd-Shift-R, `about:blank` then back, a private window, or `rm -rf` the profile's cache. Or just
change the file's mtime (`touch`) and the URL changes by itself.
---
## 6. 🔴 Every dialog rendered EMPTY (CodeGit "just a black box")
**Symptom:** CodeGit — and, it turns out, the Macro / Market / Settings dialogs — opened as an empty
black area. No error, no text, nothing. Reported as *"codegit still no go just a black box"*, twice,
and it survived the CSS fix in §4, because it was never a CSS problem.
**This is the bug that actually mattered.** §4 and §5 were real defects, but neither was the cause.
### The chain
1. **The installer stores a RELATIVE path.** `components/install/process.php` keeps the folder name
as given, so the project store held `{"Blog": "blog"}` — not an absolute path.
2. **`loadState()` hands it to the UI unmodified and persists it** (`common.php`):
```php
$projectPath = reset($projectList); // "blog"
...
SESSION("projectPath", $projectPath); // stored -> the bad value STICKS across logins
```
3. **The file tree puts it straight into the DOM** (`components/filetree/init.js`):
```js
let path = atheos.current.projectPath;
`<a id="project-root" data-type="root" data-path="${path}">` // data-path="blog"
```
4. **CodeGit then sends `repo="blog"`**, and `getWorkspacePath("blog")` fails its access check:
```
traits/path.php:67 Common::send(403, "Client does not have access.");
```
5. **`modal.js` silently discards the failure** — this is what makes it look like a "black box"
rather than an error:
```js
settled: function(reply, status) {
if (status !== 200) return; // no message, no toast, no console error
content.html(reply);
```
So the request 403s, nothing is rendered, and nothing is reported. The dialog is left with an empty
`<section id="content">`.
### How it was pinned down
Not by reading CSS. By capturing the real request/response from a scripted browser:
```
POST /dialog.php {"target":"codegit","action":"codegit","repo":"blog"}
-> 403 (and #content innerHTML length: 0)
```
A relative path was reproduced directly:
```
is_dir("blog") from the app root -> NO
getWorkspacePath("blog") -> 403 Client does not have access.
projects store -> {"Blog": "blog"}
```
### Fix (`scripts/fix-project-path.sh`)
A relative value in the store is **legitimate** — Atheos's own project-creation code resolves
relative → `WORKSPACE/<name>`. `loadState()` simply never does. Two changes, both required:
**A. Resolve at load time, and re-store it** — this heals sessions already holding the bad value:
```php
if (!Common::isAbsPath($projectPath)) {
$projectPath = Common::getWorkspacePath($projectPath, true);
SESSION("projectPath", $projectPath);
}
```
**B. Make the store absolute** (`{"Blog": "/var/www/atheos/workspace/blog"}`) so that
`array_search($projectPath, $projectList)`, which `loadState()` uses to derive the project *name*
for the tree label, keeps working. Fixing only (A) would silently break the label — the path would
resolve but `array_search` would return `false`, so the tree would show an empty name.
**Verified** by starting from a deliberately poisoned session (`projectPath="blog"`) and confirming
it self-heals:
```
projectPath : /var/www/atheos/workspace/blog <- healed from "blog"
data-path : /var/www/atheos/workspace/blog
dialog.php : 200 with content
#content : 1758 chars
visible : CodeGit Overview Log Transfer Configure Commit Amend ...
```
**An existing browser session heals on its next page load** — no hard refresh or re-login needed.
### Worth remembering
A 403 from `getWorkspacePath()` presents as *"the dialog is empty"*, with no error anywhere. If any
Atheos dialog is ever blank again, check the **project path** first, then the response code — not the CSS.
---
## 7. Checking for plugin problems in future
The plugin's JS is bundled into `public/plugins.min.js` (built by `classes/sourcemanager.php`), and
its rightbar button is rendered by `components/sidebars/sb-right.php` from `plugin.json`. Both were
@@ -258,17 +467,26 @@ verify them before trusting it.
---
## 5. Follow-ups not done (recorded so they are not forgotten)
## 8. Follow-ups not done (recorded so they are not forgotten)
These came out of review. They are **operator decisions**, not agent ones — several change what the
editor can do.
### 5.1 Remove the Terminal plugin — ✅ DONE
### 8.0 Branch protection on `master` — ACCEPTED RISK
Deliberately **not** enabled while the editor is LAN-only (operator decision). Full reasoning and the
"enable it before you expose this host" trigger are in
[`docs/writer-branch-workflow.md` §2](writer-branch-workflow.md#2-protect-master-on-the-blog-repo--recommendation-deliberately-not-enabled).
The suite still probes for it on every run and reports `ACCEPTED-RISK` (not a failure) while it is
off, so the recommendation self-cancels the moment it is enabled.
### 8.1 Remove the Terminal plugin — ✅ DONE
Removed through the market's own code path once §1 was fixed (see §2). A browser shell was the largest
attack surface in the container and macros already cover command execution.
### 5.2 ❌ `disable_functions` cannot be "on by default" here — measured
### 8.2 ❌ `disable_functions` cannot be "on by default" here — measured
Review asked for `php.ini` to disable `exec`, `shell_exec`, `passthru`, `system` by default.
**This would break the editor's core function.** Evidence:
@@ -285,18 +503,18 @@ turns the editor into a text editor that cannot commit. So it stays **a delibera
you would also lose macros and git integration, which is why S-7 already says *"test, do not
blind-apply"*.
### 5.3 Rotate the terminal password automatically
### 8.3 Rotate the terminal password automatically
If the plugin is kept, `provision.sh` should generate a random password rather than leaving the
upstream default. Not implemented: it would hardcode a secret into a repo file, which is the same
class of problem this file already documents.
### 5.4 Deploy key could be read-only
### 8.4 Deploy key could be read-only
Review suggested making the deploy key read-only if writes are unnecessary. **They are necessary** —
that is the whole point of the writer-branch workflow (macro 2 pushes). Not applicable.
### 5.5 The real upstream fix for §3
### 8.5 The real upstream fix for §3
`class.macro.php::save()` should write `[]` instead of `"N/A"` for folder macros. See §3 — the fix is
`class.macro.php::save()` should write `[]` instead of `"N/A"` for folder macros. See §4 — the fix is
currently applied to the data, so it survives updates, at the cost of being reversible from the UI.
+32 -5
View File
@@ -17,7 +17,7 @@ merging into `master` is done by the operator in Gitea.
│ operator opens a PR in Gitea
│ operator reviews
▼
master (protected)
master (protected: RECOMMENDED, currently OFF - see §2)
│
▼
existing Pelican build pipeline
@@ -43,12 +43,31 @@ This is the *same* global gitconfig php-fpm uses (`HOME=/home/atheos` — verifi
as **you**. If it is not set, macro **2** refuses with an actionable error rather than
committing under a bogus identity.
### 2. ⚠️ Protect `master` on the blog repo
### 2. Protect `master` on the blog repo — RECOMMENDATION, deliberately not enabled
**Not yet enabled — the deploy key can currently force-push `master` directly**, which defeats the
whole model. Set it in Gitea (repo owner only):
**Status: accepted risk. Branch protection is OFF, and that is a decision, not an oversight.**
`armistace/blog` → **Settings** → **Branches** → add rule for `master`:
> *"leave it as a recommendation — I won't do that unless I take this out to the wider internet"*
> — operator
The deploy key **can** push `master` directly, which means the writer-branch model is **advisory, not
enforced**. The operator has weighed this and chosen not to add the gate while the editor is LAN-only:
- **The compensating control is reachability, not immutability.** The editor is LAN-only (FR-10). The
threat this setting defends against is a compromised editor publishing straight to `master`; while
nothing off-LAN can reach the editor, and the only user on the LAN is the operator, that is largely
theoretical.
- **The cost is real now, the benefit is not.** Protection adds a merge gate for the sole user, who is
also the repo owner — friction with no realistic risk reduction today.
#### ⚠️ Enable it BEFORE either of these happens
1. **The editor becomes reachable off-LAN** — an NPM proxy entry, a port-forward, or a
Tailscale/WireGuard route to `blog-ide.aridgwayweb.com`.
2. **Anyone other than the operator gets access to the editor.**
At that point a deploy key that can force-push `master` stops being theoretical. The setting
(`armistace/blog` → **Settings** → **Branches** → rule for `master`):
| Setting | Value |
|---|---|
@@ -63,6 +82,14 @@ curl -s -H "Authorization: token $GITEA_TOKEN" \
https://git.aridgwayweb.com/api/v1/repos/armistace/blog/branch_protections
```
#### It is still checked on every test run
`build-and-test.sh` attempts a direct push to `master` with the deploy key and requires it to be
**rejected**. While protection is off that reports as `ACCEPTED-RISK`, not `FAIL` — so the suite stays
green and CI-usable — but **the check runs every time**. It flips to
`PASS master is protected` by itself the moment protection is enabled, and this recommendation
becomes moot without anyone having to remember it.
### 3. The macros
Installed directly into Atheos's macro store (`data/macros.db.php`); a copy is kept in this repo at
+18 -9
View File
@@ -7,13 +7,12 @@ server {
root /var/www/atheos;
index index.php index.html;
# --- logging ---------------------------------------------------------
access_log /var/log/nginx/atheos.access.log;
error_log /var/log/nginx/atheos.error.log;
# --- PHP -----------------------------------------------------------------
# The socket path here MUST byte-match the pool's `listen =` in
# /etc/php83/php-fpm.d/www.conf
# (interface I-1 — a mismatch yields a bare 502 with no other diagnostic).
# --- PHP: the socket path here MUST byte-match the pool's `listen =`
# in /etc/php83/php-fpm.d/www.conf (interface I-1 -> else bare 502)
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
@@ -27,11 +26,12 @@ server {
try_files $uri $uri/ =404;
}
# --- hardening -----------------------------------------------------------
location ~ /\.git { deny all; }
location ~ /\.ssh { deny all; }
location ~ /\. { deny all; }
location ~ ^/data/ { deny all; }
# --- hardening (free, no downside) -----------------------------------
# never serve the VCS directory, the app's private data, or dotfiles
location ~ /\.git { deny all; }
location ~ /\.ssh { deny all; }
location ~ /\. { deny all; }
location ~ ^/data/ { deny all; }
location ~ ^/(config\.php|common\.php|controller\.php|traits/) { deny all; }
location ~* \.(md|py|sh|log|conf|ini|sql|bak|orig)$ { deny all; }
@@ -40,4 +40,13 @@ server {
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
client_max_body_size 64m;
# Atheos serves theme/main.min.css and the generated *.min.js/css bundles from STABLE URLs
# with no cache headers. Without this, a CSS/JS change stays invisible behind a cached copy
# until the user hard-refreshes. "no-cache" permits caching but forces revalidation, so a
# 304 keeps normal loads fast while guaranteeing updates are picked up on the next load.
location ~* \.(css|js)$ {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri =404;
}
}
+69 -6
View File
@@ -5,9 +5,10 @@
# ==============================================================================
W=/var/www/atheos
B=$W/workspace/blog
pass=0; fail=0
pass=0; fail=0; note=0
ok() { echo " PASS $1"; pass=$((pass+1)); }
bad() { echo " FAIL $1"; fail=$((fail+1)); }
note() { echo " ACCEPTED $1"; note=$((note+1)); }
chk() { if [ "$2" = "$3" ]; then ok "$1 ($2)"; else bad "$1 (got '$2', want '$3')"; fi; }
asuser() { su -s /bin/sh atheos -c "$1"; }
@@ -108,9 +109,43 @@ done
echo
echo "=============================================================="
echo "REGRESSION: panel has no hardcoded height (empty-modal bug)"
echo "=============================================================="
# panel { height:500px } forced every modal panel to a fixed box: short content left a large
# empty black region and anything after the panel (codegit's Branch toolbar) was pushed outside
# the modal. docs/known-issues.md §4. Fails loudly if lost to an update.
PNL=$(grep -oE '(^|[;}])panel\{[^}]*\}' /var/www/atheos/theme/main.min.css 2>/dev/null | head -1)
if [ -z "$PNL" ]; then
bad "could not read the panel rule from theme/main.min.css"
elif echo "$PNL" | grep -q 'height:500px'; then
bad "panel is fixed at 500px - re-run scripts/fix-panel-layout.sh"
else
ok "panel is content-sized ($(echo "$PNL" | grep -oE 'height:[^;]*' | head -1))"
fi
# and the CSS/JS cache header must be present, or fixes stay invisible
if wget -qO /dev/null --server-response --header='Host: 127.0.0.1' http://127.0.0.1/theme/main.min.css 2>&1 | grep -qi 'Cache-Control.*no-cache'; then
ok "theme CSS sends Cache-Control: no-cache (changes are picked up)"
else
bad "theme CSS has no Cache-Control - a CSS fix can stay hidden behind the browser cache"
fi
echo
echo "=============================================================="
echo "REGRESSION: asset URLs are versioned (cache-busting)"
echo "=============================================================="
# Stable asset URLs with no version are why a CSS/JS fix stayed invisible behind the browser cache,
# in every browser incl. Firefox. docs/known-issues.md §5.
ASSETS=$(wget -qO /tmp/bt_page.html --header='Host: 127.0.0.1' http://127.0.0.1/ 2>/dev/null; grep -oE '(href|src)="[^"]+\.(css|js)"' /tmp/bt_page.html 2>/dev/null | wc -l | tr -d ' ')
VER=$(grep -oE '(href|src)="[^"]+\.(css|js)\?v=[0-9]+"' /tmp/bt_page.html 2>/dev/null | wc -l | tr -d ' ')
if [ "${VER:-0}" -ge 4 ]; then
ok "asset URLs are versioned ($VER asset tags carry ?v=)"
elif [ "${ASSETS:-0}" -eq 0 ]; then
note "could not read the served page from the guest (unauthenticated: expected)"
else
bad "only $VER of $ASSETS asset URLs are versioned - a CSS fix can stay hidden in the browser cache"
fi
echo "=============================================================="
echo "REGRESSION: rDelete works (GLOB_BRACE is glibc-only; musl lacks it)"
echo "=============================================================="
# traits/helpers.php::rDelete() used GLOB_BRACE, which is undefined under musl -> PHP 8 fatal.
# That broke deleting ANY file/folder in the editor AND removing a market plugin (the UI
# would spin forever because the fatal killed the response before Common::send()).
# docs/known-issues.md §1. This test fails loudly if the patch is lost (e.g. by an update).
@@ -142,14 +177,42 @@ else
if echo "$PUSH_OUT" | grep -qiE 'protected|not allowed|denied|forbidden|remote rejected'; then
ok "master is protected - the deploy key CANNOT push it"
else
bad "master is NOT protected - the deploy key could push master directly!"
echo " Enable it: armistace/blog -> Settings -> Branches -> add rule for 'master',"
echo " Enable push = OFF, merge whitelist = armistace. See docs/writer-branch-workflow.md"
# ACCEPTED RISK, not a failure. The operator has decided NOT to enable branch protection
# while this editor is LAN-only; the control is "it is not reachable off-LAN", not "master
# is immutable". So this is reported, not failed - but it is checked on EVERY run, so the
# moment protection does go on, the run says so and the recommendation below becomes moot.
note "master is NOT protected - the deploy key CAN push master directly"
echo " ACCEPTED because the editor is LAN-only (see docs/writer-branch-workflow.md)."
echo " BECOMES A REAL FINDING THE MOMENT THIS HOST IS REACHABLE OFF-LAN. Before that:"
echo " armistace/blog -> Settings -> Branches -> add rule for 'master'"
echo " Enable push = OFF, merge whitelist = armistace"
fi
fi
echo
echo "=============================================================="
echo "REGRESSION: project path is ABSOLUTE (relative => empty dialogs)"
echo "=============================================================="
# A relative project path makes every getWorkspacePath() call fail its access check with 403,
# which modal.js silently swallows - so CodeGit/Macro/Market/Settings render EMPTY with no error.
# docs/known-issues.md S6. This is the bug that presented as "just a black box".
PP=$(php83 -r '
$raw=@file_get_contents("/var/www/atheos/data/projects.db.json.php");
if(!$raw) exit;
preg_match("/<\?php\/\*\|(.*)\|\*\/\?>/s",$raw,$m);
$d=json_decode($m[1],true);
if(is_array($d)) echo reset($d);' 2>/dev/null)
case "$PP" in
/*) ok "projects store holds an absolute path ($PP)" ;;
"") bad "could not read the projects store" ;;
*) bad "projects store holds a RELATIVE path ('$PP') - dialogs will render empty; re-run scripts/fix-project-path.sh" ;;
esac
if grep -q 'isAbsPath($projectPath)' /var/www/atheos/common.php 2>/dev/null; then
ok "loadState resolves a relative projectPath (heals poisoned sessions)"
else
bad "loadState does NOT resolve relative project paths - re-run scripts/fix-project-path.sh"
fi
echo "=============================================================="
echo "IDENTITY: git identity is SET by the operator (FR-11 superseded by explicit request)"
echo "=============================================================="
# FR-11 originally required the agent to leave the identity EMPTY so it could not impersonate
@@ -191,6 +254,6 @@ echo " listening ports: $(netstat -tln 2>/dev/null | awk '/LISTEN/{split($4,a,"
echo
echo "=============================================================="
printf 'RESULT: %d passed, %d failed\n' "$pass" "$fail"
printf 'RESULT: %d passed, %d failed, %d accepted-risk\n' "$pass" "$fail" "$note"
echo "=============================================================="
[ "$fail" -eq 0 ] || exit 1
+111
View File
@@ -0,0 +1,111 @@
#!/bin/sh
# Re-apply the UI/layout fixes to a rebuilt or updated Atheos tree.
#
# Why this exists: Atheos has no releases and `scripts/update-atheos.sh` replaces the whole
# application directory, so any local patch is lost on update. These are the two that were
# needed to make the editor usable. Both are IDEMPOTENT - run it as often as you like.
#
# 1. panel { height: 500px } -> height:auto; max-height:58vh
# Every modal panel was forced to a fixed 500px box, so dialogs with short content showed
# a large empty black region and anything after the panel (codegit's "Branch:" toolbar)
# was pushed OUTSIDE the modal. See docs/known-issues.md §4.
#
# 2. nginx: Cache-Control: no-cache for *.css/*.js
# Theme/bundle changes were invisible behind the browser cache. §5.
#
# Usage: sh scripts/fix-panel-layout.sh (run ON the guest, as root)
set -e
ATH=/var/www/atheos
CSS=$ATH/theme/main.min.css
SCSS=$ATH/theme/modules/common/_panel.scss
NGX=/etc/nginx/http.d/atheos.conf
echo "=== 1. panel layout ==="
OLD='height:500px;position:relative;border-left:var(--borderLight)'
NEW='height:auto;max-height:58vh;position:relative;border-left:var(--borderLight)'
if [ ! -f "$CSS" ]; then
echo " !! $CSS not found - is Atheos installed?"
elif grep -qF "$NEW" "$CSS"; then
echo " already applied"
elif grep -qF "$OLD" "$CSS"; then
[ -f "$CSS.orig" ] || cp -a "$CSS" "$CSS.orig"
php83 -r '$f=$argv[1];$o=$argv[2];$n=$argv[3];$s=file_get_contents($f);
if(strpos($s,$o)===false){exit(1);} file_put_contents($f,str_replace($o,$n,$s));' "$CSS" "$OLD" "$NEW"
echo " applied to main.min.css"
else
echo " !! expected panel rule not found - inspect manually"
echo " (upstream may have changed it; diff against $CSS.orig)"
fi
# keep the SCSS source consistent so the shipped CSS is not contradicted by the source
if [ -f "$SCSS" ] && grep -q 'height: 500px;' "$SCSS"; then
[ -f "$SCSS.orig" ] || cp -a "$SCSS" "$SCSS.orig"
php83 -r '$f=$argv[1];$s=file_get_contents($f);
$s=str_replace("height: 500px;","height: auto;\n\tmax-height: 58vh;",$s);
file_put_contents($f,$s);' "$SCSS"
echo " synchronised _panel.scss"
fi
echo "=== 2. nginx cache headers ==="
if [ ! -f "$NGX" ]; then
echo " !! $NGX not found"
elif grep -q 'must-revalidate' "$NGX"; then
echo " already applied"
else
[ -f "$NGX.orig" ] || cp -a "$NGX" "$NGX.orig"
cat > /tmp/.cacheblock <<'EOF'
# Atheos serves theme/main.min.css and the generated *.min.js/css bundles from STABLE URLs
# with no cache headers, so a CSS/JS fix stays invisible behind the browser cache until a
# hard refresh. "no-cache" permits caching but forces revalidation: normal loads still get a
# cheap 304, and a change is guaranteed to be picked up on the next load.
location ~* \.(css|js)$ {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri =404;
}
EOF
php83 -r '$f=$argv[1];$b=$argv[2];$s=file_get_contents($f);
$p=strrpos($s,"}"); if($p===false){exit(1);}
file_put_contents($f,substr($s,0,$p).file_get_contents($b).substr($s,$p));' "$NGX" /tmp/.cacheblock
rm -f /tmp/.cacheblock
nginx -t >/dev/null 2>&1 && rc-service nginx reload >/dev/null 2>&1 && echo " applied + nginx reloaded" \
|| echo " !! nginx -t failed - restore $NGX.orig"
fi
echo "=== 3. cache-busting versioned asset URLs ==="
# Atheos serves theme/main.min.css and the generated bundles from STABLE paths with no version:
# <link rel="stylesheet" href="theme/main.min.css">
# A browser then keeps its cached copy across a change, so a CSS/JS fix appears not to work.
# Appending each file's mtime means a changed file gets a NEW url (fetched immediately) and an
# unchanged one keeps its url (stays cached). Better than headers alone: it also defeats a
# browser cache that was already poisoned before any header was set.
SM=$ATH/classes/sourcemanager.php
IX=$ATH/index.php
if grep -q 'filemtime($path)' "$SM" 2>/dev/null; then
echo " getTag already versioned"
else
[ -f "$SM.orig" ] || cp -a "$SM" "$SM.orig"
php83 -r '
$f=$argv[1]; $s=file_get_contents($f);
$old = " function getTag(\$type, \$path) {\n return \$type === \"css\" ? \"\\t<link rel=\\\"stylesheet\\\" href=\\\"\$path\\\">\\n\": \"\\t<script type=\\\"text/javascript\\\" src=\\\"\$path\\\"></script>\\n\";\n }";
if (strpos($s,$old) === false) { fwrite(STDERR, " !! getTag signature changed - inspect $f\n"); exit(1); }
$new = " function getTag(\$type, \$path) {\n // Version the URL by mtime so a changed asset is fetched immediately instead of being\n // served from the browser cache (docs/known-issues.md §5).\n if (file_exists(\$path)) {\n \$path .= \"?v=\" . filemtime(\$path);\n }\n return \$type === \"css\" ? \"\\t<link rel=\\\"stylesheet\\\" href=\\\"\$path\\\">\\n\": \"\\t<script type=\\\"text/javascript\\\" src=\\\"\$path\\\"></script>\\n\";\n }";
file_put_contents($f, str_replace($old,$new,$s)); echo " versioned getTag\n";' "$SM"
php83 -l "$SM" >/dev/null 2>&1 || echo " !! SYNTAX ERROR in $SM - restore $SM.orig"
fi
if grep -q 'theme/main.min.css?v=' "$IX" 2>/dev/null; then
echo " index.php already versioned"
else
[ -f "$IX.orig" ] || cp -a "$IX" "$IX.orig"
php83 -r '
$f=$argv[1]; $s=file_get_contents($f);
$old = " echo(\"\\t<link rel=\\\"stylesheet\\\" href=\\\"theme/main.min.css\\\">\\n\");";
if (strpos($s,$old) === false) { fwrite(STDERR, " !! theme echo line changed - inspect $f\n"); exit(1); }
$new = " // Version the theme URL by mtime; otherwise a browser keeps its cached copy across a\n // theme change and the fix appears not to work (docs/known-issues.md §5).\n \$themeV = file_exists(\"theme/main.min.css\") ? filemtime(\"theme/main.min.css\") : time();\n echo(\"\\t<link rel=\\\"stylesheet\\\" href=\\\"theme/main.min.css?v=\$themeV\\\">\\n\");";
file_put_contents($f, str_replace($old,$new,$s)); echo " versioned index.php theme link\n";' "$IX"
php83 -l "$IX" >/dev/null 2>&1 || echo " !! SYNTAX ERROR in $IX - restore $IX.orig"
fi
echo "=== done ==="
+70
View File
@@ -0,0 +1,70 @@
#!/bin/sh
# Fix: dialogs (CodeGit, Macro, Market, Settings) rendered EMPTY.
#
# Root cause: the project was registered with a RELATIVE path, and the UI used it verbatim.
#
# 1. install/process.php stores a relative folder name: projects.db -> {"Blog": "blog"}
# 2. common.php::loadState() does $projectPath = reset($projectList) -> "blog"
# and persists it to the session, so the bad value STICKS.
# 3. filetree puts it straight into the DOM: <a id="project-root" data-path="blog">
# 4. codegit sends repo="blog"; getWorkspacePath("blog") fails checkPath() -> 403
# 5. modal.js does `if (status !== 200) return;` -> SWALLOWS the error -> #content stays empty
#
# That last step is why it presents as an empty/black dialog rather than an error: the request
# fails, nothing is rendered, and nothing is reported.
#
# Atheos's own project-creation code resolves relative -> WORKSPACE/<name>, so a relative value in
# the store is legitimate - loadState() simply fails to resolve it. Both fixes are needed:
# A. resolve at load time (this also heals sessions already holding the relative value)
# B. store absolute paths, so array_search() can still map path -> name for the tree label
#
# Usage: run ON the guest as root. Idempotent.
set -e
A=/var/www/atheos
echo "=== A. resolve a relative projectPath in loadState() ==="
S=$A/common.php
if [ ! -f "$S" ]; then echo " !! $S not found"; exit 1; fi
if grep -q 'isAbsPath($projectPath)' "$S"; then
echo " already applied"
else
[ -f "$S.orig" ] || cp -a "$S" "$S.orig"
php83 -r '
$f=$argv[1]; $s=file_get_contents($f);
$old = " \$openFiles_db = Common::getObjStore(\"activeFiles\", \"users/\" . \$activeUser);";
if (strpos($s,$old) === false) { fwrite(STDERR," !! anchor not found in loadState\n"); exit(1); }
$new = " // A project may be registered with a RELATIVE folder name (the installer stores it\n // that way). Handing that to the UI yields data-path=\"blog\", and every later\n // getWorkspacePath() call then fails its access check with 403 - which the modal\n // silently swallows, leaving the dialog empty. Resolve it once here, and re-store it\n // so sessions already holding the relative value heal on their next load.\n if (!Common::isAbsPath(\$projectPath)) {\n \$projectPath = Common::getWorkspacePath(\$projectPath, true);\n SESSION(\"projectPath\", \$projectPath);\n }\n\n" . $old;
file_put_contents($f, str_replace($old,$new,$s)); echo " patched loadState\n";' "$S"
php83 -l "$S" >/dev/null 2>&1 || { echo " !! SYNTAX ERROR - restore $S.orig"; exit 1; }
fi
echo "=== B. store absolute paths in the projects store ==="
P=$A/data/projects.db.json.php
if [ ! -f "$P" ]; then
echo " !! $P not found"
elif grep -q '"\/' "$P"; then
echo " already absolute"
else
cp -a "$P" "$P.bak-$(date +%Y%m%d-%H%M%S)"
php83 -r '
$f=$argv[1]; $ws="/var/www/atheos/workspace";
$raw=file_get_contents($f);
preg_match("/<\?php\/\*\|(.*)\|\*\/\?>/s",$raw,$m);
$d=json_decode($m[1],true);
if(!is_array($d)){ fwrite(STDERR," !! store unreadable\n"); exit(1); }
$c=false;
foreach($d as $n=>$p){ if(!preg_match("#^(/|[A-Za-z]:)#",$p)){ $d[$n]=$ws."/".ltrim($p,"/"); $c=true; } }
if(!$c){ echo " already absolute\n"; exit(0); }
file_put_contents($f,"<?php/*|\n".json_encode($d, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)."\n|*/?>");
echo " store rewritten -> ".json_encode($d)."\n";' "$P"
chown atheos:atheos "$P" 2>/dev/null || true
fi
echo "=== verify ==="
php83 -r '
$raw=file_get_contents("/var/www/atheos/data/projects.db.json.php");
preg_match("/<\?php\/\*\|(.*)\|\*\/\?>/s",$raw,$m); $d=json_decode($m[1],true);
$p=reset($d); $n=array_search($p,$d);
printf(" path=%s name=%s\n",$p,var_export($n,true));
printf(" absolute: %s\n", preg_match("#^(/|[A-Za-z]:)#",$p) ? "yes" : "NO");' 2>&1
echo "=== done ==="