Skip to content
Open
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
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@
# Ignore massive diffs each time you add/update yarn plugins
/web/.yarn/releases/** binary
/web/.yarn/plugins/** binary

# Generated files — hide from GitHub diffs/language stats and mark as not
# hand-edited. The TanStack Router Vite plugin owns routeTree.gen.ts;
# `@hey-api/openapi-ts` owns api-generated/.
/web/src/app/routeTree.gen.ts linguist-generated=true
/web/src/api-generated/** linguist-generated=true
7 changes: 7 additions & 0 deletions .mise/tasks/lint.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ depends = ["install-dependencies:web"]
dir = "{{config_root}}/web"
run = "yarn compile"

["lint:web:query"]
description = "Lint TanStack Query usage in the Web application"
depends = ["install-dependencies:web"]
dir = "{{config_root}}/web"
run = "yarn lint:query"

["lint:web"]
description = "Lint the Web application"
run = [
"mise run lint:web:typecheck",
"mise run lint:web:query",
]

["lint:shell"]
Expand Down
2 changes: 1 addition & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dev = [
"mongomock>=4.1.2",
"mypy>=1.20.2",
"pytest>=8.3.0",
"types-cachetools>=7.0.0.20260503",
"types-cachetools>=5.5.0.20240820",
]

[build-system]
Expand Down
2 changes: 1 addition & 1 deletion api/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"files": {
"includes": ["web/**", "!web/build", "!web/src/api/generated"]
"includes": [
"web/**",
"!web/build",
"!web/.yarn",
"!web/.pnp.*",
"!web/src/api-generated",
"!web/src/app/routeTree.gen.ts"
]
},
"javascript": {
"formatter": {
Expand All @@ -19,6 +26,11 @@
"indentWidth": 2
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"linter": {
"rules": {
"a11y": {
Expand Down
6 changes: 6 additions & 0 deletions docker-compose.no-auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ services:
target: development
args:
AUTH_ENABLED: "0"
# Vite's dev server reads stdin for keyboard shortcuts (r/q/…).
# Without an open stdin it sees EOF at startup and exits 0, which
# combined with `restart: unless-stopped` produces an infinite
# "web-1 exited with code 0 (restarting)" loop.
stdin_open: true
tty: true
# Vite dev server with HMR. vite.config.mts already binds host: true
# on port 3000, so nginx can reach it as http://web:3000.
volumes:
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ services:
stdin_open: true
environment:
NODE_ENV: development
VITE_AUTH: $AUTH_ENABLED
tty: true
develop:
watch:
- action: sync
Expand Down
1 change: 1 addition & 0 deletions web/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
!src/**/*.ts
!src/*.tsx
!src/**/*.tsx
!src/**/*.css

!index.html
!tsconfig.json
Expand Down
32 changes: 32 additions & 0 deletions web/.yarn/sdks/eslint/bin/eslint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node

const {existsSync} = require(`fs`);
const {createRequire, register} = require(`module`);
const {resolve} = require(`path`);
const {pathToFileURL} = require(`url`);

const relPnpApiPath = "../../../../.pnp.cjs";

const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);

const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);

if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/bin/eslint.js
require(absPnpApiPath).setup();
if (isPnpLoaderEnabled && register) {
register(pathToFileURL(absPnpLoaderPath));
}
}
}

const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;

// Defer to the real eslint/bin/eslint.js your application uses
module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`));
31 changes: 31 additions & 0 deletions web/.yarn/sdks/eslint/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "eslint",
"version": "10.3.0-sdk",
"main": "./lib/api.js",
"type": "commonjs",
"bin": {
"eslint": "./bin/eslint.js"
},
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/api.js"
},
"./config": {
"types": "./lib/types/config-api.d.ts",
"default": "./lib/config-api.js"
},
"./package.json": "./package.json",
"./use-at-your-own-risk": {
"types": "./lib/types/use-at-your-own-risk.d.ts",
"default": "./lib/unsupported-api.js"
},
"./rules": {
"types": "./lib/types/rules.d.ts"
},
"./universal": {
"types": "./lib/types/universal.d.ts",
"default": "./lib/universal.js"
}
}
}
5 changes: 5 additions & 0 deletions web/.yarn/sdks/integrations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# This file is automatically generated by @yarnpkg/sdks.
# Manual changes might be lost!

integrations:
- vscode
32 changes: 32 additions & 0 deletions web/.yarn/sdks/typescript/bin/tsc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node

const {existsSync} = require(`fs`);
const {createRequire, register} = require(`module`);
const {resolve} = require(`path`);
const {pathToFileURL} = require(`url`);

const relPnpApiPath = "../../../../.pnp.cjs";

const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);

const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);

if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsc
require(absPnpApiPath).setup();
if (isPnpLoaderEnabled && register) {
register(pathToFileURL(absPnpLoaderPath));
}
}
}

const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;

// Defer to the real typescript/bin/tsc your application uses
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`));
32 changes: 32 additions & 0 deletions web/.yarn/sdks/typescript/bin/tsserver
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node

const {existsSync} = require(`fs`);
const {createRequire, register} = require(`module`);
const {resolve} = require(`path`);
const {pathToFileURL} = require(`url`);

const relPnpApiPath = "../../../../.pnp.cjs";

const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
const absRequire = createRequire(absPnpApiPath);

const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);

if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsserver
require(absPnpApiPath).setup();
if (isPnpLoaderEnabled && register) {
register(pathToFileURL(absPnpLoaderPath));
}
}
}

const wrapWithUserWrapper = existsSync(absUserWrapperPath)
? exports => absRequire(absUserWrapperPath)(exports)
: exports => exports;

// Defer to the real typescript/bin/tsserver your application uses
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`));
10 changes: 10 additions & 0 deletions web/.yarn/sdks/typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "typescript",
"version": "6.0.3-sdk",
"main": "./lib/typescript.js",
"type": "commonjs",
"bin": {
"tsc": "./bin/tsc",
"tsserver": "./bin/tsserver"
}
}
3 changes: 2 additions & 1 deletion web/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
nodeLinker: pnp
enableGlobalCache: true

nodeLinker: pnp
6 changes: 6 additions & 0 deletions web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ RUN mkdir /code && \

RUN corepack enable

# Workaround: repo.yarnpkg.com (GitHub Pages mirror corepack uses by default)
# is currently returning 404, which makes corepack silently cache an empty
# yarn.js. Pointing corepack at the npm registry uses @yarnpkg/cli-dist
# instead and produces a working yarn binary.
ENV COREPACK_NPM_REGISTRY=https://registry.npmjs.org

WORKDIR /code
USER "node"

Expand Down
119 changes: 119 additions & 0 deletions web/eslint.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Minimal ESLint flat config — runs ONLY @tanstack/eslint-plugin-query rules
// alongside Biome. Biome remains the formatter and primary linter; this file
// hosts the TanStack-specific rules Biome cannot express
// (queryKey-aware exhaustive-deps, prefer-query-options, mutation/infinite
// property order, etc.).
//
// Run with: `yarn lint:query` (or via mise: `mise run lint:web:query`).

import pluginQuery from '@tanstack/eslint-plugin-query'
import tseslint from 'typescript-eslint'

const recommended = pluginQuery.configs['flat/recommended']
const recommendedRules = {}
for (const c of Array.isArray(recommended) ? recommended : [recommended]) {
Object.assign(recommendedRules, c.rules ?? {})
}

export default [
{
ignores: ['src/api-generated/**', 'build/**', 'node_modules/**', '.yarn/**'],
},
{
files: ['src/**/*.{ts,tsx}'],
// We only enable the @tanstack/query rules — silence reports about
// pre-existing `eslint-disable` directives for rules we don't load.
linterOptions: {
reportUnusedDisableDirectives: 'off',
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
},
plugins: {
'@tanstack/query': pluginQuery,
},
rules: {
...recommendedRules,
// Feature-slice boundary: outside code may only import a feature
// through its public barrel (`@/features/<name>`), never deep paths
// like `@/features/<name>/api/...`. Inside-feature code uses
// relative paths and is exempted by the override below.
'no-restricted-imports': [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understand the use of eslint for the tanstack plugin (we should definitely pay attention to wether Biome adds support for the plugin at some point), but I don't understand adding more rules for eslint to handle. We should decide on biome, eslint or oxlint

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

@sutne sutne May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If i am understanding this correctly, biome also supports defining noRestrictedImports in biome.json, so the relvant parts from eslint could be moved there.

https://biomejs.dev/linter/rules/no-restricted-imports/

Screenshot 2026-05-26 at 08 47 43

'error',
{
patterns: [
{
group: ['@/features/*/*'],
message:
'Import from the feature barrel (`@/features/<name>`), not its internals. ' +
'If you need something not exported, add it to the barrel.',
},
{
group: ['@/shared/platform/*/*'],
message:
'Import from the platform module barrel (`@/shared/platform/<name>`), not its ' +
'internals. If you need something not exported, add it to the barrel.',
},
],
},
],
},
},
{
// Inside a feature, deep imports are fine — but prefer relative paths
// (the rule still nudges that direction by allowing only relatives here).
files: ['src/features/*/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': 'off',
},
},
{
// Layering: `shared/` is a downstream layer. It must not depend on
// `features/` or `app/` — that would create a cycle (features import
// shared; app composes both). Pure utilities only.
files: ['src/shared/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@/features/*', '@/features/*/*', '@/app/*', '@/app/*/*'],
message:
'`shared/` must not import from `features/` or `app/`. ' +
'Move the symbol down to `shared/` or invert the dependency.',
},
],
},
],
},
},
{
// Layering: `config/` may import `features/*` *barrels* (e.g.
// `accessControl.ts` types its `Permissions` map against `Todo` from
// `@/features/todos`). It must not depend on `app/`.
files: ['src/config/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@/app/*', '@/app/*/*'],
message: '`config/` must not import from `app/`.',
},
{
group: ['@/features/*/*'],
message: 'Import from the feature barrel (`@/features/<name>`), not its internals.',
},
],
},
],
},
},
]
Loading
Loading