Merge remote-tracking branch 'origin/main' into feat/journal-post-form
# Conflicts: # user
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "Grav plugin config must live in the tracked user/config/plugins/ override, not the plugin folder"
|
||||
date: 2026-07-04
|
||||
category: docs/solutions/conventions
|
||||
module: "grav / plugin configuration"
|
||||
problem_type: convention
|
||||
component: tooling
|
||||
severity: high
|
||||
applies_when:
|
||||
- "Editing functional config for any GPM-managed Grav plugin"
|
||||
- "user/plugins/ is gitignored and only pages/config/accounts/themes are tracked"
|
||||
- "Preparing a fresh install or production cutover"
|
||||
- "A plugin behaves correctly locally but ships with only default config on deploy"
|
||||
related_components:
|
||||
- "grav"
|
||||
- "gpm"
|
||||
- "api plugin"
|
||||
- "content repo"
|
||||
- "deployment"
|
||||
tags:
|
||||
- grav
|
||||
- plugin-config
|
||||
- gpm
|
||||
- config-override
|
||||
- gitignore
|
||||
- deployment
|
||||
- api-plugin
|
||||
---
|
||||
|
||||
# Grav plugin config must live in the tracked user/config/plugins/ override, not the plugin folder
|
||||
|
||||
## Context
|
||||
|
||||
Grav resolves a plugin's config by deep-merging two layers: the plugin's own shipped file `user/plugins/<name>/<name>.yaml` (installed by GPM, part of the package) and the tracked override `user/config/plugins/<name>.yaml` (which wins). In this project the content repo tracks only `pages/`, `config/`, `accounts/`, `themes/`; `user/plugins/` and `user/data/` are gitignored (GPM manages plugin *code*). So any functional config a developer edits into a plugin's own `user/plugins/<name>/<name>.yaml` is invisible to version control.
|
||||
|
||||
It was this gap that left the `api` plugin unconfigured on the fresh prod install. Its `enabled`/`route`/`session_enabled`/cors/rate_limit config existed only in the untracked plugin folder locally, while the committed `user/config/plugins/api.yaml` held only a runtime `popularity.salt`. The local machine worked because the plugin folder had been hand-edited; every fresh environment got only the plugin's shipped defaults.
|
||||
|
||||
## Guidance
|
||||
|
||||
Put **functional** plugin configuration in the TRACKED override `user/config/plugins/<name>.yaml`. Grav deep-merges it over the plugin's shipped defaults, so it need only carry the keys that must differ (or the full config, for clarity). Keep **secrets and per-install generated values** OUT of the tracked file — JWT secrets, salts, encrypted tokens belong in gitignored `*-private.php` companion files (e.g. `api-private.php`, `security-private.php`) or should be regenerated per-install.
|
||||
|
||||
Never rely on edits to the plugin's own `user/plugins/<name>/<name>.yaml`: it is gitignored (won't deploy) and is overwritten on the next `php bin/gpm update`.
|
||||
|
||||
Concrete before/after, using the `api` plugin:
|
||||
|
||||
```yaml
|
||||
# WRONG: user/plugins/api/api.yaml (gitignored, GPM-managed, wiped on update)
|
||||
enabled: true
|
||||
route: /api
|
||||
auth:
|
||||
session_enabled: true
|
||||
```
|
||||
|
||||
```yaml
|
||||
# RIGHT: user/config/plugins/api.yaml (tracked, deploys, survives gpm update)
|
||||
enabled: true
|
||||
route: /api
|
||||
version_prefix: v1
|
||||
auth:
|
||||
session_enabled: true
|
||||
# JWT secret intentionally NOT here — it lives in the gitignored api-private.php
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Reproducible deploys: a fresh clone or `make remote-install-<env>` must produce a working site from the repo alone. Config stranded in the gitignored plugin folder silently yields a plugin with only its shipped defaults on every new environment — which, for a plugin whose behavior depends on non-default config, means it's misconfigured or effectively off. On prod the `api` plugin's route/auth simply didn't work.
|
||||
|
||||
The failure is silent and per-environment: it works on the developer's machine (where the plugin folder was hand-edited) and breaks everywhere else. `gpm update` compounds it by wiping the folder edit even locally, so the "working" state is not just unshared — it is also unstable on the one machine that had it.
|
||||
|
||||
## When to Apply
|
||||
|
||||
- Any time you configure a Grav plugin whose non-default settings must work on a server (prod/test) or survive a plugin update.
|
||||
- Especially for plugins whose function depends on config: `api` (route/auth/cors), `admin2`, `flex-objects`, form/media settings, etc.
|
||||
- When auditing a fresh-install failure: check whether the "working" local config actually lives in a tracked path (`git ls-files user/config/plugins/<name>.yaml`) or was stranded in `user/plugins/<name>/`.
|
||||
|
||||
## Examples
|
||||
|
||||
- **api plugin (this project):** the functional config was moved from the untracked `user/plugins/api/api.yaml` into the tracked `user/config/plugins/api.yaml`, then `make content-push` + `make remote-fetch-content-<env>` deployed it. The JWT secret stayed in the gitignored `api-private.php`.
|
||||
- **Quick audit command:** `git -C user ls-files config/plugins/` shows exactly which plugin configs are tracked/deployable; anything you rely on that isn't listed is a latent fresh-install failure.
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md` — the config gap documented here was the *other* latent problem surfaced in that same investigation: the `api` plugin also had to be *installed* first before any config could take effect. The install gap (GPM version floor) and this config-tracking gap compounded each other on the fresh prod environment.
|
||||
- `docs/working/git-sync-notes.md` — the related third config location: on prod, Grav Admin saves config into the per-environment tree `user/env/<host>/config/`, which is *also* untracked. Same "config that doesn't reach the repo" family.
|
||||
- **CLAUDE.md §0 (plugin-management model):** only `pages/`, `config/`, `accounts/`, `themes/` are tracked in the `user/` repo; `plugins/` and `data/` are gitignored and GPM-managed. That tracking boundary is exactly why functional config must live under `config/plugins/`, not in the plugin's own folder.
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: "Grav garbage page from two conflicting Content-Encoding headers (identity + gzip)"
|
||||
date: 2026-07-04
|
||||
category: docs/solutions/integration-issues
|
||||
module: "grav / production deploy"
|
||||
problem_type: integration_issue
|
||||
component: tooling
|
||||
severity: high
|
||||
symptoms:
|
||||
- "Homepage and every dynamic Grav page render as full-screen binary/mojibake garbage in the browser"
|
||||
- "curl without Accept-Encoding returns clean HTML, so the page looks fine from a naive curl"
|
||||
- "curl -H \"Accept-Encoding: gzip\" (a browser-style request) returns raw gzip bytes"
|
||||
- "Response carries two conflicting headers: content-encoding: identity AND content-encoding: gzip"
|
||||
- "Only appeared after switching prod to production Twig mode (twig.debug: false)"
|
||||
root_cause: config_error
|
||||
resolution_type: config_change
|
||||
related_components:
|
||||
- "Apache mod_deflate"
|
||||
- "DirectAdmin shared host"
|
||||
- "Grav shutdown handler (system/src/Grav/Common/Grav.php)"
|
||||
- "deploy/env/prod/system.yaml"
|
||||
- "make remote-apply-env-prod"
|
||||
tags:
|
||||
- grav
|
||||
- content-encoding
|
||||
- gzip
|
||||
- mod-deflate
|
||||
- fastcgi-finish-request
|
||||
- apache
|
||||
- production-deploy
|
||||
- twig-debug
|
||||
---
|
||||
|
||||
# Grav garbage page from two conflicting Content-Encoding headers (identity + gzip)
|
||||
|
||||
## Problem
|
||||
|
||||
Grav renders every dynamic page as binary garbage in the browser because it emits two conflicting `Content-Encoding` headers. The response body is valid gzip, but because the server advertises both `content-encoding: identity` and `content-encoding: gzip`, the browser cannot decide how (or whether) to inflate it, and paints the raw compressed bytes to screen. Static assets are unaffected — only Grav's own dynamically generated pages are broken. The problem surfaced only after switching the site to production Twig mode (`twig.debug: false`).
|
||||
|
||||
## Symptoms
|
||||
|
||||
- The homepage and all dynamic Grav pages show a full screen of binary/mojibake characters in the browser (completely unreadable). The surrounding HTML shell and static assets are fine.
|
||||
- A naive `curl https://site/` (with **no** `Accept-Encoding` header) returns clean, correct HTML — so a quick curl sanity check looks perfectly healthy and completely hides the bug.
|
||||
- A browser-style request exposes it. `curl -H "Accept-Encoding: gzip" -D - -o /dev/null https://site/` shows **two** `Content-Encoding` response headers:
|
||||
```
|
||||
content-encoding: identity
|
||||
content-encoding: gzip
|
||||
```
|
||||
The body is valid gzip and `gunzip`s to the correct HTML.
|
||||
- A static asset served by the webserver alone (e.g. a CSS/JS file) shows a **single** clean `content-encoding: gzip` under the same request — confirming the webserver's gzip is fine and the duplication is Grav-originated.
|
||||
- The bug only appeared after switching the site to production Twig mode (`twig.debug: false`), which activates Grav's full shutdown/output path.
|
||||
|
||||
## What Didn't Work
|
||||
|
||||
- **First fix attempt: `cache.gzip: false` + `allow_webserver_gzip: true`.** This was the key dead end. It had **no effect** — the duplicated headers were unchanged. Reading Grav's source explained why: the branch that emits the bogus header fires on `if ($config->get('system.cache.gzip') || $config->get('system.cache.allow_webserver_gzip'))`. Setting `allow_webserver_gzip: true` satisfies the **same** `||` condition, so Grav still takes the identical `header('Content-Encoding: identity')` code path. The two knobs that *look* like they control this are both on the wrong side of the problem.
|
||||
- **Verifying with a naive `curl` (no `Accept-Encoding: gzip`).** This hid the problem entirely, because the server only compresses when the client advertises gzip support. Any healthcheck that omits `Accept-Encoding: gzip` reports a false "all clear." Reproduce it the way a browser does — send `Accept-Encoding: gzip`, or take a real headless-browser (Playwright) screenshot.
|
||||
|
||||
## Solution
|
||||
|
||||
Root the fix in Grav's shutdown handler, `system/src/Grav/Common/Grav.php` (~lines 615–631):
|
||||
|
||||
```php
|
||||
if ($config->get('system.debugger.shutdown.close_connection', true)) {
|
||||
$success = function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false;
|
||||
if (!$success) {
|
||||
if (!ini_get('zlib.output_compression')) {
|
||||
if ($config->get('system.cache.gzip') || $config->get('system.cache.allow_webserver_gzip')) {
|
||||
header('Content-Encoding: identity'); // <-- the bogus header
|
||||
} elseif (function_exists('apache_setenv')) {
|
||||
@apache_setenv('no-gzip', '1');
|
||||
} else {
|
||||
header('Content-Encoding: none');
|
||||
}
|
||||
header('Content-Length: ' . ob_get_length());
|
||||
}
|
||||
header('Connection: close');
|
||||
ob_end_flush();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The entire problematic block is gated by `system.debugger.shutdown.close_connection` (default `true`). Disable it so the whole branch is skipped and Grav never touches `Content-Encoding` at all.
|
||||
|
||||
In this project it is applied as a **per-environment (prod-only) override** so local dev is untouched — `deploy/env/prod/system.yaml`, deployed via `make remote-apply-env-prod`:
|
||||
|
||||
```yaml
|
||||
debugger:
|
||||
shutdown:
|
||||
close_connection: false
|
||||
```
|
||||
|
||||
Verify — the response must show **exactly one** `content-encoding`:
|
||||
|
||||
```bash
|
||||
curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" https://site/ | grep -i content-encoding
|
||||
# content-encoding: gzip
|
||||
```
|
||||
|
||||
Then take a screenshot of the rendered page to confirm it displays correctly. A `curl`-without-gzip check is **not** sufficient proof — it would have passed even while the bug was live.
|
||||
|
||||
## Why This Works
|
||||
|
||||
`debugger.shutdown.close_connection` (default `true`) makes Grav flush the full response and close the connection to the browser **early**, so slow shutdown tasks (logging, debugger teardown) don't keep the visitor waiting. On a FastCGI/PHP-FPM host, Grav does this cleanly via `fastcgi_finish_request()` and never manipulates headers — which is why the bug is invisible on most stacks.
|
||||
|
||||
On a **non-FastCGI** host (LiteSpeed, suPHP, plain CGI), `fastcgi_finish_request()` does not exist, so `$success` is `false` and Grav falls back to closing the connection *manually*. To do that it must set an explicit `Content-Length`, and to keep that length honest it tries to tell the webserver "do not compress this body" — which, on the `cache.gzip`/`allow_webserver_gzip` branch, it expresses as `header('Content-Encoding: identity')`.
|
||||
|
||||
But `identity` is not a real content transformation and is **not** a recognized "suppress compression" signal to Apache `mod_deflate`. `mod_deflate` ignores it, compresses the body anyway, and appends its **own** `Content-Encoding: gzip`. The response now carries two contradictory `Content-Encoding` headers (`identity` and `gzip`). Browsers cannot reconcile the contradiction, fail to inflate the gzip stream, and render the raw compressed bytes — the "binary garbage" screen.
|
||||
|
||||
Setting `close_connection: false` means Grav never enters the manual connection-close path, never emits `Content-Encoding: identity`, and leaves the webserver as the **sole** authority on compression. The webserver then sends a single, correct `Content-Encoding: gzip`, and the browser inflates and renders normally.
|
||||
|
||||
## Prevention
|
||||
|
||||
- On **non-FastCGI PHP hosts with server-side gzip** (Apache `mod_deflate`, LiteSpeed), set `debugger.shutdown.close_connection: false` for that environment. Deliver it as a **per-environment override**, never by editing the committed `system.yaml` (which would silently change dev behavior too).
|
||||
- **Reproduce compression bugs the way a browser sees them.** Always test with `curl -H "Accept-Encoding: gzip" -D -` and/or a headless-browser screenshot. A plain `curl` negotiates no compression and silently masks encoding bugs.
|
||||
- **Health check:** dynamic pages must return **exactly one** `Content-Encoding` header. Two of them (`identity` + `gzip`) is the unambiguous signature of this bug. Add this assertion to any smoke test.
|
||||
- **Know the trigger.** This can stay completely hidden in development mode and only appear once a site is switched to production mode (`twig.debug: false`), which activates Grav's full shutdown/output path. Re-run the browser-style compression check as part of any production cutover.
|
||||
|
||||
## Related
|
||||
|
||||
- **Grav per-environment override mechanism** — `deploy/env/prod/system.yaml` applied via `make remote-apply-env-prod`, described in `CLAUDE.md` §1 ("Production mode — per-environment override"). The pattern that lets a prod-only setting like `debugger.shutdown.close_connection: false` ship without mutating the committed dev `system.yaml`.
|
||||
- **`docs/working/git-sync-notes.md`** — documents the `user/env/<hostname>/config/` override tree (where this fix physically lives on the server) and the caveat that config saved via Admin on the server stays server-only.
|
||||
- **`docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md`** — sibling from the same cutover: Admin2 login failed because a stale `GRAV_VERSION` installed an rc core and GPM wouldn't serve the `api` plugin. Different root cause, same deploy.
|
||||
- **`docs/solutions/test-failures/new-user-grants-api-not-admin-on-admin2.md`** — a sibling gotcha from the same 2026-07-04 Grav 2.0.4 production cutover (account permission provisioning); different root cause, same deploy.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: "Admin2 login fails silently because a stale GRAV_VERSION installs an rc core below the api plugin's version floor"
|
||||
date: 2026-07-04
|
||||
category: docs/solutions/integration-issues
|
||||
module: "grav / production deploy / plugin install"
|
||||
problem_type: integration_issue
|
||||
component: authentication
|
||||
severity: high
|
||||
symptoms:
|
||||
- "Admin2 login at /admin silently fails: button disables then re-enables, no visible error, nothing written to grav.log"
|
||||
- "admin2 SPA background login POST to /api/... returns 404"
|
||||
- "/api/v1/pages returns 404 on prod but 401 locally (api plugin route not registered)"
|
||||
- "user/plugins/api directory does not exist on prod (plugin never installed)"
|
||||
- "gpm install api reports 'These packages were not found on Grav: api' even after gpm index -f"
|
||||
root_cause: config_error
|
||||
resolution_type: environment_setup
|
||||
related_components:
|
||||
- "gpm"
|
||||
- "admin2 plugin"
|
||||
- "api plugin"
|
||||
- "scripts/server-install.sh"
|
||||
- "Makefile remote targets"
|
||||
- ".env.prod"
|
||||
tags:
|
||||
- grav
|
||||
- gpm
|
||||
- admin2
|
||||
- api-plugin
|
||||
- plugin-dependency
|
||||
- version-compatibility
|
||||
- production-deploy
|
||||
- env-config
|
||||
---
|
||||
|
||||
# Admin2 login fails silently because a stale GRAV_VERSION installs an rc core below the api plugin's version floor
|
||||
|
||||
## Problem
|
||||
|
||||
On a fresh Grav production install, Admin2 login fails silently because the `api` plugin — which Admin2 authenticates through — never installed. GPM refused to serve it: a stale `GRAV_VERSION` in `.env.prod` had installed Grav `2.0.0-rc.10`, and the `api` plugin requires Grav core `>=2.0.4`. GPM filters offered packages by the installed core version, so on an rc.10 core the `api` plugin was excluded from results entirely and reported as "not found." Admin2 was present (and depends on `api`), but its login POST hit an `/api/...` route that was never registered, so authentication silently 404'd before it ever reached Grav's auth layer.
|
||||
|
||||
## Symptoms
|
||||
|
||||
- Admin login at `/admin` silently fails: the login button disables briefly, re-enables, and shows no error. **Nothing appears in `logs/grav.log`** — a wrong password *would* log a failed-attempt warning, so its absence means auth was never reached.
|
||||
- The Admin2 SPA's background login request (to an `/api/...` endpoint) returns **HTTP 404** with `content-type: application/json`.
|
||||
- `GET /api/v1/pages` returns **404** on prod, but **401 Unauthorized** on the working local install — i.e. the api route isn't registered on prod at all.
|
||||
- `ls user/plugins/api` on the server: **No such file or directory** — the plugin was never installed, even though `admin2` (which depends on it) was.
|
||||
- `php bin/gpm install ... api -y` → `"These packages were not found on Grav: api"`, even after `php bin/gpm index -f`.
|
||||
|
||||
## What Didn't Work
|
||||
|
||||
- **Committing/deploying the api plugin config** (`enabled` / `route` / `session_enabled`, moved from the untracked `user/plugins/api/api.yaml` into the tracked `user/config/plugins/api.yaml`). This was a real, necessary fix for a *different* latent problem, but it did not fix login: you cannot configure a plugin that isn't installed. Still 404.
|
||||
- **Forcing a GPM index refresh** (`php bin/gpm index -f`). No effect. "Package not found" here is not a stale-index problem — GPM filters the packages it offers by the installed Grav **core** version, and rc.10 is below the api plugin's `>=2.0.4` requirement, so `api` is excluded from results entirely.
|
||||
- **Assuming "same channel = same availability."** Local (Grav 2.0.4, `stable` channel) found `api` via `gpm info api`; prod (also `stable`) reported it "not found." The channel was identical — the difference was the Grav **core** version, which silently filtered the plugin out.
|
||||
|
||||
## Solution
|
||||
|
||||
The real cause is that prod was running the wrong Grav core. `scripts/server-install.sh` downloads `grav-admin-v${GRAV_VERSION}.zip`, and `.env.prod` still carried the stale pre-upgrade `GRAV_VERSION=2.0.0-rc.10`.
|
||||
|
||||
1. Upgrade the Grav core in place to stable (rc.10 → 2.0.7):
|
||||
```bash
|
||||
make remote-upgrade-grav-prod # php bin/gpm self-upgrade -y && php bin/grav cache
|
||||
```
|
||||
2. Install the plugins now that a compatible core is present (the api plugin resolves):
|
||||
```bash
|
||||
make remote-install-plugins-prod # php bin/gpm index -f && php bin/gpm install <plugins.txt> -y
|
||||
# => "Preparing to install API [v1.0.8] ... Success!"
|
||||
```
|
||||
3. Clear cache, then verify the api route is live and login works:
|
||||
```bash
|
||||
make remote-clean-prod
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://site/api/v1/pages
|
||||
# 401 (was 404) => plugin installed + routed
|
||||
```
|
||||
4. **Prevent recurrence:** update `.env.prod` to `GRAV_VERSION=2.0.4` so a future *fresh* install doesn't reinstall rc.10 (self-upgrade fixed the running server, not the env file). Keep the api plugin's functional config in the tracked `user/config/plugins/api.yaml` so it deploys on a clean clone.
|
||||
|
||||
## Why This Works
|
||||
|
||||
GPM (Grav Package Manager) only offers a plugin version whose declared Grav requirement is satisfied by the **installed core**. The `api` plugin requires Grav `>=2.0.4`; on a `2.0.0-rc.10` core there is no compatible version, so GPM reports the package as "not found" rather than a version conflict. Admin2 declares `api` as a hard dependency and performs all authentication over the api plugin's `/api/v1` JWT endpoints, so with `api` absent the login POST hits a route that doesn't exist (404) and never reaches Grav's auth layer — hence the silent failure with no `grav.log` entry. Upgrading the core to a stable `>=2.0.4` build makes GPM offer `api` again; installing it registers `/api/v1`, and Admin2's login flow succeeds.
|
||||
|
||||
## Prevention
|
||||
|
||||
- **Keep `.env.<env>` `GRAV_VERSION` current.** It is the version a *fresh* `make remote-install-<env>` bakes in; a stale value silently installs an old core. After any core upgrade, bump the env file too — self-upgrade only moves the running server. Note this is a *third* version-authority surface alongside `user/config/system.yaml` `gpm.releases` (channel) and `plugins.txt` — they must stay in sync.
|
||||
- **When GPM says "package not found" for a package you know is on your channel, check the target's Grav core version first** (`php bin/grav --version` on the server, or `make remote-diag-<env>`). GPM filters by core compatibility; "not found" often means "no version compatible with your core," not "missing from the index." `gpm index -f` will not help.
|
||||
- **Don't trust a top-level install "Success" to mean dependencies installed.** A fresh install can leave a plugin's declared dependency unsatisfied (here `admin2` installed but its `api` dependency didn't). Verify with `ls user/plugins/<dependency>`.
|
||||
- **Know the Admin2 ⇄ api coupling.** Admin2 authenticates via the api plugin's `/api/v1` endpoints; a missing or unrouted api plugin makes admin login fail *silently* (login POST 404s, nothing logged). A quick `curl /api/v1/pages` expecting `401` (not `404`) is a good post-deploy smoke check.
|
||||
|
||||
## Related
|
||||
|
||||
This is one of three independent gotchas from the same **2026-07-04 Grav 2.0.4 production cutover**:
|
||||
|
||||
- `docs/solutions/integration-issues/grav-double-content-encoding-garbage-page.md` — sibling: garbage-rendered pages from a double `Content-Encoding` header on a non-FastCGI host. Different root cause (HTTP compression), same deploy.
|
||||
- `docs/solutions/test-failures/new-user-grants-api-not-admin-on-admin2.md` — sibling: an authenticated account is denied an admin-gated page because `login new-user` auto-detect granted `api.*` but not `admin.*`. Different root cause (permission provisioning), same admin2/api area.
|
||||
- `docs/working/plans/2026-07-04-grav-2.0.4-upgrade.md` — the upgrade plan whose Global Constraints spell out the GPM version floors (`grav >=2.0.4`, `api >=1.0.6`) that cause the "package not found" on an rc core.
|
||||
- `docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md` — the *other* latent problem from this same investigation: the api plugin's functional config (`enabled` / `route` / `session_enabled`) must live in the tracked `user/config/plugins/api.yaml` to deploy at all. Necessary but not sufficient here (the plugin must be installed first), but a durable convention in its own right.
|
||||
@@ -78,5 +78,6 @@ This site runs **Admin2 only** (the classic `admin` plugin is disabled), so auto
|
||||
|
||||
## Related Issues
|
||||
- `docs/working/plans/2026-07-04-grav-2.0.4-upgrade.md` — the self-contained test-account infrastructure shipped alongside the Grav 2.0.4 upgrade.
|
||||
- Sibling gotchas from the same 2026-07-04 Grav 2.0.4 production cutover (all surface around admin2/api but with distinct root causes): `docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md` (stale `GRAV_VERSION` → rc core → GPM won't serve the `api` plugin → login 404s) and `docs/solutions/integration-issues/grav-double-content-encoding-garbage-page.md` (double `Content-Encoding` header → garbage page).
|
||||
- `docs/solutions/architecture-patterns/dual-repo-submodule-workflow.md` — accounts live in the `user/` repo; the `testrunner` account is gitignored so it never reaches production.
|
||||
- GPX manager auth model (`access.admin.login: true` frontmatter + Login plugin) — see the project's GPX manager notes.
|
||||
|
||||
@@ -6,11 +6,12 @@ Ideas and improvements not yet planned or scheduled.
|
||||
|
||||
## Production — remaining items
|
||||
|
||||
- [ ] Set `twig.cache: true` in `user/config/system.yaml` on the server (do not commit — breaks local dev)
|
||||
- [x] Prod Twig prod-mode (`cache: true`, `debug/auto_reload: false`) — applied as a per-environment override via `make remote-apply-env-prod` (source: `deploy/env/prod/system.yaml`); committed `system.yaml` stays dev
|
||||
- [ ] Smoke test: submit one post via `/post`, confirm entry appears in dailies immediately (verifies cache-on-save with twig cache on)
|
||||
- [ ] Confirm `/post` requires login — unauthenticated visitors must not be able to post
|
||||
- [x] Confirm `/post` requires login — verified on prod (returns the login gate to unauthenticated visitors)
|
||||
- [ ] Register at carto.com and review terms for production traffic
|
||||
- [ ] Japan & Korea 2026 trip page: set `date_start`, add `cover_image`, upload GPX route file(s)
|
||||
- [ ] Update `GRAV_VERSION` in `.env.prod` to `2.0.4` (was stale `2.0.0-rc.10`; fixed on the running server via self-upgrade, but a future fresh install would repeat the RC)
|
||||
- [ ] git-sync on prod: install, add encrypted token, apply `folders:` fix, enable after first content round-trip
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
# Git Sync Plugin — Setup Notes
|
||||
|
||||
## Folders YAML bug
|
||||
## ⚠️ Config lives in the ENVIRONMENT tree, not `user/config/` (IMPORTANT)
|
||||
|
||||
The plugin UI always saves the folders field as a single comma-string:
|
||||
Prod has a per-environment override directory `user/env/<hostname>/config/`
|
||||
(created for Twig prod-mode — see CLAUDE.md §1). **A crucial Grav side effect:
|
||||
once that env dir exists, the Admin panel saves ALL config changes — system and
|
||||
plugin — into the active environment's config tree**, not `user/config/`.
|
||||
|
||||
```yaml
|
||||
folders:
|
||||
- 'pages,config,themes'
|
||||
So on prod, `git-sync.yaml` (configured via Admin) lives at:
|
||||
|
||||
```
|
||||
user/env/intotheeast.com/config/plugins/git-sync.yaml ← here (env tree)
|
||||
user/config/plugins/git-sync.yaml ← NOT here
|
||||
```
|
||||
|
||||
But the plugin code iterates the array expecting separate items. This causes `git status pages,config,themes` to be passed as a single path, so git sees nothing to commit and sync silently does nothing.
|
||||
Why this matters:
|
||||
|
||||
**Fix:** Edit `user/config/plugins/git-sync.yaml` directly:
|
||||
- **Server-only, not synced, not committed.** `user/env/` is outside the
|
||||
content repo's tracked folders (`pages`/`config`/`accounts`/`themes`) and is
|
||||
not one of git-sync's synced folders (`pages`/`config`/`themes`). So config
|
||||
saved via Admin *on the server* never reaches Gitea or local. This is ideal
|
||||
for the git-sync token (it stays server-only) but means **prod Admin config
|
||||
edits silently diverge** — author durable config in the repo, not prod Admin.
|
||||
- **Look in both places.** When inspecting/toggling server config, check
|
||||
`user/config/plugins/<name>.yaml` **and**
|
||||
`user/env/<host>/config/plugins/<name>.yaml` (env wins).
|
||||
- **Tooling is env-path-aware.** `scripts/git-sync-toggle.sh` takes a `WEBROOT`
|
||||
and searches `user/env/*/config/plugins/git-sync.yaml` first, then
|
||||
`user/config/plugins/git-sync.yaml`. `make remote-git-sync-disable/enable-<env>`
|
||||
and `make remote-diag-<env>` use it.
|
||||
|
||||
## Folders format
|
||||
|
||||
Older plugin versions' UI saved the `folders` field as a single comma-string
|
||||
(`- 'pages,config,themes'`), which the plugin iterated as one path, so sync
|
||||
silently did nothing. **git-sync v3.4.4 (installed on prod 2026-07-04) saves it
|
||||
correctly** as separate list items:
|
||||
|
||||
```yaml
|
||||
folders:
|
||||
@@ -20,8 +44,33 @@ folders:
|
||||
- themes
|
||||
```
|
||||
|
||||
Never use the Admin UI to change folders — it will rewrite the broken format.
|
||||
If you see the comma-string form on an older version, fix it by editing
|
||||
`git-sync.yaml` directly (at whichever path it lives — see above); do not
|
||||
re-save folders via the Admin UI on the buggy version.
|
||||
|
||||
## Files to gitignore
|
||||
## Per-install / secret files — must be gitignored (never synced)
|
||||
|
||||
`user/config/plugins/git-sync.yaml` contains an encrypted token and is server-specific. `user/config/security.yaml` contains Grav nonces/salts, also server-specific. Both are in `.gitignore` and must never be committed.
|
||||
git-sync syncs the `config/` folder, so any per-install or secret file tracked
|
||||
there would get pushed to Gitea and pollute every environment. Keep these out
|
||||
of the content repo (all in `user/.gitignore`):
|
||||
|
||||
| File | Why |
|
||||
|---|---|
|
||||
| `config/plugins/git-sync.yaml` | encrypted token; server-specific (also lives at env path on prod) |
|
||||
| `config/plugins/api-private.php` | API JWT secret |
|
||||
| `config/security.yaml` | Grav nonces/salts (legacy location) |
|
||||
| `config/versions.yaml` | per-install Grav schema-migration state — differs per env (dev 2.0.4, prod 2.0.7); Grav regenerates it. Untracked 2026-07-04. |
|
||||
| `config/security-private.php` | **TODO:** committed salt secret; should be gitignored like `api-private.php` (deferred — untracking resets server sessions) |
|
||||
|
||||
## git-sync config summary (prod, 2026-07-04)
|
||||
|
||||
- `repository: https://git.gorinskat.nl/m038/intotheeast-com-content.git`,
|
||||
`branch: main`, HTTPS + token auth (SSH is Tailscale-only).
|
||||
- `sync.direction: both`, `on_save/on_delete/on_media: true` → prod Admin edits
|
||||
and `/post` push to Gitea; content-repo pushes pull to prod **via webhook**
|
||||
(`/_git-sync`). The webhook is configured in Gitea repo settings (same secret
|
||||
as the test instance).
|
||||
- **Before enabling on a fresh server**, reset the synced folders clean
|
||||
(`make remote-fetch-content-<env>`) so no install-time drift (e.g. a stale
|
||||
`versions.yaml`) gets pushed on the first sync. Toggle with
|
||||
`make remote-git-sync-disable/enable-<env>`.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Status:** ✅ Complete (2026-07-04) — Phase 1 (local) and Phase 2 (remote test env) both validated and shipped; git-sync re-enabled on test. Phase 3 (prod) is documentation-only per design. See "Known issue" below re: Form 9.1.10 filepond.
|
||||
**Status:** ✅ Complete — Phase 1 (local), Phase 2 (test env), and **Phase 3 (production) all executed and shipped**. Phase 3 was run for real on 2026-07-05 (see the Phase 3 section for the execution outcome and the three `docs/solutions/` gotchas it produced). git-sync re-enabled on test and set up on prod. See "Known issue" below re: Form 9.1.10 filepond.
|
||||
|
||||
**Goal:** Upgrade Grav core `2.0.0-rc.10` → `2.0.4` stable and promote `admin2`/`api`/`flex-objects` to GPM management, validated on local then the remote test env (prod is documented-only).
|
||||
|
||||
@@ -539,11 +539,32 @@ Content, config, and accounts are in git, so no data restore is required — but
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Production (fresh install, NOT executed)
|
||||
## Phase 3 — Production (fresh install) — EXECUTED 2026-07-05
|
||||
|
||||
Production is empty, so this is a **fresh install**, not an upgrade — and it is **documentation only**. Do not run it as part of this plan.
|
||||
> **Execution outcome (2026-07-05):** the fresh prod install was run for real
|
||||
> (`make remote-install-prod`) and the site is live at `https://intotheeast.com`.
|
||||
> The runbook below was followed, but three non-obvious gotchas surfaced — each
|
||||
> now has its own learning in `docs/solutions/`:
|
||||
> - **Stale `.env.prod GRAV_VERSION`** installed Grav rc.10, so GPM wouldn't
|
||||
> serve the `api` plugin (needs ≥2.0.4) → Admin2 login 404'd silently. Fixed
|
||||
> via `make remote-upgrade-grav-prod` (→ 2.0.7) + reinstall. See
|
||||
> `docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md`.
|
||||
> **TODO: bump `.env.prod GRAV_VERSION` to `2.0.4`** so a future fresh install
|
||||
> doesn't repeat the RC.
|
||||
> - **Double `Content-Encoding` header** (non-FastCGI host + mod_deflate)
|
||||
> rendered a garbage page once prod switched to `twig.debug: false`. Fixed via
|
||||
> `debugger.shutdown.close_connection: false` in the prod env override. See
|
||||
> `docs/solutions/integration-issues/grav-double-content-encoding-garbage-page.md`.
|
||||
> - **Plugin config stranded in the untracked `user/plugins/`** doesn't deploy.
|
||||
> See `docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md`.
|
||||
>
|
||||
> Twig prod-mode is applied as a per-environment override (`deploy/env/prod/system.yaml`
|
||||
> via `make remote-apply-env-prod`); git-sync is installed, configured, and enabled
|
||||
> (see `docs/working/git-sync-notes.md`). Remaining minor follow-ups: gitignore
|
||||
> `config/security-private.php` (committed salt); optional `popularity.salt` strip.
|
||||
|
||||
When prod is provisioned:
|
||||
The original runbook (production was empty, so this was a **fresh install**, not
|
||||
an upgrade):
|
||||
|
||||
1. **Provision creds:** copy the REMOTE section of `.env.example` into `.env.prod` with production values (never commit it). Run `make remote-env-setup-prod`.
|
||||
2. **Fresh install at 2.0.4:** `make remote-install-prod` with `GRAV_VERSION=2.0.4` in `.env.prod`. `scripts/server-install.sh` installs core, then all of `plugins.txt` — `admin2`/`api`/`flex-objects` now install purely via `php bin/gpm install` (no zip-stash; that special-casing was removed in Task 4). The `gpm.releases: stable` channel arrives with the `user/` content clone.
|
||||
|
||||
Reference in New Issue
Block a user