fix: empty dialogs - relative project path caused a silently-swallowed 403 (+ cache-busting) #7

Merged
hermes merged 2 commits from fix/cache-busting into master 2026-09-29 16:55:36 +10:00
Owner

Summary

Two commits. The second one is the actual cause of the empty CodeGit dialog — the first two
attempts (§4 CSS, §5 cache) were real bugs but neither was it.


1. The real cause: a relative project path → a silently-swallowed 403

Not a CSS problem, and not Firefox. Pinned down by capturing the actual request/response from a
scripted browser rather than by reading stylesheets:

POST /dialog.php   {"target":"codegit","action":"codegit","repo":"blog"}
-> 403      #content innerHTML length: 0

The chain:

# Step
1 install/process.php stores a relative folder name → projects.db = {"Blog":"blog"}
2 common.php::loadState() does $projectPath = reset($projectList) → "blog", then persists it to the session — so the bad value sticks across logins
3 filetree/init.js 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

Step 5 is why this presented as a black box rather than an error: the request fails, nothing
renders, and nothing reports it. It is also why it survived the CSS and cache fixes.

Fix (scripts/fix-project-path.sh) — both parts are required:

  • A. Resolve the path in loadState() and re-store it, which heals sessions already holding
    the relative value.
  • B. Make the projects store absolute, so 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
    and the tree would show an empty project name.

Verified from a deliberately poisoned session (projectPath="blog"):

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.


2. Cache-busting asset URLs

Atheos serves theme/main.min.css and the generated bundles from stable paths with no version,
so a browser keeps its cached copy across a change and never even asks the server. getTag() and the
theme link now append the file mtime (?v=<mtime>): a changed file gets a new URL and is fetched
immediately; an unchanged one stays cached. This works even against an already-poisoned cache,
which the earlier Cache-Control header alone could not do.

Not Firefox

Tested properly, since that was the direct question: installed real Firefox 155 and drove the live
server with it, at three viewport sizes with a macOS UA. All four codegit panels rendered correctly —
Log returned 34,630 chars of commit history, zero JS errors. Firefox was never the problem; it was
faithfully rendering a page whose data request had failed.

Tests — 47 passed, 0 failed, 1 accepted-risk

Added:

  • the projects store must hold an absolute path
  • loadState must resolve a relative projectPath
  • asset URLs must carry ?v= (so this class of "my fix didn't apply" cannot silently return)

The one accepted-risk is branch protection, per the operator's decision.

Test plan

  • Real Firefox 155, authenticated, all four codegit panels render
  • Request/response captured: 403 → 200 after the fix
  • Poisoned session self-heals; #content fills with 1758 chars
  • php83 -l clean on the patched common.php
  • fix-project-path.sh verified idempotent
  • Temp diagnostic files removed from the guest
## Summary Two commits. **The second one is the actual cause of the empty CodeGit dialog** — the first two attempts (§4 CSS, §5 cache) were real bugs but neither was *it*. --- ### 1. The real cause: a relative project path → a silently-swallowed 403 **Not a CSS problem, and not Firefox.** Pinned down by capturing the actual request/response from a scripted browser rather than by reading stylesheets: ``` POST /dialog.php {"target":"codegit","action":"codegit","repo":"blog"} -> 403 #content innerHTML length: 0 ``` The chain: | # | Step | |---|---| | 1 | `install/process.php` stores a **relative** folder name → `projects.db` = `{"Blog":"blog"}` | | 2 | `common.php::loadState()` does `$projectPath = reset($projectList)` → `"blog"`, then **persists it to the session** — so the bad value sticks across logins | | 3 | `filetree/init.js` 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 | Step 5 is why this presented as a *black box* rather than an error: the request fails, nothing renders, and nothing reports it. It is also why it survived the CSS and cache fixes. **Fix** (`scripts/fix-project-path.sh`) — both parts are required: - **A.** Resolve the path in `loadState()` and **re-store it**, which heals sessions already holding the relative value. - **B.** Make the projects store absolute, so `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` and the tree would show an empty project name. **Verified from a deliberately poisoned session** (`projectPath="blog"`): ``` 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. --- ### 2. Cache-busting asset URLs Atheos serves `theme/main.min.css` and the generated bundles from **stable paths with no version**, so a browser keeps its cached copy across a change and never even asks the server. `getTag()` and the theme link now append the file mtime (`?v=<mtime>`): a changed file gets a new URL and is fetched immediately; an unchanged one stays cached. This works **even against an already-poisoned cache**, which the earlier `Cache-Control` header alone could not do. ### Not Firefox Tested properly, since that was the direct question: installed real Firefox 155 and drove the live server with it, at three viewport sizes with a macOS UA. All four codegit panels rendered correctly — Log returned **34,630 chars** of commit history, zero JS errors. Firefox was never the problem; it was faithfully rendering a page whose data request had failed. ### Tests — 47 passed, 0 failed, 1 accepted-risk Added: - the projects store must hold an **absolute** path - `loadState` must resolve a relative `projectPath` - asset URLs must carry `?v=` (so this class of "my fix didn't apply" cannot silently return) The one `accepted-risk` is branch protection, per the operator's decision. ## Test plan - [x] Real Firefox 155, authenticated, all four codegit panels render - [x] Request/response captured: 403 → 200 after the fix - [x] Poisoned session self-heals; `#content` fills with 1758 chars - [x] `php83 -l` clean on the patched `common.php` - [x] `fix-project-path.sh` verified idempotent - [x] Temp diagnostic files removed from the guest
hermes added 2 commits 2026-09-29 16:55:33 +10:00
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)
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.
hermes merged commit 4163885e4c into master 2026-09-29 16:55:36 +10:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: hermes/atheos-blog-lxc#7