# Hobbesgram — Changelog
## v1.12
### Mirroring infrastructure
- **`/catalog.json`** — public endpoint (no credentials required) that serves
a full JSON manifest of every approved file. Each entry includes title,
description, category path, size, uploader, version, author, license,
requirements, tags, download count, and absolute `download_url`, `meta_url`,
and `info_url`. Response carries `Cache-Control: public, max-age=300` and
`Access-Control-Allow-Origin: *`.
- **`/download/{path}.json` metadata sidecars** — every file exposes its
metadata at the download URL with `.json` appended
(e.g. `/download/utils/archivers/unzip.zip.json`). Always public regardless
of the site's auth settings. Mirrors can fetch the binary (credentials
required if the site enforces them) and the sidecar (always open) in one
pass — the same `.zip` + `.zip.json` pairing used by the Archive.org bulk
item upload.
- **`/mirror` page** — public-facing instructions page with the catalog URL,
per-file sidecar pattern, `wget`/`curl` examples, and a copy-paste shell
script for bulk mirroring (requires `wget` and `jq`).
- **`file_public_meta(array $meta, string $base = '')`** added to
`includes/functions.php`. Strips internal fields (`stored_name`,
`archiveorg_*`) and appends computed `download_url`, `meta_url`, and
`info_url`. Accepts an optional absolute URL prefix.
- **`.htaccess`** created for v1.12 (based on v1.01). Comment clarifies why
the `FilesMatch "\.(json|ini)$"` block does not affect virtual routes
(`/catalog.json`, `*.zip.json`): those rewrite to `index.php` before Apache's
authorization phase evaluates `FilesMatch`.
- **Access gate** exemption in `index.php` — `.json`-suffixed requests under
`/download/` bypass the auth gate via `$is_public_meta`, so sidecars are
always accessible even when binary downloads require a login.
### Category slug stability
- Renaming a category in **Admin → Categories** no longer regenerates its
slug. The slug — and therefore every file URL stored under that category —
is fixed at creation time and never altered by a rename.
### Contributor upload category limits
- **`upload_category` field** on user records — single category ID that
restricts a contributor to submitting only to that category. Empty string
means no per-user restriction.
- **Global default** (`contributor_category` in `settings.json`, `config.php`
`DEFAULT_SETTINGS`) — set from **Admin → Site Settings**. Applies to every
contributor with no per-user override.
- **Priority chain**: per-user `upload_category` → global
`contributor_category` → no restriction (any category).
- **Per-user override page** `/admin/user-limits/{username}` — single
dropdown showing full category paths (e.g. `Utilities › Archivers`).
Selecting "No restriction" clears the override. Only affects the
`contributor` role; editors and admins are unaffected.
- **Admin → Users** — new "Cat. Access" column shows the restricted category
name or "All" for each contributor. A "Limits" button (contributor rows
only) links to the per-user limits page.
- **Upload form** — when a restriction is active the category field renders as
a locked read-only input; a hidden `<input>` carries the value for
submission. Server-side validation rejects any submission to a different
category even if the form is tampered with.
### Pool approval workflow
- **Inline edit + approve** — web-upload items in `/pool` now show a full
editable form (title, description, author, version, homepage, category, OS/2
version, license, requirements, tags) pre-filled from the contributor's
submission. The editor reviews and edits everything in place, then clicks
**Approve** once. The separate "Edit Metadata" step has been removed.
- **Category override on approval** — changing the category before approving
moves the physical file on disk to the new category directory and updates
`stored_name` in the metadata record, consistent with the post-approval edit
flow in `file/edit.php`.
- **Submitted-by line** — each pool item shows
"Submitted by *username* to *category path*" so the editor can compare the
contributor's original intent with the category selected in the form.
- **Reject form** is a separate `<form>` below the approve form (HTML does not
allow nested forms).
---
## v1.07
### Theming overhaul
- **CSS custom properties:** The entire inline style block in `templates/header.php`
now uses CSS custom properties (`--c-*`) on `:root`. All PHP color echoes have
been removed from the template; every element references variables.
- **`THEME_PRESETS` constant** added to `config.php` — five named presets with
full color arrays: OS/2 Classic, Dark Mode, Green Terminal, Hobbes OG, Amber.
- **Site-default preset selector** in Admin → CSS. The chosen preset key is
saved as `active_preset` in `settings.json`. Applying a named preset marks it
active; editing individual palette values clears the active preset name.
- **Per-user theme override:** Users can select a personal display theme from
their Profile page. The selection is stored as `theme_preset` in their user
JSON and applied at header render time, overriding the site default. Choosing
"Site Default" reverts to the admin-selected preset.
- **Links styled as buttons** site-wide via a global `a { ... }` rule with
inverse-color hover. Structural links (header, nav, sidebar, tag pills) use
targeted overrides to opt out of the button appearance.
- **Semantic status classes** — `.status-ok`, `.status-warn`, `.status-err` —
replace hardcoded hex color strings throughout admin pages (meta_merge, repair,
bulk_delete, admin dashboard stat numbers).
- **Readability fixes** in `pages/admin/meta_merge.php`, `repair.php`, and
`bulk_delete.php`: hardcoded white and off-white backgrounds replaced with
`var(--c-panel-bg)` and `var(--c-tr-alt)`; hardcoded border colors replaced
with `var(--c-border)`.
### Admin: file rename
- Admins can rename the physical file on disk from the Danger Zone section of
`/file/edit/{id}`.
- Validation: extension must match the original; the new name is passed through
`safe_filename()`; a collision check via `realpath()` prevents overwriting an
existing file.
- `original_name` and `stored_name` are updated in metadata. If a category move
is requested in the same form submission, the move step uses the post-rename
path.
- Orphaned-file case: if the physical file is missing, metadata is updated only.
### Admin: Quality Reports (`/admin/reports`)
New page with three reports for archive hygiene:
- **Duplicate Files** (`?r=duplicates`): groups approved files by size, then
computes MD5 within each size group to confirm true duplicates. Shows each
group with file locations and Edit links.
- **Same Filename** (`?r=samename`): groups files by lowercase filename
regardless of category path; highlights only groups with two or more locations.
- **Missing Descriptions** (`?r=nodesc`): lists approved files whose description
is absent or a placeholder value ("Unknown", "N/A", etc.).
All reports link directly to `/file/edit/{id}` for quick remediation.
### Admin: Archive.org mirroring (`/admin/mirror`)
- **Credentials** stored in `settings.json` under `archiveorg` (key, secret,
collection); configured in Admin → Settings → Archive.org Mirror Credentials.
- **Per-file upload:** PUT to `https://s3.us.archive.org/hobbesgram-{id}/{filename}`
using `Authorization: LOW key:secret`. All available metadata fields are sent
as `x-archive-meta-*` headers. `x-archive-auto-make-bucket: 1` creates the
item on first upload.
- **Batch upload:** selectable batch size (5, 10, 25, or 50); uploads the next
N unmirrored approved files in sequence. `set_time_limit(7200)` and
`CURLOPT_TIMEOUT => 7200` handle large files.
- On success, `archiveorg_id` and `archiveorg_ts` are saved to file metadata.
The file detail page shows an Archive.org link when `archiveorg_id` is set.
- Mirror status panel shows mirrored / unmirrored / total counts.
- Re-upload button available for already-mirrored files.
- Admin → index links updated to include Quality Reports and Mirror to Archive.org.
### wget / curl HTTP Basic Auth
- `auth_try_basic()` added to `includes/auth.php`. Checks `PHP_AUTH_USER` /
`PHP_AUTH_PW` (set by Apache `CGIPassAuth On`) and falls back to parsing the
`Authorization` header from `HTTP_AUTHORIZATION` or `REDIRECT_HTTP_AUTHORIZATION`
(set by `.htaccess` `RewriteRule E=HTTP_AUTHORIZATION`).
- Static helper `_basic_auth_flag()` tracks whether the current request was
authenticated via Basic Auth within the PHP request cycle.
- `auth_try_basic()` is called immediately after `auth_start()` in `index.php`.
- **401 challenge:** Unauthenticated requests to `/download/*` return
`HTTP 401 Unauthorized` with a `WWW-Authenticate: Basic realm="..."` header
and a plain-text body showing `wget` and `curl` usage, so command-line tools
know to prompt for credentials.
- **`.htaccess`** updated with `CGIPassAuth On` and
`RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]` for broad
shared-hosting compatibility.
- File detail page shows a pre-filled `wget` command for logged-in users.
### wget / curl download log
- `DLLOG_FILE` constant in `config.php` points to `data/index/dllog.json`.
- `dllog_append()` and `dllog_load()` added to `includes/storage.php`. Log is
capped at 5,000 entries (oldest entries dropped when cap is reached).
- `pages/download.php` calls `dllog_append()` whenever `basic_auth_user()` is
non-null, recording: timestamp, username, file ID, filename, IP address,
User-Agent string.
- **Download Log tab** added to `/admin/mirror`. Shows all log entries most-
recent-first, paginated at 50 per page (columns: date/time, user, file,
IP, User-Agent).
---
## v1.06
### Meta Merge: ZIP bundle support
- The Meta Merge upload form now accepts `.zip` files in addition to `.txt`
files. Each `.txt` entry inside the ZIP is extracted and parsed as a separate
source; all matched entries are merged in a single review session.
- Upload size overrun detection: when PHP silently drops the POST body because
`post_max_size` is exceeded, an informative error is shown rather than a blank
form.
### Meta Merge: improved filename auto-matching
- A file is now auto-matched when its filename is unique across the entire
archive, regardless of whether a `DIR` path was present in the `.txt` entry.
Previously auto-match only fired when the path field was empty.
### Storage: legacy encoding robustness
- `storage_write()` now uses `JSON_INVALID_UTF8_SUBSTITUTE` when encoding JSON.
Invalid byte sequences (e.g. CP850 / CP437 characters in legacy OS/2 text
files) are replaced with U+FFFD instead of causing `json_encode()` to return
`false` and silently lose data.
---
## v1.05
### Shared hosting server config
- **`.htaccess`** added to web root with: `Options -Indexes`,
`mod_rewrite` rules for clean URLs, `DirectoryIndex index.php`,
PHP limit overrides (`upload_max_filesize 200M`, `post_max_size 210M`,
`memory_limit 256M`, `max_execution_time 300`), block rules for direct
access to `.json` and `.ini` files.
- **`.user.ini`** added for PHP-FPM environments (equivalent limit overrides
without requiring `.htaccess` PHP directives support).
---
## v1.04
### Admin: Archive Repair tool (`/admin/repair`)
New read-only integrity report for administrators:
- **Orphaned files** — physical files in `data/uploads/` that have no matching
metadata record in `data/files/`.
- **Empty-browse categories** — categories with no approved files and no
sub-categories (candidates for pruning).
- **Duplicate categories** — category names that appear more than once under
the same parent node.
No automatic changes are made; the report is informational only.
---
## v1.03 (2026-03-24)
### Meta Merge tool (Admin / Editor, `/admin/meta-merge`)
- Upload a `.txt` file to bulk-import metadata into existing archive entries
without re-uploading the files themselves.
- Parses two formats automatically:
- **hobbes.txt:** dashed-separator blocks with `DIR` / `FILE` / `DESC` fields.
- **pmmail.txt:** aligned `key: value` pairs (Archive Filename, Short
Description, Long Description, Proposed directory, Your name, Program URL,
Operating System/Version, Additional requirements).
- Matches entries to archive files by normalising the `DIR` path to slugified
category segments and comparing against each file's stored
`category_upload_path` + `original_name`.
- Three-phase workflow: upload → review report → apply.
- Review page shows:
- *Matched entries* — before/after diff table per field; individual checkboxes
to include or exclude each entry.
- *Unmatched entries* — filename-only suggestions, manual file-ID input, or
Skip.
- *Skipped summary* — entries marked skip listed for reference.
- Merge mode: fill blank/"Unknown" fields only (default), or overwrite all
existing values.
- Approved files are re-indexed in the search index after merge.
- Temporary sessions stored in `data/merges/` and auto-expired at 2 hours.
### Bulk Delete tool (Admin only, `/admin/bulk-delete`)
- Select any category to list its files (approved and pending).
- Per-page selector: 25 / 50 / 100 / All. Warning banner when "All" is chosen
with more than 250 files.
- "Select all on this page" and "Select ALL N files in this category" checkboxes.
- JavaScript confirmation dialog shows the deletion count before committing.
- Removes physical file from disk, metadata JSON, and all search index entries.
- Empty category upload directories are pruned after deletion.
### Single-file delete from edit page (Admin only)
- Danger Zone section added at the bottom of `/file/edit/{id}`.
- Visible to admin role only. Confirmation dialog includes the filename.
- Handled by `POST /admin/file-delete/{id}`; redirects to the file's category
browse page after deletion.
### Download counter displayed in all views
- **browse.php:** "DLs" column added to the file listing table.
- **file.php:** "Downloads" row added to the file detail info table.
- **search.php:** "DLs" column added to search results table.
- Counter has been tracked since v1.01; this release makes it visible to all
users including unauthenticated OS/2 guests.
### Approve All Web Uploads button (`/pool`)
- A summary bar appears when one or more web-uploaded files are pending approval.
- "Approve All Web Uploads" approves every pending web upload in a single action
(`POST /pool/approve_all`, CSRF-protected).
- FTP single files and FTP folder imports are intentionally excluded (they require
metadata to be filled in or imported first).
- JavaScript confirmation dialog shows the count before committing.
### Admin dashboard links updated
- "Meta Merge" and "Bulk Delete" (styled red) buttons added to the Site
Management panel.
---
## v1.02
### FTP folder batch import (Editor+, `/pool`)
- Drop an entire directory tree into `data/pool/` via FTP.
- The pool listing detects folders and presents a Batch Import Folder button.
- Sub-folders are mapped to nested categories using `category_find_or_create()`,
which creates missing categories on the fly (slugified, de-duplicated) without
overwriting existing ones.
- Files receive titles derived from their filenames; required fields default to
"Unknown" and can be corrected post-import.
- Approve-immediately checkbox available for trusted bulk imports.
- The pool folder is recursively removed after a successful import.
- Reject & Delete Folder button purges the folder without importing.
### "Hobbes OG" CSS preset (Admin → CSS)
- Fourth built-in color preset added.
- Deep navy blue palette inspired by the original hobbes.nmsu.edu color scheme:
dark blue panels, cream text, teal accents.
### New helper functions
- **`category_find_or_create()`** (`includes/functions.php`) — looks up a
category by name + parent; creates it with a unique slug if absent. Used by
folder import.
- **`category_upload_path_from_cats()`** (`includes/functions.php`) — variant
of `category_upload_path()` that accepts an explicit array instead of the
static-cached result. Required during batch import when the category list has
been mutated but not yet written to disk.
- **`rmdir_recursive()`** (`includes/functions.php`) — recursively removes a
directory tree. Used to clean up pool folders after import or rejection.
- **`pool_folder_file_count()`**, **`pool_folder_files()`**
(`includes/storage.php`) — helpers for counting and listing files inside pool
folders during batch import.
### Fixed: Pool route regex (`index.php`)
- Changed `[a-z]+` to `[a-z_]+` so that `import_folder` and `reject_folder`
action names are matched correctly.
---
## v1.01 (initial release)
### Core archive system
- Flat-file JSON storage; no database required.
- Atomic file writes via temp file + `rename()` (no `flock()`).
- Custom PHP session handler using the same atomic pattern.
### Browsing and downloads
- Category tree browser (`/browse`, `/browse/{slug}`).
- File detail page with full metadata display.
- Download streaming in 64 KB chunks with per-file download counter.
- Canonical URL scheme: `/files/{cat/path}/{filename}` and
`/download/{cat/path}/{filename}`.
- Legacy `/file/{id}` redirects (301) to canonical URL.
### Search
- Inverted keyword index (`data/index/search.json`).
- AND search with OR fallback; stop-word filter; minimum 3 characters.
- Index updated on approval and on metadata edit.
- Admin: Rebuild Search Index action.
### User system
- Four roles: guest, contributor, editor, admin.
- OS/2 User-Agent auto-detection for guest browse access.
- Invite-code registration (invite generates code, recipient redeems).
- Open registration toggle in site settings.
- Password hashing via bcrypt (`PASSWORD_BCRYPT`).
### Uploads and approval
- Web upload form for contributors (`/upload`).
- FTP pool import for single files (`/pool`).
- Optional `.meta.json` companion file to pre-fill pool import forms.
- Editor approval workflow: approve, reject, or edit before publishing.
- File stored using category slug path (`uploads/{cat}/{subcat}/file`).
### File editing and organisation
- Edit metadata for any approved file (`/file/edit/{id}`).
- Category change moves physical file on disk and updates `stored_name`.
- Legacy per-file `id/` directory layout supported and cleaned up on move.
### Admin tools
- Site settings: name, tagline, open registration toggle.
- CSS theme editor: 26 individually configurable colors.
- Three CSS presets: OS/2 Classic, Dark Mode, Green Terminal.
- Category manager: add, rename, nest, delete.
- User manager: list, change role, deactivate.
- Landing page and splash screen Markdown editors.