# Link Menu System

A self-hostable **link portal**: a responsive grid of your organization's links, set as
everyone's browser new-tab / start page — plus a **canary router** that makes sure a device
off the office network never lands on a "page not found."

No database, no framework, no build step. Two PHP pages over flat-file JSON.

- **Portal** (`int/index.php`) — the panel grid, with inline admin editing (edit links,
  panels, headings, banner, and settings in place; drag-and-drop reorder; hide/delete
  panels). Renders from `config.json`.
- **Canary router** (`ext/index.php`) — the page you actually set as the start URL. It loads
  a tiny "canary" image that only exists on the internal server: if it loads, redirect to the
  internal portal; if it errors or times out (3s), redirect to a public fallback URL. Reads
  `router-config.json`.

## Requirements

- A web host that runs **PHP** (no extensions or database needed).
- The two halves are usually deployed to two locations: the **portal** on an internal/in-house
  server, the **router** on the public web. For a demo they can sit side by side.

## Install

For the full walkthrough — verifying both branches and troubleshooting — see
**[`INSTALL.md`](INSTALL.md)**. The short version:

1. **Unpack** this bundle on your web host. Serve `int/index.php` and `ext/index.php` as
   `index.php` at whatever paths you choose (e.g. `int/` internal, `ext/` public).
2. **Portal first run:** browse to the portal. It auto-creates `config.json` (seeded with a
   sample layout) and walks you through creating a single admin account (stored bcrypt-hashed
   inside `config.json`). Log in via the small lock icon in the footer, then edit in place.
3. **Canary image:** the portal ships a real 1×1 `int/canary.png`. The router probes this to
   detect the internal server. Keep it a valid, tiny, loadable image — an empty/broken file
   makes the router always fall back to external.
4. **Router config:** the router auto-creates `router-config.json` with placeholders. Edit it
   so:
   - `internal_url` → your internal portal URL
   - `external_url` → your public fallback URL
   - `canary_image` → the URL of the portal's `canary.png` on the internal server
5. Set the **router** URL as the browser start / new-tab page on your devices.

## Instance state (not shipped, not committed)

These files are generated/edited on the server and are **not** part of this bundle:

- `config.json` — portal layout, site settings, and the bcrypt-hashed admin credential
  created at first-run setup (auto-generated on first run)
- `router-config.json` — the router URLs (auto-generated, then edited)

> Security note: `config.json` (which holds the hashed admin credential) lives inside the
> webroot, so it may be fetchable by URL. The bundle ships a `.htaccess` denying `*.json`
> on Apache/LiteSpeed; nginx users add the equivalent from [`NGINX.md`](NGINX.md). Moving
> these outside the webroot via `APP_*` env vars is optional per-deployment hardening, not
> something the stock install does. Don't put real credentials on a public host before at
> least the deny rule is in place.

## Versioning

Each PHP file carries an internal `lms <build>.<ts6>` stamp near the top. Auto-generated JSON
files carry a `_lms_version` field tying the data to the code that generated it.

## License

MIT — see `LICENSE`. © 2026 smisco &lt;info@smisco.biz&gt;
Website: https://smisco.biz/lms
