oxynote/CONTRIBUTING.md
swithek 78c4244619
overhaul the notif inbox (#200)
* overhaul the notif inbox

* make hook summary abstract

---------

Co-authored-by: David <SE.David@protonmail.com>
Co-authored-by: david <58231312+davseby@users.noreply.github.com>
2026-10-06 19:03:49 +03:00

12 KiB

Contributing

This guide covers running Oxynote from a source checkout and the commands you need day to day. Everything is driven from the repository root through make; the component-specific docs are linked at the end.

Prerequisites

Tool Used for
Docker with Compose the dev stack (Postgres, Caddy, the app containers, …) and the Go database-layer tests, which start throwaway containers
Go 1.27+ server/core and datagen
Node.js 24+ web, server/auth-realtime, e2e and the production launcher
pnpm package manager for the Node packages. Any recent install works: pnpm switches to the version pinned in each package's packageManager field on its own
make orchestrates every setup, build and run command
goreleaser builds the Go binaries and their docker images — needed by every target that starts a stack
golangci-lint lints the Go code (make lint, make check-lint)
moq only to regenerate the Go test mocks with go generate; the generated files are committed

First run

make setup     # install dependencies + create the local env files from templates
make start     # build all images + run the dev stack in the background

Then open http://localhost:8080 and sign up. make stop stops the stack.

Everyday commands

Running the dev stack

make run       # foreground: streams all logs, ctrl-c stops the stack
make start     # the same, in the background
make stop      # stop the background stack
make dev       # backend containers + the web dev server on the host

make dev is the frontend loop: the backend runs in containers and Nuxt runs on the host with hot reload on http://localhost:3000. Ctrl-c stops the dev server; make stop stops the containers.

All three targets rebuild the images before starting, so a code change is picked up by simply running them again.

Environment files

The dev stack reads docker/env/*.local.env, which make setup creates from the committed *.example.env templates. The files are gitignored and the defaults work out of the box. Integrations (GitHub app, Slack app, AI assistant, email, changedetection.io) are optional and stay disabled until their variables are set; the frontend hides features the deployment does not offer. Search is built in: core keeps its index on the container's volume and rebuilds it from Postgres when it is missing.

make check-env   # report variables that drifted between the templates and your local files
make sync-env    # rewrite the local files from the templates, keeping your values
make setup-force # setup, but the local files are recreated from the templates, values and all

make setup only copies a template when the local file is missing, so a variable renamed or added in a template never reaches an existing local file. The dev stack targets run make check-env first and refuse to start on drift — make sync-env fixes it.

Linting

make lint        # fix lint, format and type issues in every component
make check-lint  # the same gates, verification only (what CI runs)

End-to-end tests

make e2e-dev     # build the dev stack, run the Playwright suite, tear it down
make e2e-prod    # the same suite against the production all-in-one image

Both are one-shot: every run builds the stack and drops it afterwards. To iterate on tests, build the stack once with make e2e-dev-stack-build (or make e2e-prod-stack-build), then run pnpm test:dev (or pnpm test:prod) from e2e/ as often as you like, and finish with make e2e-dev-stack-stop (or make e2e-prod-stack-stop). The e2e stacks use their own ports and throwaway state, so they run alongside the dev stack.

Production image

make prod-build  # build the all-in-one image
make prod-run    # build it, then run the example deployment on :8080 (ctrl-c stops it)
make prod-stop   # stop it

make prod-run also starts a Mailpit on http://localhost:8025 so the signup verification mail is readable. Stop the dev stack first — both listen on :8080.

Ports

Port Dev stack
8080 front door: the app, with the APIs under /core and /auth-realtime
8082 changedetection.io
8083 Grafana
8084 Mailpit (dev email inbox)
3000 the web dev server, only with make dev

The e2e dev stack listens on 18080 (Mailpit on 18025) and the e2e prod stack on 19080 (19025).

Repository layout

  • web/ — Nuxt 4 + Vue 3 frontend; ships as both a web app (SSR) and an Electron desktop app. See web/README.md.
  • server/core/ — Go API server: storage, search, integrations, AI pipelines. See server/README.md.
  • server/auth-realtime/ — Node service running Better Auth and a Hocuspocus (Yjs) real-time server.
  • datagen/ — demo-data generator (Go); demo/testing only.
  • e2e/ — Playwright end-to-end suite and the docker-compose stacks it drives.
  • docker/ — dev docker-compose stack, Caddyfile, env templates, demo-data configs.
  • docker/prod/ — the production all-in-one image and its example deployment. See docker/prod/README.md.
  • scripts/ — helpers the root Makefile calls.

AI coding agents

Instructions for coding agents live in AGENTS.md at the repository root, in each component directory, and in a few core packages with rules of their own; the root file maps the components and links their files. Every CLAUDE.md next to one is a one-line import that points Claude Code at the AGENTS.md beside it — edit AGENTS.md, never the shim.

Adding a data source

Decide first what you are adding. If the new type is compatible with an existing processor, as MariaDB is with the MySQL one, it needs no processor of its own, only the wiring in the steps below. Otherwise it needs its own processor, client interface, routes and assistant tools.

  1. Create the type constant. The type constant, the credential rules and the runner that opens a client for a stored data source all live in server/core/internal/datasource/. Add the constant, then follow an existing type through the package: the credential check, the runner's per-type construction and the runner method that returns the client, which refuses types it does not accept. A type with its own processor also gets its client interface there, with a mock regenerated by go generate (moq is in the prerequisites).
  2. Write the processor. server/core/internal/datasource/processor/ holds one processor per kind of source. It connects, tests the connection, including that the credentials cannot write (every data source is read-only), maps errors to a connection status, runs queries and shapes their results. What a new source needs ranges from a set of macro definitions to a whole client; reuse what the existing processors share, such as time-range handling, the $__ macros, SQL metadata and result transformation, and add only what the new source does differently.
  3. Add the assistant tools. Make sure every action the data source offers is also an assistant tool in server/core/internal/assistant/tools/; the MCP server exposes the same tools.
    • If metric blocks draw from the new type, add a case to the query handler in server/core/internal/server/internal/datasource/ and to the simulation checker in server/core/internal/datasource/simulation/, which decides when a metric block stops showing generated data; without a case it never does.
  4. Add the type to the web app. The frontend knows the types through the enum in web/app/utils/api/data-source/. From there, follow an existing type through web/app/components/: the status icon, the settings section that lists connectable sources (without it the type cannot be added) and the read-only warning in the connect dialog. Their strings live in web/i18n/locales/. The help and warning links are runtime config, declared in web/nuxt.config.ts and web/index.d.ts and set in docker/env/web.example.env and the production launcher's env mapping under docker/prod/launcher/.
    • If metric blocks draw from the new type, add the query editor's language support and the query help popover under editor/blocks/metrics/config-modal/.
  5. Add demo data (optional). datagen/ fills the demo databases and docker/ runs them. Add a generator package and a block in the datagen command that runs it when its env variables are set, a compose service with its init script under docker/demo/, then make sync-env.
  6. Extend the tests and docs. Every place above that switches on the type has a table-driven test with one row per type; extend the tables in the packages you touched, and the unit test beside each web component. The processor tests start a throwaway container per database, so a new one needs a container in their TestMain and Docker to run. Assistant tool descriptions are compared against a golden file; regenerate it from server/core/ with UPDATE_GOLDEN=1 go test -run Test_Info_toEino ./internal/assistant/tools/ and review the diff. Update the supported-sources line in README.md, and the prerequisites above if a new container became a test dependency. The e2e suite creates no data sources and needs nothing.

Adding a hook

Decide first whether the hook depends on an integration the deployment may not have, the way GitHub tracking needs the GitHub app and the website watcher needs changedetection.io. Such a hook also needs a client under server/core/internal/apps/, a capability flag and the gating in step 3.

  1. Create the type constant. The type constant, its validation and its human-readable name (used in notification text), and the switch that turns a stored hook into a running processor all live in server/core/internal/document/hook/. Add the constant, then follow an existing type through the package. A hook with its own client gets it through the input that hands processors their clients, declared in the processor package and implemented in this one.
  2. Write the processor. server/core/internal/document/hook/processor/ holds one processor per type. Its struct is the hook's settings, and it computes a freshness score and a state from them, resets that state, and tears down whatever it holds outside the deployment.
  3. Gate on the integration. If the hook depends on an integration, three places refuse or skip it when the deployment lacks it: the manager in server/core/internal/document/hook/manager/ skips it while processing, the assistant's create refuses it, and the capabilities endpoint in server/core/internal/server/ reports whether it is configured, so the web app can hide it.
  4. Add the assistant tools. The hook tools in server/core/internal/assistant/tools/ take settings as one object whose shape the type decides: add the type's schema variant and its decoding case.
  5. Add the type to the web app. The frontend knows the types, their settings and their state through web/app/utils/api/document.ts. Add a directory with the config menu under web/app/components/editor/hooks/, then follow an existing type through the menu content beside it (the component and props switches and the add-menu entry, gated on the capability when the hook depends on one) and through NotificationRow.vue and notification.ts in web/app/components/. Their strings live in web/i18n/locales/.
  6. Extend the tests. Every place above that switches on the type has a table-driven test with one row per type; extend the tables in the packages you touched, and the unit test beside each web component. Assistant tool descriptions are compared against a golden file; regenerate it from server/core/ with UPDATE_GOLDEN=1 go test -run Test_Info_toEino ./internal/assistant/tools/ and review the diff. The e2e suite creates no hooks and needs nothing.

Before opening a pull request

Run make check-lint. CI runs the same gates per component, plus the unit tests and both end-to-end suites.