# Architecture

How the rebuilt board at `app/Forum/` is put together. For *why* it works the
way it does — what the legacy board got wrong and what was rescued — read
[`HISTORY.md`](HISTORY.md).

---

## One engine, two boards

`hef.cfm` served two products through an `hTbl` switch, and so does this:

| `src` | Tables | What it is |
|---|---|---|
| `hef` | `hef`, `hef2`, `hef_old`, `hef2_old`, `hef_s` | The **Forum** — open discussion |
| `he` | `he`, `he2`, `he_old`, `he2_old`, `he_s` | The **Help Center** — a ticket desk |

Both are selected with `?b=hef` / `?b=he`. Every page works on either board;
only the chrome and a few Help-Center-only pages (`tickets`, `queue`) differ.

`Base.board()` is the validator — it rejects anything not in `SRC_OK` before it
can reach a table name. It is called `board()` and **not** `src()` because an
argument named `src` would shadow a method of the same name (see
[`CONVENTIONS.md`](CONVENTIONS.md)).

`request.forumBoard` is set once by the router, and `Base.link()` appends `b=`
automatically — so once you are in the Help Center every link stays there
without each call site remembering.

---

## Request flow

```
index.cfm  (front controller)
  │
  ├─ bootstrap ......... ../s_loadvar.cfm if the application scope is cold
  ├─ F = new components.People()      one object, the whole chain
  ├─ board ............. ?b= / form.b / legacy ?f=he*  →  SRC
  ├─ route ............. ?p=<page>, plus legacy ?hi= / ?ch= compatibility
  ├─ POST action? ...... do it, stow a flash, redirect (POST-Redirect-GET)
  ├─ cfsavecontent ..... views/<page>.cfm
  └─ render ............ chrome (top bar, sidebar, rail, footer) around it
```

**Writes always redirect.** The legacy board re-rendered the list inline after a
write, so a browser refresh re-posted and the back button showed a stale page.

**Route parameters read `form` first, then `url`.** Actions POST their target as
a hidden field; reading `url` alone made `id` 0 on every POST and broke reply,
edit, pin, lock, move, archive, report and subscribe simultaneously.

---

## Why server-rendered

The design mockup (`app/New_Forum/`) built its chrome in JavaScript because it
had to work from `file://`. This has a server, so all of it is HTML.

That is not just tidiness. A forum is a body of text people find through search
engines and link to each other; chrome and content that only exist after a
script runs are not reliably indexable, not linkable, and gone entirely if the
script fails. `assets/js/forum.js` is **enhancement only** — delete it and the
board still renders, navigates, posts and moderates.

---

## Component chain

A single inheritance chain, so a view instantiates **one** object and gets
everything:

```
People  →  Mod  →  Posts  →  Board  →  Text  →  Base
```

| Component | Owns |
|---|---|
| `Base` | viewer identity (`me()`), **all permissions**, query runner, section map, formatting, `link()` |
| `Text` | storage form in (`store`), sanitised HTML out (`render`), `safeStyle`, `toEditable`, excerpts |
| `Board` | index, section listings, threads, replies, search, tickets, read state, view counts |
| `Posts` | create, reply, edit, remove, CSRF, flood control, subscriptions, notifications |
| `Mod` | pin, lock, move, archive, restore, mark-answer, reports, modlog |
| `People` | member directory, profiles, **staff directory**, notifications, PM bridge |

Subclasses can call the private helpers above them — `visibleTypeList()`,
`visibilitySQL()`, `shapeThread()`, `threadCols()` all live on `Board` and are
used by `Posts` and `People`. **Do not re-implement them locally.** `People`
once carried its own copies and they drifted: neither knew that types 200+ are
public, so profiles silently hid an author's Announcement posts.

---

## No `Application.cfc` in this directory

Deliberate. `app/Forum/` inherits `app/Application.cfc` and therefore the live
player session (`session.userid`, `session.username`, `session.adminflag`) set
by `p_login.cfm`.

Adding one — the way `app/Admin/` does — would give the forum its own empty
session scope and log every player out at the door.

Consequences to remember:

- `onRequestStart` sets `enablecfoutputonly = true`; the router turns it off
  with `<cfsetting enablecfoutputonly="no">` so views can be written as ordinary
  HTML with targeted `<cfoutput>` blocks.
- `url.f` is intercepted by `Application.cfc` to include
  `Modules/Controllers/<f>.cfc` when such a file exists. The forum routes on
  `p`, so there is no collision — but do not add a page named after a
  controller.

---

## Data model

Nothing is imported or mirrored. Every query reads the legacy tables in place,
so what the forum shows is what the database holds and `hef.cfm` keeps working
against the same rows.

```
hef        id, nshort(30), nlong, usernic(20), userid, datetime,
           lastuserid, lastusernic, lastpost, type, apost, auserid,
           ausernic, atime, closeflag, publicflag, paidflag
hef2       id, belongto, nlong, usernic(20), userid, datetime
hef_s      id, nic          -- a row's PRESENCE means "pinned"
he_type    id, short, name, detail, publicflag, access, orderby, emailflag
```

Everything the new board adds lands in a side table keyed by
`(src, thread_id)` — see [`DATA-MODEL.md`](DATA-MODEL.md).

### Live and archive are one listing

The legacy board treated `_old` as a *mode* you switched into
(`session.hearchive`), so archived threads were invisible unless you found the
dropdown. Here they are UNIONed with an `archived` marker, because "closed in
2011" is a property of a thread, not a different website.

**The dedup is load-bearing.** Closing a thread copies it to `_old` *and*
deletes the live row; "tag as answered" copies **without** deleting. So 2,400
`hef2` rows and 73 `he` rows sit in both tables, and a naive `UNION ALL` lists
them twice. Every union filters the archive branch with `NOT EXISTS`.

---

## Related

- [`ACCESS-CONTROL.md`](ACCESS-CONTROL.md) — the three-permission model. Read
  this before touching anything that decides who sees what.
- [`DATA-MODEL.md`](DATA-MODEL.md) — the `forum_*` tables and what each is for.
- [`CONVENTIONS.md`](CONVENTIONS.md) — CFML/SQL rules and the traps already hit.
- [`DEVELOPMENT.md`](DEVELOPMENT.md) — running it, testing it, migrations.
- [`HISTORY.md`](HISTORY.md) — what the legacy board was and what changed.
- [`../Skills/`](../Skills/) — task playbooks.
