Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 0 additions & 102 deletions .github/copilot-instructions.md

This file was deleted.

68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
go:
name: Go
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Build
run: go build ./...

- name: Vet
run: go vet ./...

- name: Test
run: go test -race ./...

ui:
name: UI
runs-on: ubuntu-latest
defaults:
run:
working-directory: ui
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
package_json_file: ui/package.json

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: ui/pnpm-lock.yaml

- name: Install
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm lint

- name: Typecheck
run: pnpm exec tsc -b

- name: Test
run: pnpm test

- name: Build
run: pnpm build
130 changes: 130 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# CLAUDE.md

Thoughts is a self-hosted, real-time collaborative retrospective tool: a Go
backend serving a React/TypeScript SPA. In production the binary embeds the
built UI; in development the two run separately.

## Commands

```bash
task dev # Hot-reload backend (Air, watches *.go + migrations/)
task build # Bundles the UI, then compiles with -tags bundled
task run # Build and run the production binary
task tools # Installs goose + goreleaser
```

```bash
cd ui
pnpm run dev # Vite on :5173, proxying /api/* to :3000
pnpm run build
pnpm run lint
pnpm test
```

Before saying a change is done:

```bash
go build ./... && go vet ./... && go test ./...
cd ui && pnpm lint && pnpm exec tsc -b && pnpm test && pnpm build
```

## Architecture

```
cmd/thoughts/
├── main.go Server setup, DB init, migration runner
├── config.go Viper config — every env var is defined here
├── routes.go All route registration (applyRoutes)
├── controllers/ HTTP handlers
├── model/ Entity structs (Retro, Note, Vote, Task, User)
├── dal/ Raw sqlx queries, no ORM, one file per entity
├── resources/ Outbound DTOs
├── requests/ Inbound DTOs with go-playground/validator tags
├── event/ Pub/sub broker for WebSocket events
├── socket/ WebSocket upgrade and message dispatch
├── session/ Gorilla session management
├── auth/ Session cookie middleware
├── ai/ OpenAI via langchaingo
└── gif/ GIF search behind a swappable provider
```

HTTP requests flow routes → auth middleware → controller → DAL → SQLite.
Real-time updates flow `event.Broker` → WebSocket → frontend.

Migrations live in `migrations/*.sql` with `-- +goose Up` / `-- +goose Down`,
are embedded with `//go:embed`, and run automatically on startup.

Frontend routing is TanStack Router, file-based under `ui/src/routes/`. HTTP is
axios, the socket is react-use-websocket, shared state is React context in
`ui/src/hooks/`.

## Conventions

Config is environment variables with a `THOUGHTS_` prefix, defaulted in
`config.go`. `THOUGHTS_GIF_API_KEY` and `THOUGHTS_GIF_PROVIDER` control GIF
search; pasting a link works without either. `THOUGHTS_OPENAI_API_KEY` enables
AI template generation.

Never return a model struct from a handler; map it through `resources/`.

When a mutation should reach other people in the retro, publish to
`event.Broker` after the DAL write. Payloads are `map[string]any` and are
decoded by `requests.FromMap`, which handles only flat scalars — no nested
structs or slices of structs.

Auth is name-only, no passwords. **A name is a label, not an account.** Each
login creates a new user row, so two people called Alex are two people, and one
person entering their name twice gets two sessions. Do not add uniqueness to
`users.name` or try to reuse a row by name.

Migrations are append-only once merged. A migration that changes or deletes
existing rows needs asking about first.

## Writing code here

Match the surrounding code. Prefer clear naming and small functions over
explanation.

### Comments

**The default is no comment.** Code that needs prose to be understood should be
rewritten instead. Most functions, types, props and exported symbols in this
repo have no comment, and that is correct — do not "improve" them by adding
one.

A comment has to earn its place by carrying information that is *not in the
code and not inferable from it*. In practice that is almost always one of:

- A library, browser or API quirk, named. *"popLayout clones each child with a
ref of its own, which used to overwrite this one."*
- Why a reasonable-looking alternative is wrong here. *"A transaction is the
wrong tool: sqlx issues a deferred BEGIN, so under WAL a concurrent writer
fails rather than serialising."*
- A value that must stay in step with something in another file.

If you cannot state which of those a comment is, delete it.

Never write a comment that:

- Restates the code, the function name, or a type name in prose.
- Describes what a component renders or how a layout is arranged.
- Explains a design or styling choice nobody would question.
- Acts as a section header inside a function.
- Says what a test is testing when the test name already says it.

One or two lines. A block comment longer than three lines needs a reason to
exist. Do not comment every branch of a switch, every field of a struct or
interface, or every step of a sequence.

When editing existing code, leave surrounding comments alone unless they are
now wrong.

## Verifying

Run the app and check the change rather than assuming it works. Where a claim
cannot be verified — because the tooling cannot drive the interaction, or no key
is available — say so plainly rather than implying it was tested.

A test written from documentation proves the code agrees with the documentation,
not with reality. Where an external API is involved, shape fixtures from a real
response and keep a live contract test behind an env var.
9 changes: 8 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ RUN wget -qO /app/thoughts.tar.gz "https://github.com/ellgreen/thoughts/releases
FROM alpine:latest

ENV THOUGHTS_ADDRESS=":3000"
ENV THOUGHTS_DATA="/data"
# The config key is data_path, so THOUGHTS_DATA was never read and the database
# landed in the container's working directory instead of the mounted volume.
ENV THOUGHTS_DATA_PATH="/data"

ENV THOUGHTS_TLS_CERT_PATH=""
ENV THOUGHTS_TLS_KEY_PATH=""

# Optional integrations, pass with -e to enable.
ENV THOUGHTS_GIF_PROVIDER=""
ENV THOUGHTS_GIF_API_KEY=""
ENV THOUGHTS_OPENAI_API_KEY=""

COPY --from=setup /thoughts /usr/local/bin/

RUN mkdir -p /data
Expand Down
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,28 @@ You can follow the instructions to get an API key here:

<https://platform.openai.com/api-keys>

#### GIF Support
#### Images and GIFs

To enable support for gifs via Tenor, you will need to set the
`THOUGHTS_TENOR_API_KEY` environment variable.
Adding an image to a thought always works — paste any `https` image or GIF
link and you get a preview before it is attached. No configuration needed.

You can follow the instructions to get an API key here:
To also get in-app GIF **search**, set `THOUGHTS_GIF_API_KEY`. The default
provider is [Klipy](https://klipy.com/developers), which is free for life:
a test key works immediately, and a production key (a form in their partner
panel) lifts the rate limit.

<https://developers.google.com/tenor/guides/quickstart#setup>
```shell
docker run -p 3000:3000 \
-v $PWD/data:/data \
-e THOUGHTS_GIF_API_KEY="your-key" \
ghcr.io/ellgreen/thoughts:latest
```

| Variable | Values | Default |
| --- | --- | --- |
| `THOUGHTS_GIF_API_KEY` | Your provider's API key | unset — search disabled |
| `THOUGHTS_GIF_PROVIDER` | `klipy`, `giphy`, `none` | auto: `klipy` when a key is set |

> [!NOTE]
> Google shut the Tenor API down on 30 June 2026, so `THOUGHTS_TENOR_API_KEY`
> no longer does anything. Thoughts logs a warning if it is still set.
Loading
Loading