# Runbook

> Attempt #2's only setup instructions lived in a plan file describing an architecture
> that had already been deleted, so they were actively wrong. Keep this file honest:
> if a command here doesn't work, fix it or delete it.

## Status

The app boots and serves. Everything marked **(#2)** is carried from the previous
attempt and has **not** been re-verified against Topcoat — treat it as a sketch.

## Requirements

- **rustc ≥ 1.95** — Topcoat 0.5 requires it, and on an older toolchain `cargo add
  topcoat` silently resolves to an empty `topcoat v0.0.0` placeholder rather than
  failing. Verified on 1.97.1.
- `git` on `PATH` (from Milestone 3 — the protocol server shells out to it)

## Dev setup

```bash
cargo run                     # serves on http://127.0.0.1:3000
cargo test
```

```bash
cargo install topcoat-cli     # dev server: watch, rebuild, asset bundling
topcoat dev                   # not yet tried on this project
```

`topcoat dev` builds, bundles assets, watches sources, and live-reloads pages that
include `topcoat::dev::script()`. How that interacts with an embedded SSH server is
unresolved — see [current.md](current.md#open-questions).

## Configuration

`STEID_`-prefixed env vars via `dotenvy` + `envy`, read into `AppConfig`. Every value
has a default, so a bare `cargo run` works with no environment at all.

Live now:

```
STEID_DATABASE_URL=sqlite:steid.db?mode=rwc   # default
STEID_DATA_DIR=./data                         # default; bare repos, used from M2
```

The bind address is **not** a `STEID_` variable — Topcoat owns it:

```bash
HOST=0.0.0.0 PORT=8080 cargo run
```

That supersedes attempt #2's `STEID_LISTEN_ADDR`.

Arriving with Milestone 1 (#2) — owner bootstrap and registration policy:

```
STEID_REGISTRATION=personal          # personal | invite | open
STEID_OWNER_EMAIL=admin@localhost.dev
STEID_OWNER_PASSWORD=changeme
STEID_OWNER_USERNAME=admin
```

In `personal` mode the owner account is bootstrapped from `STEID_OWNER_*` on first
boot. Keep `.env` and `.env.prod` out of git — both are gitignored.

## Repo layout on disk (#2)

Bare repos at `{STEID_DATA_DIR}/{org}/{repo}.git`. Created empty — no initial commit.

## SSH (#2)

The SSH server is embedded (russh), not OpenSSH — there is no `authorized_keys`
configuration and no forced command. Users register public keys through the web UI at
`/{owner}/keys`, and the server matches incoming keys by SHA256 fingerprint.

Host key generation and persistence was never written down. Sort it out during
Milestone 3 and document it here — a host key regenerated on each boot means every
client gets a changed-host-key warning.

```bash
git clone git@host:owner/repo
```

## Manual verification checklist (#2)

Attempt #2 verified these by hand each milestone but never wrote down the steps. They
are the smoke test for Milestones 2–4:

- [ ] Create a repo via the web UI → bare repo appears at
      `{data_dir}/{org}/{repo}.git`
- [ ] `git clone` an empty repo → succeeds
- [ ] `git clone` a repo with history → succeeds
- [ ] `git clone` a non-existent repo → clean error, not a hang or panic
- [ ] First push to an empty repo → succeeds
- [ ] Push to a repo with history → succeeds
- [ ] Clone with an unregistered key → `Permission denied`, exit 128
- [ ] Register key via `/{owner}/keys` → clone and push both succeed
- [ ] Revoke key via web UI → subsequent clone rejected at auth
- [ ] Clone a private repo as a non-member → rejected
- [ ] Push as a non-owner member → rejected

Worth automating as an integration test rather than re-running by hand a fourth time.

## Suggested `.gitignore` additions

Not applied yet — no code to ignore. When the app lands:

```
/data
*.db
*.db-shm
*.db-wal
.env.prod
```

**Do not add `/plans`.** Attempt #2 did, and that is why these docs had to be
hand-carried between repos.
