# Grav 1.7 → 2.0 Migration Simulation — Handover

**Status as of 2026-08-25:** Investigation in progress, not concluded. Nothing has
touched production. This document is for a future session to pick up where this
one left off.

## What this is

The user asked (advisory, then "let's look into it — zero impact on production")
what the best strategy is to upgrade local Grav from 1.7.49.5 to the latest 2.x,
and to try installing a 2.x admin UI. Per explicit instruction, instead of
upgrading in place, a **second, fully isolated Docker container** running Grav
2.0.21 was stood up next to the existing 1.7 dev container, using a **copy** of
the real site content, to see what breaks before committing to a real migration.

**Nothing about the live dev container, the `user/` git content repo, or
production has been changed.** All `make remote-*` targets remain untouched.

## Current environment state

| | Container | Image | Port | Data | State |
|---|---|---|---|---|---|
| existing dev | `natascha_grav` | `lscr.io/linuxserver/grav:1.7.49.5-ls244` | 8080 | `./user` (live mount) | **Up**, compose project `site-ai` (`docker-compose.yml`) |
| migration sandbox | `natascha_grav2_test` | `lscr.io/linuxserver/grav:2.0.21-ls271` | 8081 | `./migration-test/user` (copy, no `.git`) | **Stopped** (exited cleanly, `restart: "no"` so it doesn't auto-start) |

Docker runs via **Colima** (no Docker Desktop). Colima must be running before any
of this works: check with `colima status`, start with `colima start`.

### To resume the sandbox container

```bash
cd /Users/mischa/Nextcloud/Projects/Natascha/natascha-rieter.nl/site-ai
docker compose -f docker-compose.grav2.yml -p natascha-grav2-test up -d
```

Then it's reachable at **http://localhost:8081** (front-end) and
**http://localhost:8081/admin** (admin login).

### To tear it down completely (once done investigating)

```bash
cd /Users/mischa/Nextcloud/Projects/Natascha/natascha-rieter.nl/site-ai
docker compose -f docker-compose.grav2.yml -p natascha-grav2-test down
rm -rf migration-test
```

`docker-compose.grav2.yml` itself can also be deleted once the simulation is
finished and a real decision is made — it was created purely as a scratch
sandbox definition and is separate from the production-mirroring
`docker-compose.yml`.

## Files created this investigation (all under `site-ai/`)

- `docker-compose.grav2.yml` — the sandbox compose file (image `2.0.21-ls271`,
  port 8081, mounts `./migration-test/user`, `restart: "no"`).
- `migration-test/user/` — `rsync -a --exclude='.git'` copy of the live `user/`
  folder (~50M, `.git` excluded on purpose since `user/` is itself a separate
  git content-sync repo — copying `.git` would have created a confusing nested
  repo). **This copy is stale** as of whenever it was taken; re-sync before
  further testing if `user/` content has changed since:
  ```bash
  rsync -a --exclude='.git' ./user/ ./migration-test/user/
  ```
- `~/.docker/cli-plugins/docker-compose` (outside repo, machine-level) —
  symlink to the Homebrew `docker-compose` binary, registering it as the
  `docker compose` CLI plugin. This was a durable fix for `make start` (which
  calls `docker compose up -d` and previously failed with
  `unknown shorthand flag: 'd'` because no compose plugin was registered).
  Chosen explicitly by the user over editing the Makefile. Not something a
  future session needs to redo, but worth knowing about if `make start` ever
  breaks again on a different machine.

## Findings so far

1. **Grav 2.0 is not an in-place upgrade.** Official docs say so explicitly.
   Also, in this project's setup, Grav core lives inside the Docker image, not
   in the `./user` volume mount — so an in-place `gpm selfupgrade` wouldn't
   persist across container recreation anyway. The real upgrade path is an
   **image tag swap** (`1.7.49.5-ls244` → `2.0.21-ls271` or whatever is current)
   combined with a content/plugin compatibility pass.

2. **Front-end renders correctly** on Grav 2.0.21 core against the unmodified
   1.7-era `user/` content and theme — verified via curl against
   `http://localhost:8081/`, including the `langswitcher` redirect (`/` → `/nl`).

3. **Classic `admin` plugin (v1.10.55) login page renders correctly** on Grav
   2.0.21 — confirmed via `curl http://localhost:8081/admin`: full CSS/JS asset
   loading, Dutch-localized labels, working nonce, no visible PHP errors. This
   is a **better result than official Grav 2.0 docs suggest** — the docs imply
   classic admin is incompatible with 2.0 and that migration should go through
   `admin2` (the alpha SvelteKit rewrite) instead.

4. **No errors, exceptions, fatals, deprecation notices, or warnings** appeared
   in `docker logs natascha_grav2_test` for either the `/` or `/admin` requests.

5. `bin/grav`/`bin/gpm` CLI layout is the same path as 1.7
   (`/app/www/public/bin/`, must `cd` there first — `FATAL: Must be run from
   ROOT directory of Grav!` otherwise), but the **CLI command set differs**
   between 1.7 and 2.0 — e.g. `php bin/grav plugins` doesn't exist in 2.0
   (confirm via `php bin/grav list`).

6. Grav's built-in `page-system-validator` tool (meant for before/after
   render-diffing across an upgrade) **cannot be used from the CLI on this
   setup** — it throws a PHP fatal
   (`Call to a member function param() on null in .../admin/admin.php:354`)
   because the `admin` plugin's `onPageInitialized()` handler expects an HTTP
   request object that doesn't exist under CLI invocation. This happened on the
   **old 1.7 container**, so it's an artifact of the admin plugin's CLI
   handling generally, not a 2.0-specific regression. Don't waste time trying
   to make this tool work as a validation method — it needs a different
   approach (see "Next steps").

## What has NOT been tested yet

- **The actual admin dashboard post-login** — page editing, media management,
  anything behind auth. This requires real admin credentials, which were
  deliberately not obtained/guessed. If the user provides a throwaway
  password (or creates one via `bin/grav user create` in the sandbox
  container, which is safe since it's isolated test data), this is the
  obvious next concrete step.
- **Individual plugin compatibility**, specifically the ones most likely to
  break because they hook deep into core APIs that changed in 2.0:
  - `git-sync`
  - `flex-objects`
  - `admin-media-move`, `admin-media-replace`, `admin-media-actions`
  - `automagic-images`
  - `social-meta-tags`
  - `langswitcher` (front-end redirect already confirmed working, but not
    deeper admin-side behavior)
  - `draft-preview`

  Grav 2.0's plugin blueprints can declare a `compatibility:` key; check each
  plugin's `blueprints.yaml` under `migration-test/user/plugins/<name>/` for
  this key, and/or check each plugin's upstream repo/changelog for 2.0
  support statements. `bin/gpm info <plugin>` may also surface something once
  run from inside the sandbox container (untested).

- **`admin2`** (the alpha SvelteKit/Vite/Tailwind admin rewrite) has not been
  installed or tested in the sandbox at all. It's not GPM-installable — it
  requires manually git-cloning `grav-plugin-api` and `grav-plugin-admin2`
  into `user/plugins/`. Given it's explicitly alpha/non-production-ready
  upstream, and classic admin already renders fine on 2.0 in this test, it's
  worth explicitly asking the user whether they still want this installed
  before spending time on it, rather than assuming yes from the original
  advisory question.

## Suggested next steps (pick up here)

1. Bring the sandbox back up (command above), re-sync `migration-test/user`
   from `./user` if content has changed since.
2. Either:
   - Ask the user for a throwaway admin password to log in and click through
     the real dashboard/page-editing/media flows, **or**
   - Create a fresh test admin user inside the sandbox container only
     (`docker exec -it natascha_grav2_test sh -c "cd /app/www/public && php bin/grav user create"`)
     — safe since it only affects the isolated `migration-test/user` copy.
3. Go through the plugin list above one by one: check blueprint
   `compatibility:` declarations, and exercise each plugin's actual feature
   (git-sync a commit, upload/replace media, edit a flex-objects-backed page)
   inside the sandbox to see what actually breaks vs. what merely lacks a
   compatibility flag.
4. Once a real compatibility picture exists, come back to the original
   question — decide whether the production upgrade path should be:
   (a) image-tag swap + fix whatever plugins broke, or
   (b) use Grav's official `migrate-grav` GPM plugin (side-by-side staged
   migration with Reset/Restart/Promote controls) instead of doing it by hand.
   This official tool was identified during research but deliberately not
   used yet, since the user asked specifically for a separate-container
   simulation first.
5. Revisit whether `admin2` is still wanted given classic admin's
   better-than-expected 2.0 compatibility.
