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
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 master2026-09-29 16:55:36 +10:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
The chain:
install/process.phpstores a relative folder name →projects.db={"Blog":"blog"}common.php::loadState()does$projectPath = reset($projectList)→"blog", then persists it to the session — so the bad value sticks across loginsfiletree/init.jsputs it straight into the DOM →<a id="project-root" data-path="blog">repo="blog";getWorkspacePath("blog")failscheckPath()→traits/path.php:67 Common::send(403, "Client does not have access.")modal.jssilently discards it:settled: function(reply,status){ if (status !== 200) return; ... }— no toast, no console error,#contentleft emptyStep 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:loadState()and re-store it, which heals sessions already holdingthe relative value.
array_search($projectPath, $projectList)— whichloadState()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_searchwould returnfalseand the tree would show an empty project name.
Verified from a deliberately poisoned session (
projectPath="blog"):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.cssand 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 thetheme link now append the file mtime (
?v=<mtime>): a changed file gets a new URL and is fetchedimmediately; an unchanged one stays cached. This works even against an already-poisoned cache,
which the earlier
Cache-Controlheader 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:
loadStatemust resolve a relativeprojectPath?v=(so this class of "my fix didn't apply" cannot silently return)The one
accepted-riskis branch protection, per the operator's decision.Test plan
#contentfills with 1758 charsphp83 -lclean on the patchedcommon.phpfix-project-path.shverified idempotentReported 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.