# Subnetris - Block Subnetting Trainer

**Block subnetting the way the lecture draws it.**

Subnetris is a single-page IPv4 subnetting trainer built around one picture:
the allocation as a horizontal block, its network number on the left edge in
blue, its broadcast on the right edge in red, its prefix in the middle. You cut
the block by dragging inside it. Every cut is a halving, the parent prefix greys
out at the bar, and the table underneath fills in with the ITS 2300 handout's
columns, down to the `Total IPs` and `USABLE IPs` summary. The columns have
since been reworked at the instructor's request (the size column dropped, the
prefix folded into the network number, shorter labels), so it carries the
handout's content rather than its exact layout.

The lesson it exists to teach is one sentence: **a subnet is a block, and a
block can only be cut in half.** Everything else follows from that.

**The tool cannot draw an illegal subnet.** The model is a binary tree, so the
only operation that exists is halving, and a bar can only sit on a power-of-two
boundary. Binary Law is not a rule the tool checks; it is a rule the tool is
made of. Point at an address that is not a boundary and the cut still lands on
one, with the readout naming the address you wanted, the address it used, and
how many halvings would reach yours.

**Every number is derived, never stored.** A block's network number, broadcast,
mask and usable count all come from its position in the tree, so the table and
the diagram cannot drift apart.

It works with the two tools the course already uses: a link from the ECT Visual
Subnet Calculator opens here with its divisions and labels intact, and the menu
opens the current plan back in the calculator.

## Documentation

| Page | Audience |
| --- | --- |
| [`docs/index.html`](docs/index.html) | Entry tile page. What Subnetris is and the two decisions that shape everything else. |
| [`docs/student.html`](docs/student.html) | The block picture, the four ways to cut, why the bar snapped, the table column by column, the N-2 rule and the tax rows, the course conventions, exercises and what each Check message means, random codes, drill mode, sharing, the cheat sheet and the keyboard. |
| [`docs/faculty.html`](docs/faculty.html) | The built-in set with what each exercise teaches, posting a code that gives a whole class the same problem, editing `files/exercises.csv`, what the checker enforces versus what it only notes, drill mode in class, reading a student's link, and the known limits. |
| [`docs/maintainer.html`](docs/maintainer.html) | The tree model and the cut primitive, the share-link grammar and calculator interoperation, the render geometry, the pointer state machine, the checker and the solver, the generator, and the traps that have already bitten. |

The in-application **Help** modal is user-facing documentation too, and it is
the first thing a student reads. It carries the cheat sheet.

## Run

Open [`index.html`](index.html) directly, or serve the directory:

```sh
python -m http.server 8000     # then visit http://localhost:8000/
```

Double-clicking the file works. Under `file://` there is no origin, so the
exercise library and the picker's categories are taken from the copies
compiled into `src/exercises.js` instead of `files/exercises.csv` and
`files/categories.csv`; nothing else differs.

There is no build step, no package manager, no framework and no external
resource of any kind.

## Share links

The address bar carries the whole plan. Keys are elided at their defaults, so a
fresh page has an empty hash and any two identical plans produce identical
links.

| Key | Meaning | Default |
| --- | --- | --- |
| `n` | The allocation in CIDR form | `10.200.5.0/24` |
| `d` | Division string, pre-order, `1` split, `0` leaf | `0` |
| `r` | `name:hosts` with the exercise file's `+` modifiers, names URI-encoded | empty, and elided under an exercise |
| `a` | `index:path` assignments | empty |
| `x` | Exercise id or random code | absent |
| `f` | Floor prefix | `30` |
| `c` | Conventions in force, `key:value` pairs joined by `;`, only the keys that differ: `gateway:first`, `statics:end`, `dhcp:none`, `dns:a.b.c.d\|e.f.g.h` or `dns:none` | elided at the lecture's defaults |
| `p` | Addressing plans, `index~gateway~first-last~name@addr` records | elided while every plan is empty |
| `q` | Drill fields; presence turns the drill on | absent |
| `v` | Grammar version | `1` |

Examples:

```
#d=1100100                                  four /26 out of the default /24
#x=E4                                       exercise E4
#x=SN1-DPK7M3Q                              a random exercise from its code
#n=45.2.148.0/22&d=100&r=NY:32,Austin:60    a plan of your own
?network=10.200.5.0&mask=24&division=7.31   an ECT calculator link, accepted and rewritten
```

Drill answers never travel, so a shared drill link always arrives blank.

## Layout

| File | What it is |
| --- | --- |
| [`index.html`](index.html) | The shell: one toolbar (brand, mode pill, network box, Exercises, Check, hamburger), the three regions (needs panel, diagram, table) with their resizers, then every modal and the ordered `<script>` tags. There is deliberately no ECT header band; the attribution lives in the About modal, as ENE carries it. |
| [`styles.css`](styles.css) | Token-only theming: one `:root` block declares every colour, font and dimension, one `body.dark` block re-declares the same properties, and High-Visibility and the breakpoints re-declare tokens rather than restating rules. |
| [`scan-deps.json`](scan-deps.json) | The module layering, asserted by `w:\tools\scan.py` against the script order. |
| [`files/exercises.csv`](files/exercises.csv) | The exercise library, RFC 4180, editable in a spreadsheet. Mirrored in `src/exercises.js` for `file://`; a harness keeps the two byte-identical. |
| [`files/categories.csv`](files/categories.csv) | The picker's categories: name, kind (`list` or `random`) and the sentence shown under the pull-down. Mirrored and byte-checked the same way. |
| [`images/`](images/) | `ducky-small-canvas-clear-subnetris-128.png`, a 128px Lanczos resize of the supplied ducky, is the favicon, the apple-touch icon, the toolbar brand mark and the docs pages' favicon. The full-size `ducky-small-canvas-clear-subnetris.png` stays for the portal tile in `apps.json`. `../Ohio-Primary-Logo.png` appears in the About modal. `knife-cursor.svg` is the pointer over a block in the diagram, with `knife-cursor.png`, a 32px raster of the same drawing, as the fallback for a browser that will not take an SVG cursor; the hotspot is the tip of the blade at (2,2) in both, and the PNG is regenerated from the SVG, never edited. |
| [`docs/`](docs/) | The four documentation pages, sharing the root `../../docs.css`. Their tables of contents are hand-maintained and checked by `sndev/toc.py`. |
| [`testing/`](testing/) | Seventeen harnesses, each named after the backlog row that introduced it. Each loads the real shipped sources with `window.__skipAppInit = true` and reports `PASS n` or `FAIL n` in the document title. |
| [`CLAUDE.md`](CLAUDE.md) | The design document and durable context. Read it before changing anything. |
| [`TODO.md`](TODO.md) | The backlog, sorted S to L, with the Done rows recording why the code looks the way it does. |

Python tooling lives in the sibling `w:\tools\sndev\`, never here: this
directory is a public docroot.

| Tool | What it does |
| --- | --- |
| `sndev/sweep.py` | Runs every harness headless and prints a `test / status / detail` table ending in ALL PASS or SOME FAILED. |
| `sndev/shot.py` | Drives the real page through a scripted gesture over `file://`, writes `snshot.png` and prints the model state and any console error. |
| `sndev/verbs.py` | Compares every `data-action` in the shell against the verbs the router registers, so a dead button cannot ship. |
| `sndev/toc.py` | Asserts every docs heading has a table-of-contents entry and every entry has a heading. |

### src modules

One IIFE per file, all hanging off `window.Subnetris`. The script order in
`index.html` is the dependency order; a lower module never names a higher one.

| Module | Responsibility |
| --- | --- |
| [`src/rng.js`](src/rng.js) | FNV-1a into mulberry32, derived streams spun off by XOR, and the tier and flag mix. A pass may consume only its own stream, so adding a tier later cannot reshuffle an already-posted code. |
| [`src/net.js`](src/net.js) | IPv4 arithmetic, DOM-free and Number-based: parse and format, total and usable counts, masks in dotted decimal and binary, alignment, ranges, subdivision, the prefix a host count needs, the trailing-octet shorthand and the cheat sheet. Usable counts follow the lecture, so a /31 and a /32 hold none. |
| [`src/plan.js`](src/plan.js) | The addressing plan inside one subnet: gateway, DHCP pool, named statics. Conventions A to D made checkable and configurable (`CONVENTIONS` is the lecture's default set, `normalizeConventions` the one vetting point), with problems (a plan that cannot work, including no gateway at all) reported separately from notes (a plan that bends a convention). Pure, no DOM. |
| [`src/model.js`](src/model.js) | The block tree. Nodes carry only their children and their assignment; every number is derived from the path. One `commit` chokepoint drives undo, drill pruning, selection pruning and the change notification. `cutAt` is the single primitive behind the drag, the click, the typed address and the keyboard. |
| [`src/codes.js`](src/codes.js) | The hash grammar, the ECT calculator's query form in both directions, and `validate`, the one type-restricted function that vets external state. |
| [`src/exercises.js`](src/exercises.js) | The library and the picker's categories (`files/categories.csv`, one row per category with its kind and description, Random among them), an RFC 4180 reader and writer for both files, row validation, and the built-in fallbacks used when there is no origin to fetch from. |
| [`src/gen.js`](src/gen.js) | Seeded random exercises by tier from a code. Host counts are drawn so exactly one prefix is correct, and block sizes are drawn so the set always fits. |
| [`src/check.js`](src/check.js) | The hierarchy checker, its message catalogue, and the buddy-allocation reference solver that proves an exercise is solvable. |
| [`src/drill.js`](src/drill.js) | Which cells are asked, what each should hold, how a typed answer is normalised, and the per-cell grade. |
| [`src/render.js`](src/render.js) | Pure: model in, SVG or HTML out. No handlers, no state, no stylesheet reads. Colours arrive as options and are written as attributes so a serialised copy survives PNG export; every string is filtered to printable ASCII. |
| [`src/ui.js`](src/ui.js) | The one `data-action` router, the pointer state machine (hover selects, a drag from a block cuts or assigns, a drag from a needs row assigns from the other end, a bar press rejoins), the keyboard, the modals, the clipboard and PNG paths, and the refresh pipeline, which rebuilds everything from the model except on a selection change, where the marks move in place. |
| [`src/theme.js`](src/theme.js) | Light, dark and auto computed to `body.dark` with a live media-query listener, High-Visibility, panel sizes and collapse, Block Display (both panes closed, restored as they were), the resizers, and the Firefox reset of exactly four keys. |
| [`src/menu.js`](src/menu.js) | The hamburger drawer, its accordion flyouts positioned from JavaScript, and the state badges. |
| [`src/app.js`](src/app.js) | Boot order, the address bar in both directions, the first-run card, and the boot guard on the last line. |

The `render.js` and `ui.js` split is the one that matters: render is pure and
returns elements, ui owns every handler and all state.

## Conventions

- **No build, no server, no external resources.** No CDN, no fonts, no
  libraries. Verified by `python scan.py subnetris` from `w:\tools`.
- **Public docroot.** This directory is served publicly. Never leave a scratch
  file here; scratch lives in `w:\tools\sndev\` and is deleted.
- **The model is the source of truth.** Render derives from it and every
  mutation goes through `commit`, so the screen and the address bar cannot fall
  out of step with it.
- **Validation lives at one boundary.** `codes.validate` is the only function
  that vets a link, a query string or anything else externally supplied.
- **A need states its terms, never its answer.** A requirement may carry a
  growth percentage and a flag saying the gateway is not part of its count.
  `model.hostsNeeded` is the one place that combines them, and the total is
  printed only by the check report, because working it out is the exercise.
- **A broken plan fails, a bent convention does not.** `plan.validate` returns
  problems and notes separately, and each problem names the rule of the
  hierarchy it breaks so the checker files it under N-2 or business need
  without re-reading the wording. A missing gateway is a problem, because
  a network without one does not work.
- **The tool never places the gateway.** No placeholder, note or button
  hands the student the gateway address before they commit one; the
  conventions say where it usually goes, and Check says so afterwards.
- **Conventions are the model's, and travel in the link.** `model.conventions`
  defaults to the lecture's A to D and is changed from the menu; `c` carries
  only what differs. The check report, the plan notes, the drill's gateway
  answer and the reference solver all read it.
- **Typing must not rebuild the DOM.** A keystroke commits quietly: the model
  and the address bar update and nothing on screen is replaced, so the caret
  survives. Validation appears when the field is left.
- **Hovering must not rebuild the DOM either.** Pointing at a block selects
  it, and a selection change moves the marks on the diagram and the table in
  place (`render.markSelected`) and rebuilds only the card, so the table and
  the needs list keep their scroll positions under a moving pointer.
- **Default elision keeps links byte-stable.** A new persisted field is elided
  at its default, so adding a feature does not change the link of a plan that
  does not use it.
- **Lecture semantics for usable counts.** A /30 holds 2, a /31 and a /32 hold
  none, matching the handout cheat sheet. RFC 3021 is noted in Help as an aside
  and is deliberately not modelled.
- **ASCII only inside the SVG.** Arrows are `->`. The picture has to survive a
  PNG export and a printed page.
- **Only preferences persist.** Four `subnetris.` keys in `localStorage`, and
  on Firefox a reload clears exactly those four. Work travels in the link.
- **Never an em dash or an en dash** in code, comments, UI text or docs.
- **No ellipsis in a menu or button label.**
- Attribution is **Ohio University, ECT Dept**. Not Ohio State, not ITS.
- Comments earn their place on non-obvious mechanics: the cut candidate search,
  the width clamp, the specificity override on the shared drawer stylesheet.
  DOM plumbing does not get narrated.
- `git-go.bat` (add, commit, push) is the user's to run.

## License

See [LICENSE](LICENSE). MIT, Copyright (c) 2026 Ohio University - ECT Department.
