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
8 changes: 7 additions & 1 deletion .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ concurrency:
cancel-in-progress: true

jobs:
i18n:
check:
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
timeout-minutes: 10
Expand All @@ -34,3 +34,9 @@ jobs:
- name: Check i18n dictionaries
working-directory: frontend
run: pnpm run check:i18n
- name: Typecheck
working-directory: frontend
run: pnpm run typecheck
- name: Unit tests
working-directory: frontend
run: pnpm run test
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@

- This project is an open-source, lightweight RSS reader and aggregator.
- Prioritize simplicity and maintainability over complexity.
- HTTP API facts live in code: routes in `backend/internal/handler/handler.go` (`SetupRouter`), response shapes in `backend/internal/model/model.go` and request bodies in the handler files (both mirrored by `frontend/src/lib/api/types.ts`); cross-cutting conventions in `docs/api-conventions.md`.

## Code Standards

- Follow best practices without over-engineering.
- Default to no backward-compatibility work unless explicitly requested; if a change may break data formats, public APIs, or migrations, clearly state the impact.
- Docs answer why, code says what: never copy facts that live in a file (versions, routes, dependencies, env defaults) into documentation — link to the source instead. See `docs/README.md`.
- Write self-explanatory code with clear naming.
- Add comments in English only when they provide non-obvious value:
- **DO write comments for:**
Expand All @@ -36,5 +38,6 @@

## Frontend Development

- Verify TypeScript/TSX compilation with `npx tsc -b --noEmit`.
- Verify TypeScript/TSX compilation with `pnpm run typecheck` (regenerates the TanStack route tree first — `routeTree.gen.ts` is a generated, gitignored file).
- Run frontend unit tests with `pnpm test` (vitest); check i18n completeness with `pnpm run check:i18n`.
- shadcn components use Base UI (`@base-ui/react`) on the `base-vega` style. Regenerate via CLI (`pnpm dlx shadcn@latest add <component> --overwrite`) instead of hand-editing source files in `frontend/src/components/ui/`.
40 changes: 21 additions & 19 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
# Contributing to Fusion

Thanks for contributing.
Thanks for your interest in Fusion.

Fusion is maintained by a small team working closely with coding agents, and
the sustainable way to contribute is [issues](https://github.com/0x2E/fusion/issues):
report bugs, propose features, or ask questions there. There is no need to
open pull requests.

Fusion values simple, maintainable changes over complex abstractions.

## 1. Before you start
## 1. Reporting problems

- Search existing issues/PRs first.
- Search existing issues first to avoid duplicates.
- For bugs, include reproducible steps, expected behavior, and actual behavior.
- For features, explain the concrete use case and user value.

## 2. Local setup

Requirements:
Requirements (versions are pinned in-repo, not here):

- Go `1.25+`
- Node.js `24+`
- Go: [`backend/go.mod`](./backend/go.mod)
- Node.js: [`frontend/package.json`](./frontend/package.json) (`engines`)
- pnpm

Install dependencies:
Expand All @@ -27,7 +32,7 @@ cd frontend && pnpm install

Backend uses Go modules and installs dependencies automatically via `go` tooling.

## 3. Validation checklist (required before PR)
## 3. Validation checklist (for local changes)

### Backend

Expand All @@ -42,23 +47,20 @@ go build -o /dev/null ./cmd/fusion

```shell
cd frontend
npx tsc -b --noEmit
pnpm lint
pnpm typecheck
pnpm test
pnpm run check:i18n
pnpm build
```

If your change is scoped, you can run a smaller test subset first, but run the relevant final checks before requesting review.

## 4. Pull request expectations
`pnpm lint` currently cannot start: `typescript-eslint` does not support
TypeScript 7 yet (typescript-eslint/typescript-eslint#10940). Re-add it to
the checklist once upstream support lands.

- Keep PRs focused. One PR should solve one clear problem.
- Explain why the change is needed, not only what changed.
- Include screenshots/GIFs for UI changes.
- Link related issue(s).
- Mark as Draft if not ready for review.
- If you use AI tools, review and validate outputs carefully before submission.
If your change is scoped, you can run a smaller test subset first, but run
the relevant final checks before committing.

## 5. Code style guidelines
## 4. Code style guidelines

- Prefer readable, self-explanatory naming.
- Avoid over-engineering.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,17 @@ Legacy env names (`DB`, `PASSWORD`, `PORT`) are still accepted for backward comp

## Documentation

- API contract (OpenAPI): [`docs/openapi.yaml`](./docs/openapi.yaml)
- API conventions (the route inventory lives in `backend/internal/handler/handler.go`): [`docs/api-conventions.md`](./docs/api-conventions.md)
- Fever API compatibility: [`docs/fever-api.md`](./docs/fever-api.md)
- Backend design: [`docs/backend-design.md`](./docs/backend-design.md)
- Frontend design: [`docs/frontend-design.md`](./docs/frontend-design.md)
- Legacy schema reference (kept for migration work): [`docs/old-database-schema.md`](./docs/old-database-schema.md)

## Development

- Requirements: Go `1.25+`, Node.js `24+`, pnpm
- Requirements: Go and Node.js versions are pinned by [`backend/go.mod`](./backend/go.mod) and [`frontend/package.json`](./frontend/package.json) (`engines`); plus pnpm
- Helpful commands are in [`scripts.sh`](./scripts.sh)
- Frontend i18n key check: `cd frontend && npm run check:i18n`
- Frontend i18n key check: `cd frontend && pnpm run check:i18n`

Example:

Expand All @@ -136,7 +136,7 @@ Example:

## Contributing

Contributions are welcome. Please read [Contributing Guidelines](./CONTRIBUTING.md) before opening a PR.
Please report bugs and propose features via [issues](https://github.com/0x2E/fusion/issues) — there is no need to open pull requests. See [Contributing](./CONTRIBUTING.md) for local development.

## Credits

Expand Down
37 changes: 29 additions & 8 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
# Documentation Index
# Documentation

- `openapi.yaml`: current HTTP API contract (source of truth)
- `fever-api.md`: Fever API compatibility contract (`/fever*` endpoints)
- `backend-design.md`: backend architecture and operational notes
- `frontend-design.md`: frontend architecture and interaction model
- `old-database-schema.md`: legacy schema snapshot kept for migration work
## How documentation is written here

Release note: API and design docs should be updated together with any behavior or contract changes.
- Code is the single source of truth for what it does. Docs answer
**why**: design decisions, trade-offs, history, and cross-cutting rules
that are scattered across the code.
- Never copy facts that already live in a file — versions, dependencies,
route inventories, env-var lists and defaults, schema fields. Link to
the file instead. The usual truth sources: `backend/go.mod`,
`frontend/package.json`, `SetupRouter` in
`backend/internal/handler/handler.go`,
`backend/internal/store/migrations/`, `frontend/src/routes/`,
[`.env.example`](../.env.example).
- Each fact lives in exactly one place. Breaking-change notes and other
history are kept once, in the doc that owns the topic.
- Update docs together with any behavior or contract change they
describe; if code and a doc disagree, the code wins and the doc is a
bug.

Current breaking API note: feed runtime fields are now under `feed.fetch_state.*` (top-level `last_build/last_failure_at/failure/failures` removed).
## Index

- `api-conventions.md`: cross-cutting HTTP API rules (auth, envelopes,
cursors, errors, status codes)
- `fever-api.md`: Fever protocol compatibility — an external contract
with third-party clients
- `backend-design.md`: backend architecture, feed pull strategy, schema
and cascade design
- `frontend-design.md`: frontend interaction model, URL state, data-flow
and read-state undo design
- `old-database-schema.md`: legacy pre-migrations schema snapshot, kept
for migration work only
85 changes: 85 additions & 0 deletions docs/api-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# API Conventions

The HTTP contract lives in the code: route inventory in
`backend/internal/handler/handler.go` (`SetupRouter`), response shapes in
`backend/internal/model/model.go` (JSON tags), request bodies in the
handler files (`createFeedRequest`, `createBookmarkRequest`, ...), and the
frontend call surface in `frontend/src/lib/api/index.ts`. This page
records only the cross-cutting rules that are distributed across handlers,
so they do not need to be rediscovered per endpoint. If code and this page
disagree, the code wins — fix the page.

## Base path and authentication

- All endpoints are under `/api`. Two exceptions: the Fever compatibility
API (`/fever*`, see `docs/fever-api.md`) and a bare `GET /oidc/callback`
registered only when OIDC is configured, for deployments whose redirect
URI omits `/api`.
- Authentication is a session cookie named `session` (`POST /api/sessions`
with `{"password": ...}` sets it; `DELETE /api/sessions` clears it, 204).
- Anonymous mode: when both password and OIDC are unconfigured, the auth
middleware lets every request through.
- Login attempts are rate limited: excess attempts get `429` with a
`Retry-After` header (seconds).
- OIDC: `GET /api/oidc/enabled` always exists; `/api/oidc/login` and
`/api/oidc/callback` are registered only when OIDC is configured. Login
returns `{data: {auth_url}}` for the client to navigate to; the callback
redirects (307) to `/` on success or `/login?error=oidc_failed` on failure.

## Response envelopes

- Single resource: `{"data": <object>}`
- Group and feed lists (small, fully returned): `{"data": [...], "total": n}`
- Item and bookmark lists (cursor-paginated):
`{"data": [...], "total": n, "next_cursor": <string|null>}`;
`next_cursor` is `null` when there are no more pages.

## Cursor pagination

- Pass the previous page's `next_cursor` as the `before` query parameter;
omit it on the first page.
- Cursor format: `"<value>_<id>"` (two int64s, underscore-separated). The
value is `pub_date` (unix) for items and `created_at` (unix) for bookmarks;
`id` breaks ties.
- The items cursor is keyed on `pub_date`, so `before` is only valid with
the default ordering: combining `order_by=created_at` with `before` is
rejected with 400. Only the first page can use `created_at`.
- Ordering is always DESC. Items support `order_by=pub_date|created_at`
(default `pub_date`); any other value silently falls back to `pub_date`.
- `limit` is capped at 100 on every endpoint that accepts one (items,
bookmarks, search); `maxListLimit` in `backend/internal/handler/item.go`.

## Errors

Handlers report failures as `{"error": "<message>"}`:

- `400` — invalid input (message varies)
- `401` — `"unauthorized"`
- `404` — `"<resource> not found"`
- `429` — `"too many login attempts"` (plus `Retry-After`)
- `500` — `"internal server error"` (details are logged server-side only)

Three cases return the status with an empty body instead: unknown routes
(404), CORS origin rejection (403), and panic recovery (500).

## Status codes

- `200` — any response with a body: reads, creates, and updates
- `204` — body-less success: mark items read/unread, all deletes, logout,
and the `GET /api/sessions` validity probe (valid → 204, invalid → 401)
- `202` — feed refresh requests (`/feeds/refresh`, `/feeds/{id}/refresh`):
refresh happens asynchronously, no body

## Request quirks worth knowing

- `POST /api/bookmarks` has two modes: `{item_id}` snapshots the bookmark from
that item server-side (preferred), or a full `{link, title, content,
feed_name, pub_date?}` snapshot when there is no item. Sending `item_id`
makes the server ignore all other fields.
- `PATCH /api/items/-/read` and `-/unread` accept 1–1000 ids
(`maxBatchUpdateIDs`).
- `PATCH /api/settings` is a partial update: an absent field leaves the stored
value unchanged, and there is deliberately no way to clear a preference
back to null. Closed sets: locale `en|zh|de|fr|es|ru|pt|sv`, theme
`light|dark|system`, `auto_mark_read` `off|open|5|10|30`,
`article_page_size` 1–100.
Loading
Loading