Compare commits
17
Commits
a6fb61d0ea
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4163885e4c | ||
|
|
be8c959de2 | ||
|
|
48e439ed6e | ||
|
|
012711614c | ||
|
|
c57aff3c33 | ||
|
|
1ff8aaf2f1 | ||
|
|
2d57173e31 | ||
|
|
c65900638e | ||
|
|
c4a4de1da7 | ||
|
|
0bdd15b31b | ||
|
|
976b3f0005 | ||
|
|
8ea645386f | ||
|
|
5ff6a06e82 | ||
|
|
47b1860623 | ||
|
|
0b72cf48b1 | ||
|
|
1c7db911df | ||
|
|
e038f758ac |
@@ -55,10 +55,12 @@ posts and commit them to Gitea — without VS Code, without a cloud IDE, and wit
|
|||||||
├── RUNBOOK.md ← operational guide; START AT STEP 1
|
├── RUNBOOK.md ← operational guide; START AT STEP 1
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── security-notes.md credential model + hardening TODOs if ever exposed
|
│ ├── security-notes.md credential model + hardening TODOs if ever exposed
|
||||||
│ └── resiliency-notes.md what a non-PoC deployment would need
|
│ ├── resiliency-notes.md what a non-PoC deployment would need
|
||||||
|
│ └── writer-branch-workflow.md how writing/publishing works from the editor
|
||||||
├── files/
|
├── files/
|
||||||
│ ├── atheos.conf nginx vhost (install to /etc/nginx/http.d/atheos.conf)
|
│ ├── atheos.conf nginx vhost (install to /etc/nginx/http.d/atheos.conf)
|
||||||
│ └── ssh-config ~atheos/.ssh/config (the gitea-blog alias)
|
│ ├── ssh-config ~atheos/.ssh/config (the gitea-blog alias)
|
||||||
|
│ └── macros.db.php installed copy of the Atheos writer macros
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── provision.sh rebuild the whole guest from scratch
|
│ ├── provision.sh rebuild the whole guest from scratch
|
||||||
│ └── update-atheos.sh update Atheos to latest upstream + re-pin
|
│ └── update-atheos.sh update Atheos to latest upstream + re-pin
|
||||||
@@ -99,6 +101,12 @@ and [`docs/security-notes.md`](docs/security-notes.md).
|
|||||||
|
|
||||||
## Security at a glance
|
## Security at a glance
|
||||||
|
|
||||||
|
- **Writing happens on a branch, never on `master`.** The editor commits to a long-lived
|
||||||
|
`atheos-writer` branch and you merge via PR in Gitea — see
|
||||||
|
[`docs/writer-branch-workflow.md`](docs/writer-branch-workflow.md).
|
||||||
|
⚠️ This only holds if **branch protection is enabled on `armistace/blog`'s `master`** — without it
|
||||||
|
the deploy key could force-push master directly. Confirm with the `branch_protections` API call in
|
||||||
|
that document.
|
||||||
- Git auth: **deploy key scoped to `armistace/blog`**, private key mode `600`, owned by the php-fpm user.
|
- Git auth: **deploy key scoped to `armistace/blog`**, private key mode `600`, owned by the php-fpm user.
|
||||||
- The SSH alias uses `IdentitiesOnly yes`, so no other key is ever offered.
|
- The SSH alias uses `IdentitiesOnly yes`, so no other key is ever offered.
|
||||||
- The web app runs as a **dedicated unprivileged user** (`atheos`), which also owns the workspace.
|
- The web app runs as a **dedicated unprivileged user** (`atheos`), which also owns the workspace.
|
||||||
|
|||||||
+23
@@ -296,3 +296,26 @@ ssh root@192.168.178.69 "pct exec 136 -- sh -c 'echo \"<your-pubkey>\" >> /root/
|
|||||||
| Date | Change |
|
| Date | Change |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 2026-09-29 | Initial build: LXC 136 on scotty, Alpine 3.23.2, nginx + php83-fpm (socket), Atheos pinned at `f90cd18` (build 611), `armistace/blog` cloned over HTTPS, per-repo deploy key generated. LAN-only. |
|
| 2026-09-29 | Initial build: LXC 136 on scotty, Alpine 3.23.2, nginx + php83-fpm (socket), Atheos pinned at `f90cd18` (build 611), `armistace/blog` cloned over HTTPS, per-repo deploy key generated. LAN-only. |
|
||||||
|
| 2026-09-29 | Added the **writer-branch workflow**: 6 macros installed into Atheos (start/join, commit+push, destructive reset-to-master, status, refresh master, merge master) writing to a long-lived `atheos-writer` branch; merging stays in Gitea. See `docs/writer-branch-workflow.md`. |
|
||||||
|
|
||||||
|
## Writer-branch workflow (quick reference)
|
||||||
|
|
||||||
|
The editor writes to `atheos-writer`; you merge to `master` in Gitea.
|
||||||
|
|
||||||
|
**Macros** (right-click the `blog` folder → Macros):
|
||||||
|
|
||||||
|
| # | Macro | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Start/Join Writer Branch | creates or re-joins `atheos-writer` (idempotent) |
|
||||||
|
| 2 | Commit + Push Writer Branch | the everyday button |
|
||||||
|
| 3 | **DESTRUCTIVE**: Reset Writer Branch to Master | wipes writer's commits, keeps your files |
|
||||||
|
| 4 | Status + Recent Log | read-only |
|
||||||
|
| 5 | Refresh Master (ff-only) | pull master's merges down |
|
||||||
|
| 6 | Merge Master Into Writer Branch | when master has moved ahead |
|
||||||
|
|
||||||
|
⚠️ **Requires branch protection on `armistace/blog`'s `master`** — it is **not currently enabled**,
|
||||||
|
so the deploy key can force-push master directly. See `docs/writer-branch-workflow.md` §Setup.
|
||||||
|
⚠️ **Macro 2 uses `git add -A`** — everything left in the repo gets published to Gitea. Keep scratch
|
||||||
|
files outside `workspace/blog/`.
|
||||||
|
|
||||||
|
Full detail, recovery steps and caveats: [`docs/writer-branch-workflow.md`](docs/writer-branch-workflow.md).
|
||||||
|
|||||||
@@ -0,0 +1,520 @@
|
|||||||
|
# Known Issues & Patches
|
||||||
|
|
||||||
|
Upstream Atheos is an actively-moving project with **no releases**, and its plugin market ships
|
||||||
|
plugins that are **ports from Codiad** (Atheos's predecessor). Two of those plugins use APIs that
|
||||||
|
Atheos has since removed or changed. This file records what we hit, what we changed, and why the
|
||||||
|
fix is done the way it is.
|
||||||
|
|
||||||
|
Pinned Atheos commit: `f90cd1806dbe0e25673a77e27390e8b1a240c3f6` (build 611).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 🔴 **Deleting any file or folder was broken** — `GLOB_BRACE` does not exist on musl
|
||||||
|
|
||||||
|
**Symptom:** removing the Terminal plugin from the market **cycled forever in the UI** (never
|
||||||
|
completed, never errored). Deleting a file or folder in the editor was broken by the same cause.
|
||||||
|
|
||||||
|
**Root cause — a glibc-only constant referenced on musl.** `traits/helpers.php::rDelete()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE|GLOB_MARK);
|
||||||
|
```
|
||||||
|
|
||||||
|
`GLOB_BRACE` is a **glibc extension**. Alpine uses **musl**, where the constant is *undefined* —
|
||||||
|
and touching an undefined constant is a **fatal error** in PHP 8 (a warning in older versions):
|
||||||
|
|
||||||
|
```
|
||||||
|
PHP Fatal error: Uncaught Error: Undefined constant "GLOB_BRACE"
|
||||||
|
in /var/www/atheos/traits/helpers.php:138
|
||||||
|
#0 components/market/class.market.php(178): Common::rDelete()
|
||||||
|
#1 components/market/controller.php(46): Market->remove()
|
||||||
|
```
|
||||||
|
|
||||||
|
Because the fatal kills the request before `Common::send(200)`, the front end never receives a
|
||||||
|
settle response — so the UI **spins indefinitely**. A hang, not an error.
|
||||||
|
|
||||||
|
**Blast radius — measured, two call sites:**
|
||||||
|
|
||||||
|
| Caller | What breaks |
|
||||||
|
|---|---|
|
||||||
|
| `components/market/class.market.php:178` | removing a plugin from the market |
|
||||||
|
| `components/filetree/class.filetree.php:59` | **deleting any file or folder in the editor** |
|
||||||
|
|
||||||
|
The second is core editor functionality, so this is **not** a plugin bug — installing the Terminal
|
||||||
|
plugin is merely what made it visible. Verified directly: with the original code, `rDelete()` fatals
|
||||||
|
and the target is left on disk.
|
||||||
|
|
||||||
|
**Fix applied** (`patches/helpers-rDelete-GLOB_BRACE.diff`) — expand the brace pattern by hand when
|
||||||
|
`GLOB_BRACE` is absent, keeping upstream's exact behaviour where it *is* defined:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (defined("GLOB_BRACE")) {
|
||||||
|
$files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE | GLOB_MARK);
|
||||||
|
} else {
|
||||||
|
$files = array_merge(
|
||||||
|
glob($target . "/*", GLOB_MARK) ?: [],
|
||||||
|
glob($target . "/.[!.]*", GLOB_MARK) ?: [],
|
||||||
|
glob($target . "/..?*", GLOB_MARK) ?: []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The three patterns are precisely what the brace expansion produces (`*`, dotfiles, and `.`/`..`-like
|
||||||
|
names), so the directory listing is equivalent.
|
||||||
|
|
||||||
|
**Verified** — `php83 -l` clean, then a real deletion of a tree containing nested dirs and two
|
||||||
|
dotfiles:
|
||||||
|
|
||||||
|
```
|
||||||
|
entries before: 10
|
||||||
|
rDelete() returned: true
|
||||||
|
exists after: gone (OK)
|
||||||
|
single file: rDelete=true exists=gone (OK)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Upstream has NOT fixed this** — the same line is still in `main` (checked directly), so it cannot
|
||||||
|
be resolved by updating Atheos. Re-apply this patch after any update (`scripts/update-atheos.sh`).
|
||||||
|
|
||||||
|
**Consequence for the platform choice:** this is the real cost of Alpine. A glibc-based image
|
||||||
|
(Debian/Ubuntu) would not have hit it. It is patchable in one place, so Alpine stays — but it is worth
|
||||||
|
knowing that `GLOB_BRACE` is the kind of thing musl breaks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 🔴 Plugin market: the **Terminal** plugin does not work as shipped
|
||||||
|
|
||||||
|
**Symptom:** installed the Terminal plugin from the market; it failed / did not function.
|
||||||
|
|
||||||
|
**Root cause — a removed API.** `plugins/Terminal/terminal.php` calls `Common::execute(...)`, which
|
||||||
|
**no longer exists** in Atheos (it was renamed):
|
||||||
|
|
||||||
|
```
|
||||||
|
plugin calls : Common::execute(...) -> MISSING
|
||||||
|
available : Common::raw_execute(...) -> returns ["code" => int, "output" => array]
|
||||||
|
Common::safe_execute(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin's header says it is a port of `Fluidbyte/Codiad-Terminal`, so it targets the old API. Its
|
||||||
|
return-shape assumption is also stale (the old `execute()` returned `text`; `raw_execute()` returns
|
||||||
|
`output` as an array).
|
||||||
|
|
||||||
|
**Fix applied** (`patches/Terminal-terminal.php.patched`):
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- $output = Common::execute("pwd");
|
||||||
|
+ $output = Common::raw_execute("pwd");
|
||||||
|
- $_SESSION["activeDir"] = array([$_SESSION["project"]] => $output["text"]);
|
||||||
|
+ $_SESSION["activeDir"] = array([$_SESSION["project"]] => implode("\n", (array)$output["output"]));
|
||||||
|
|
||||||
|
- $output = Common::execute($cmd);
|
||||||
|
- return $output["text"];
|
||||||
|
+ $output = Common::raw_execute($cmd);
|
||||||
|
+ return implode("\n", (array)$output["output"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verified** against the real method (`php83 -l` clean; both call patterns exercised):
|
||||||
|
|
||||||
|
| Call | Result |
|
||||||
|
|---|---|
|
||||||
|
| `raw_execute("pwd")` | `code=0`, `output=["/root",""]` |
|
||||||
|
| `raw_execute("echo ...")` | `code=0`, returns the text |
|
||||||
|
| after `chdir` to the repo | returns `/var/www/atheos/workspace/blog` — so the terminal opens in the blog |
|
||||||
|
|
||||||
|
**Re-apply after an Atheos update** — `scripts/update-atheos.sh` replaces the application files, so
|
||||||
|
this patch is lost on update. Re-apply it, or drop the plugin.
|
||||||
|
|
||||||
|
### Two further defects in the same plugin (NOT fixed — upstream, and mostly harmless)
|
||||||
|
|
||||||
|
1. **`terminal.php:95`** — `$blocked = explode(",", BLOCKED === true);` passes a **boolean** to
|
||||||
|
`explode()`. This evaluates to `explode(",", false)`, so the intended block-list
|
||||||
|
(`define("BLOCKED","ssh,telnet")`) **never actually blocks anything**. The plugin's own guard is
|
||||||
|
dead code. Cosmetic in effect (the command would just fail on its own), but worth knowing rather
|
||||||
|
than trusting.
|
||||||
|
2. **`terminal.php:124`** — `if (strtolower($command === "exit"))` passes a boolean to
|
||||||
|
`strtolower()`. Should be `strtolower($command) === "exit"`. Result: typing `exit` does not clear
|
||||||
|
`term_auth`; the session stays authenticated until the browser session ends.
|
||||||
|
|
||||||
|
Left as-is: they are upstream bugs, and fixing them changes the plugin's behaviour beyond the
|
||||||
|
minimum needed to make it run. Recorded here so nobody "fixes" them by accident or trusts the
|
||||||
|
block-list.
|
||||||
|
|
||||||
|
### ⚠️ Security note
|
||||||
|
|
||||||
|
This plugin gives a **browser terminal that executes shell commands as the php-fpm user**
|
||||||
|
(`atheos`). Two things matter:
|
||||||
|
|
||||||
|
1. **The plugin ships with a well-known, hardcoded gate constant.** Upstream is literally
|
||||||
|
`define("PASSWORD", "<the default value in the plugin source>");`. Anyone who can reach the page
|
||||||
|
can read the source on GitHub and unlock it. **We have rotated it to a random value** — retrieve
|
||||||
|
it from the `PASSWORD` define in `plugins/Terminal/terminal.php` on the guest. It is deliberately
|
||||||
|
**not recorded in this repo** (see below).
|
||||||
|
2. **It is a shell, so treat it as a privilege decision, not a convenience one.** Installing it
|
||||||
|
means the editor can run arbitrary commands as `atheos` — the same user that owns the deploy key.
|
||||||
|
|
||||||
|
Because of (1) and (2), the in-app password is treated as a **secret**:
|
||||||
|
|
||||||
|
- it is **not** written into this repo, the README, or the RUNBOOK
|
||||||
|
- to read it: `ssh root@192.168.178.57 'grep PASSWORD /var/www/atheos/plugins/Terminal/terminal.php'`
|
||||||
|
- it changes per install; `scripts/provision.sh` does not set one, so a rebuild returns to the
|
||||||
|
upstream default — **rotate it again after a rebuild**
|
||||||
|
|
||||||
|
**Outcome: the plugin has been REMOVED.** Once the `GLOB_BRACE` fault (§1) was fixed, removal through
|
||||||
|
the market succeeded — and that is the right end state: a browser shell is the single largest attack
|
||||||
|
surface in the container, and **Atheos macros already provide command execution**, so it added little
|
||||||
|
you did not already have. Removal was done by invoking the market's own `remove()` path, so the plugin
|
||||||
|
list, the `addons` cache and the generated bundle all stayed consistent:
|
||||||
|
|
||||||
|
```
|
||||||
|
{"text":"Plugin successfully removed.","status":200}
|
||||||
|
plugins/ -> (only README.md remains)
|
||||||
|
addons cache references to Terminal: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
The notes above are kept because they document what the plugin *is* and how it failed — and because
|
||||||
|
the same stale-API pattern will recur with any other Codiad-era plugin. To reinstall, use the
|
||||||
|
market: it will re-introduce the `Common::execute` fault, so `patches/Terminal-terminal.php.patched`
|
||||||
|
was the fix — **it has been deleted along with the plugin**. If you ever reinstall, re-derive it:
|
||||||
|
`grep -rhoE 'Common::[a-zA-Z_]+' plugins/Terminal/` and expect `Common::execute` to be missing.
|
||||||
|
|
||||||
|
`docs/security-notes.md` §2 S-7 covers disabling `exec`/`proc_open`, which would neutralise **both**
|
||||||
|
this plugin and the macros. That is the correct lever if you ever expose this container.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 🟠 The Macro dialog crashed when folder-scoped macros existed
|
||||||
|
|
||||||
|
**Symptom:** the Settings → Macro dialog returned HTTP 500.
|
||||||
|
|
||||||
|
**Root cause — invalid data by Atheos's own design.** `components/macro/dialog.php:35` does:
|
||||||
|
|
||||||
|
```php
|
||||||
|
implode(", ", $macro["fTypes"]) // requires an ARRAY
|
||||||
|
```
|
||||||
|
|
||||||
|
but `components/macro/class.macro.php` `save()` writes the **string** `"N/A"` for folder macros:
|
||||||
|
|
||||||
|
```php
|
||||||
|
"fTypes" => $type === "file" ? explode(",", $fTypes) : "N/A",
|
||||||
|
```
|
||||||
|
|
||||||
|
So **any folder-type macro** makes that dialog fatal:
|
||||||
|
|
||||||
|
```
|
||||||
|
PHP Fatal error: Uncaught TypeError: implode(): Argument #2 ($array) must be of type ?array,
|
||||||
|
string given in /var/www/atheos/components/macro/dialog.php:35
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix applied — correct the DATA, not Atheos's code.** The stored `fTypes` for folder macros is now
|
||||||
|
`[]` (an empty array) instead of `"N/A"`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sed -i 's/"fTypes": "N\/A"/"fTypes": []/g' /var/www/atheos/data/macros.db.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Verified: `implode()` now succeeds for all 8 macros.
|
||||||
|
|
||||||
|
**Why data rather than code:** patching `dialog.php` would be overwritten by `update-atheos.sh`
|
||||||
|
(and by any Atheos update), whereas valid data keeps working. The trade-off is that the UI's own
|
||||||
|
"save" path could re-introduce `"N/A"` if a macro is edited in the UI — see the follow-up below.
|
||||||
|
|
||||||
|
**Follow-up (not done):** the real upstream fix is
|
||||||
|
`$type === "file" ? explode(",", $fTypes) : []` in `class.macro.php::save()`. Applying it would stop
|
||||||
|
the UI re-introducing the bad value, at the cost of a patch that must be re-applied after updates.
|
||||||
|
Worth doing if the dialog starts 500-ing again after editing a macro in the UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
verified working:
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins.min.js contains 'plugins/Terminal/init.js' -> YES (bundle is fine)
|
||||||
|
rightbar HTML -> <a onclick="atheos.Terminal.open();">
|
||||||
|
<i class="fas fa-terminal"></i>Terminal</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
So when a plugin misbehaves, check in this order:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. did it install?
|
||||||
|
tail /var/www/atheos/data/log/market.log.php
|
||||||
|
|
||||||
|
# 2. is its JS in the served bundle?
|
||||||
|
grep -c "plugins/<Name>/init.js" /var/www/atheos/public/plugins.min.js
|
||||||
|
|
||||||
|
# 3. is it a stale-API port? (Codiad-era plugins are the risk)
|
||||||
|
grep -rhoE 'Common::[a-zA-Z_]+' /var/www/atheos/plugins/<Name>/ | sort -u
|
||||||
|
# then check each exists: grep -rn "function <name>" /var/www/atheos/common.php /var/www/atheos/traits/
|
||||||
|
|
||||||
|
# 4. is there a PHP fatal?
|
||||||
|
grep -E 'PHP Fatal' /var/log/nginx/atheos.error.log | tail
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rule of thumb:** if a plugin was ported from Codiad, assume its `Common::` calls are stale and
|
||||||
|
verify them before trusting it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```
|
||||||
|
components/codegit/* → Common::safe_execute (31 call sites — every git operation)
|
||||||
|
components/macro/* → Common::raw_execute (1 call site)
|
||||||
|
common.php raw_execute → function_exists("system") / function_exists("exec")
|
||||||
|
```
|
||||||
|
|
||||||
|
Every git commit, push, fetch and status from the editor goes through `exec`/`system`. Disabling them
|
||||||
|
turns the editor into a text editor that cannot commit. So it stays **a deliberate exposure lever**
|
||||||
|
(S-7 in `docs/security-notes.md`), to be applied only when the container is exposed — at which point
|
||||||
|
you would also lose macros and git integration, which is why S-7 already says *"test, do not
|
||||||
|
blind-apply"*.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 8.5 The real upstream fix for §3
|
||||||
|
|
||||||
|
`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.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# Writer-Branch Workflow
|
||||||
|
|
||||||
|
How writing and publishing work from the Atheos editor, and why the branch is structured this way.
|
||||||
|
|
||||||
|
## The model
|
||||||
|
|
||||||
|
The editor **never touches `master`**. It writes to a long-lived branch (`atheos-writer`), and
|
||||||
|
merging into `master` is done by the operator in Gitea.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
Atheos editor ────┤ atheos-writer (long-lived) │
|
||||||
|
(macros) │ • write here │
|
||||||
|
│ • commit + push here │
|
||||||
|
│ • can be reset --hard back to master │
|
||||||
|
└───────────────┬──────────────────────────┘
|
||||||
|
│ operator opens a PR in Gitea
|
||||||
|
│ operator reviews
|
||||||
|
▼
|
||||||
|
master (protected: RECOMMENDED, currently OFF - see §2)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
existing Pelican build pipeline
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** the editor is a web app with shell-exec reach (`codegit` + `macro` both run commands as the
|
||||||
|
php-fpm user). Bounding what it can reach is the whole point of design decision D6. Keeping `master`
|
||||||
|
behind a PR means a compromised or fat-fingered editor **cannot** publish directly — it can only
|
||||||
|
propose.
|
||||||
|
|
||||||
|
## Setup — do these once
|
||||||
|
|
||||||
|
### 1. Set your git identity (required, RUNBOOK STEP 1)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh root@192.168.178.57
|
||||||
|
su -s /bin/sh atheos
|
||||||
|
git config --global user.name "Your Name"
|
||||||
|
git config --global user.email "you@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the *same* global gitconfig php-fpm uses (`HOME=/home/atheos` — verified), so macros commit
|
||||||
|
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 — RECOMMENDATION, deliberately not enabled
|
||||||
|
|
||||||
|
**Status: accepted risk. Branch protection is OFF, and that is a decision, not an oversight.**
|
||||||
|
|
||||||
|
> *"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 |
|
||||||
|
|---|---|
|
||||||
|
| **Enable push** | **off** ← direct pushes blocked, including the deploy key |
|
||||||
|
| **Enable merge whitelist** | **on** → add `armistace` |
|
||||||
|
| Require approvals | optional (you're the only human here) |
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
`files/macros.db.php` so a rebuild reproduces them. They appear when you **right-click the `blog`
|
||||||
|
folder** in the file tree → **Macros**.
|
||||||
|
|
||||||
|
| # | Macro | Command | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **Start/Join Writer Branch** | `git checkout atheos-writer \|\| git checkout -b atheos-writer` then `push -u` | idempotent — safe to run repeatedly |
|
||||||
|
| 2 | **Commit + Push Writer Branch** | identity guard → `git add -A` → commit (only if staged changes) → `git push origin atheos-writer` | the everyday button |
|
||||||
|
| 3 | **DESTRUCTIVE: Reset Writer Branch to Master** | `git fetch` → `git reset --hard origin/master` → `git push --force-with-lease` | wipes the branch's commits, keeps your files |
|
||||||
|
| 4 | **Status + Recent Log** | `git status --short --branch`, `git log --oneline -8`, `git branch` | read-only |
|
||||||
|
| 5 | **Refresh Master (ff-only)** | `git checkout master` → `git pull --ff-only` → back to `atheos-writer` | brings master's merges down |
|
||||||
|
| 6 | **Merge Master Into Writer Branch** | `git merge origin/master` → push | for when master has moved ahead |
|
||||||
|
|
||||||
|
Output is returned into the UI — you see git's actual stdout/stderr, not a silent success.
|
||||||
|
|
||||||
|
## Day-to-day
|
||||||
|
|
||||||
|
1. Right-click `blog` → Macros → **1. Start/Join Writer Branch** (first time; also safe later)
|
||||||
|
2. Write your post, save
|
||||||
|
3. Right-click `blog` → Macros → **2. Commit + Push Writer Branch**
|
||||||
|
4. Open a PR: `atheos-writer` → `master`
|
||||||
|
5. Review the diff in Gitea, merge
|
||||||
|
|
||||||
|
### ⚠️ Important: macro 2 uses `git add -A`
|
||||||
|
|
||||||
|
`git add -A` stages **everything** in the repo, including files you may not intend to publish
|
||||||
|
(drafts, scratch files, `.orig`/`.bak` leftovers). A `reset --hard` will also **delete any untracked
|
||||||
|
file that has been staged**.
|
||||||
|
|
||||||
|
Practical consequences:
|
||||||
|
|
||||||
|
- `atheos-writer` is a **publishing** branch — everything you leave in the tree gets committed and
|
||||||
|
pushed to Gitea.
|
||||||
|
- If you want a scratch area, keep it **outside** the repo (e.g. `workspace/scratch/`, not
|
||||||
|
`workspace/blog/`).
|
||||||
|
- To unstage before a reset: `git reset` (clears the index without touching your files).
|
||||||
|
|
||||||
|
## Recovering a deleted file
|
||||||
|
|
||||||
|
`reset --hard` does not touch the object database, so a file that was ever staged or committed is
|
||||||
|
still recoverable:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
su -s /bin/sh atheos
|
||||||
|
cd /var/www/atheos/workspace/blog
|
||||||
|
git fsck --lost-found | grep commit # find dangling commits
|
||||||
|
git ls-tree <commit> path/to/file # confirm it is there
|
||||||
|
git cat-file -p <blob-sha> > path/to/file # restore it
|
||||||
|
```
|
||||||
|
|
||||||
|
`git reflog --all` also shows recently discarded commits. This is how the empty
|
||||||
|
`src/content/please_no_facebook.md` was recovered after a `reset --hard` in testing.
|
||||||
|
|
||||||
|
## Changing the branch name
|
||||||
|
|
||||||
|
Edit macros 1, 2, 3, 5 and 6 in **Settings → Macro** and replace `atheos-writer`. The branch name is
|
||||||
|
literal in each command — there is no variable. (Macros have no input prompt, so a
|
||||||
|
branch name that changes per task would need edit-then-run.)
|
||||||
+18
-9
@@ -7,13 +7,12 @@ server {
|
|||||||
root /var/www/atheos;
|
root /var/www/atheos;
|
||||||
index index.php index.html;
|
index index.php index.html;
|
||||||
|
|
||||||
|
# --- logging ---------------------------------------------------------
|
||||||
access_log /var/log/nginx/atheos.access.log;
|
access_log /var/log/nginx/atheos.access.log;
|
||||||
error_log /var/log/nginx/atheos.error.log;
|
error_log /var/log/nginx/atheos.error.log;
|
||||||
|
|
||||||
# --- PHP -----------------------------------------------------------------
|
# --- PHP: the socket path here MUST byte-match the pool's `listen =`
|
||||||
# The socket path here MUST byte-match the pool's `listen =` in
|
# in /etc/php83/php-fpm.d/www.conf (interface I-1 -> else bare 502)
|
||||||
# /etc/php83/php-fpm.d/www.conf
|
|
||||||
# (interface I-1 — a mismatch yields a bare 502 with no other diagnostic).
|
|
||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
@@ -27,11 +26,12 @@ server {
|
|||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- hardening -----------------------------------------------------------
|
# --- hardening (free, no downside) -----------------------------------
|
||||||
location ~ /\.git { deny all; }
|
# never serve the VCS directory, the app's private data, or dotfiles
|
||||||
location ~ /\.ssh { deny all; }
|
location ~ /\.git { deny all; }
|
||||||
location ~ /\. { deny all; }
|
location ~ /\.ssh { deny all; }
|
||||||
location ~ ^/data/ { deny all; }
|
location ~ /\. { deny all; }
|
||||||
|
location ~ ^/data/ { deny all; }
|
||||||
location ~ ^/(config\.php|common\.php|controller\.php|traits/) { deny all; }
|
location ~ ^/(config\.php|common\.php|controller\.php|traits/) { deny all; }
|
||||||
location ~* \.(md|py|sh|log|conf|ini|sql|bak|orig)$ { 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;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
client_max_body_size 64m;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php/*|[
|
||||||
|
{
|
||||||
|
"uuid": "macro-947e-4763-b7b6f2",
|
||||||
|
"title": "Execute",
|
||||||
|
"type": "file",
|
||||||
|
"fTypes": [
|
||||||
|
"sh"
|
||||||
|
],
|
||||||
|
"command": "%PATH%",
|
||||||
|
"icon": "fas fa-magic",
|
||||||
|
"owner": "ATHEOS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-53a0-4ebb-56e800",
|
||||||
|
"title": "Execute",
|
||||||
|
"type": "file",
|
||||||
|
"fTypes": [
|
||||||
|
"php"
|
||||||
|
],
|
||||||
|
"command": "cd %FOLDER% && php %BASENAME%",
|
||||||
|
"icon": "fas fa-magic",
|
||||||
|
"owner": "ATHEOS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0001-start",
|
||||||
|
"title": "1. Start/Join Writer Branch",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && (git checkout atheos-writer 2>/dev/null || git checkout -b atheos-writer) && git push -u origin atheos-writer",
|
||||||
|
"icon": "fas fa-code-branch",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0002-commit",
|
||||||
|
"title": "2. Commit + Push Writer Branch",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && (git config --get user.email >/dev/null || { echo 'ERROR: no git identity set. Run over SSH: git config --global user.name \"Your Name\" && git config --global user.email \"you@example.com\" (RUNBOOK STEP 1)'; exit 1; }) && git add -A && (git diff --cached --quiet || git commit -m \"writer: $(date +%F_%H%M)\") && git push origin atheos-writer",
|
||||||
|
"icon": "fas fa-cloud-upload-alt",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0003-reset",
|
||||||
|
"title": "3. DESTRUCTIVE: Reset Writer Branch to Master",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && git fetch origin && git reset --hard origin/master && git push --force-with-lease origin atheos-writer",
|
||||||
|
"icon": "fas fa-exclamation-triangle",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0004-status",
|
||||||
|
"title": "4. Status + Recent Log",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && git status --short --branch && echo '--- recent commits:' && git log --oneline -8 && echo '--- local branches:' && git branch",
|
||||||
|
"icon": "fas fa-info-circle",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0005-refresh",
|
||||||
|
"title": "5. Refresh Master (ff-only)",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && git checkout master && git pull --ff-only && git checkout atheos-writer",
|
||||||
|
"icon": "fas fa-sync-alt",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0006-pullmaster",
|
||||||
|
"title": "6. Merge Master Into Writer Branch",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && (git config --get user.email >/dev/null || { echo 'ERROR: no git identity set. Run over SSH: git config --global user.name \"Your Name\" && git config --global user.email \"you@example.com\" (RUNBOOK STEP 1)'; exit 1; }) && git fetch origin && git merge origin/master -m \"merge master into atheos-writer\" && git push origin atheos-writer",
|
||||||
|
"icon": "fas fa-code-merge",
|
||||||
|
"owner": "armistace"
|
||||||
|
}
|
||||||
|
]|*/?>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
--- /var/www/atheos/traits/helpers.php.orig
|
||||||
|
+++ /var/www/atheos/traits/helpers.php
|
||||||
|
@@ -135,7 +135,18 @@
|
||||||
|
|
||||||
|
if (is_dir($target)) {
|
||||||
|
|
||||||
|
- $files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE|GLOB_MARK); //GLOB_MARK adds a slash to directories returned
|
||||||
|
+ // GLOB_BRACE is a glibc extension and is UNDEFINED under musl (Alpine), where
|
||||||
|
+ // referencing it is a fatal error in PHP 8. Expand the brace pattern by hand
|
||||||
|
+ // so this works on both libcs.
|
||||||
|
+ if (defined("GLOB_BRACE")) {
|
||||||
|
+ $files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE | GLOB_MARK);
|
||||||
|
+ } else {
|
||||||
|
+ $files = array_merge(
|
||||||
|
+ glob($target . "/*", GLOB_MARK) ?: [],
|
||||||
|
+ glob($target . "/.[!.]*", GLOB_MARK) ?: [],
|
||||||
|
+ glob($target . "/..?*", GLOB_MARK) ?: []
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
// $files = glob($target . "/*");
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
+139
-8
@@ -5,9 +5,10 @@
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
W=/var/www/atheos
|
W=/var/www/atheos
|
||||||
B=$W/workspace/blog
|
B=$W/workspace/blog
|
||||||
pass=0; fail=0
|
pass=0; fail=0; note=0
|
||||||
ok() { echo " PASS $1"; pass=$((pass+1)); }
|
ok() { echo " PASS $1"; pass=$((pass+1)); }
|
||||||
bad() { echo " FAIL $1"; fail=$((fail+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; }
|
chk() { if [ "$2" = "$3" ]; then ok "$1 ($2)"; else bad "$1 (got '$2', want '$3')"; fi; }
|
||||||
asuser() { su -s /bin/sh atheos -c "$1"; }
|
asuser() { su -s /bin/sh atheos -c "$1"; }
|
||||||
|
|
||||||
@@ -17,7 +18,14 @@ echo "=============================================================="
|
|||||||
CODE=$(wget -qO /tmp/idx.html --server-response http://127.0.0.1/ 2>&1 | sed -n 's/.*HTTP\/1\.[01] \([0-9]*\).*/\1/p' | head -1)
|
CODE=$(wget -qO /tmp/idx.html --server-response http://127.0.0.1/ 2>&1 | sed -n 's/.*HTTP\/1\.[01] \([0-9]*\).*/\1/p' | head -1)
|
||||||
chk "HTTP status for /" "$CODE" "200"
|
chk "HTTP status for /" "$CODE" "200"
|
||||||
if grep -qi '<title>Atheos IDE</title>' /tmp/idx.html 2>/dev/null; then ok "served page is the Atheos IDE"; else bad "served page is not Atheos"; fi
|
if grep -qi '<title>Atheos IDE</title>' /tmp/idx.html 2>/dev/null; then ok "served page is the Atheos IDE"; else bad "served page is not Atheos"; fi
|
||||||
if grep -qiE 'install|setup' /tmp/idx.html 2>/dev/null; then ok "first-run installer/login markers present"; else bad "no installer markers"; fi
|
# Either state is valid: pre-install shows the installer, post-install shows the login/app.
|
||||||
|
if grep -qiE 'install|setup' /tmp/idx.html 2>/dev/null; then
|
||||||
|
ok "installer markers present (pre-install state)"
|
||||||
|
elif [ -f "$W/config.php" ] && [ -s "$W/config.php" ]; then
|
||||||
|
ok "installer already completed (config.php populated)"
|
||||||
|
else
|
||||||
|
bad "neither installer markers nor a populated config.php"
|
||||||
|
fi
|
||||||
SOCK=$(sed -n 's/^listen = //p' /etc/php83/php-fpm.d/www.conf | head -1)
|
SOCK=$(sed -n 's/^listen = //p' /etc/php83/php-fpm.d/www.conf | head -1)
|
||||||
NGX=$(sed -n 's/.*fastcgi_pass unix:\([^;]*\);.*/\1/p' /etc/nginx/http.d/atheos.conf | head -1)
|
NGX=$(sed -n 's/.*fastcgi_pass unix:\([^;]*\);.*/\1/p' /etc/nginx/http.d/atheos.conf | head -1)
|
||||||
echo " --- socket contract (I-1):"
|
echo " --- socket contract (I-1):"
|
||||||
@@ -29,7 +37,8 @@ echo
|
|||||||
echo "=============================================================="
|
echo "=============================================================="
|
||||||
echo "AC-3 'blog' is present with files that can be opened"
|
echo "AC-3 'blog' is present with files that can be opened"
|
||||||
echo "=============================================================="
|
echo "=============================================================="
|
||||||
chk "posts found" "$(ls $B/src/content/*.md 2>/dev/null | wc -l | tr -d ' ')" "20"
|
POSTS=$(ls $B/src/content/*.md 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
if [ "$POSTS" -ge 20 ]; then ok "posts found ($POSTS, expected >= 20)"; else bad "posts found ($POSTS, expected >= 20)"; fi
|
||||||
if [ -d "$W/workspace/blog/.git" ]; then ok "it is a git working copy"; else bad "not a git working copy"; fi
|
if [ -d "$W/workspace/blog/.git" ]; then ok "it is a git working copy"; else bad "not a git working copy"; fi
|
||||||
chk "theme present" "$(ls $B/src/themes 2>/dev/null | tr -d ' ')" "cleanblog"
|
chk "theme present" "$(ls $B/src/themes 2>/dev/null | tr -d ' ')" "cleanblog"
|
||||||
chk "workspace owner" "$(stat -c '%U' $W/workspace/blog)" "atheos"
|
chk "workspace owner" "$(stat -c '%U' $W/workspace/blog)" "atheos"
|
||||||
@@ -56,7 +65,13 @@ case "$REMOTE" in
|
|||||||
gitea-blog:armistace/blog*) ok "remote targets armistace/blog via the deploy-key alias" ;;
|
gitea-blog:armistace/blog*) ok "remote targets armistace/blog via the deploy-key alias" ;;
|
||||||
*) bad "remote does not target the blog repo" ;;
|
*) bad "remote does not target the blog repo" ;;
|
||||||
esac
|
esac
|
||||||
chk "branch" "$(asuser "git -C $B rev-parse --abbrev-ref HEAD" 2>/dev/null)" "master"
|
# A repo may legitimately sit on master, or on the long-lived writer branch (docs/writer-branch-workflow.md).
|
||||||
|
BR=$(asuser "git -C $B rev-parse --abbrev-ref HEAD" 2>/dev/null)
|
||||||
|
case "$BR" in
|
||||||
|
master|main) ok "branch ($BR)" ;;
|
||||||
|
atheos-writer) ok "branch ($BR - writer-branch workflow)" ;;
|
||||||
|
*) bad "branch is unexpected: $BR" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Phase 2: once the SSH alias is in use, prove the transport actually authenticates
|
# Phase 2: once the SSH alias is in use, prove the transport actually authenticates
|
||||||
case "$REMOTE" in
|
case "$REMOTE" in
|
||||||
@@ -94,10 +109,126 @@ done
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "=============================================================="
|
echo "=============================================================="
|
||||||
echo "FR-11 GUARD: agent created NO identity or armistace credential"
|
echo "REGRESSION: panel has no hardcoded height (empty-modal bug)"
|
||||||
echo "=============================================================="
|
echo "=============================================================="
|
||||||
chk "git user.name empty" "$(asuser 'git config --get user.name' 2>/dev/null)" ""
|
# panel { height:500px } forced every modal panel to a fixed box: short content left a large
|
||||||
chk "git user.email empty" "$(asuser 'git config --get user.email' 2>/dev/null)" ""
|
# 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 "=============================================================="
|
||||||
|
# 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).
|
||||||
|
rm -rf /tmp/rdel_guard 2>/dev/null
|
||||||
|
if mkdir -p /tmp/rdel_guard/a/b/c /tmp/rdel_guard/.hd 2>/dev/null \
|
||||||
|
&& touch /tmp/rdel_guard/f.txt /tmp/rdel_guard/.dot /tmp/rdel_guard/a/b/c/deep.txt; then
|
||||||
|
ROUT=$(php83 -r 'require_once("/var/www/atheos/common.php"); echo Common::rDelete("/tmp/rdel_guard/") ? "ok" : "fail";' 2>&1 | grep -viE 'deprecat|warning|notice' | tail -1)
|
||||||
|
if [ "$ROUT" = "ok" ] && [ ! -e /tmp/rdel_guard ]; then
|
||||||
|
ok "rDelete removed a nested tree incl. dotfiles (delete works)"
|
||||||
|
else
|
||||||
|
if echo "$ROUT" | grep -q 'GLOB_BRACE'; then
|
||||||
|
bad "rDelete FATALS on GLOB_BRACE - re-apply patches/helpers-rDelete-GLOB_BRACE.diff"
|
||||||
|
else
|
||||||
|
bad "rDelete failed (got: $ROUT)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bad "could not build the rDelete test tree"
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/rdel_guard 2>/dev/null
|
||||||
|
echo "=============================================================="
|
||||||
|
# The whole branch-based workflow assumes the deploy key CANNOT push master. That is a
|
||||||
|
# repo-side setting we cannot verify from here by inspection - so test it the way an
|
||||||
|
# attacker would: attempt a direct push to master and require it to be REJECTED.
|
||||||
|
if [ "${SKIP_MASTER_PUSH_TEST:-0}" = "1" ]; then
|
||||||
|
echo " SKIPPED (SKIP_MASTER_PUSH_TEST=1)"
|
||||||
|
else
|
||||||
|
PUSH_OUT=$(asuser "git -C $B push origin HEAD:master --dry-run" 2>&1)
|
||||||
|
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
|
||||||
|
# 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
|
||||||
|
# the operator. The operator has since explicitly asked for it to be set, so the requirement is
|
||||||
|
# superseded - and the guard is inverted: the identity must now be present and must be the
|
||||||
|
# OPERATOR's, not an agent-invented one. Guarding the old expectation would assert the opposite
|
||||||
|
# of the current design and fail forever (which is what it did).
|
||||||
|
NAME=$(asuser 'git config --get user.name' 2>/dev/null)
|
||||||
|
MAIL=$(asuser 'git config --get user.email' 2>/dev/null)
|
||||||
|
if [ -n "$NAME" ]; then ok "git user.name set ($NAME)"; else bad "git user.name is empty - macro 2 will refuse to commit"; fi
|
||||||
|
if [ -n "$MAIL" ]; then ok "git user.email set ($MAIL)"; else bad "git user.email is empty"; fi
|
||||||
|
case "$MAIL" in
|
||||||
|
*hermes*|*bot*|*noreply@anthropic*|*agent*) bad "identity looks agent-generated: $MAIL" ;;
|
||||||
|
"") ;;
|
||||||
|
*) ok "identity is an operator address, not an agent-invented one" ;;
|
||||||
|
esac
|
||||||
if [ -f /home/atheos/.ssh/id_ed25519_blog ]; then ok "deploy key present"; else bad "deploy key missing"; fi
|
if [ -f /home/atheos/.ssh/id_ed25519_blog ]; then ok "deploy key present"; else bad "deploy key missing"; fi
|
||||||
chk "deploy key mode" "$(stat -c '%a' /home/atheos/.ssh/id_ed25519_blog 2>/dev/null)" "600"
|
chk "deploy key mode" "$(stat -c '%a' /home/atheos/.ssh/id_ed25519_blog 2>/dev/null)" "600"
|
||||||
chk "deploy key owner" "$(stat -c '%U' /home/atheos/.ssh/id_ed25519_blog 2>/dev/null)" "atheos"
|
chk "deploy key owner" "$(stat -c '%U' /home/atheos/.ssh/id_ed25519_blog 2>/dev/null)" "atheos"
|
||||||
@@ -123,6 +254,6 @@ echo " listening ports: $(netstat -tln 2>/dev/null | awk '/LISTEN/{split($4,a,"
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
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 "=============================================================="
|
echo "=============================================================="
|
||||||
[ "$fail" -eq 0 ] || exit 1
|
[ "$fail" -eq 0 ] || exit 1
|
||||||
|
|||||||
@@ -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 ==="
|
||||||
@@ -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 ==="
|
||||||
Reference in New Issue
Block a user