Closes#8, #9, #10; partially addresses #11.
middlewares (#8):
- Log: log once from the Middleware closure after the handler returns
instead of spawning a goroutine per Write (data race on the shared
LogWriter, multiple log lines per request). Bodiless responses
(204/304/redirects) are now logged too, and the status-class switch has
a default branch so a stray sub-100 status can't nil-deref the event.
- logWriter forwards http.Flusher / http.Hijacker / io.ReaderFrom so
streaming handlers and the file server keep their fast paths.
- Both middlewares use the closure idiom rather than storing next on a
shared instance, so they're safe to register on more than one chain.
- CorsOrigin now honours its argument: a comma-separated allow-list,
with "*" meaning allow-any. Wired to a new --allow-origin flag that
defaults to "*" (unchanged behaviour out of the box).
ReadFile (#9):
- Add the missing returns after GetFile / open failures (the old code
fell through to os.ReadFile(nil path) and wrote a second body).
- Hand off to http.ServeContent for Content-Type, Range and
If-Modified-Since, and set Content-Disposition: inline so the embedded
audio player streams instead of downloading (the download button
already uses the HTML download attribute).
lib.Coalesce (#10):
- Constrain to comparable and compare against a fresh zero value. The
old IsZero type switch compared an any against untyped constants, so
every narrow numeric type (int8..int64, uint*, float32) reported
non-zero and never fell back. IsZero is removed (was unused elsewhere).
tests (#11, high-value/easy set):
- lib.Coalesce table across every numeric width, string, bool, pointer.
- database.Migrate against a temp sqlite: schema created, recorded once,
second call is a no-op.
- types.NewFile: id = sha256(path), hash = sha256(contents), scan fields
copied, and the missing-file path still yields an id.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>