# forums-btw A minimal forum. PHP, SQLite, server-rendered HTML, hand-written CSS. No Composer, no build step. JavaScript is optional and vanilla (no npm) - the whole thing works with JS off. Tokyo Night palette, Iosevka mono - matches my Alacritty/nvim. ## Run ```sh just dev # php -S localhost:8889 -t public just dev 3000 # custom port just reset-db # wipe the local sqlite db ``` The database is created automatically at `data/forum.db` (override with `FORUM_DB`). Schema is applied on first connection. Structure follows the classic phpBB / Gentoo-forums index (categories → boards with topic/post counts + last-post column), which is also roughly the GameFAQs board-list layout. ## Features - Register / login / logout - sessions, `password_hash`, CSRF tokens - Boards grouped into categories, all created via `/admin` after first login - Threads with an OP post and replies, scoped to a board - Roles: `admin`, `mod`, `normal` (first registered user becomes `admin`) - mods & admins can pin, lock, and delete threads, and delete replies - locked threads block replies for normal users - admin panel at `/admin` (admin-only): user list with role/join/counts, promote/demote (CSRF-protected, can't change your own role) - **bootstrap:** the first account registered on a fresh DB becomes `admin`; everyone after is `normal`. On a public deploy this is first-to-register-wins, so register immediately after going live. - Per-user profile pages with role badge - Post formatting: **Markdown** via vendored Parsedown (safe mode - raw HTML escaped, no XSS); legacy inline tags (` `) still render - Fenced code blocks (```` ```c ````) get syntax highlighting via the bundled `Hl` highlighter (C implemented; other languages fall back to plain text) - **Search** over threads + posts via SQLite FTS5 (`/search`, works with no JS), kept in sync by insert/delete triggers - **telescope.js** - a Telescope-style fuzzy-finder modal (open with `/` or `Ctrl+K`): live results, list + preview, `↑↓`/`Ctrl-j/k` to move, `⏎` to open. Progressive enhancement over the `/search` page. - **Image uploads** (login required) - paste, drag-drop, or pick a file into a post box; inserts `![](…)` Markdown. Local disk under `public/uploads`, up to 25 MB. - Security: validated by content (`getimagesize`), not extension; only PNG/JPEG/GIF/WebP (no SVG); random filenames; never writes/executes a `.php`. `public/uploads/.htaccess` disables script execution + sets `nosniff` (Apache); on nginx, serve `/uploads` statically and don't route it through PHP. - No-JS fallback: the `/upload` page returns a Markdown snippet to paste. - Dev server raises PHP upload limits via flags in the `justfile`; production must set `upload_max_filesize`/`post_max_size` accordingly. - Server-side **Preview** button on the new-thread and reply forms (no JavaScript) - **Tokyo Night** theme (matches my Alacritty/nvim) and a lualine-style modeline footer with contextual segments - Optional **vim mode** in the post boxes - a single vendored `public/js/vim.js` (no npm, no build, off by default, preference saved in localStorage). Supports normal/insert/visual, `hjkl w b e 0 ^ $ gg G`, `i a A I o O`, `x dd D dw cw C r`, `yy p P`, `u`, visual `d y c x`, and `:w`/`:wq`/`:x` to submit. The textarea works normally with JS disabled. ## Layout Procedural, data-oriented MVC - free functions over plain data, enums for fixed types, globals for shared state (`$dbh`, `$current_user`, `$route`, `$params`). No classes-as-services, no DI. ``` public/index.php thin front controller (match route -> call controller fn) config/paths.php PATH_TO_* constants config/init.php opens global $dbh, runs migrations, boots session, sets $current_user config/routes.php path-regex -> controller function name lib/db.php db_connect() / db_migrate() (global $dbh) lib/session.php session_boot(), auth(), csrf_token()/check_csrf() lib/render.php set_view() / render() / render_json() / require_login()/_mod()/_admin() lib/helpers.php esc / redirect / time_ago / role_badge / format_body lib/Highlight.php Hl syntax highlighter (C); lib/Parsedown.php Markdown types/Role.php enum Role { is_mod(), is_admin(), badge() } models/*.php free functions, global $dbh, heredoc SQL (board/thread/post/user/search) controllers/*.php free functions: validate -> call models -> set_view/render views/*_View.php plain PHP templates; partials included via PATH_TO_VIEWS_DIR ``` Request flow: `public/index.php` matches the path against `config/routes.php`, sets `$route`/`$params`, and calls the controller function, which ends in `render()` (which `include`s the `*_View.php`) or `redirect()`. Conventions: `snake_case` functions/variables, `Upper_Snake` classes/types/enums, lowercase `/** this function … */` docblocks, heredoc SQL, `$dbh`/`$sth` handles.