# Changelog ## Changes 10/20/2025 (v1.5.3) security(acl): enforce folder-scope & own-only; fix file list “Select All”; harden ops ### fileListView.js (v1.5.3) - Restore master “Select All” checkbox behavior and row highlighting. - Keep selection working with own-only filtered lists. - Build preview/thumb URLs via secure API endpoints; avoid direct /uploads. - Minor UI polish: slider wiring and pagination focus handling. ### FileController.php (v1.5.3) - Add enforceFolderScope($folder, $user, $perms, $need) and apply across actions. - Copy/Move: require read on source, write on destination; apply scope on both. - When user only has read_own, enforce per-file ownership (uploader==user). - Extract ZIP: require write + scope; consistent 403 messages. - Save/Rename/Delete/Create: tighten ACL checks; block dangerous extensions; consistent CSRF/Auth handling and error codes. - Download/ZIP: honor read vs read_own; own-only gates by uploader; safer headers. ### FolderController.php (v1.5.3) - Align with ACL: enforce folder-scope for non-admins; require owner or bypass for destructive ops. - Create/Rename/Delete: gate by write on parent/target + ownership when needed. - Share folder link: require share capability; forbid root sharing for non-admins; validate expiry; optional password. - Folder listing: return only folders user can fully view or has read_own. - Shared downloads/uploads: stricter validation, headers, and error handling. This commits a consistent, least-privilege ACL model (owners/read/write/share/read_own), fixes bulk-select in the UI, and closes scope/ownership gaps across file & folder actions. feat(dnd): default cards to sidebar on medium screens when no saved layout - Adds one-time responsive default in loadSidebarOrder() (uses layoutDefaultApplied_v1) - Preserves existing sidebarOrder/headerOrder and small-screen behavior - Keeps user changes persistent; no override once a layout exists --- ## Changes 10/19/2025 (v1.5.2) fix(admin): modal bugs; chore(api): update ReDoc SRI; docs(openapi): add annotations + spec - adminPanel.js - Fix modal open/close reliability and stacking order - Prevent background scroll while modal is open - Tidy focus/keyboard handling for better UX - style.css - Polish styles for Folder Access + Users views (spacing, tables, badges) - Improve responsiveness and visual consistency - api.php - Update Redoc SRI hash and pin to the current bundle URL - OpenAPI - Add/refresh inline @OA annotations across endpoints - Introduce src/openapi/Components.php with base Info/Server, common responses, and shared components - Regenerate and commit openapi.json.dist - public/js/adminPanel.js - public/css/style.css - public/api.php - src/openapi/Components.php - openapi.json.dist - public/api/** (annotated endpoints) --- ## Changes 10/19/2025 (v1.5.1) fix(config/ui): serve safe public config to non-admins; init early; gate trash UI to admins; dynamic title; demo toast (closes #56) Regular users were getting 403s from `/api/admin/getConfig.php`, breaking header title and login option rendering. Issue #56 tracks this. ### What changed - **AdminController::getConfig** - Return a **public, non-sensitive subset** of config for everyone (incl. unauthenticated and non-admin users): `header_title`, minimal `loginOptions` (disable* flags only), `globalOtpauthUrl`, `enableWebDAV`, `sharedMaxUploadSize`, and OIDC `providerUrl`/`redirectUri`. - For **admins**, merge in admin-only fields (`authBypass`, `authHeaderName`). - Never expose secrets or client IDs. - **auth.js** - `loadAdminConfigFunc()` now robustly handles empty/204 responses, writes sane defaults, and sets `document.title` from `header_title`. - `showToast()` override: on `demo.filerise.net` shows a longer demo-creds toast; keeps TOTP “don’t nag” behavior. - **main.js** - Call `loadAdminConfigFunc()` early during app init. - Run `setupTrashRestoreDelete()` **only for admins** (based on `localStorage.isAdmin`). - **adminPanel.js** - Bump visible version to **v1.5.1**. - **index.html** - Keep `FileRise` static; runtime title now driven by `loadAdminConfigFunc()`. ### Security v1.5.1 - Prevents info disclosure by strictly limiting non-admin fields. - Avoids noisy 403 for regular users while keeping admin-only data protected. ### QA - As a non-admin: - Opening the app no longer triggers a 403 on `getConfig.php`. - Header title and login options render; document tab title updates to configured `header_title`. - Trash/restore UI is not initialized. - As an admin: - Admin Panel loads extra fields; trash/restore UI initializes. - Title updates correctly. - On `demo.filerise.net`: - Pre-login toast shows demo credentials for ~12s. Closes #56. --- ## Changes 10/17/2025 (v1.5.0) Security and permission model overhaul. Tightens access controls with explicit, server‑side ACL checks across controllers and WebDAV. Introduces `read_own` for own‑only visibility and separates view from write so uploaders can’t automatically see others’ files. Fixes session warnings and aligns the admin UI with the new capabilities. > **Security note** > This release contains security hardening based on a private report (tracked via a GitHub Security Advisory, CVE pending). For responsible disclosure, details will be published alongside the advisory once available. Users should upgrade promptly. ### Highlights - **ACL** - New `read_own` bucket (own‑only visibility) alongside `owners`, `read`, `write`, `share`. - **Semantic change:** `write` no longer implies `read`. - `ACL::applyUserGrantsAtomic()` to atomically set per‑folder grants (`view`, `viewOwn`, `upload`, `manage`, `share`). - `ACL::purgeUser($username)` to remove a user from all buckets (used when deleting a user). - Auto‑heal `folder_acl.json` (ensure `root` exists; add missing buckets; de‑dupe; normalize types). - More robust admin detection (role flag or session/admin user). - **Controllers** - `FileController`: ACL + ownership enforcement for list, download, zip download, extract, move, copy, rename, create, save, tag edit, and share‑link creation. `getFileList()` now filters to the caller’s uploads when they only have `read_own` (no `read`). - `UploadController`: requires `ACL::canWrite()` for the target folder; CSRF refresh path improved; admin bypass intact. - `FolderController`: listing filtered by `ACL::canRead()`; optional parent filter preserved; removed name‑based ownership assumptions. - **Admin UI** - Folder Access grid now includes **View (own)**; bulk toolbar actions; column alignment fixes; more space for folder names; dark‑mode polish. - **WebDAV** - WebDAV now enforces ACL consistently: listing requires `read` (or `read_own` ⇒ shows only caller’s files); writes require `write`. - Removed legacy “folderOnly” behavior — ACL is the single source of truth. - Metadata/uploader is preserved through existing models. ### Behavior changes (⚠️ Breaking) - **`write` no longer implies `read`.** - If you want uploaders to see all files in a folder, also grant **View (all)** (`read`). - If you want uploaders to see only their own files, grant **View (own)** (`read_own`). - **Removed:** legacy `folderOnly` view logic in favor of ACL‑based access. ### Upgrade checklist 1. Review **Folder Access** in the admin UI and grant **View (all)** or **View (own)** where appropriate. 2. For users who previously had “upload but not view,” confirm they now have **Upload** + **View (own)** (or add **View (all)** if intended). 3. Verify WebDAV behavior for representative users: - `read` shows full listings; `read_own` lists only the caller’s files. - Writes only succeed where `write` is granted. 4. Confirm admin can upload/move/zip across all folders (regression tested). ### Affected areas - `config/config.php` — session/cookie initialization ordering; proxy header handling. - `src/lib/ACL.php` — new bucket, semantics, healing, purge, admin detection. - `src/controllers/FileController.php` — ACL + ownership gates across operations. - `src/controllers/UploadController.php` — write checks + CSRF refresh handling. - `src/controllers/FolderController.php` — ACL‑filtered listing and parent scoping. - `public/api/admin/acl/*.php` — includes `viewOwn` round‑trip and sanitization. - `public/js/*` & CSS — folder access grid alignment and layout fixes. - `src/webdav/*` & `public/webdav.php` — ACL‑aware WebDAV server. ### Credits - Security report acknowledged privately and will be credited in the published advisory. ### Fix - fix(folder-model): resolve syntax error, unexpected token - Deleted accidental second `5 MB; lighter CM settings for big files. - fileListView.js: latest-call-wins; compute editable via ext + sizeBytes (no blink). - FileModel.php: add sizeBytes; cap inline content to ≤5 MB (INDEX_TEXT_BYTES_MAX). - HTML: load extra CM modes: htmlmixed, php, clike, python, yaml, markdown, shell, sql, vb, ruby, perl, properties, nginx. --- ## Changes 10/5/2025 v1.3.14 fix(admin): OIDC optional by default; validate only when enabled (fixes #44) - AdminModel::updateConfig now enforces OIDC fields only if disableOIDCLogin=false - AdminModel::getConfig defaults disableOIDCLogin=true and guarantees OIDC keys - AdminController default loginOptions sets disableOIDCLogin=true; CSRF via header or body - Normalize file perms to 0664 after write --- ## Changes 10/4/2025 v1.3.13 fix(scanner): resolve dirs via CLI/env/constants; write per-item JSON; skip trash fix(scanner): rebuild per-folder metadata to match File/Folder models chore(scanner): skip profile_pics subtree during scans - scan_uploads.php now falls back to UPLOAD_DIR/META_DIR from config.php - prevents double slashes in metadata paths; respects app timezone - unblocks SCAN_ON_START so externally added files are indexed at boot - Writes per-folder metadata files (root_metadata.json / folder_metadata.json) using the same naming rule as the models - Adds missing entries for files (uploaded, modified using DATE_TIME_FORMAT, uploader=Imported) - Prunes stale entries for files that no longer exist - Skips uploads/trash and symlinks - Resolves paths from CLI flags, env vars, or config constants (UPLOAD_DIR/META_DIR) - Idempotent; safe to run at startup via SCAN_ON_START - Avoids indexing internal avatar images (folder already hidden in UI) - Reduces scan noise and metadata churn; keeps firmware/other content indexed --- ## Changes 10/4/2025 v1.3.12 Fix: robust PUID/PGID handling; optional ownership normalization (closes #43) - Remap www-data to PUID/PGID when running as root; skip with helpful log if non-root - Added CHOWN_ON_START env to control recursive chown (default true; turn off after first run) - SCAN_ON_START unchanged, with non-root fallback --- ## Changes 10/4/2025 v1.3.11 Chore: keep BASE_URL fallback, prefer env SHARE_URL; fix HTTPS auto-detect - Remove no-op sed of SHARE_URL from start.sh (env already used) - Build default share link with correct scheme (http/https, proxy-aware) --- ## Changes 10/4/2025 v1.3.10 Fix: index externally added files on startup; harden start.sh (#46) - Run metadata scan before Apache when SCAN_ON_START=true (was unreachable after exec) - Execute scan as www-data; continue on failure so startup isn’t blocked - Guard env reads for set -u; add umask 002 for consistent 775/664 - Make ServerName idempotent; avoid duplicate entries - Ensure sessions/metadata/log dirs exist with correct ownership and perms No behavior change unless SCAN_ON_START=true. --- ## Changes 5/27/2025 v1.3.9 - Support for mounting CIFS (SMB) network shares via Docker volumes - New `scripts/scan_uploads.php` script to generate metadata for imported files and folders - `SCAN_ON_START` environment variable to trigger automatic scanning on container startup - Documentation for configuring CIFS share mounting and scanning - Clipboard Paste Upload Support (single image): - Users can now paste images directly into the FileRise web interface. - Pasted images are renamed to `image.png` and added to the upload queue using the existing drag-and-drop logic. - Implemented using a `.isClipboard` flag and a delayed UI cleanup inside `xhr.addEventListener("load", ...)`. --- ## Changes 5/26/2025 - Updated `REGEX_FOLDER_NAME` in `config.php` to forbids < > : " | ? * characters in folder names. - Ensures the whole name can’t end in a space or period. - Blocks Windows device names. - Updated `FolderController.php` when `createFolder` issues invalid folder name to return `http_response_code(400);` --- ## Changes 5/23/2025 v1.3.8 - **Folder-strip context menu** - Enabled right-click on items in the new folder strip (above file list) to open the same “Create / Rename / Share / Delete Folder” menu as in the main folder tree. - Bound `contextmenu` event on each `.folder-item` in `loadFileList` to: - Prevent the default browser menu - Highlight the clicked folder-strip item - Invoke `showFolderManagerContextMenu` with menu entries: - Create Folder - Rename Folder - Share Folder (passes the strip’s `data-folder` value) - Delete Folder - Ensured menu actions are wrapped in arrow functions (`() => …`) so they fire only on menu-item click, not on render. - Refactored folder-strip injection in `fileListView.js` to: - Mark each strip item as `draggable="true"` (for drag-and-drop) - Add `el.addEventListener("contextmenu", …)` alongside existing click/drag handlers - Clean up global click listener for hiding the context menu - Prevented premature invocation of `openFolderShareModal` by switching to `action: () => openFolderShareModal(dest)` instead of calling it directly. - **Create File/Folder dropdown** - Replaced standalone “Create File” button with a combined dropdown button in the actions toolbar. - New markup - Wired up JS handlers in `fileActions.js`: - `#createFileOption` → `openCreateFileModal()` - `#createFolderOption` → `document.getElementById('createFolderModal').style.display = 'block'` - Toggled `.dropdown-menu` visibility on button click, and closed on outside click. - Applied dark-mode support: dropdown background and text colors switch with `.dark-mode` class. --- ## Changes 5/22/2025 v1.3.7 - `.folder-strip-container .folder-name` css added to center text below folder material icon. - Override file share_url to always use current origin - Update `fileList` css to keep file name wrapping tight. --- ## Changes 5/21/2025 - **Drag & Drop to Folder Strip** - Enabled dragging files from the file list directly onto the folder-strip items. - Hooked up `folderDragOverHandler`, `folderDragLeaveHandler`, and `folderDropHandler` to `.folder-strip-container .folder-item`. - On drop, files are moved via `/api/file/moveFiles.php` and the file list is refreshed. - **Restore files from trash Toast Message** - Changed the restore handlers so that the toast always reports the actual file(s) restored (e.g. “Restored file: foo.txt”) instead of “No trash record found.” - Removed reliance on backend message payload and now generate the confirmation text client-side based on selected items. --- ## Changes 5/20/2025 v1.3.6 - **domUtils.js** - `updateFileActionButtons` - Hide selection buttons (`Delete Files`, `Copy Files`, `Move Files` & `Download ZIP`) until file is selected. - Hide `Extract ZIP` until selecting zip files - Hide `Create File` button when file list items are selected. --- ## Changes 5/19/2025 v1.3.5 ### Added Folder strip & Create File - **Folder strip in file list** - `loadFileList` now fetches sub-folders in parallel from `/api/folder/getFolderList.php`. - Filters to only direct children of the current folder, hiding `profile_pics` and `trash`. - Injects a new `.folder-strip-container` just below the Files In above (summary + slider). - Clicking a folder in the strip updates: - the breadcrumb (via `updateBreadcrumbTitle`) - the tree selection highlight - reloads `loadFileList` for the chosen folder. - **Create File feature** - New “Create New File” button added to the file-actions toolbar and context menu. - New endpoint `public/api/file/createFile.php` (handled by `FileController`/`FileModel`): - Creates an empty file if it doesn’t already exist. - Appends an entry to `_metadata.json` with `uploaded` timestamp and `uploader`. - `fileActions.js`: - Implemented `handleCreateFile()` to show a modal, POST to the new endpoint, and refresh the list. - Added translations for `create_new_file` and `newfile_placeholder`. --- ## Changees 5/15/2025 ### Drag‐and‐Drop Upload extended to File List - **Forward file‐list drops** Dropping files onto the file‐list area (`#fileListContainer`) now re‐dispatches the same `drop` event to the upload card’s drop zone (`#uploadDropArea`) - **Visual feedback** Added a `.drop-hover` class on `#fileListContainer` during drag‐over for a dashed‐border + light‐background hover state to indicate it accepts file drops. --- ## Changes 5/14/2025 v1.3.4 ### 1. Button Grouping (Bootstrap) - Converted individual action buttons (`download`, `edit`, `rename`, `share`) in both **table view** and **gallery view** into a single Bootstrap button group for a cleaner, more compact UI. - Applied `btn-group` and `btn-sm` classes for consistent sizing and spacing. ### 2. Header Dropdown Replacement - Replaced the standalone “User Panel” icon button with a **dropdown wrapper** (`.user-dropdown`) in the header. - Dropdown toggle now shows: - **Profile picture** (if set) or the Material “account_circle” icon - **Username** text (between avatar and caret) - Down-arrow caret span. ### 3. Menu Items Moved to Dropdown - Moved previously standalone header buttons into the dropdown menu: - **User Panel** opens the modal - **Admin Panel** only shown when `data.isAdmin` and on `demo.filerise.net` - **API Docs** calls `openApiModal()` - **Logout** calls `triggerLogout()` - Each menu item now has a matching Material icon (e.g. `person`, `admin_panel_settings`, `description`, `logout`). ### 4. Profile Picture Support - Added a new `/api/profile/uploadPicture.php` endpoint + `UserController::uploadPicture()` + corresponding `UserModel::setProfilePicture()`. - On **Open User Panel**, display: - Default avatar if none set - Current profile picture if available - In the **User Panel** modal: - Stylish “edit” overlay icon on the avatar to launch file picker - Auto-upload on file selection (no “Save” button click needed) - Preview updates immediately and header avatar refreshes live - Persisted in `users.txt` and re-fetched via `getCurrentUser.php` ### 5. API Docs & Logout Relocation - Removed API Docs from User Panel - Removed “Logout” buttons from the header toolbar. - Both are now menu entries in the **User Dropdown**. ### 6. Admin Panel Conditional - The **Admin Panel** button was: - Kept in the dropdown only when `data.isAdmin` - Removed entirely elsewhere. ### 7. Utility & Styling Tweaks - Introduced a small `normalizePicUrl()` helper to strip stray colons and ensure a leading slash. - Hidden the scrollbar in the User Panel modal via: - Inline CSS (`scrollbar-width: none; -ms-overflow-style: none;`) - Global/WebKit rule for `::-webkit-scrollbar { display: none; }` - Made the User Panel modal fully responsive and vertically centered, with smooth dark-mode support. ### 8. File/List View & Gallery View Sliders - **Unified “View‐Mode” Slider** Added a single slider panel (`#viewSliderContainer`) in the file‐list actions toolbar that switches behavior based on the current view mode: - **Table View**: shows a **Row Height** slider (min 31px, max 60px). - Adjusts the CSS variable `--file-row-height` to resize all `` heights. - Persists the chosen height in `localStorage`. - **Gallery View**: shows a **Columns** slider (min 1, max 6). - Updates the grid’s `grid-template-columns: repeat(N, 1fr)`. - Persists the chosen column count in `localStorage`. - **Injection Point** The slider container is dynamically inserted (or updated) just before the folder summary (`#fileSummary`) in `loadFileList()`, ensuring a consistent position across both view modes. - **Live Updates** Moving the slider thumb immediately updates the visible table row heights or gallery column layout without a full re‐render. - **Styling & Alignment** - `#viewSliderContainer` uses `inline-flex` and `align-items: center` so that label, slider, and value text are vertically aligned with the other toolbar elements. - Reset margins/padding on the label and value span within `#viewSliderContainer` to eliminate any vertical misalignment. ### 9. Fixed new issues with Undefined username in header on profile pic change & TOTP Enabled not checked **openUserPanel** - **Rewritten entirely with DOM APIs** instead of `innerHTML` for any user-supplied text to eliminates “DOM text reinterpreted as HTML” warnings. - **Default avatar fallback**: now uses `'/assets/default-avatar.png'` whenever `profile_picture` is empty. - **TOTP checkbox initial state** is now set from the `totp_enabled` value returned by the server. - **Modal title sync** on reopen now updates the `(username)` correctly (no more “undefined” until refresh). - **Re-sync on reopen**: background color, avatar, TOTP checkbox and language selector all update when reopen the panel. **updateAuthenticatedUI** - **Username fix**: dropdown toggle now always uses `data.username` so the name never becomes `undefined` after uploading a picture. - **Profile URL update** via `fetchProfilePicture()` always writes into `localStorage` before rebuilding the header, ensuring avatar+name stay in sync instantly. - **Dropdown rebuild logic** tweaked to update the toggle’s innerHTML with both avatar and username on every call. **UserModel::getUser** - Switched to `explode(':', $line, 4)` to the fourth “profile_picture” field without clobbering the TOTP secret. - **Strip trailing colons** from the stored URL (`rtrim($parts[3], ':')`) so we never send `…png:` back to the client. - Returns an array with both `'username'` and `'profile_picture'`, matching what `getCurrentUser.php` needs. ### 10. setAttribute + encodeURI to avoid “DOM text reinterpreted as HTML” alerts ### 11. Fix duplicated Upload & Folder cards if they were added to header and page was refreshed --- ## Changes 5/8/2025 ### Docker 🐳 - Ensure `/var/www/config` exists and is owned by `www-data` (chmod 750) so that `start.sh`’s `sed -i` updates to `config.php` work reliably --- ## Changes 5/8/2025 v1.3.3 ### Enhancements - **Admin API** (`updateConfig.php`): - Now merges incoming payload onto existing on-disk settings instead of overwriting blanks. - Preserves `clientId`, `clientSecret`, `providerUrl` and `redirectUri` when those fields are omitted or empty in the request. - **Admin API** (`getConfig.php`): - Returns only a safe subset of admin settings (omits `clientSecret`) to prevent accidental exposure of sensitive data. - **Frontend** (`auth.js`): - Update UI based on merged loginOptions from the server, ensuring blank or missing fields no longer revert your existing config. - **Auth API** (`auth.php`): - Added `$oidc->addScope(['openid','profile','email']);` to OIDC flow. (This should resolve authentik issue) --- ## Changes 5/8/2025 v1.3.2 ### config/config.php - Added a default `define('AUTH_BYPASS', false)` at the top so the constant always exists. - Removed the static `AUTH_HEADER` fallback; instead read the adminConfig.json at the end of the file and: - Overwrote `AUTH_BYPASS` with the `loginOptions.authBypass` setting from disk. - Defined `AUTH_HEADER` (normalized, e.g. `"X_REMOTE_USER"`) based on `loginOptions.authHeaderName`. - Inserted a **proxy-only auto-login** block before the usual session/auth checks: If `AUTH_BYPASS` is true and the trusted header (`$_SERVER['HTTP_' . AUTH_HEADER]`) is present, bump the session, mark the user authenticated/admin, load their permissions, and skip straight to JSON output. - Relax filename validation regex to allow broader Unicode and special chars ### src/controllers/AdminController.php - Ensured the returned `loginOptions` object always contains: - `authBypass` (boolean, default false) - `authHeaderName` (string, default `"X-Remote-User"`) - Read `authBypass` and `authHeaderName` from the nested `loginOptions` in the request payload. - Validated them (`authBypass` → bool; `authHeaderName` → non-empty string, fallback to `"X-Remote-User"`). - Included them when building the `$configUpdate` array to pass to the model. ### src/models/AdminModel.php - Normalized `loginOptions.authBypass` to a boolean (default false). - Validated/truncated `loginOptions.authHeaderName` to a non-empty trimmed string (default `"X-Remote-User"`). - JSON-encoded and encrypted the full config, now including the two new fields. - After decrypting & decoding, normalized the loaded `loginOptions` to always include: - `authBypass` (bool) - `authHeaderName` (string, default `"X-Remote-User"`) - Left all existing defaults & validations for the original flags intact. ### public/js/adminPanel.js - **Login Options** section: - Added a checkbox for **Disable All Built-in Logins (proxy only)** (`authBypass`). - Added a text input for **Auth Header Name** (`authHeaderName`). - In `handleSave()`: - Included the new `authBypass` and `authHeaderName` values in the payload sent to `updateConfig.php`. - In `openAdminPanel()`: - Initialized those inputs from `config.loginOptions.authBypass` and `config.loginOptions.authHeaderName`. ### public/js/auth.js - In `loadAdminConfigFunc()`: - Stored `authBypass` and `authHeaderName` in `localStorage`. - In `checkAuthentication()`: - After a successful login check, called a new helper (`applyProxyBypassUI()`) which reads `localStorage.authBypass` and conditionally hides the entire login form/UI. - In the “not authenticated” branch, only shows the login form if `authBypass` is false. - No other core fetch/token logic changed; all existing flows remain intact. ### Security old - **Admin API**: `getConfig.php` now returns only a safe subset of admin settings (omits `clientSecret`) to prevent accidental exposure of sensitive data. --- ## Changes 5/4/2025 v1.3.1 ### Modals - **Added** a shared `.editor-close-btn` component for all modals: - File Tags - User Panel - TOTP Login & Setup - Change Password - **Truncated** long filenames in the File Tags modal header using CSS `text-overflow: ellipsis`. - **Resized** File Tags modal from 400px to 450px wide (with `max-width: 90vw` fallback). - **Capped** User Panel height at 381px and hidden scrollbars to eliminate layout jumps on hover. ### HTML - **Moved** `
` out of `.main-wrapper` so the login form can show independently of the app shell. - **Added** `
` immediately inside `` to cover the UI during auth checks. - **Inserted** inline `