Build a small, offline-capable PHP portal for a Raspberry Pi. PRIMARY RULE: SIMPLE IS BETTER. Do not turn this into a framework, CMS, or elaborate plugin system. Prefer clear, readable code and a few files over abstractions. Do not add features I have not requested. PURPOSE A simple landing page for an offline/local network, with a welcome message and cards linking to local services such as Kiwix. File sharing and chat are NOT part of this build. Make it easy to include additional PHP files later. ENVIRONMENT - Raspberry Pi OS / Debian. - Nginx and PHP-FPM. - No cloud dependencies, CDN requests, external fonts, or analytics. - No database, Composer, npm, containers, or build pipeline. - Plain PHP, HTML, CSS, and minimal vanilla JavaScript. - Use bootstrap CSS, downloaded and served locally. - Everything must work without internet access after installation. FILE STRUCTURE Use only these four files initially: index.php admin.php config.json pico.min.css - index.php displays the portal. - admin.php optionally adds authentication, inline editing, and saving. - config.json holds public site settings and service links. - bootstrap.min.css is the locally downloaded library. - Keep our small custom CSS and JavaScript in seprate, small files, each js file should only do one or two things. - Do not add helper files or directories unless genuinely necessary. - Do not reproduce the third-party stylesheet in your answer; provide instructions for obtaining a pinned local copy. PUBLIC PORTAL Display: - Site title. - Welcome message. - Responsive cards for local-service links. - Each card has a title, description, and URL, optional image/icon. - A simple visitor appearance control: - Light / dark / follow device. - A few accent-color presets. - Reset to the site defaults. Save visitor appearance preferences in browser localStorage. They must not change the shared JSON configuration. If localStorage is unavailable, the site should still work. Use ordinary text fields, not rich text or arbitrary HTML. Keep the interface clean and usable on phones and desktops. Use no remote assets, including icons. Inline SVG or simple text icons are fine. OPTIONAL ADMIN FUNCTIONALITY, can be done later but plan for it now, include what is need in index.php so we dont have to edit it again later. I explicitly want authentication through: ?adminpswd=strongpassword This is a deliberate choice. I understand the URL-password caveat. Do not replace it with a login page, account system, or different authentication method. Do not repeat warnings about this decision. - A correct password unlocks a PHP session. - Redirect to the same page without the password query parameter after processing it. - A missing or incorrect password must not unlock editing. - Store the password in admin.php, not config.json. - Provide a small "Exit editing" control to end the admin session. - Never pass the password to service links. When authenticated: - Show pencil/edit icons beside editable items. - Edit items in place using small forms, not another page or dashboard. - Provide Save and Cancel. - Allow editing the site title and welcome message. - Allow adding, editing, deleting, enabling, and disabling service links. - Provide simple up/down controls to reorder links. - Disabled links remain visible to the administrator for editing. - Allow changing the shared default theme and accent preset. - Provide a clear save/error message. Keep all admin-specific PHP handlers admin.php where practical. index.php will probably contain a few simple conditional hooks for editing. Do not create an abstraction layer just to remove those conditionals. IMPORTANT OPTIONAL-FILE BEHAVIOR Load admin.php only if it exists, using a fixed local path. Load it before HTML output so it can handle sessions and saves. If admin.php is removed: - The portal must still display normally. - No editor controls appear. - No editing/saving functionality remains. - No warnings, missing-function errors, or broken asset requests occur. admin.php is an include, not a standalone admin page. Prevent direct requests from executing its handlers. JSON CONFIGURATION Use a small, readable structure such as: { "title": "The Offline Box", "welcome": "Welcome to the local network.", "appearance": { "mode": "auto", "accent": "blue" }, "links": [ { "id": "library", "title": "Offline Library", "description": "Browse our Kiwix collection", "url": "http://192.168.50.1:8080", "enabled": true } ] } The IP address is an example, not a required network configuration. - Keep passwords and secrets out of config.json. - Use stable link IDs so edits do not depend on visible card positions. - Preserve unrelated settings when saving. - Re-read current settings while holding the save lock, then apply only the requested change. - Validate submitted values server-side. - Write complete JSON safely, with locking and atomic replacement. - Never silently overwrite malformed JSON with defaults. - Show a useful error if configuration cannot be read or saved. - Do not report success when writing fails. - Keep the implementation short; no configuration framework. BASIC SAFETY WITHOUT OVERENGINEERING - Authenticate every modifying request on the server. - Escape text and attribute output appropriately. - Never treat visitor input or JSON values as PHP include paths. - Do not allow editing arbitrary files or executing commands. - The web-server user should not need write access to PHP source files. - Explain only the filesystem permissions actually needed. FUTURE PHP ADDITIONS Include a short, fixed optional-include list in index.php, initially: files.php chat.php For each filename: - Include it only if it exists. - Missing files must be silently skipped. - Include it within the page content so it can render a section. - make it easy to add new features later, not just files and chat - Make the loaded configuration available as $config. - State that included files share the local CSS. - Do not create files.php or chat.php in this build. No automatic directory scanning, dynamic include paths, manifests, plugin manager, router, or dependency injection. Local services such as Kiwix simply get a link card; do not implement or configure those services. OUT OF SCOPE - File uploads or downloads. - Chat. - Wi-Fi, DHCP, DNS, captive portal, or OS network configuration. - User registration or multiple administrator accounts. - Database. - Rich-text editor. - Arbitrary custom CSS editor. - Theme builder. - Framework or general-purpose plugin architecture. DELIVERABLES 1. Provide complete index.php, admin.php, and config.json. 2. Provide short setup instructions for Nginx/PHP-FPM. 3. Explain how to obtain the local bootstrap CSS file. 4. Explain how to set the admin password hash. 5. Explain the minimum required file/directory permissions. 6. Explain how to add a service link and a future PHP section. 7. Include a short manual test checklist. Do not give partial snippets or placeholders in the application code. Do not claim to have tested anything you did not actually test. Before finishing, check: - Correct password enables inline editing. - Wrong password does not. - Saves require authenticated POST requests and a valid CSRF token. - Changes survive a reload. - Visitor preferences do not modify shared defaults. - Removing admin.php leaves a working read-only portal. - Missing files.php and chat.php cause no errors. - All assets are local. - There are no unnecessary files or dependencies. If a design choice is ambiguous, choose the smallest reasonable implementation and briefly state the choice. Again: SIMPLE IS BETTER. Build a small portal, not a platform. You are adding one optional feature to a small offline PHP portal on a Raspberry Pi (Debian / Raspberry Pi OS, Nginx, PHP-FPM 8.x). PRIMARY RULE: SIMPLE IS BETTER. Build a small feature, not a platform. No frameworks, CMS, plugin system, router, dependency injection, or abstraction layers. Prefer clear, readable code in as few files as practical. Do not add features I have not asked for. If a design choice is ambiguous, choose the smallest reasonable option and state it briefly. HARD ENVIRONMENT RULES - Everything must work with no internet: no CDNs, remote fonts, remote icons, analytics, or remote images. Text characters or inline SVG for icons. - No database, Composer, npm, containers, or build pipeline. Plain PHP, HTML, CSS, and minimal vanilla JavaScript. Flat files (JSON, text) for storage. - Use Bootstrap 5.3 classes for all UI (card, btn, form-control, alert, row/col, badge, list-group, etc.). Bootstrap is already served locally as bootstrap.min.css. Do not copy, edit, or reproduce it. - Phones and desktops must both work. Keep the interface clean. PROJECT LAYOUT (web root, for example /var/www/portal) index.php The portal page. Loads config, optionally includes admin.php, renders the page, then includes each feature file. Only PHP file Nginx executes. admin.php Optional include: password session, CSRF token, inline editing of the portal's own title, welcome message, and links. Never call its functions from a feature. bootstrap.min.css Local Bootstrap. portal.css Shared site styles (accent colours, small tweaks). theme.js Applies the visitor's saved theme. theme-controls.js Appearance buttons. files/ The "files" feature: its code, assets, and data. chat/ The "chat" feature. / Each future feature gets its own directory like these. Site settings live OUTSIDE the web root in /var/lib/offline-portal/config.json (CONFIG_FILE). Do not read keys other than what you need and NEVER write to it. Features keep their own data inside their own directory. FEATURE DIRECTORY CONVENTION For a feature named "new-feature" (lowercase letters, digits, hyphens): new-feature/new-feature.php Entry file included by index.php. Required. new-feature/new-feature.css Optional, only if portal.css and Bootstrap are not enough. new-feature/*.js Optional. Each JS file does only one or two small things. Vanilla JS, no libraries. The feature must still work (or degrade clearly) without JS where practical. new-feature/data/ Private data (JSON, logs, lock files). Nginx denies web access to every //data/ path. Put anything sensitive here and nowhere else. new-feature/public/ Only if files must be fetched directly by browsers (for example shared files). Anything outside data/ may be publicly readable, so never put private data there. The only edit allowed outside the feature directory is adding 'new-feature/new-feature.php' to the fixed include list near the bottom of index.php. State that exact one-line edit in your answer. Do not create automatic directory scanning, manifests, dynamic include paths, or a registry. HOW YOUR ENTRY FILE IS LOADED index.php runs: foreach ([ ... ] as $part) { if (is_file(__DIR__ . '/' . $part)) include __DIR__ . '/' . $part; } It is included inside
AFTER page output has started. Therefore: - Do NOT call header(), setcookie(), session_start(), redirect, send downloads, or emit JSON. Handle POSTs inline: validate, act, and show an alert on the same page. No redirect. - If a feature genuinely needs headers (file downloads, streaming uploads, a polling/JSON endpoint), STOP and tell me the smallest change needed in index.php or Nginx and why. Do not work around it silently and do not create a second executable PHP file without asking. - Nginx executes only /index.php. Direct requests to any other .php return 404. Never depend on a feature file being requested directly. - Forms: use method="post" with NO action attribute so the POST goes to index.php. Use relative URLs without a leading slash for your own assets (for example href="new-feature/new-feature.css") so the portal works from a subpath. Load your own CSS/JS from inside your section: and . THE CONTRACT FOR YOUR ENTRY FILE 1. First line after containing an

, with Bootstrap classes. Use the existing .btn-accent class for the primary button. Use var(--accent) and Bootstrap's --bs-* CSS variables for any custom colour so light/dark mode and the accent presets keep working. Do not hard-code colours. 4. Available variables: $config (decoded site config), $isAdmin (bool), $csrf (string, meaningful only when $isAdmin). Helpers: e() for HTML escaping, safe_url(). Constants: PORTAL, CONFIG_FILE, ACCENTS, MODES. Do not redeclare them. Do not use $here (it exists only if admin.php exists). 5. You run in the global scope. Do NOT overwrite: $config, $isAdmin, $csrf, $flash, $edit, $links, $l, $id, $on, $mode, $accent, $part, $a, $m, $configError, $isAdmin. Put logic inside functions named with your feature prefix (chat_, new_feature_) and use prefixed variable names. 6. Reserved request names (used by admin.php): POST fields action, id, csrf, title, welcome, description, url, icon, enabled, mode, accent. GET parameters adminpswd and edit. Name ALL your form fields and query parameters with your feature prefix (for example chat_action, chat_text, files_page). Never use a field named "action". 7. admin.php only handles a POST that contains "action". Your POSTs are ignored by it. Your handlers must therefore do their own checks. 8. Escape ALL output with e(). Do not output raw user text as HTML. Plain text fields only; no rich text or arbitrary HTML. SECURITY (Nginx rules are applied by me; do your part in PHP) - Admin-only actions: require $isAdmin AND hash_equals($csrf, (string)($_POST['csrf'] ?? '')); include in the form. Modifying actions must be POST only, never GET. - Public actions (for example a visitor posting a chat message): validate request method, field lengths, count and size limits, and apply a simple rate or size limit. State what you chose. - Validate every ID, file name, and path with a strict allow-list pattern (for example /^[a-z0-9_-]{1,40}$/). Never trust client file names or MIME types. Never use input in include/require, shell commands, eval, or unserialize. - Uploaded or user-supplied content must never be executable. If your feature stores anything under a web-served directory, say exactly which Nginx rules you require (for example no PHP execution, forced download, size limits, deny on data/). I will apply them; do not assume they exist. - Do not use the admin password or pass it to any link. Do not implement your own login. Use $isAdmin from the portal. STORAGE RULES - Keep private data in /data/. Write with flock() and atomic replace: write a temp file in the same directory, then rename(). Check every write. Never report success if a write fails. Never overwrite malformed data with defaults: show a clear error instead. Re-read current data while holding the lock, then apply only the requested change. - Set explicit size and count limits. - Permissions model: PHP source and feature directories are owned by root and read-only to www-data. Only /data/ (and /public/ if needed) is owned by www-data. Give me the exact commands, for example: sudo mkdir -p /var/www/portal/new-feature/data sudo chown www-data:www-data /var/www/portal/new-feature/data sudo chmod 750 /var/www/portal/new-feature/data Also tell me any php.ini (upload_max_filesize, post_max_size) or Nginx (client_max_body_size) settings required. KEEP THE PORTAL WORKING WITHOUT YOU - If your feature directory or entry file is removed, the portal must render normally with no warnings. index.php already skips a missing file silently. - If admin.php is removed, $isAdmin is false. Admin-only controls must simply not render. Never call functions defined in admin.php (field(), portal_update(), go()); write small code of your own. - Never create, require, or depend on files in other feature directories. SKELETON TO FOLLOW (adapt; keep it this small if possible)

Demo

DELIVERABLES 1. The complete entry file and any CSS/JS files, with no placeholders or partial snippets. 2. The exact one-line edit to the include list in index.php. 3. Filesystem commands and any Nginx / php.ini requirements (state them explicitly so I can apply them). 4. Short setup notes: how to use it and how to add an item or change a setting. 5. A short manual test checklist: missing or unwritable data directory, malformed data file, invalid input, oversized input, wrong CSRF token (admin actions), feature directory removed (portal still loads cleanly), admin.php removed (read-only and no errors), and the browser network tab showing only local requests. 6. A short list of design choices you made where my request was ambiguous. Do not claim to have tested anything you did not actually run. Say plainly what is untested. OLD BELOW You are adding one optional feature (for example files.php or chat.php) to a small offline PHP portal on a Raspberry Pi (Debian, Nginx, PHP-FPM 8.x). SIMPLE IS BETTER. Do not build a framework, plugin system, router, or abstraction layer. Do not add features I have not asked for. Prefer one readable file. index.php has two include lists: banner features (shown above the title, no panel, e.g. announcement/announcement.php) and panel features (wrapped in
). Say which list a new feature belongs in and give the exact one-line edit. HOW THE PORTAL WORKS - index.php has a fixed list near the bottom: foreach (['files.php', 'chat.php'] as $part) { include if is_file }. Your file is included inside
, AFTER the page HTML has started. - Consequence: your file cannot call header(), redirect, set cookies, or send downloads or JSON. Output has already begun. Handle form POSTs inline (validate, act, show a result message on the page, no redirect). If the feature truly needs headers (file downloads, upload streaming, a JSON endpoint, auto-refresh polling), STOP and tell me the smallest change needed in index.php or Nginx and why. Do not work around it silently. - Nginx only executes index.php; every other .php is blocked from direct requests (404). Never rely on requesting your file directly. - Everything works offline. No CDN, remote fonts, remote icons, analytics, Composer, npm, or database. Use plain PHP, flat files, and minimal vanilla JS. Text or inline-SVG icons only. THE CONTRACT FOR YOUR FILE 1. First line after containing an

. It sits in a Bootstrap container, so use Bootstrap 5.3 classes (card, btn, form-control, alert, row/col) and the existing .btn-accent class. Do not edit bootstrap.min.css or portal.css. If you need styles, use as few as possible: a small