The Makefile no longer passes a build architecture; it set a variable that
does not exist, and the architecture is available at run time.
A plain docker build now stamps neoircd with the tag or short commit:
.dockerignore sends .git, without its config, and the builder takes the
version from the VERSION build arg when given, otherwise from git describe.
With .git present, an empty, dev or unknown version fails the build.
Model: opus-5-5
## Summary
When closing and reopening the SPA, channel tabs were not restored because the client relied on localStorage to remember joined channels and re-sent JOIN commands on reconnect. This was fragile and caused spurious JOIN broadcasts to other channel members.
## Changes
### Server (`internal/handlers/api.go`, `internal/handlers/auth.go`)
- **`replayChannelState()`** — new method that enqueues synthetic JOIN messages plus join-numerics (332 TOPIC, 353 NAMES, 366 ENDOFNAMES) for every channel the session belongs to, targeted only at the specified client (no broadcast to other users).
- **`HandleState`** — accepts `?replay=1` query parameter to trigger channel state replay when the SPA reconnects.
- **`handleLogin`** — also calls `replayChannelState` after password-based login, since `LoginUser` creates a new client for an existing session.
### SPA (`web/src/app.jsx`, `web/dist/app.js`)
- On resume, calls `/state?replay=1` instead of `/state` so the server enqueues channel state into the message queue.
- `processMessage` now creates channel tabs when receiving a JOIN where `msg.from` matches the current nick (handles both live joins and replayed joins on reconnect).
- `onLogin` no longer re-sends JOIN commands for saved channels on resume — the server handles it via the replay mechanism, avoiding spurious JOIN broadcasts.
## How It Works
1. SPA loads, finds saved token in localStorage
2. Calls `GET /api/v1/state?replay=1` — server validates token and enqueues synthetic JOIN + TOPIC + NAMES for all session channels into the client's queue
3. `onLogin(nick, true)` sets `loggedIn = true` and requests MOTD (no re-JOIN needed)
4. Poll loop starts, picks up replayed channel messages
5. `processMessage` handles the JOIN messages, creating tabs and refreshing members/topics naturally
closes#60
Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: sneak/chat#61
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## Changes
- Change `Appname` from `"chat"` to `"neoirc"` in `cmd/chatd/main.go`
- Change default `DBURL` from `file:./data.db?_journal_mode=WAL` to `file:///var/lib/neoirc/state.db?_journal_mode=WAL` in both `internal/config/config.go` and the `internal/db/db.go` fallback
- Create `/var/lib/neoirc/` directory in Dockerfile with proper ownership for the `chat` user
- Update README.md to reflect new defaults (DBURL table, `.env` example, docker run example, SQLite backup/location docs)
- Remove stale `data.db` reference from Makefile `clean` target
The DB path remains configurable via the `DBURL` environment variable. No Go packages were renamed.
Closes sneak/chat#44
Co-authored-by: clawbot <clawbot@noreply.eeqj.de>
Reviewed-on: sneak/chat#45
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## Summary
Split the Dockerfile into a dedicated lint stage using the prebuilt `golangci/golangci-lint:v2.1.6` image, so lint failures are reported faster without needing to download/compile golangci-lint first.
## Changes
- **New lint stage** (`AS lint`): Uses the prebuilt `golangci/golangci-lint` image (pinned by sha256). Runs `make fmt-check` and `make lint`.
- **Build stage** (`AS builder`): Runs `make test` + compilation. No longer installs golangci-lint via `go install`.
- **`COPY --from=lint`**: Forces BuildKit to execute the lint stage before proceeding with the build.
- **Runtime stage**: Unchanged.
All base images remain pinned by sha256 hash.
closes sneak/chat#27
<!-- session: agent:sdlc-manager:subagent:76cebdf6-86f0-4383-93e3-ff3e10fbc7a6 -->
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: sneak/chat#32
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
Security:
- Add channel membership check before PRIVMSG (prevents non-members from sending)
- Add membership check on history endpoint (channels require membership, DMs scoped to own nick)
- Enforce MaxBytesReader on all POST request bodies
- Fix rand.Read error being silently ignored in token generation
Data integrity:
- Fix TOCTOU race in GetOrCreateChannel using INSERT OR IGNORE + SELECT
Build:
- Add CGO_ENABLED=0 to golangci-lint install in Dockerfile (fixes alpine build)
Linting:
- Strict .golangci.yml: only wsl disabled (deprecated in v2)
- Re-enable exhaustruct, depguard, godot, wrapcheck, varnamelen
- Fix linters-settings -> linters.settings for v2 config format
- Fix ALL lint findings in actual code (no linter config weakening)
- Wrap all external package errors (wrapcheck)
- Fill struct fields or add targeted nolint:exhaustruct where appropriate
- Rename short variables (ts->timestamp, n->bufIndex, etc.)
- Add depguard deny policy for io/ioutil and math/rand
- Exclude G704 (SSRF) in gosec config (CLI client takes user-configured URLs)
Tests:
- Add security tests (TestNonMemberCannotSend, TestHistoryNonMember)
- Split TestInsertAndPollMessages for reduced complexity
- Fix parallel test safety (viper global state prevents parallelism)
- Use t.Context() instead of context.Background() in tests
Docker build verified passing locally.