FlatPanel
Browser-based editor for the template files your site is built from — no SSH, no FTP, no deploy cycle for copy tweaks.
What it is
FlatPanel is a CakePHP plugin shipped with Flat Cake that mounts a tiny
admin at /panel (configurable). It lets you edit any PHP
template under templates/Pages/, templates/layout/
or templates/element/ straight from the browser using the
Monaco editor, with PHP syntax validation before every save.
The plugin is fully isolated from the public site: its own
AppController, its own layout, its own stylesheet, its
own JS. A full visual reset of the public site cannot break the panel.
- Plugin path:
plugins/FlatPanel/ - Mount point:
/panel(override viaFlatPanel.prefix) - Config file:
config/panel.php(gitignored) - Tests:
vendor/bin/phpunit --testsuite flat-panel
Setup
Copy the example config, generate a password hash using the built-in command, paste the line it prints, and you're in.
cp config/panel.example.php config/panel.php
bin/cake hash_password admin
# → paste the printed line into config/panel.php under FlatPanel.users
The command prompts for the password interactively (echo is hidden on
real terminals) so the plaintext never reaches your shell history.
Run it once per user — FlatPanel.users accepts any number
of entries.
Default URL prefix is /panel. Override via
FLAT_PANEL_PREFIX in config/.env or edit
the prefix key directly.
Writable directories
The web server user needs write access to:
templates/— the files you edittmp/panel_backups/— timestamped copies made before every changetmp/panel_auth_attempts/— rate-limit bookkeeping
Authentication
Sessions are cookie-based with a 2-hour inactivity timeout —
every request refreshes the timer. Credentials are verified against
password_verify() on bcrypt hashes kept in
config/panel.php.
Login attempts are rate-limited per client IP via a file-based
counter in tmp/panel_auth_attempts/. After repeated
failures the IP is blocked for 15 minutes, regardless of the
username it tried.
Multiple users
Add one entry per person under FlatPanel.users:
'users' => [
'admin' => '$2y$12$...',
'editor' => '$2y$12$...',
'chief' => '$2y$12$...',
],
All users share the same permissions. If you need role separation, it's one extra check away.
Editor
Editing opens the file in Monaco (VS Code's editor core) loaded from a public CDN. If the CDN is unreachable, the panel silently falls back to a plain textarea so you can still make changes.
- Save: Ctrl/Cmd+S, or the Save button.
- Preview: for
templates/Pages/*files two controls live in the top header. The eye button toggles a side-by-side live preview — the editor slides to 50% while the rendered page slides in from the right, and re-loads automatically after every save. The Preview ↗ button opens the same page in a new tab. - Lint before save: every save runs
php -lon a temporary copy; syntax errors are rejected and the live file is untouched. If PHP CLI is unavailable on the host a banner is shown and saves proceed without the check. - Theme sync: Monaco follows the panel's light/dark theme, re-themed live when you switch.
Whitelist
Only .php files under these three directories are accepted:
templates/Pages/
templates/element/
templates/layout/
Path validation resolves symlinks and rejects anything that doesn't fall inside one of these trees. There's no way to address files outside the whitelist via the UI or the API.
Create, rename, delete
The dashboard splits templates into Layouts, Elements, and Pages. Each box has a small "+ New X" button in its header that opens the create form for that kind.
- Slug rules: lowercase letters, digits, hyphens and underscores. No path components.
- Position (optional): a number that pins the file at that order among siblings. Stored as an
NN_prefix on the filename; stripped from the label and URL so20_about.phpstill renders as "About" at/about. - Boilerplate: new pages start with a minimal
<section>skeleton. - Rename: creates a backup of the original first; the URL changes with the filename. The modal has the same Position field as the create form — it auto-fills with the current
NN_prefix (if any), and clearing it drops the pin back to alphabetical ordering. - Delete: creates a backup first, then unlinks. Entries listed under
FlatPanel.protectedFiles(by defaulthome.php) cannot be deleted.
Previewing file metadata
Hovering a file row reveals its size and last-modified time without visual clutter in the default state. The filename stays as the primary affordance.
Backups & restore
Every save, rename and delete drops a timestamped copy of the affected
file into tmp/panel_backups/, mirroring the original path.
Microsecond precision keeps rapid changes (e.g. restore-over-live) from
colliding.
tmp/panel_backups/
templates/Pages/
home.php.2026-04-22_14-35-02_123456.php
home.php.2026-04-22_09-11-48_087221.php
templates/element/
header.php.2026-04-21_16-02-17_401003.php
Trash view
The Backups button on the dashboard opens
/panel/files/trash. Each group is a file with a
Live or Deleted badge, showing every
stored version with its time and size. Two actions per version:
- Preview — opens Monaco in read-only mode so you can review the contents before committing to anything.
- Restore — copies the version back to its original location. If the live file still exists it gets a fresh backup first, so restoration itself is reversible.
Cleanup
On every write, backups older than 30 days for that specific file are
pruned automatically. Nothing else deletes backups — if you need more
retention, bump the cutoff in FileManager::cleanOldBackups().
Appearance
The cog in the top-right opens a small popover with two settings:
theme mode and accent colour. Choices persist in cookies
(fp_theme, fp_accent) and are rendered
server-side into the <html> tag, so the panel never
flashes the wrong colours on the first paint.
- Theme: Auto / Light / Dark. Auto follows the OS via
prefers-color-schemeand re-syncs Monaco when the OS pref changes. - Accent: six presets. The accent drives the logo
layers, buttons, focus ring, status messages — one variable
(
--fp-accent) powers the whole palette.
The panel's stylesheet uses --fp-* tokens exclusively —
no --cb-*, no shared custom properties with the public
site. If you strip or redesign the frontend, the panel keeps working
with its own design system intact.
Command line
hash_password
Generates a bcrypt hash for FlatPanel.users. Always asks
for the password interactively — the secret never lives on the command
line, so it stays out of shell history. Requires confirmation.
bin/cake hash_password editor
# → Password:
# → Confirm password:
# → 'editor' => '$2y$12$...'
Omit the username to be prompted for it too. Password echo is hidden
via stty -echo on real terminals; on non-TTY environments
(CI, piped stdin) the command falls back to a plain prompt with a
visible notice.
Security notes
- Path traversal: every file operation validates the
resolved
realpathagainst the whitelist. Symlinks,..segments and absolute paths all get the same treatment. - Restore scope: restore paths are validated twice —
the source must sit inside
tmp/panel_backups/, and the derived target must land in a whitelistedtemplates/subdir and end in.php. - CSRF: Cake's CSRF middleware runs with
httponly=true. Forms use the auto-injected_csrfTokenfield; the editor's save XHR reads the token from a server-rendered constant instead of the cookie. - Protected files:
home.phpis listed underFlatPanel.protectedFilesby default and cannot be deleted from the UI. Extend the list to cover anything else that shouldn't disappear. - Rate limit: 5 failed logins per IP per 15 minutes. The successful login counter resets on success.
Gallery
The second module of the panel lives at
/panel/gallery. It manages image sources under
webroot/img/gallery/<folder>/ — the same
directory that cell('Gallery', ['kuchnia'])
reads from on the public site. The panel only writes source
files; thumbnails are generated lazily by the existing
/thumb/<size>/ pipeline.
Browse folders
Click Gallery in the top-bar. Each folder is shown as a card with a cover thumbnail, image count, and total source size. Click a card to open the folder.
Create a folder
From /panel/gallery click
+ New folder. Folder names are lowercase
only — letters, digits, hyphens, underscores — and become
part of the URL when you reference the folder from a
template (cell('Gallery', ['my-slug'])).
Upload images
- Open a folder view.
- Drag images onto the dropzone, or use choose files.
- Files are validated server-side: extension
(
jpg, jpeg, png, webp, gifby default), MIME content sniff, per-file size cap (10 MB by default, seeFlatPanel.gallery.uploadMaxBytes), and per-batch count cap (50 by default). - If a file with the same name already exists, the upload
renames the incoming file to
name-2.jpg,name-3.jpg, …
Preview, rename, delete images
Hover over a thumbnail — three buttons appear: the
eye opens a full-size preview modal,
the pencil renames the file, and the
trash deletes it. Deletion is permanent
and also removes every generated thumbnail in the sibling
thumb/ directory.
For batch operations, tick the checkbox on the corner of each card. A bulk-action bar appears above the grid with a single Delete selected action.
Rename or delete a folder
Folder cards in the index also have hover buttons. Folder
rename and folder delete both open a confirmation modal
that runs a quick scan of your templates for references
to the slug — both the cell('Gallery', ['slug'])
call and any literal /gallery/slug/ URL.
Hits are listed for review before you confirm. The panel
does not block the rename or delete — your
operator judgment is the final say.
What the panel does not do
- No image editing (crop, rotate, resize).
- No trash/recycle bin for images — delete is final.
Templates have backups under
tmp/panel_backups/; images do not. - No multi-folder reorganisation or move-between-folders.
- No nested folders — galleries are flat, matching
GalleryCell's expected layout.
Self-hosting Monaco
Monaco is loaded from cdnjs.cloudflare.com by default.
If your CSP blocks external script origins, vendor the editor locally:
- Copy Monaco's
min/vs/bundle intoplugins/FlatPanel/webroot/monaco/vs/. - Update the two script URLs in
plugins/FlatPanel/templates/layout/panel.phpto/flat_panel/monaco/vs/loader.min.js(and match the AMDpaths.vsconfig above it).
If Monaco fails to load for any reason, the editor transparently
degrades to a plain <textarea> — you never get
stuck.