63 KiB
63 KiB
v1.0.0-rc.15
06/16/2026
-
- Added a dashboard security endpoint that hands the admin Dashboard a sentinel URL under
user/data, so it can detect from the browser whether the sensitiveuser/folders are downloadable over the web. - Added user preferences for keeping the Markdown editor toolbar pinned while scrolling and for setting a fixed editor height.
- Plugins can now contribute dashboard notifications via a new
onApiDashboardNotificationsevent, letting them raise a persistent, dismissible admin banner (grouped by location —top,dashboard,feed) that flows through the existing dismiss andreappear_afterhandling. - Plugin endpoints can now return a toast hint to control the message, type and duration of the notification the admin shows after a save, including errors that stay until dismissed (getgrav/grav-plugin-admin2#38).
- Added a dashboard security endpoint that hands the admin Dashboard a sentinel URL under
-
- Public API endpoints now recognize logged-in callers when credentials are provided, returning their richer permission-filtered responses instead of treating everyone as a guest.
- Plugins can now mark public routes as read-only by method, so browsing stays open while writes on the same paths still require login.
- Page responses now include the on-disk folder name, including any numeric ordering prefix, so admin tools can show and diagnose page ordering.
- File upload fields now honor their blueprint's
random_name,avoid_overwriting,accept, andfilesizesettings, matching the classic admin.
-
- Creating, listing, downloading, and deleting site backups now requires a dedicated backup permission (or API super user) instead of the broader system read/write access, because the backup archive includes account password hashes and configuration secrets.
- Page and account content saved through the API is now checked for cross-site scripting, closing a hole where an editor without full admin rights could store a script that later ran in other visitors' browsers.
- Blueprint fields that use relative dot-naming inside a section (such as
.optionA) now save their values again, restoring the nested-field behaviour from the classic admin (getgrav/grav#4120). - Pages on a template the current theme doesn't define now fall back to the default page form in the editor instead of showing a blank screen, matching the classic admin.
- Toggle and select options whose Yes/No labels were turned into booleans by strict YAML parsing now render as Yes/No again instead of a blank or "true" button (getgrav/grav-plugin-admin2#36).
- A caller restored via the login plugin's remember me cookie (left
authenticatedbut notauthorized) is now accepted by session authentication, so a remembered user who shows as signed in can use the API instead of being silently rejected on every write until a fresh login. Per-route permission checks still gate what they can actually do. - The page template selector no longer breaks with a "callable not found" error when a blueprint references the classic admin's page-types helper but the classic admin isn't active, falling back to the built-in page types instead (getgrav/grav-plugin-admin2#41).
- Custom fields provided by a theme are now reported with their provider type and included in the theme's own info, so the admin can load them from the correct route instead of always assuming a plugin (getgrav/grav-admin-next#3).
- Editing or deleting a specific translation with
?lang=now targets that language's file instead of silently overwriting or failing to find the default language (getgrav/grav-plugin-api#6).
v1.0.0-rc.14
06/09/2026
-
- The page editor now includes a Security tab for setting page access and permissions, matching the classic admin, with the permissions section limited to users who hold API super or configuration rights.
- Page Authors is now a searchable multiselect of the users who can edit pages, instead of free-text username entry.
- The users API can now filter accounts by permission or group, so the admin can list everyone who holds a given permission such as admin access.
- The user listing now includes each account's group memberships.
-
- A failed Grav core upgrade now reports the real reason and records it in the log, instead of a generic "Failed to upgrade Grav core" message.
- A Grav core upgrade blocked by a compatibility check now lists the packages responsible and can be retried with an explicit override, matching the command-line upgrader.
- Saving config, pages or accounts now validates the submitted fields against the blueprint, so a required field left empty or an invalid value is rejected instead of silently saved (getgrav/grav-plugin-admin2#30).
- Media upload handling is now shared, so other plugins such as Flex Objects can let you attach files to their own records.
-
- Saving a page no longer corrupts its frontmatter with stray internal keys, which previously accumulated on every save when editing in Expert mode (getgrav/grav-plugin-admin2#31).
- Saving a page through the API no longer fails when an admin-aware plugin posts a flash message from its save handler (#5).
- Pages under the home page now appear nested beneath it in the tree and columns views, instead of being listed at the top level (getgrav/grav-plugin-admin2#32).
v1.0.0-rc.13
06/04/2026
-
- Custom top-level configuration files now appear as their own tab in admin2, alongside System, Site, Media and Security, so the long-standing "add a custom YAML file" cookbook recipe works again.
- Configuration fields that override an inherited default now expose a one-click revert, with a "Reset overrides" action to clear them all at once, for both the base configuration and per-environment overlays.
-
- Saving configuration no longer fails with a "modified elsewhere" error on servers that compress responses with zstd, matching the gzip and brotli handling already in place (follow-up to getgrav/grav-plugin-admin2#28).
- Configuration now reads from the correct source behind a reverse proxy that forwards a different hostname than the server booted under, so the base "Default" view no longer shows another environment's overridden values.
- Configuration saved without an explicit environment now lands in the environment the site is actually running, even behind a reverse proxy, instead of silently writing to the base configuration.
v1.0.0-rc.12
06/03/2026
-
- Invite a user by email instead of creating the account yourself: pre-set their permissions and groups, send a time-limited invite link, and they choose their own username, name and password when they accept (they can never grant themselves more access than you set).
-
- Usernames containing periods (e.g.
john.doe) are now accepted and listed correctly, matching the characters admin classic has always allowed.
- Usernames containing periods (e.g.
-
- Saving a configuration twice in a row no longer fails with a "Configuration was modified elsewhere" error when you toggle an option whose neighbour is left at its default value. Fixes getgrav/grav-plugin-admin2#28.
v1.0.0-rc.11
05/29/2026
-
- Sidebar
authorizenow accepts an array of permissions (any-of semantics, matching admin-classic'snav-quick-tray.html.twigpattern), and the menubar and floating-widget APIs gained the sameauthorizefiltering. Plugins that register sidebar / menubar / widget items can now hide them from users who lack the relevant permission without needing to check inside their own event handler. Used across grav-plugin-git-sync, grav-plugin-license-manager, grav-plugin-algolia-pro, grav-plugin-cloudflare, grav-plugin-comments-pro, grav-plugin-image-optimize, grav-plugin-rsync, grav-plugin-seo-magic, grav-plugin-translation-service, grav-plugin-warm-cache, grav-plugin-ai-pro, and grav-plugin-ai-translate to fix getgrav/grav-plugin-admin2#23.
- Sidebar
-
- Hebrew and Arabic admin language responses are correctly marked right-to-left again, restoring the RTL admin shell that broke after the BCP-47 language code switch.
- Media files and folders whose names contain non-ASCII characters (e.g.
imäge1.png,Földer1) can be deleted again. Captured route params arrived percent-encoded because Grav's URL parser does not decode them, so the controller looked for a file that didn't exist. Route params are now rawurldecoded once in the dispatcher. Fixes getgrav/grav-plugin-api#3. - Media files whose extension matches a configured page type (
.txt,.md,.html, …) can be deleted again. Grav's URL router strips known page-type extensions before any plugin sees the route, so/media/notes.txtarrived as/media/notesand 404'd. The dispatcher now re-attaches the stripped extension before matching, fixing every plugin route at once. Fixes getgrav/grav-plugin-api#3. - Saving a page or configuration with YAML list values no longer grows the file with quoted
'0','1','2'keys mixed in alongside the original entries. Reported via getgrav/grav-theme-quark2#8.
v1.0.0-rc.10
05/26/2026
-
- New
GET /blueprint-filesendpoint browses any Grav stream (user://media,theme://images,account://, …),self@:scope token, or relative path underuser/, so file-picker blueprint fields can list arbitrary folders the way admin-classic always could. - New
?locate=/some/routeparameter onGET /pagesreturns the page-of-results that contains a given route in one round trip, so the admin can jump straight to a deep child of a long folder without walking the listing.
- New
-
- Listing a folder with more than 100 children no longer silently drops the rest — the per-request cap default is now 1000 (raise it further via
plugins.api.pagination.max_per_pageif you need to). Fixes getgrav/grav#4096.
- Listing a folder with more than 100 children no longer silently drops the rest — the per-request cap default is now 1000 (raise it further via
v1.0.0-rc.9
05/21/2026
-
- Page show, create, and update endpoints now enforce the new
security.twig_content.*gates. Requests that try to enable Twig on a page without permission, or to load a page that already has Twig enabled when the current user can't edit it, return a 403 with a stable reason code so the admin UI can render the right toast. Requires grav ≥ 2.0.0-rc.4. - New
DELETE /system/environments/{name}endpoint. Removes theuser/env/<name>/folder and everything under it after validating the name, refusing to delete the request's currently active environment, refusing to act on legacyuser/<name>/config/layouts (those overlap with user-managed paths and must be cleaned up by hand), and guarding against symlink escape via a realpath boundary check. - New User Groups CRUD endpoints.
GET /groups,POST /groups,GET /groups/{name},PATCH /groups/{name},DELETE /groups/{name}for managing entries inuser/config/groups.yaml. Reads through the Flexuser-groupsdirectory when available, falls back to direct YAML I/O otherwise. ETag concurrency on show/update; writes gated onadmin.superto match thesecurity@on the group blueprint. - New Accounts Configuration endpoints.
GET /config/accountsandPATCH /config/accountsread and writeuser/config/flex/accounts.yaml— the Flex compatibility + caching toggles classic admin shows under the Users → Configuration tab. Gated onadmin.super. - New blueprint endpoints for group editing and accounts config.
GET /blueprints/groupsandGET /blueprints/groups/newserve the resolveduser/group.yamlanduser/group_new.yamlblueprints.GET /blueprints/config/accountsdelegates toFlexDirectory::getDirectoryBlueprint()so both the Compatibility tab (from the user-accounts blueprint'sblueprints.configure.fields.import@) and the shared Caching tab (fromblueprints://flex/shared/configure.yaml) come through together, matching admin classic. - Four new Tier B preference keys.
usersViewMode,groupsViewMode,pluginsViewMode,themesViewMode(eachcardsortable) let admin2 remember per-user list-view choices server-side, the same waypagesViewModealready does.
- Page show, create, and update endpoints now enforce the new
-
POST /pages/{route}/moveno longer corrupts folder names with a double-dot prefix.$page->order()in Grav core returns the matched numeric prefix including the trailing dot (e.g.'04.'), and the endpoint was treating it as a plain number then appending its own separator, producing folders like04..an-api-drive-future-for-grav. Grav then strips only the first04.from that name and exposes a slug starting with., which gives the page a route like/blog/.foothat the SvelteKit router silently normalizes back to/blog/foo— net result was a "Page not found" right after a successful move. The fallback now strips the trailing dot and converts to int before buildingdirName.POST /pages/reorganizerejects batches where a destination parent is also being moved. Mid-batch, Phase 2 renames every page to a temp directory under its captured destination path; if the destination parent itself is in the operation list, its on-disk path moves out from under any earlier op that already landed in it, and Phase 3's rename then fails with the confusingrename(...): No such file or directory. The endpoint now walks the ancestor chain of every op's destination during validation and throws a clearValidationExceptionnaming the conflicting op, so the rollback path keeps disk state intact and the client sees something actionable.POST /pages/reorganizeno longer rejects batches that contain no-op renumbers of the source's parent. The "destination parent is also being moved" guard was flagging every route in the batch as moved, even ones whose position and parent were unchanged on disk. Dragging a child page back to root with the tree-view drop handler (which renumbers all root siblings to match the new order) failed withOperation index N targets parent '/foo', but '/foo' is also being moved in the same batcheven though/foowas just being renumbered to its existing position. The conflict check now only counts routes that actually rename on disk (parent changed OR position changed vs current order).POST /pages/reorganizedefensively strips leading dots from page slugs. Mirrors the sanitization the single-page/moveendpoint already applies. Without this, a page whose slug somehow started with.(e.g. inherited from an earlier corrupted folder) would get rebuilt into another double-dot folder on the next batch operation.- Page listings now serve the real frontmatter title instead of a slug-humanized fallback. The flex-indexed
PageObjectreturned byGET /pagesdoesn't materialize the page header in memory, so$page->title()and->menu()were silently falling back toucfirst(slug)— pages namedcontact-uswith a realtitle: "Contact Us"in their.mdshowed up as"Contact-us".PageSerializeralready re-parses the frontmatter from disk when the in-memory header is empty; it now also readstitleandmenuout of that parsed array in preference to the page-object accessors. POST /menubar/actions/{plugin}/{action}returns HTTP 200 when a handler ran and reported a domain-level failure, instead of HTTP 400 with the failure embedded in the body. The action result envelope ({ status, message }) already differentiates success from failure, so the admin client'sresult.statustoast branch handles both — but on 400 the client's generic error handler was looking fordetail/titlefields that aren't part of the menubar envelope, leaving the error toast blank. HTTP 4xx is now reserved for the genuine API error of "no plugin registered for that action" (returns 404). Visible symptom this fixes: clicking the Cloudflare purge button with bad credentials used to surface an empty red toast instead of "Cloudflare purge failed: Authentication error".
v1.0.0-rc.8
05/17/2026
-
- New
GET /admin/languagesendpoint. Enumeratesuser/plugins/admin2/languages/*.yamland returns each available admin UI locale with its native name and RTL flag. Distinct fromGET /languages(which lists site content languages fromsystem.yaml) — the admin UI language and site content language are different concepts and shouldn't be conflated. POST /pagesnow accepts akindparameter. Mirrors classic admin's three-way add-page split:page(default — folder +<template>.md, unchanged behaviour),folder(creates the folder only, no.mdfile), ormodule(a modular sub-page — the slug is automatically prefixed with_per Grav's modular-folder convention). Validation rejects any other value.GET /blueprints/pagesnow accepts?modular=true. ReturnsPages::modularTypes()instead ofPages::types()so the admin can populate a Module-specific template picker (the fourmodular/*templates a theme provides, rather than every standard template).GET /translations/{lang}now returnsdir: 'rtl'|'ltr'. Lets admin2 set<html dir>andwindow.__GRAV_I18N.dirfrom the same payload it's already fetching, avoiding a second roundtrip on every locale switch.
- New
-
GET /translations/{lang}no longer silently falls back to the default language when the requested locale isn't a site content language. The validation was gating on$language->getLanguages()(the list configured undersystem.languages.supported), so a request for Hebrew on an English-only site quietly returned English strings. Admin UI translations and site content languages are independent — the endpoint now validates only the shape of the lang code and letsLanguages::flattenByLang()decide whether the strings exist.- Blueprint labels now translate against the user's admin language, not the site default. Every
/blueprints/*endpoint (pages, plugins, themes, users, permissions, config, plugin pages, page types) used to call$lang->translate()with no language hint, so labels resolved against Grav's globally-active content language. The same admin-next user could see their topbar nav in Hebrew (those come from/translations/hewhich knows the language) and the System config page still in English (those come pre-resolved here).BlueprintControllernow reads each request's authenticated user, resolves their effectiveadminLanguageviaPreferencesResolver, and passes[$adminLanguage, 'en']as the language fallback chain on every translate call. Falls back cleanly to English when no user is attached. - Modular templates now save with the correct on-disk filename.
POST /pageswith templatemodular/herowas writing<folder>/modular/hero.md(creating a nestedmodular/subdirectory inside the new page folder); it now correctly writes<folder>/hero.md. Themodular/prefix in the template key is purely for template resolution, never part of the filename.
v1.0.0-rc.7
05/14/2026
-
- New
/admin-next/preferencesendpoint family for admin2 UI settings.GET /admin-next/preferencesreturns a single resolved payload of branding, site defaults, the caller's overrides, and the merged effective values.PATCH /admin-next/preferences/usersaves the current user's overrides (debounced from the SPA);DELETEclears them.PATCH /admin-next/preferences/sitewrites site-wide defaults (super-admin only) and routes Tier B keys (overridable per-user) touser/config/admin-next.yaml > ui.defaultsand Tier A2 keys (site-only behavioral — auto-save, real-time collab, menubar links) toui.settings.PATCH /admin-next/brandingandPOST/DELETE /admin-next/branding/logohandle the site logo (mode/text plus light/dark image uploads stored underuser://media/admin-next/). The newPreferencesResolverservice inclasses/Api/Services/mirrors the dashboard-layout pattern: built-in defaults overlaid with site values overlaid with per-user overrides, all schema-validated on write.
- New
v1.0.0-rc.6
05/13/2026
-
- Plugin log files now show up in the admin Logs viewer. A new
onApiLogFilesevent lets plugins register their own log file alongside the coregrav.log/email.log/scheduler.logset, andGET /system/logsaccepts a?file=query to pick which one to read. A companionGET /system/logs/filesendpoint returns the registered list so the admin can populate a selector. File names are whitelisted by the registered set to prevent path traversal. POST /pagesacceptsorder: "auto"for sibling-aware numeric prefixing. Mirrors admin-classic's add-page behavior: when the parent already has numerically prefixed children the new page is created with the next free number; when no sibling uses a prefix the new page is created without one. Existing integer andnullsemantics are unchanged.
- Plugin log files now show up in the admin Logs viewer. A new
-
- Blueprint serializer now passes through the
createfield property. Lets array fields opt into admin2's constrained-dropdown rendering by settingcreate: falsein their blueprint. - Self-service paths on
/usersno longer requireapi.users.read. A caller can fetch their own row (GET /users/{me}) and the user-form schema (GET /blueprints/users) with justapi.access— symmetric with whatPATCH /users/{me}already permitted. The blueprint endpoint is just the form definition with no per-user data leak; the show endpoint still requiresapi.users.readfor anyone else's account. GET /usersauto-filters to self for restricted callers. Withoutapi.users.readthe listing endpoint used to 403 outright; it now returns a single-row paginated envelope containing only the caller's own user. Admins (super orapi.users.read) still get the full listing as before.- Admin login now flows through the standard
Login::login()event chain. The previous directUser::authenticate()call skippedonUserLoginAuthenticate, which meant the LDAP plugin (and any other auth-extending plugin that hooks that event) couldn't participate in admin2 logins. Auth requests now route throughLogin::login()with'admin' => trueand'authorize' => 'admin.login', with a clean fallback to direct authentication when the Login plugin isn't installed (grav-plugin-admin2#9). - Popularity visitor IPs now hashed with HMAC-SHA256 plus a server-private salt. The legacy
sha1($ip)hash was reversible via precomputed table over the IPv4 space, which under GDPR Recital 26 / Art. 4(1) kept the stored value classified as personal data. Hashing now useshash_hmac('sha256', $ip, $salt)with an auto-generated 32-byte salt stored inuser/config/plugins/api.yaml(never shipped with a default), treated like the JWT secret and redacted from config-API responses. Legacy hashes already on disk age out via the existing visitor-history cap, so no migration is needed (grav-plugin-admin2#12).
- Blueprint serializer now passes through the
-
- API plugin now activates and dispatches correctly on Grav installs mounted at a subpath. Both the activation check in
setup()and the path-stripping inApiRouter::dispatch()were comparing against the raw request path, which on a subpath install starts with the base — so/<base>/api/...requests fell through to a page-not-found 404. Both code paths now strip the base before testing the api prefix. - Page Template selector now shows the right list for modular children. Serializing a
modular/*page blueprint was hardcoded to callPages::pageTypes('standard'), so a modular child's template dropdown listed every standard template instead of the fourmodular/*ones the theme provides. Serializer now picks'modular'or'standard'based on the blueprint name. The merge of resolved options with whatever Grav core'sdynamicData()may have already filled in was also changed to a replace, so themes can't end up with the standard and modular lists concatenated.
- API plugin now activates and dispatches correctly on Grav installs mounted at a subpath. Both the activation check in
v1.0.0-rc.5
05/08/2026
-
- Blueprint label resolver now prefers
ICU.<key>over the flat<key>. Admin2 ships its canonicalPLUGIN_ADMIN.*vocabulary under the ICU namespace; checking ICU first guarantees admin2 wins for every key it ships, even when admin classic (or any plugin still using the Grav 1 flat convention) is also present. The flat lookup remains as a transition fallback for keys admin2 doesn't ship — but only for keys contributed by enabled plugins (see below). - Disabled plugins no longer influence translations. Grav core's
flattenByLang()reads every plugin's lang yaml regardless of enabled state, so a disabled plugin (most painfully, admin classic mid-migration on a Grav 2 site) used to leak its strings into both the/translations/{lang}payload served to admin2 and the server-side blueprint label resolver. Both code paths now consult a newDisabledPluginLangIndexservice that returns the keys contributed exclusively by disabled plugins; those keys are stripped from the response and skipped intranslateLabel(). Keys also shipped by an enabled plugin stay — the enabled plugin owns them. /translations/{lang}shadow-strips flat duplicates. When the same key exists under both<key>andICU.<key>, only the ICU side is sent to admin2. Admin2's client-sidet()already preferred ICU, but stripping flat duplicates at the source shrinks the payload and removes ambiguity for any caller that might bypass the ICU-first lookup.
- Blueprint label resolver now prefers
v1.0.0-rc.4
05/06/2026
-
POST /gpm/upgradenow emits agrav:updateinvalidation header alongsidegpm:update, so admin clients can refresh cached version info (e.g. the sidebarGrav v…label) after a Grav core self-upgrade without waiting for a full page reload.
v1.0.0-rc.3
05/05/2026
-
- Config saves now land where Grav loads them. When an environment overlay was active (e.g. a hostname-derived
user/<host>/config/),PATCH /config/{scope}always wrote to baseuser/config/regardless — so any field already pinned in the env file silently shadowed the write and the change appeared to "succeed but not stick" (classic case: enabling a plugin pinnedenabled: falseinuser/localhost/config/plugins/). Writes now follow Grav's active environment by default; theX-Config-Environmentheader still wins when set, and an explicitly-empty header opts back into a base write.
- Config saves now land where Grav loads them. When an environment overlay was active (e.g. a hostname-derived
v1.0.0-rc.2
05/05/2026
-
- Module-page blueprints (
modular/hero,modular/feature, etc.) now resolve.GET /blueprints/pages/{template}was registered with FastRoute's default placeholder, which doesn't allow slashes — so the embedded/in module-page templates 404'd before the controller ran (grav-plugin-admin2#1). Route placeholder widened to accept slashes; downstream resolver already handled slashed templates. - Custom theme page blueprints (replace@, @extends.context, import@, ordering@) now render correctly. The hand-rolled YAML resolver in
BlueprintController::loadPageBlueprint()silently droppedreplace@/unset@/replace-<prop>@directives, ignored@extends.context, and mergedimport@as a map instead of inline-inserting fields — so themes that override fields, switch the parent context, or import partials saw most of their customizations vanish. Resolver now delegates to Grav core's standardPages::blueprints()pipeline (Blueprint::load()->init()), the same path admin-classic uses, which honors every BlueprintForm directive (grav-plugin-admin2#3). pagemediaselect/filepickerfield properties now round-trip. Blueprint serializer's field-property whitelist was missingpreview_images,preview_image,on_demand,folder,filter,self,display,resize, andmedia_picker_field, so themes that configured those props on a media picker saw them silently stripped from the API response.- Folders prefixed
00.now sort to the top of the page tree. Default-sort branch inPagesController::indexViaDefaultSort()bucketed pages byif ($page->order()), but Flex'sPage::order()returns(int) 0for00.— so00.sectionslanded in the "unordered" bucket and sorted alphabetically after every numbered sibling instead of first (grav-plugin-admin2#5). Bucket check changed to!== false(the actual sentinel for unordered folders).
- Module-page blueprints (
v1.0.0-rc.1
05/04/2026
-
- Fire new
onApiBlueprintResolved()event - Add support for configurable ordering prefixes
- Fire new
-
- Page creation and reorder now match the digit width of the parent's existing children — adding into a 3-digit collection stays 3-digit, and reorder no longer renormalizes existing 3- or 4-digit prefixes back to two (grav-plugin-admin#2492). New collections fall back to the new
system.pages.order_digitssetting.
- Page creation and reorder now match the digit width of the parent's existing children — adding into a 3-digit collection stays 3-digit, and reorder no longer renormalizes existing 3- or 4-digit prefixes back to two (grav-plugin-admin#2492). New collections fall back to the new
v1.0.0-beta.17
04/28/2026
-
- Security: privilege escalation via blueprint-upload (GHSA-6xx2-m8wv-756h). A user with only basic media-upload permission could plant an account file and instantly become a super-admin. The endpoint now restricts where files can land, who can target another user's avatar, and which file types are allowed.
- Security: hardened destination handling on
/blueprint-upload. Added pre-locator input validation that rejects path-traversal characters in thedestinationstring before it ever reaches Grav's stream resolver. - Security: path-traversal hardening in GPM endpoints.
GET /gpm/{plugins,themes}/{slug}/...(readme, changelog, blueprints, fields, etc.) now validates theslugparameter so a..slug can no longer reach files outside the package directory.
v1.0.0-beta.16
04/28/2026
-
- Fix:
POST /gpm/update-allnow updates packages instead of skipping all of them. A regression introduced by beta.14's dep-resolution rewrite tripped a Grav core static-cache quirk and mis-labelled every outdated package as "already up to date (installed as a dependency)". The per-packagePOST /gpm/updateendpoint was unaffected and remained a working workaround.
- Fix:
v1.0.0-beta.15
04/27/2026
-
onApiBlueprintResolvednow fires for the user-account blueprint.GET /blueprints/userspreviously returned the serializedaccount.yamlstraight to the wire — plugins could extend page / plugin / theme blueprints via the event but had no way to add fields to the user form. The handler now firesonApiBlueprintResolvedwithtemplate: 'account'and the requesting user, so admin2 can inject the state-toggle field (or other plugins can add their own fields) on a permission-aware basis.onApiBlueprintResolvednow carries an explicitcontextdiscriminator, and theme blueprints now fire the event. Every firing tags itself withcontext: 'page' | 'plugin' | 'theme' | 'account'alongside the existingtemplate/plugin/themekeys. Lets listeners gate behavior to a specific blueprint family without inferring from which sibling key happens to be set — e.g. ai-translate auto-annotates onlycontext === 'page'and lets plugins hook a separateonAiTranslateAnnotateFieldsevent for opt-in coverage of other contexts.themeBlueprint()previously returned the serialized YAML straight to the wire and skipped the event entirely; it now firescontext: 'theme'symmetrically with the others, so theme-targeted blueprint extensions are finally possible.
-
- Dashboard layout no longer disables the user account.
DashboardLayoutResolver::saveUserLayout()was writing the per-user widget layout tostate.admin_next.dashboardin the user's account YAML, but Grav's top-levelstate:field is the account-state string (enabled/disabled). Replacing it with a map flipped affected accounts to "Disabled" in the users list and broke any code that readstate === 'enabled'. Storage moved to the top-leveladmin_next.dashboardkey (no collision), and a one-time read-side migration inmigrateLegacyState()lifts any pre-existing legacy data out ofstate.*and restoresstate: enabled(ordisabledif a legacystate.enabled: falseflag was present). Migration runs on the next dashboard read for each affected user; the save path also calls it as a safety net so an admin reordering widgets self-heals their own record. - Security: privilege escalation via self-edit (GHSA-r945-h4vm-h736).
PATCH /users/{username}allowed any authenticated user withapi.accessto send anaccesspayload against their own profile and self-promote to super admin. The self-edit branch only requiredapi.access(notapi.users.write), but the field whitelist still includedaccess(andstate) for everyone — overwritingaccess.api.super/access.admin.superon yourself granted full system control and a Twig-template path to RCE.UsersController::updatenow splits the whitelist into self-editable fields (email, fullname, title, language, content_editor, twofa_enabled) and admin-only fields (state, access); a non-manager that sendsaccessorstatein the body now gets a403 Forbiddenwith an explicit "requires the 'api.users.write' permission" message instead of having the field silently land. Managers (super-admin orapi.users.write) keep full control over both fields, including on their own account. New regression testUsersControllerUpdatePrivescTestpins the boundary across five cases: low-priv self-edit ofaccessrejected (and access map verified untouched), low-priv self-edit ofstaterejected, low-priv self-edit of plain profile fields succeeds, an admin updates another user'saccessfield, and a user holdingapi.users.writeself-edits their ownaccess.Grav\Framework\Acl\PermissionsandGrav\Common\Utils::arrayFlattenDotNotationwere added as minimal stubs intests/Stubs/GravStubs.phpsoPermissionResolvercan be exercised in unit tests without the Grav core on the classpath.
- Dashboard layout no longer disables the user account.
v1.0.0-beta.14
04/25/2026
-
core.recent-pagesdashboard widget's registereddefaultSizewassminstead ofmd— out of sync with theDefaultpreset, which sets it tomd. Fresh installs (no saved user layout, no site layout) fell through to the registered default and rendered Recent Pages atsmeven though clicking the Default preset would correctly snap it back tomd. Bumped the registereddefaultSizetomdso a new account's first dashboard render matches the canonical Default layout. The other seven core widgets were already aligned with the preset, audited as part of the fix.POST /gpm/update-allnow enforces the same dependency validation as the per-packagePOST /gpm/updatepath. The old bulk flow iteratedgetUpdatable()and calledGpmService::update()directly per slug with no checks, so a "Update All" click would happily update plugins whose blueprint declaredgrav(orphp) requirements the running install didn't satisfy — the dep-resolution intelligence already living inGPM::checkPackagesCanBeInstalled()+GPM::getDependencies()was being bypassed entirely. The bulk path now runs each package throughgetDependencies()up-front: a Grav-too-old or PHP-too-old failure surfaces as afailed[]entry with the original GPM message (color tags stripped) so the toast reads "needy: One of the packages require Grav >=2.0.0-beta.2. Please update Grav to the latest release." instead of silently mis-updating. Plugin-level deps that themselves need an update are processed before the package that needs them, mirroring admin-classic's resolution order — a plugin update that requiresshortcode-core >= 5.0.0will pullshortcode-coreforward first. The response shape gainsskipped[](packages that were originally listed updatable but became current via cascade earlier in the batch — re-checked against a fresh GPM read each iteration, no double-update) andcascaded_dependencies[](slugs installed/updated as deps of others in the batch), so callers can render "also updated as deps: x, y". Admin-next's three "Update All" toasts (dashboard, plugins, themes) now expand failure reasons inline ("<slug>: <reason>") instead of just listing slugs, so the underlying constraint is visible without opening the network panel.- 6 new unit tests in
GpmControllerUpdateAllTestcover the matrix: Grav-dep mismatch lands infailed[]and never invokesupdatePackage; cascade install runs in correct order and the cascaded dep lands inskipped[](notupdated[]) on its own iteration; a throwing dep install aborts the parent update with a partial-failure message;theme: trueis passed only for theme packages andinstall_deps: falseis always set; anupdatePackage()returning non-success surfaces as afailed[]entry; an empty batch returns four empty buckets. To enable mocking,GpmController::getGpm()was promoted fromprivatetoprotectedand the staticGpmService::install/updatecalls inupdateAll()now route through new protectedinstallPackage()/updatePackage()wrappers — overridable in a test subclass without touching network or filesystem. A minimalGrav\Common\GPM\GPMstub was added totests/Stubs/GravStubs.phpsocreateMock(GPM::class)works when running the suite outside a Grav installation.
v1.0.0-beta.13
04/25/2026
-
- Customizable dashboard widgets — three new endpoints back the new admin-next dashboard's per-user / per-site customization.
GET /dashboard/widgetsreturns the resolved widget list (visibility, size, order) merged from a built-in core registry + plugin contributions viaonApiDashboardWidgets+ the site-default layout (super-admin) + the current user's overrides.PATCH /dashboard/layoutsaves the user's layout (visibility/size/order per widget);PATCH /dashboard/site-layoutsaves the site-wide default and is super-admin only. The merge is layered: site-hidden widgets are dropped entirely from the user's view (cannot be re-enabled per-user), the user's overrides win for size/order on the rest, and any widget not in either layout falls back to its registereddefaultSize/ priority. A newDashboardLayoutResolverservice owns the resolution and persistence; resolved widgets carry theirsizes[]/defaultSize/ icon / authorize permission, so the client can render the customize-mode size picker without a second round-trip. Plugin widgets are contributed by listening toonApiDashboardWidgetsand pushing entries into$event['widgets']; each widget can declare its own permission gate so the resolver hides widgets the user lacks access to. Allowed sizes are validated server-side against a per-widget allowlist (xs,sm,md,lg,xl) and silently coerced back todefaultSizeif a stale layout asks for an unsupported size. - Notifications v2 —
GET /dashboard/notificationsnow fetches fromhttps://getgrav.org/notifications2.jsonand stores its cache underuser/data/notifications/{md5}_v2.yaml(separate file so v1 caches don't collide). The new schema replaces the v1 "embedded HTML inmessage" approach with structured fields:type(info|notice|warning|promo),icon(emoji or icon name),title,message(markdown),link(whole-row click),image+accent(forpromocards),action: {label, url}, anddependencies(e.g.admin: '>= 2.0.0'). Lets admin-next render every notification natively in its own design language instead of dumping admin-classic CSS into the page. - Password policy endpoint at
GET /auth/password-policy(public, no auth). Parses the configuredsystem.pwd_regexinto a structured{ regex, min_length, rules[] }response by recognizing the common lookahead form ((?=.*\d),(?=.*[a-z]),(?=.*[A-Z]),(?=.*\W),.{N,}) and mapping each to a human-readable rule label. Admins can override the auto-detected rules with an optionalsystem.pwd_rules: [{id, label, pattern}, …]list for custom or localized messaging without touchingpwd_regex. The same structure is piggybacked on theGET /auth/setupresponse so the first-run setup screen can render its strength meter without a second round-trip.POST /auth/setupadditionally validates the first-user password againstpwd_regexserver-side (previously only enforced>= 8chars) — keeps the server authoritative regardless of what the UI is showing. - HTTP method override fallback for shared-hosting nginx configs that 405
DELETE/PATCH/PUTat the edge before the request ever reaches PHP. A newMethodOverrideMiddlewareruns right after CORS/body-parse and transparently rewrites anyPOSTthat carries anX-HTTP-Method-Override: DELETE|PATCH|PUTheader to the target method before dispatch, so the FastRoute handlers downstream see the semantic verb they expect. Only the three mutation verbs are honored (neverGET), and the override is opt-in per request — clients that don't need it pay zero cost. Admin-next complements this with client-side auto-detection: a failed mutation that 405s is retried once asPOST + override, and the fallback is cached insessionStorageso subsequent requests in the same session skip straight to the compatible path. - Destination-aware blueprint file uploads at
POST /blueprint-uploadandDELETE /blueprint-upload. Accepts a blueprintdestination(Grav stream liketheme://images/logo,user://assets,account://avatars;self@:subpathrelative to a blueprint owner; or a plain user-rooted relative path) plus ascope(plugins/<slug>,themes/<slug>,pages/<route>,users/<username>) and writes the uploaded file to the right place, mirroring admin-classic'staskFilesUploadsemantics. Streams resolve through Grav's locator so symlinked theme/plugin folders (common in dev setups) work cleanly — the response returns a logical user-rooted path (user/themes/quark2/images/logo/foo.png) independent of realpath, so a subsequentDELETEround-trips through the symlink to remove the actual file. Enforces the usual safety gates:..traversal and absolute paths are rejected, filenames are sanitized, and the dangerous-extension allowlist is checked. This gives admin-next parity with admin-classic's file-field uploads for theme/plugin config forms that previously only worked on page-media contexts.
- Customizable dashboard widgets — three new endpoints back the new admin-next dashboard's per-user / per-site customization.
-
- Rate limiter
excluded_paths— newplugins.api.rate_limit.excluded_pathsconfig (defaults to['/sync/']) skips the per-user bucket for matching path prefixes. Phase 6 of the sync plugin fires steady ~90 req/min per editing client (1s pull + ~0.5s presence + per-keystroke pushes); the default 120 req/min anti-abuse bucket would trip an actively-typing user within a minute and put sync into a 429 stop/start loop. The bypass is gated by normal auth (sync still requiresapi.access+api.collab.*), so it's not a free pass — just removes the global anti-scraping limit from authenticated collaboration traffic. Operators can extend the list to exempt other high-frequency authenticated endpoints (e.g. polling integrations from internal services).
- Rate limiter
-
PATCH /config/{scope}(and every other ETag-guarded endpoint) no longer returns a spurious409 Conflictwhen the admin is served behind Apachemod_deflateor an nginx build that applies gzip/br compression. Both servers weaken the ETag on compressed responses by suffixing it (<hash>-gzip,<hash>-br, sometimes;gzip), and clients echo that suffixed value back inIf-Matchon the next PATCH. The PATCH response body is typically uncompressed, sogenerateEtag()produced the bare hash and the strict equality check failed on every first save.validateEtag()now strips known transport suffixes (-gzip,;gzip,-br,-deflate) and the weak-validator prefix (W/) from inboundIf-Matchheaders before comparing, so the hash round-trips cleanly through a compressing proxy. Invisible onphp -S/ MAMP; only reproduces behind real reverse proxies with content compression enabled.GET /usersno longer emits phantom entries for stray files inuser/accounts/. Grav's FlexFileStorage::buildIndex()indexes every file in the accounts folder regardless of extension, so snapshot/backup files from other plugins (e.g. revisions-pro's.revsnapshots) surfaced as indexable "user" objects.UsersController::indexViaFlexnow constrains the collection to keys matching the username pattern[a-z0-9_-]+before search / sort / pagination, so stray files are filtered out before they ever reach the serializer.PATCH /config/{scope}now uses Grav's blueprint-aware merge (Blueprint::mergeData()) instead of a blindarray_replace_recursive. The old recursive merge deep-merged map values at every level, so when a client sent a file field as{}after the user removed the last file, the old path keys from$existingsurvived the merge and the YAML kept referencing the deleted file. Blueprint-aware merge respects field-type semantics —type: file(and any other "collection of items" field) is REPLACED wholesale from the incoming body, so removing map entries actually propagates. Falls back toarray_replace_recursivewhen no blueprint is available (rare — mostly test fixtures).DELETE /blueprint-uploadis now idempotent: a missing file returns204 No Contentinstead of404 Not Found. The endpoint's contract is "this file should not be on disk" — already-gone and just-deleted are indistinguishable end states, and surfacing a 404 forced clients into special-case error suppression. Real misuses (path resolves to a directory or something non-file) still error.
v1.0.0-beta.12
04/22/2026
-
- Environment management API at
GET /system/environments(now returning a richer shape:detectedhost,environments[]withname,label,exists,hasOverrides) andPOST /system/environmentsto create a newuser/env/<name>/config/folder. Writes toconfig/plugins/*,config/themes/*, andconfig/{system,site,media,security,…}now honor a newX-Config-Environmentrequest header that targets an existing env folder — empty/missing defaults to baseuser/config/, and a non-empty value that doesn't match an existing folder returns a clear400instead of silently creating anything. Env folders are never created implicitly; clients must opt in viaPOST /system/environments. A sharedEnvironmentServiceowns resolution acrossuser/env/*and legacy Grav 1.6user/<host>/layouts so both the listing endpoint and the write path see the same set of envs. - Differential config saves. Config writes now persist only the delta against the relevant parent yaml, matching the hand-edit workflow instead of forking full defaulted copies. Parent resolution:
system / site / media / security / scheduler / backups→system/config/<scope>.yaml(Grav core defaults)plugins/<name>→user/plugins/<name>/<name>.yaml(the plugin's own shipped defaults)themes/<name>→user/themes/<name>/<name>.yaml(the theme's own shipped defaults)- Env-targeted writes (
X-Config-Environmentset) additionally layeruser/config/<scope>.yamlon top of defaults, so env files only carry keys that differ from the effective base — move a key from base to env by setting it to a different value; leave it alone to inherit. - Defaults come from the raw yaml files on disk, not from blueprints — blueprints describe the admin form and routinely diverge from what actually loads at runtime. Sequential arrays (
languages.supported,pages.types, etc.) are treated atomically: any difference retains the whole new list, avoiding the classic admin-classic bug where shortening a list silently merged removed entries back in. Ships with 24 unit tests covering diff semantics, deep merges, key-ordering tolerance, null overrides, and a full parent-resolution round trip against a tempdir Grav layout.
- Environment management API at
-
- Config saves no longer return a
409 Conflicton every second edit when sensitive fields are present.ConfigController::update()was hashing the PATCH response body from the in-memory$merged(non-redacted) but the next save'sIf-Matchvalidation hashed the redactedconfig->get()representation, so the etag the client stored was never going to match on the next round-trip. Both the response body and theIf-Matchcomparison now flow through a singleconfigEtagData()helper that reads viaconfig->get()after the save and applies the same redaction, so the client's stored etag stays valid across consecutive saves and reflects the shape a freshGETwould return (including any blueprint defaults or type coercion applied server-side during the filter step). - Config writes no longer silently create
user/<hostname>/config/folders on save.writeConfigFile()was resolving the target directory vialocator->findResource('config://', true, true), whose first match can be the hostname-derived env path Grav auto-infers whenuser/env/doesn't exist — thenmkdirmaterialized the path on first save, producing orphanuser/localhost/config/,user/<ddev-host>/config/, etc. that then began overridinguser/config/on every subsequent read. The write path now explicitly resolves touser://config(or an existinguser/env/<env>/whenX-Config-Environmentis set), and themkdiris reserved for plugin/theme sub-directories inside an already-existing write root. Env roots must be created deliberately viaPOST /system/environments.
- Config saves no longer return a
v1.0.0-beta.11
04/21/2026
-
POST /gpm/installandPOST /gpm/updatenow install missing blueprint dependencies before installing the requested package — mirroring admin-classic's behavior viaGPM::checkPackagesCanBeInstalled()+GPM::getDependencies(), which resolves version constraints, checks PHP/Grav requirements, and returns a slug-keyedinstall/update/ignoremap. Previously the naive recursive branch inGpmService::install()passed the raw blueprintdependencies:list (arrays of{name, version}) back into itself wherearray_mapsilently filtered them all tofalse, so deps were never installed and the user got a half-wired plugin (e.g. installingshortcode-uiwithoutshortcode-core). Response bodies andonApiPackageInstalled/onApiPackageUpdatedevents now carry adependencies: string[]list of slugs that were installed alongside, and cache-invalidation tags cover each new dep so list views refresh accordingly. Failure modes are surfaced cleanly: requiring a newer Grav core, a newer PHP version, or hitting an incompatible version constraint between packages returns a422 Unprocessable Entitywith the originalGPM::getDependencies()error message (e.g. "One of the packages require Grav 1.8.0. Please update Grav to the latest release.") — the API never auto-upgrades Grav itself, matching admin-classic. CLI color markup (<red>,<cyan>, …) is stripped from the propagated message. Deps are installed one-at-a-time so mid-install failures report partial state — the 500 detail includes a "Dependencies already installed before failure: foo, bar." suffix so callers know exactly what got through before the failure.
v1.0.0-beta.10
04/19/2026
-
GET /pagesnow returns accuratepublishedandvisiblevalues for every page. Flex-indexedPageObjectinstances expose an empty header during listings, so$page->published()/visible()fell back to Grav's default "true" even when the frontmatter explicitly set them to false — making draft / hidden pages indistinguishable from published ones in list/tree/columns views.PageSerializernow parses the YAML frontmatter directly from the.mdfile on disk (with multilang filename resolution: page language → active language → untyped default → glob) whenever it gets a flex page with an empty header, and re-exposes the full header dict alongside correctpublished/visiblebooleans.PATCH /pages/{route}now reflectspublished/visiblechanges in the response without requiring a reload. LegacyPagecaches$this->publishedand$this->visibleat init and doesn't re-derive them from header mutations, so after updating the header the API was returning the pre-save values. The update controller now calls thePage::published()andPage::visible()setters in addition to header replacement whenever those fields are sent (either as top-level keys or nested underheader), keeping the in-memory object in sync with the just-written file.
v1.0.0-beta.9
04/17/2026
-
GET /blueprints/pages/{template}now honours the newer'@extends':and'@import':directives (string or{type, context}array form) alongside the legacyextends@:/import@:spellings. Previously, page blueprints using the newer syntax silently lost their inheritance chain — fields defined in the parent (e.g.content: type: markdown,header.media_order: type: pagemediafromsystem://blueprints/pages/default.yaml) were dropped, leaving only the fields the child blueprint declared locally. Caused custom page templates in themes like Helios to render with raw text inputs instead of markdown editors / page media uploaders in admin-next.GET /blueprints/pages/{template}now firesPages::getTypes()before resolving, which triggers theonGetPageBlueprintsevent and registers plugin-contributed blueprint paths into theblueprints://pages/locator stream. Without this, blueprints declared by plugins (via$types->scanBlueprints('plugin://.../blueprints')) were unreachable from the API even when the plugin was subscribed correctly.
v1.0.0-beta.8
04/17/2026
-
description_htmlfield added to the plugin/theme package serializer. Plugin and themedescriptionstrings are YAML-authored and routinely contain inline markdown (links, bold, emphasis) that renders as literal syntax in admin UIs. The API now ships a safe-mode Parsedown rendering alongside the rawdescriptionso clients can{@html}it for detail views and strip tags for one-line list cards without reinventing a markdown pipeline. Present onGET /gpm/plugins,GET /gpm/plugins/{slug},GET /gpm/themes,GET /gpm/themes/{slug}, and the/gpm/repository/*endpoints.
-
GET /pages/{route}?summary=trueno longer 500s on pages whose content contains plugin shortcodes that rely on the frontend Twig/theme environment (e.g.[poll]). Shortcode processing runs as part of Grav'ssummary()pipeline and can throw when it tries to render template partials that aren't wired up in the API request context. The page serializer now catches the failure and falls back to a plain-text rendering of the raw markdown (shortcodes stripped, trimmed tosummary_sizeor 300 chars) so admin previews keep working.
v1.0.0-beta.7
04/17/2026
-
X-API-Tokenheader added as the preferred transport for JWT access tokens. Sidesteps FastCGI / PHP-FPM / CGI setups (notably MAMP'smod_fastcgi) that silently strip the standardAuthorizationheader before it reaches PHP — a common source of 401 errors on shared hosts. Accepts either a bare JWT (X-API-Token: eyJ...) or the traditional Bearer form (X-API-Token: Bearer eyJ...).Authorization: Bearerstill works as a fallback for standards-compliant clients on hosts that don't strip it.GET /menow returnsgrav_versionandadmin_versionso admin UIs can surface the running Grav core and admin plugin versions without a separate request.admin_versionresolves to the enabled admin2 or admin-classic plugin blueprint.is_symlinkfield added to the installed-package serializer (present onGET /gpm/plugins,GET /gpm/plugins/{slug},GET /gpm/themes,GET /gpm/themes/{slug}). Detected viais_link()on the resolvedplugins://{slug}orthemes://{slug}path so admin UIs can flag symlinked packages.POST /pages/{route}/adopt-language— claims an untyped base page file (e.g.,default.md) as a specific language by renaming it in-place to{template}.{lang}.md. Pure filesystem rename + cache bust; content is untouched. Fails if the page already has an explicit file for that language, or if the page has no untyped base file. FiresonApiBeforePageAdoptLanguage/onApiPageLanguageAdopted. Enables "Save as English" workflows on sites that started single-language and later enabled multilang.- Page translation response (
GET /pages,GET /pages/{route}with?translations=true) now includes two new fields to disambiguate Grav's fallback behaviour:has_default_file(true when an untyped{template}.mdexists) andexplicit_language_files(the subset of site languages with a real{template}.{lang}.mdon disk). Needed because Grav reports the default lang intranslated_languageswheneverdefault.mdexists — admin UIs can now tell whether each lang is backed by an explicit file or the implicit fallback.
-
JwtAuthenticator::extractBearerToken()now readsX-API-Tokenfirst, then falls back toAuthorization: Bearer, then?token=query param. When both custom and standard headers are set, the custom header wins (so clients can send both for maximum host compatibility without ambiguity).- OpenAPI spec, README, and Newman test runner updated to lead with
X-API-Token. - Default CORS allow-headers list in
api.yamlnow includesX-API-Tokenalongside the existing entries, so cross-origin preflights succeed out of the box on fresh installs.
-
GET /meno longer 500s when resolving the admin plugin version. Previous implementation called$grav['plugins']->get($slug)->getBlueprint(), but->get()returns aDataconfig object, not aPlugininstance (nogetBlueprint()method). Now readsplugins://{slug}/blueprints.yamldirectly via the locator, matching the pattern used for themes.POST /pages/{route}/adopt-languageno longer spuriously rejects the default language with "A translation already exists". The previous check used$page->translatedLanguages()which always includes the default lang whendefault.mdexists (because it serves as a fallback). The guard now checks the filesystem directly for{template}.{lang}.md, so adoption proceeds whenever the concrete language file is genuinely absent.
v1.0.0-beta.6
04/16/2026
-
POST /gpm/update-all— bulk update every updatable plugin + theme in one request (returns{updated[], failed[]})POST /gpm/upgrade— Grav core self-upgrade (refuses to run when Grav is installed via symlink)GET /gpm/updatesresponse now includesgrav.is_symlinkand counts Grav itself intotalso admin UIs can show the true update count- Events
onApiBeforePackageUpdate/onApiPackageUpdated/onApiBeforeGravUpgrade/onApiGravUpgradedfire around the new write operations
-
POST /gpm/updateauto-detects whether the slug is a theme and passestheme: trueto the installer so theme updates land in the right directoryGpmService— all GPM write operations (install / update / remove / direct-install / self-upgrade) are now implemented locally in the API plugin, removing the hard dependency onGrav\Plugin\Admin\Gpm. admin2 users can manage packages without the classic admin plugin installed
-
- Previously
POST /gpm/updatecalled the admin plugin's Gpm helper, which meant admin2-only sites (no classic admin) got500 Admin Plugin Requiredwhen trying to update anything
- Previously
v1.0.0-beta.5
04/16/2026
-
/auth/tokennow delegates password check toUser::authenticate()so the core trait's plaintext-password fallback fires — restores long-standing Grav behavior (admin-classic, Login plugin, frontend login) where apassword:declared directly inuser/accounts/*.yamlauto-hashes on first successful login. Previous directAuthentication::verify()call required users to pre-populatehashed_password, which broke the "edit yaml and log in" workflow that operators rely on when the CLI is unavailable- Persist the auto-generated JWT secret on fresh installs. The previous
findResource(..., true, true)call returned an array, the fallback concatenated that array into"Array/...", and the write silently went nowhere — so every request minted a different secret, producing a login-then-immediately-expire loop on every fresh 2.0 install. Now resolves the path with default flags and logs+degrades gracefully if persistence genuinely fails.
v1.0.0-beta.4
04/15/2026
-
- Page-view popularity tracker — single-file flat-JSON store with
flock(), replaces admin-classic's four-file scheme; subscribesonPageInitializedfor frontend hits only - One-shot import + rename of legacy
daily/monthly/totals/visitors.jsoninto the newpopularity.json(ISO-keyed,pagescapped at 500) popularity.{enabled, history.daily/monthly/visitors, ignore}config block inapi.yamlraw_routefield on serialized pages so admin clients can navigate home / aliased pages correctly
- Page-view popularity tracker — single-file flat-JSON store with
-
- Strict super-user scoping:
isSuperAdmin()honors onlyaccess.api.super(no fallback toadmin.super); operators can grant API authority without admin-classic implications SetupControllerwrites a minimal admin-next-native account (site.login+api.superonly), with race guards and explicit avatar/2FA reset to prevent flex-stored ghost dataissueTokenPair()lifted toAbstractApiControllerso setup, login, refresh, and 2FA share one token-shape source- Pages list / dashboard stats no longer skip the home page — the virtual pages-root is now distinguished by
$page->exists()instead of byroute() === '/' - Dashboard
popularityendpoint reads fromPopularityStore(handles legacy import transparently)
- Strict super-user scoping:
-
- Pages list and dashboard
pages.totalundercounted by 1 (the home page was being filtered out)
- Pages list and dashboard
v1.0.0-beta.3
04/15/2026
- [new]
- Add intial user funtionality
- Add
ai.superpermissions - Add missing vendor library
v1.0.0-beta.2
04/15/2026
- [improved]
- Default
enabledtotruesince the plugin is not installed by default and admin2 requires it
- Default
v1.0.0-beta.1
04/12/2026
- [new]
- Initial beta release