mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
feat(docs): add OpenAPI API reference from oRPC contract
Generate an OpenAPI 3.1 spec from the oRPC contract at build time using @orpc/openapi, then use fumadocs-openapi to render interactive API reference pages grouped by tag (Titles, Episodes, Seasons, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+9
-1
@@ -7,7 +7,15 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!node_modules", "!.next", "!dist", "!build", "!.source"]
|
||||
"includes": [
|
||||
"**",
|
||||
"!node_modules",
|
||||
"!.next",
|
||||
"!dist",
|
||||
"!build",
|
||||
"!.source",
|
||||
"!openapi.json"
|
||||
]
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
|
||||
+65
-3
@@ -7,13 +7,15 @@
|
||||
"dependencies": {
|
||||
"@takumi-rs/image-response": "^0.73.0",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"fumadocs-core": "16.6.17",
|
||||
"fumadocs-mdx": "14.2.10",
|
||||
"fumadocs-ui": "16.6.17",
|
||||
"fumadocs-core": "^16.6.17",
|
||||
"fumadocs-mdx": "^14.2.10",
|
||||
"fumadocs-openapi": "^10.3.18",
|
||||
"fumadocs-ui": "^16.6.17",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "16.1.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"shiki": "^4.0.2",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -118,6 +120,10 @@
|
||||
|
||||
"@fumadocs/tailwind": ["@fumadocs/tailwind@0.0.3", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-/FWcggMz9BhoX+13xBoZLX+XX9mYvJ50dkTqy3IfocJqua65ExcsKfxwKH8hgTO3vA5KnWv4+4jU7LaW2AjAmQ=="],
|
||||
|
||||
"@fumari/json-schema-ts": ["@fumari/json-schema-ts@0.0.2", "", { "dependencies": { "esrap": "^2.2.3" }, "peerDependencies": { "json-schema-typed": "^8.0.2" }, "optionalPeers": ["json-schema-typed"] }, "sha512-A2x8nj45r8Kc3Gqa+HpWRF9uzIMc9dySB6L2R2kiyjLHXWBsZUX99Atj5+Yup/iRQXQ9s8AX+uAPwPze7Xn05A=="],
|
||||
|
||||
"@fumari/stf": ["@fumari/stf@1.0.3", "", { "peerDependencies": { "@types/react": "*", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@types/react"] }, "sha512-rSHaCnCREbDFTD4BNU40ZNjvkhg5wcKzJ1OPsPeKbFzVo9gmm1JE2lGhqsfPhDhuBOV+ZNqZy9+5T+JudKY1NA=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
@@ -244,6 +250,8 @@
|
||||
|
||||
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
||||
|
||||
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||
|
||||
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||
@@ -268,6 +276,14 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@scalar/helpers": ["@scalar/helpers@0.4.1", "", {}, "sha512-XfyYqSUA597wfzS8lmMY1xvYv4si2WytuoBWj7JDpx3E/lVq7YEsticXF/Q30ttFVRBfgQErg8MQI6b6IkZr2Q=="],
|
||||
|
||||
"@scalar/json-magic": ["@scalar/json-magic@0.12.3", "", { "dependencies": { "@scalar/helpers": "0.4.1", "pathe": "^2.0.3", "yaml": "^2.8.0" } }, "sha512-SX4LbdCk/0JLP4t4oz1nzqfMzHsG9gY1jSgKB7Q2BsQHJ4V1zapQ+aFrMmbjaCrz8hueGG/3UCtgyWBNuGmT9g=="],
|
||||
|
||||
"@scalar/openapi-types": ["@scalar/openapi-types@0.6.0", "", { "dependencies": { "zod": "^4.3.5" } }, "sha512-OeaCrQmXGqO+ZRGgo/mAA8X1nTsdNWW2BxQjyDWTCSD1Qv5Cm58QvKzINKvJPFWrS0LXebRF6OXN+2H2hu9xpA=="],
|
||||
|
||||
"@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.2.0", "", { "dependencies": { "@scalar/openapi-types": "0.6.0" } }, "sha512-T8XKPaYBrxn+8THq8HgXjqpnqp71YZsPCamNYIYJrIM5pXSbBLyLoAh6uie7YfhcNvdQWc/KqahHQebEIcmeqA=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="],
|
||||
@@ -354,6 +370,8 @@
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
|
||||
@@ -368,6 +386,8 @@
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@vercel/analytics": ["@vercel/analytics@2.0.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "nuxt": ">= 3", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "nuxt", "react", "svelte", "vue", "vue-router"] }, "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g=="],
|
||||
@@ -376,6 +396,8 @@
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||
@@ -422,6 +444,8 @@
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"dereference-json-schema": ["dereference-json-schema@0.2.2", "", {}, "sha512-w8dUsJyrzH4Zsj8W/tKcjLsmcTKXfdNf+n3BBm1SAfnqpaCodgEUWqQGJ+pNb9NOqPwYMGvUnZZ8nQfeFjJlbQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||
@@ -440,6 +464,8 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"esrap": ["esrap@2.2.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@typescript-eslint/types": "^8.2.0" } }, "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg=="],
|
||||
|
||||
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
||||
|
||||
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
|
||||
@@ -458,14 +484,26 @@
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fast-xml-builder": ["fast-xml-builder@1.1.3", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@5.5.5", "", { "dependencies": { "fast-xml-builder": "^1.1.3", "path-expression-matcher": "^1.1.3", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-NLY+V5NNbdmiEszx9n14mZBseJTC50bRq1VHsaxOmR72JDuZt+5J1Co+dC/4JPnyq+WrIHNM69r0sqf7BMb3Mg=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"foreach": ["foreach@2.0.6", "", {}, "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg=="],
|
||||
|
||||
"framer-motion": ["framer-motion@12.36.0", "", { "dependencies": { "motion-dom": "^12.36.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw=="],
|
||||
|
||||
"fumadocs-core": ["fumadocs-core@16.6.17", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.8.1", "@orama/orama": "^3.1.18", "@shikijs/rehype": "^4.0.2", "@shikijs/transformers": "^4.0.2", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "path-to-regexp": "^8.3.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "^0.46.0", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0 || ^1.0.0", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-ssHz9a7+ZZSkHjB4/sfHq9rO2fPW8jtw2fPeDVzkPJd34DqOPbxuaP0TQ6CEs1Pei99Fky9CzE8ENS3H8WFxnQ=="],
|
||||
|
||||
"fumadocs-mdx": ["fumadocs-mdx@14.2.10", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.27.3", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.3.6" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^15.0.0 || ^16.0.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "*", "vite": "6.x.x || 7.x.x || 8.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "vite"], "bin": { "fumadocs-mdx": "dist/bin.js" } }, "sha512-0gITZiJb92c7xJwSMdcGBEY2+pFcRvklSNwxIAMTy4gjnuLZANjaXKw+qJ6E5+s9dO0IGlimHv5zyMYLjReg0w=="],
|
||||
|
||||
"fumadocs-openapi": ["fumadocs-openapi@10.3.18", "", { "dependencies": { "@fumari/json-schema-ts": "^0.0.2", "@fumari/stf": "1.0.3", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@scalar/json-magic": "^0.12.2", "@scalar/openapi-upgrader": "^0.2.0", "ajv": "^8.18.0", "class-variance-authority": "^0.7.1", "dereference-json-schema": "^0.2.2", "github-slugger": "^2.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "js-yaml": "^4.1.1", "lucide-react": "^0.577.0", "next-themes": "^0.4.6", "openapi-sampler": "^1.7.2", "react-hook-form": "^7.71.2", "remark": "^15.0.1", "remark-rehype": "^11.1.2", "tailwind-merge": "^3.5.0", "xml-js": "^1.6.11" }, "peerDependencies": { "@scalar/api-client-react": "*", "@types/react": "*", "fumadocs-core": "^16.5.0", "fumadocs-ui": "^16.5.0", "json-schema-typed": "*", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@scalar/api-client-react", "@types/react", "json-schema-typed"] }, "sha512-jniuRFO0aFCEZzr6NtN0kANTIK4e+/i13W6kIXJaRMXDaqI3hpV2Hby4kWlhqXaQX79EMx1eIMcJTMAdmqvj7Q=="],
|
||||
|
||||
"fumadocs-ui": ["fumadocs-ui@16.6.17", "", { "dependencies": { "@fumadocs/tailwind": "0.0.3", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^0.577.0", "motion": "^12.36.0", "next-themes": "^0.4.6", "react-medium-image-zoom": "^5.4.1", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.5.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.6.17", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next"] }, "sha512-RLr1Dsujq3YoOEi4cLu52mZkT8fBJUl1rq4DtVoQWhvk20WYl1aDxlBhMr4guAvG5Malwh6Vy1QJ5KbE/k2E6w=="],
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
@@ -514,6 +552,10 @@
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"json-pointer": ["json-pointer@0.6.2", "", { "dependencies": { "foreach": "^2.0.4" } }, "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||
@@ -672,12 +714,18 @@
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
|
||||
|
||||
"openapi-sampler": ["openapi-sampler@1.7.2", "", { "dependencies": { "@types/json-schema": "^7.0.7", "fast-xml-parser": "^5.5.1", "json-pointer": "0.6.2" } }, "sha512-OKytvqB5XIaTgA9xtw8W8UTar+uymW2xPVpFN0NihMtuHPdPTGxBEhGnfFnJW5g/gOSIvkP+H0Xh3XhVI9/n7g=="],
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"path-expression-matcher": ["path-expression-matcher@1.1.3", "", {}, "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
@@ -692,6 +740,8 @@
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.71.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA=="],
|
||||
|
||||
"react-medium-image-zoom": ["react-medium-image-zoom@5.4.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-DD2iZYaCfAwiQGR8AN62r/cDJYoXhezlYJc5HY4TzBUGuGge43CptG0f7m0PEIM72aN6GfpjohvY1yYdtCJB7g=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||
@@ -732,6 +782,10 @@
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
|
||||
@@ -750,6 +804,8 @@
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
@@ -806,6 +862,10 @@
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
|
||||
|
||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
@@ -818,6 +878,8 @@
|
||||
|
||||
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="],
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Administration
|
||||
description: Server administration and management features.
|
||||
icon: ShieldUser
|
||||
---
|
||||
|
||||
All admin features are found in the **Settings** page, in sections marked as admin-only.
|
||||
|
||||
## Admin Account
|
||||
|
||||
The first user to register automatically becomes the admin. After the first account is created, registration closes by default.
|
||||
|
||||
There is currently no way to transfer the admin role to another user.
|
||||
|
||||
## Registration
|
||||
|
||||
Toggle user registration open or closed in **Settings → Security → Open Registration**.
|
||||
|
||||
<Callout type="info">
|
||||
Registration is always open when no users exist — this is how the initial admin account is created. After the first account is registered, it closes automatically.
|
||||
</Callout>
|
||||
|
||||
When registration is closed, new users cannot create accounts unless they authenticate via [OIDC/SSO](/docs/configuration#oidc--sso) with `OIDC_AUTO_REGISTER` enabled.
|
||||
|
||||
## System Health
|
||||
|
||||
The **Server** section in Settings displays health information across three cards:
|
||||
|
||||
### Status
|
||||
|
||||
- **Database** — Size of the SQLite database and WAL file.
|
||||
- **TMDB** — Connection status (Connected, Invalid token, or Unreachable) with response time.
|
||||
- **Environment** — Configured environment variables (sensitive values are redacted) and data directory write permission.
|
||||
|
||||
### Background Jobs
|
||||
|
||||
A table of all scheduled jobs showing:
|
||||
|
||||
- Job name and schedule
|
||||
- Last run status (success, error, or never run) with duration
|
||||
- Next scheduled run time
|
||||
- A **trigger button** to run any job immediately
|
||||
|
||||
See [Background Jobs](/docs/configuration#background-jobs) for the full schedule reference.
|
||||
|
||||
### Storage
|
||||
|
||||
- **Image cache** — Enabled/disabled status, total size, image count, and breakdown by category (posters, backdrops, stills, logos, profiles).
|
||||
- **Backups** — Total count, combined size, and time since last backup.
|
||||
|
||||
## Backup Management
|
||||
|
||||
### Manual Backups
|
||||
|
||||
Click **Create backup** to generate an immediate database backup. Each backup is an atomic SQLite dump named with a timestamp (e.g. `sofa-manual-2025-01-15-143052123.db`). You can download or delete individual backups from the list.
|
||||
|
||||
### Scheduled Backups
|
||||
|
||||
Enable scheduled backups with the toggle and configure:
|
||||
|
||||
| Setting | Default | Options |
|
||||
|---|---|---|
|
||||
| Frequency | Every day | `6h`, `12h`, `1d`, `7d` |
|
||||
| Time | 02:00 | Any `HH:MM` (UTC) |
|
||||
| Day of week | Sunday | For weekly backups only |
|
||||
| Retention | 7 | Max number of scheduled backups to keep |
|
||||
|
||||
Old backups beyond the retention count are automatically pruned. Scheduled backups are named `sofa-scheduled-*.db`.
|
||||
|
||||
### Restoring from a Backup
|
||||
|
||||
Upload a backup file or select one from the existing backup list to restore. Before restoring:
|
||||
|
||||
1. A **pre-restore snapshot** of the current database is automatically created, so you can revert if needed.
|
||||
2. The backup is **validated** — Sofa checks database integrity, foreign key consistency, and required tables before proceeding.
|
||||
3. After the database is replaced, **migrations run automatically** to bring older backups up to the current schema.
|
||||
|
||||
<Callout type="warn">
|
||||
Restoring a backup replaces the entire database. All data since the backup was created will be lost. The pre-restore snapshot lets you undo the restore if something goes wrong.
|
||||
</Callout>
|
||||
|
||||
## Background Jobs
|
||||
|
||||
Sofa runs several automated background jobs. Each can be triggered manually from the Background Jobs card in Settings by clicking the play button next to any job.
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Forcing a metadata refresh after adding titles to your library
|
||||
- Updating streaming availability data on demand
|
||||
- Checking for new episodes of tracked shows
|
||||
- Running a backup outside the regular schedule
|
||||
- Checking for app updates immediately
|
||||
|
||||
See the [Background Jobs](/docs/configuration#background-jobs) table in Configuration for the full list with schedules.
|
||||
|
||||
## Update Checking
|
||||
|
||||
Found in **Settings → Security**. When enabled (the default), Sofa checks for new versions every 6 hours. If an update is available, the current and latest versions are displayed with a link to the release notes.
|
||||
|
||||
Toggle this off if you prefer to check for updates manually or manage updates through your own deployment pipeline.
|
||||
|
||||
## Telemetry
|
||||
|
||||
Anonymous usage telemetry can be enabled in Settings. It is **disabled by default** and sends only aggregated, non-identifying data. See the [Telemetry](/docs/telemetry) page for full details on what is and isn't collected.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Account
|
||||
description: User account management
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Update Name
|
||||
url: '#update-name'
|
||||
- depth: 2
|
||||
title: Upload Avatar
|
||||
url: '#upload-avatar'
|
||||
- depth: 2
|
||||
title: Remove Avatar
|
||||
url: '#remove-avatar'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Update Name
|
||||
id: update-name
|
||||
- content: Upload Avatar
|
||||
id: upload-avatar
|
||||
- content: Remove Avatar
|
||||
id: remove-avatar
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/account/name","method":"put"},{"path":"/account/avatar","method":"post"},{"path":"/account/avatar","method":"delete"}]} showTitle={true} />
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Admin
|
||||
description: Server administration
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: List
|
||||
url: '#list'
|
||||
- depth: 2
|
||||
title: Create
|
||||
url: '#create'
|
||||
- depth: 2
|
||||
title: Delete
|
||||
url: '#delete'
|
||||
- depth: 2
|
||||
title: Restore
|
||||
url: '#restore'
|
||||
- depth: 2
|
||||
title: Schedule
|
||||
url: '#schedule'
|
||||
- depth: 2
|
||||
title: Update Schedule
|
||||
url: '#update-schedule'
|
||||
- depth: 2
|
||||
title: Registration
|
||||
url: '#registration'
|
||||
- depth: 2
|
||||
title: Toggle Registration
|
||||
url: '#toggle-registration'
|
||||
- depth: 2
|
||||
title: Update Check
|
||||
url: '#update-check'
|
||||
- depth: 2
|
||||
title: Toggle Update Check
|
||||
url: '#toggle-update-check'
|
||||
- depth: 2
|
||||
title: Telemetry
|
||||
url: '#telemetry'
|
||||
- depth: 2
|
||||
title: Toggle Telemetry
|
||||
url: '#toggle-telemetry'
|
||||
- depth: 2
|
||||
title: Trigger Job
|
||||
url: '#trigger-job'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: List
|
||||
id: list
|
||||
- content: Create
|
||||
id: create
|
||||
- content: Delete
|
||||
id: delete
|
||||
- content: Restore
|
||||
id: restore
|
||||
- content: Schedule
|
||||
id: schedule
|
||||
- content: Update Schedule
|
||||
id: update-schedule
|
||||
- content: Registration
|
||||
id: registration
|
||||
- content: Toggle Registration
|
||||
id: toggle-registration
|
||||
- content: Update Check
|
||||
id: update-check
|
||||
- content: Toggle Update Check
|
||||
id: toggle-update-check
|
||||
- content: Telemetry
|
||||
id: telemetry
|
||||
- content: Toggle Telemetry
|
||||
id: toggle-telemetry
|
||||
- content: Trigger Job
|
||||
id: trigger-job
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/admin/backups","method":"get"},{"path":"/admin/backups","method":"post"},{"path":"/admin/backups/{filename}","method":"delete"},{"path":"/admin/backups/restore","method":"post"},{"path":"/admin/backups/schedule","method":"get"},{"path":"/admin/backups/schedule","method":"put"},{"path":"/admin/registration","method":"get"},{"path":"/admin/registration","method":"put"},{"path":"/admin/update-check","method":"get"},{"path":"/admin/update-check","method":"put"},{"path":"/admin/telemetry","method":"get"},{"path":"/admin/telemetry","method":"put"},{"path":"/admin/jobs/trigger","method":"post"}]} showTitle={true} />
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Dashboard
|
||||
description: User dashboard data
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Stats
|
||||
url: '#stats'
|
||||
- depth: 2
|
||||
title: Continue Watching
|
||||
url: '#continue-watching'
|
||||
- depth: 2
|
||||
title: Library
|
||||
url: '#library'
|
||||
- depth: 2
|
||||
title: Recommendations
|
||||
url: '#recommendations'
|
||||
- depth: 2
|
||||
title: Watch History
|
||||
url: '#watch-history'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Stats
|
||||
id: stats
|
||||
- content: Continue Watching
|
||||
id: continue-watching
|
||||
- content: Library
|
||||
id: library
|
||||
- content: Recommendations
|
||||
id: recommendations
|
||||
- content: Watch History
|
||||
id: watch-history
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/dashboard/stats","method":"get"},{"path":"/dashboard/continue-watching","method":"get"},{"path":"/dashboard/library","method":"get"},{"path":"/dashboard/recommendations","method":"get"},{"path":"/dashboard/watch-history","method":"get"}]} showTitle={true} />
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Discover
|
||||
description: Advanced content discovery with filters
|
||||
full: true
|
||||
_openapi:
|
||||
method: GET
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Discover
|
||||
url: '#discover'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Discover
|
||||
id: discover
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/discover","method":"get"}]} showTitle={true} />
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Episodes
|
||||
description: Episode watch tracking
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Watch
|
||||
url: '#watch'
|
||||
- depth: 2
|
||||
title: Unwatch
|
||||
url: '#unwatch'
|
||||
- depth: 2
|
||||
title: Batch Watch
|
||||
url: '#batch-watch'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Watch
|
||||
id: watch
|
||||
- content: Unwatch
|
||||
id: unwatch
|
||||
- content: Batch Watch
|
||||
id: batch-watch
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/episodes/{id}/watch","method":"post"},{"path":"/episodes/{id}/unwatch","method":"post"},{"path":"/episodes/batch-watch","method":"post"}]} showTitle={true} />
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Explore
|
||||
description: Discover trending and popular content
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Trending
|
||||
url: '#trending'
|
||||
- depth: 2
|
||||
title: Popular
|
||||
url: '#popular'
|
||||
- depth: 2
|
||||
title: Genres
|
||||
url: '#genres'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Trending
|
||||
id: trending
|
||||
- content: Popular
|
||||
id: popular
|
||||
- content: Genres
|
||||
id: genres
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/explore/trending","method":"get"},{"path":"/explore/popular","method":"get"},{"path":"/explore/genres","method":"get"}]} showTitle={true} />
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Integrations
|
||||
description: Media server integrations
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: List
|
||||
url: '#list'
|
||||
- depth: 2
|
||||
title: Create
|
||||
url: '#create'
|
||||
- depth: 2
|
||||
title: Delete
|
||||
url: '#delete'
|
||||
- depth: 2
|
||||
title: Regenerate Token
|
||||
url: '#regenerate-token'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: List
|
||||
id: list
|
||||
- content: Create
|
||||
id: create
|
||||
- content: Delete
|
||||
id: delete
|
||||
- content: Regenerate Token
|
||||
id: regenerate-token
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/integrations","method":"get"},{"path":"/integrations","method":"post"},{"path":"/integrations/{provider}","method":"delete"},{"path":"/integrations/{provider}/regenerate-token","method":"post"}]} showTitle={true} />
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "API Reference",
|
||||
"icon": "Code"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: People
|
||||
description: Cast and crew information
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Detail
|
||||
url: '#detail'
|
||||
- depth: 2
|
||||
title: Resolve
|
||||
url: '#resolve'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Detail
|
||||
id: detail
|
||||
- content: Resolve
|
||||
id: resolve
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/people/{id}","method":"get"},{"path":"/people/resolve","method":"post"}]} showTitle={true} />
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Search
|
||||
description: Search for movies and TV shows
|
||||
full: true
|
||||
_openapi:
|
||||
method: GET
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Search
|
||||
url: '#search'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Search
|
||||
id: search
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/search","method":"get"}]} showTitle={true} />
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Seasons
|
||||
description: Season watch tracking
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Watch
|
||||
url: '#watch'
|
||||
- depth: 2
|
||||
title: Unwatch
|
||||
url: '#unwatch'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Watch
|
||||
id: watch
|
||||
- content: Unwatch
|
||||
id: unwatch
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/seasons/{id}/watch","method":"post"},{"path":"/seasons/{id}/unwatch","method":"post"}]} showTitle={true} />
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: System
|
||||
description: Server status and configuration
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Public Info
|
||||
url: '#public-info'
|
||||
- depth: 2
|
||||
title: Auth Config
|
||||
url: '#auth-config'
|
||||
- depth: 2
|
||||
title: Status
|
||||
url: '#status'
|
||||
- depth: 2
|
||||
title: Health
|
||||
url: '#health'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Public Info
|
||||
id: public-info
|
||||
- content: Auth Config
|
||||
id: auth-config
|
||||
- content: Status
|
||||
id: status
|
||||
- content: Health
|
||||
id: health
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/system/public-info","method":"get"},{"path":"/system/auth-config","method":"get"},{"path":"/system/status","method":"get"},{"path":"/system/health","method":"get"}]} showTitle={true} />
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Titles
|
||||
description: Movie and TV show management
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Detail
|
||||
url: '#detail'
|
||||
- depth: 2
|
||||
title: Resolve
|
||||
url: '#resolve'
|
||||
- depth: 2
|
||||
title: Update Status
|
||||
url: '#update-status'
|
||||
- depth: 2
|
||||
title: Update Rating
|
||||
url: '#update-rating'
|
||||
- depth: 2
|
||||
title: Watch Movie
|
||||
url: '#watch-movie'
|
||||
- depth: 2
|
||||
title: Watch All
|
||||
url: '#watch-all'
|
||||
- depth: 2
|
||||
title: User Info
|
||||
url: '#user-info'
|
||||
- depth: 2
|
||||
title: Recommendations
|
||||
url: '#recommendations'
|
||||
- depth: 2
|
||||
title: Hydrate Seasons
|
||||
url: '#hydrate-seasons'
|
||||
- depth: 2
|
||||
title: Quick Add
|
||||
url: '#quick-add'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Detail
|
||||
id: detail
|
||||
- content: Resolve
|
||||
id: resolve
|
||||
- content: Update Status
|
||||
id: update-status
|
||||
- content: Update Rating
|
||||
id: update-rating
|
||||
- content: Watch Movie
|
||||
id: watch-movie
|
||||
- content: Watch All
|
||||
id: watch-all
|
||||
- content: User Info
|
||||
id: user-info
|
||||
- content: Recommendations
|
||||
id: recommendations
|
||||
- content: Hydrate Seasons
|
||||
id: hydrate-seasons
|
||||
- content: Quick Add
|
||||
id: quick-add
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
<APIPage document={"./openapi.json"} webhooks={[]} operations={[{"path":"/titles/{id}","method":"get"},{"path":"/titles/resolve","method":"post"},{"path":"/titles/{id}/status","method":"put"},{"path":"/titles/{id}/rating","method":"put"},{"path":"/titles/{id}/watch","method":"post"},{"path":"/titles/{id}/watch-all","method":"post"},{"path":"/titles/{id}/user-info","method":"get"},{"path":"/titles/{id}/recommendations","method":"get"},{"path":"/titles/{id}/hydrate-seasons","method":"post"},{"path":"/titles/quick-add","method":"post"}]} showTitle={true} />
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Configuration
|
||||
description: Environment variables and server settings.
|
||||
icon: Settings
|
||||
---
|
||||
|
||||
## Required Variables
|
||||
@@ -110,6 +111,21 @@ OIDC_PROVIDER_NAME=Keycloak
|
||||
|
||||
To enforce SSO-only login, set `DISABLE_PASSWORD_LOGIN=true`. This only takes effect when OIDC is configured. The admin account created during setup will still work via OIDC.
|
||||
|
||||
### Account Linking
|
||||
|
||||
When OIDC is enabled, account linking is active by default. If a user logs in via OIDC with an email address that matches an existing Sofa account, the accounts are automatically linked — no manual confirmation is required. This means existing users can start using SSO without losing their library or watch history.
|
||||
|
||||
### Auto-Registration and Password Login
|
||||
|
||||
`OIDC_AUTO_REGISTER` and `DISABLE_PASSWORD_LOGIN` interact to control how users can access your instance:
|
||||
|
||||
| `OIDC_AUTO_REGISTER` | `DISABLE_PASSWORD_LOGIN` | Behavior |
|
||||
|---|---|---|
|
||||
| `true` (default) | `false` (default) | Both login methods available. New OIDC users get accounts created automatically. |
|
||||
| `true` | `true` | OIDC only. Accounts are auto-created on first login. Password form is hidden. |
|
||||
| `false` | `false` | Both login methods available. OIDC users must have a pre-existing account with a matching email. |
|
||||
| `false` | `true` | OIDC only, no auto-registration. Admin must create accounts first via the registration toggle. |
|
||||
|
||||
## TMDB Overrides
|
||||
|
||||
| Variable | Default | Description |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: Deploy Sofa with Docker in under a minute.
|
||||
icon: Zap
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -22,7 +22,7 @@ Sofa can automatically log movies and episodes when you finish watching them on
|
||||
The webhook URL looks like:
|
||||
|
||||
```
|
||||
https://your-sofa-server.com/api/webhooks/{token}
|
||||
https://sofa.example.com/api/webhooks/{token}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Integrations
|
||||
description: Connect Sofa to your media stack.
|
||||
icon: Puzzle
|
||||
---
|
||||
|
||||
Sofa integrates with popular media servers and the *arr stack to automate your tracking workflow.
|
||||
|
||||
@@ -23,7 +23,7 @@ Sofa can automatically log movies and episodes when you finish watching them on
|
||||
The webhook URL looks like:
|
||||
|
||||
```
|
||||
https://your-sofa-server.com/api/webhooks/{token}
|
||||
https://sofa.example.com/api/webhooks/{token}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Integrations",
|
||||
"pages": ["index", "plex", "jellyfin", "emby", "sonarr", "radarr"]
|
||||
"pages": ["plex", "jellyfin", "emby", "sonarr", "radarr"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ Sofa can automatically log movies and episodes when you finish watching them on
|
||||
The webhook URL looks like:
|
||||
|
||||
```
|
||||
https://your-sofa-server.com/api/webhooks/{token}
|
||||
https://sofa.example.com/api/webhooks/{token}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -18,7 +18,7 @@ Sofa can export your movie watchlist as a custom import list that Radarr polls a
|
||||
The list URL looks like:
|
||||
|
||||
```
|
||||
https://your-sofa-server.com/api/lists/{token}
|
||||
https://sofa.example.com/api/lists/{token}
|
||||
```
|
||||
|
||||
Radarr polls the list periodically (default: every 12 hours) and adds any new movies it finds.
|
||||
@@ -29,13 +29,13 @@ By default, the list exports titles with a **watchlist** status. You can customi
|
||||
|
||||
```
|
||||
# Only watchlist items (default)
|
||||
https://your-sofa-server.com/api/lists/{token}?status=watchlist
|
||||
https://sofa.example.com/api/lists/{token}?status=watchlist
|
||||
|
||||
# Watchlist and in-progress items
|
||||
https://your-sofa-server.com/api/lists/{token}?status=watchlist,in_progress
|
||||
https://sofa.example.com/api/lists/{token}?status=watchlist,in_progress
|
||||
|
||||
# All tracked items
|
||||
https://your-sofa-server.com/api/lists/{token}?status=watchlist,in_progress,completed
|
||||
https://sofa.example.com/api/lists/{token}?status=watchlist,in_progress,completed
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -18,7 +18,7 @@ Sofa can export your TV show watchlist as a custom import list that Sonarr polls
|
||||
The list URL looks like:
|
||||
|
||||
```
|
||||
https://your-sofa-server.com/api/lists/{token}
|
||||
https://sofa.example.com/api/lists/{token}
|
||||
```
|
||||
|
||||
Sonarr polls the list periodically (default: every 6 hours) and adds any new shows it finds.
|
||||
@@ -29,13 +29,13 @@ By default, the list exports titles with a **watchlist** status. You can customi
|
||||
|
||||
```
|
||||
# Only watchlist items (default)
|
||||
https://your-sofa-server.com/api/lists/{token}?status=watchlist
|
||||
https://sofa.example.com/api/lists/{token}?status=watchlist
|
||||
|
||||
# Watchlist and in-progress items
|
||||
https://your-sofa-server.com/api/lists/{token}?status=watchlist,in_progress
|
||||
https://sofa.example.com/api/lists/{token}?status=watchlist,in_progress
|
||||
|
||||
# All tracked items
|
||||
https://your-sofa-server.com/api/lists/{token}?status=watchlist,in_progress,completed
|
||||
https://sofa.example.com/api/lists/{token}?status=watchlist,in_progress,completed
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
{
|
||||
"pages": ["index", "configuration", "integrations", "mobile-app", "telemetry"]
|
||||
"pages": [
|
||||
"index",
|
||||
"using-sofa",
|
||||
"admin",
|
||||
"configuration",
|
||||
"integrations",
|
||||
"troubleshooting",
|
||||
"api",
|
||||
"mobile-app",
|
||||
"telemetry"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Mobile App
|
||||
description: Set up the iOS and Android app.
|
||||
icon: Smartphone
|
||||
---
|
||||
|
||||
Sofa has a native mobile app for iOS and Android built with [Expo](https://expo.dev/) and React Native. It connects to your self-hosted Sofa server and provides the same core features as the web app.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Telemetry
|
||||
description: How Sofa collects anonymous usage statistics.
|
||||
icon: RadioTower
|
||||
---
|
||||
|
||||
Sofa includes optional, privacy-focused telemetry to help understand how the project is used. Both systems are **opt-in** and can be disabled at any time.
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
description: Solutions for common issues.
|
||||
icon: LifeBuoy
|
||||
---
|
||||
|
||||
## TMDB API Issues
|
||||
|
||||
### Invalid or Missing Token
|
||||
|
||||
**Symptom**: Health status shows "Invalid token" or "Not configured."
|
||||
|
||||
- Verify that `TMDB_API_READ_ACCESS_TOKEN` is set. This is the **Read Access Token** (a long `eyJ...` string), not the shorter API Key.
|
||||
- Double-check the token at [themoviedb.org/settings/api](https://www.themoviedb.org/settings/api).
|
||||
- Restart the container after changing environment variables.
|
||||
|
||||
### TMDB Unreachable
|
||||
|
||||
**Symptom**: Health status shows "Unreachable."
|
||||
|
||||
- Check network connectivity from the container: `docker exec sofa ping api.themoviedb.org`
|
||||
- If you're using a proxy, verify `TMDB_API_BASE_URL` points to the correct endpoint.
|
||||
- TMDB may be experiencing downtime — check [status.themoviedb.org](https://status.themoviedb.org).
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Sofa includes a 300ms delay between TMDB requests per title to stay within API limits. If you see intermittent errors during large library refreshes, the affected titles will be retried on the next scheduled run. Avoid manually triggering multiple jobs at the same time.
|
||||
|
||||
## Webhook Integration Issues
|
||||
|
||||
### Watches Not Being Logged
|
||||
|
||||
Check the **event log** in Settings → Integrations → your provider. Common errors:
|
||||
|
||||
- **"Could not resolve TMDB ID"** — The media's external IDs (TMDB, IMDB, or TVDB) in your media server don't match a TMDB entry. Refresh the metadata in your media server.
|
||||
- **"Season/episode not found"** — The show's season data hasn't been loaded in Sofa. Search for the show in Sofa first to trigger a metadata import, then try playing again.
|
||||
- **"Failed to import"** — A TMDB API error occurred. Check the TMDB connection in [System Health](/docs/admin#status).
|
||||
|
||||
### Events Showing as "Ignored"
|
||||
|
||||
- **"Duplicate watch within 5 minutes"** — Normal deduplication. Some media servers send the same event multiple times.
|
||||
- **Partial playback** — For Jellyfin and Emby, only playback events with `PlayedToCompletion: true` are processed. Partially watched content is intentionally ignored.
|
||||
- **Wrong event type** — For Plex, only `media.scrobble` events are processed. Test events and other event types are ignored.
|
||||
|
||||
### Plex
|
||||
|
||||
- Plex Pass is required for webhooks.
|
||||
- Configure webhooks at [app.plex.tv → Settings → Webhooks](https://app.plex.tv/desktop/#!/settings/webhooks).
|
||||
- Plex's "Test Webhook" button sends a non-scrobble event that Sofa will ignore — this is expected.
|
||||
|
||||
### Jellyfin
|
||||
|
||||
- The [Webhook plugin](https://github.com/jellyfin/jellyfin-plugin-webhook) must be installed from the Jellyfin plugin catalog (Dashboard → Plugins → Catalog).
|
||||
- Ensure the **Playback Stop** notification type is enabled in the webhook destination settings.
|
||||
- Restart Jellyfin after installing the plugin.
|
||||
|
||||
### Emby
|
||||
|
||||
- Emby Premiere and Emby Server 4.7.9+ are required.
|
||||
- Both `playback.stop` and `PlaybackStop` event formats are supported — no special configuration needed.
|
||||
|
||||
## Image Caching Issues
|
||||
|
||||
### Images Not Loading
|
||||
|
||||
- Verify `IMAGE_CACHE_ENABLED` is `true` (the default).
|
||||
- Check that the data directory is writable — look at the write permission indicator in [System Health](/docs/admin#status).
|
||||
- Check available disk space. The image cache grows with your library and stores five categories: posters (500px), backdrops (1280px), stills (1280px), logos (92px), and profiles (185px).
|
||||
|
||||
### Clearing the Image Cache
|
||||
|
||||
Image files are stored in subdirectories under `CACHE_DIR` (default: `/data/images/`). To clear the cache:
|
||||
|
||||
1. Delete the contents of the images directory.
|
||||
2. Trigger the **Image cache** job from [Background Jobs](/docs/admin#background-jobs) to re-download images.
|
||||
|
||||
Alternatively, set `IMAGE_CACHE_ENABLED=false` to bypass the cache entirely and serve images directly from TMDB's CDN.
|
||||
|
||||
## OIDC / SSO Issues
|
||||
|
||||
### Login Button Not Appearing
|
||||
|
||||
All three OIDC environment variables must be set for the SSO button to appear: `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, and `OIDC_ISSUER_URL`. The button label is controlled by `OIDC_PROVIDER_NAME` (defaults to "SSO"). Restart the container after changing OIDC configuration.
|
||||
|
||||
### "Account Not Found" After OIDC Login
|
||||
|
||||
If `OIDC_AUTO_REGISTER` is set to `false`, new users must have a pre-existing Sofa account with a matching email address. Either set `OIDC_AUTO_REGISTER=true` to auto-create accounts on first OIDC login, or create accounts manually before users attempt SSO.
|
||||
|
||||
### Account Linking
|
||||
|
||||
When OIDC is enabled, account linking is automatic. If a user logs in via OIDC with an email that matches an existing password-based account, the accounts are linked — no manual step is required. See [Account Linking](/docs/configuration#account-linking) in the configuration docs for more details.
|
||||
|
||||
### Password Login Disabled But OIDC Not Working
|
||||
|
||||
`DISABLE_PASSWORD_LOGIN=true` hides the email/password form but only takes effect when OIDC is properly configured. If you're locked out:
|
||||
|
||||
1. Unset `DISABLE_PASSWORD_LOGIN` in your environment (or set it to `false`).
|
||||
2. Restart the container.
|
||||
3. Log in with email/password and fix your OIDC configuration.
|
||||
|
||||
### Callback URL Mismatch
|
||||
|
||||
The redirect URI configured in your identity provider must exactly match:
|
||||
|
||||
```
|
||||
https://your-sofa-url/api/auth/oauth2/callback/oidc
|
||||
```
|
||||
|
||||
Replace `https://your-sofa-url` with your `BETTER_AUTH_URL` value. The protocol, hostname, and port must match exactly.
|
||||
|
||||
## Database & Backup Issues
|
||||
|
||||
### Database Locked or Corrupt
|
||||
|
||||
Sofa uses SQLite with WAL (Write-Ahead Logging) mode. If the database appears locked:
|
||||
|
||||
- Ensure only **one Sofa instance** is accessing the same database file. Running multiple containers against the same volume will cause locking issues.
|
||||
- After an unclean shutdown, stale `-wal` and `-shm` files may remain alongside the database. These are usually handled automatically on next startup.
|
||||
- If corruption is detected, [restore from a backup](/docs/admin#restoring-from-a-backup).
|
||||
|
||||
### Restore Fails Validation
|
||||
|
||||
Backup validation checks integrity (`PRAGMA integrity_check`), foreign keys (`PRAGMA foreign_key_check`), and the presence of required tables. If a backup fails validation:
|
||||
|
||||
- It may be from an incompatible version or was corrupted during transfer.
|
||||
- Try downloading the backup again if it was transferred over a network.
|
||||
- A pre-restore snapshot is created before every restore attempt, so your current data is always safe.
|
||||
|
||||
### Data Directory Permissions
|
||||
|
||||
The `/data` directory (or your custom `DATA_DIR`) must be writable by the container process. Check the write permission indicator in [System Health](/docs/admin#status). For Docker, ensure the volume mount has the correct ownership and permissions.
|
||||
|
||||
## General Tips
|
||||
|
||||
### Checking Logs
|
||||
|
||||
Set `LOG_LEVEL=debug` for verbose output during troubleshooting. Sofa uses structured JSON logging via [Pino](https://getpino.io/).
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker logs sofa
|
||||
|
||||
# Follow logs in real time
|
||||
docker logs -f sofa
|
||||
```
|
||||
|
||||
### Health Endpoint
|
||||
|
||||
`GET /api/health` returns `{"status": "healthy"}` when the server is running. Use this for uptime monitoring or reverse proxy health checks. The Docker image includes a built-in health check that polls this endpoint every 30 seconds.
|
||||
|
||||
### Forcing a Full Refresh
|
||||
|
||||
Trigger jobs manually from the [Background Jobs](/docs/admin#background-jobs) panel in Settings:
|
||||
|
||||
- **Library refresh** — Re-fetch metadata for all tracked titles.
|
||||
- **Streaming availability** — Update provider data for all titles.
|
||||
- **New episodes** — Check for new episodes of tracked TV shows.
|
||||
- **Image cache** — Re-download any missing cached images.
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: Using Sofa
|
||||
description: A guide to tracking movies and TV shows with Sofa.
|
||||
icon: Compass
|
||||
---
|
||||
|
||||
## Dashboard
|
||||
|
||||
The dashboard is your home screen after logging in. It shows a personalized overview of your tracking activity.
|
||||
|
||||
- **Stats** — Cards showing movies watched, episodes watched, library size, and completed titles. Each card has a period selector (today, this week, this month, this year).
|
||||
- **Continue Watching** — TV shows you're currently watching, with the next unwatched episode highlighted. Click a show to jump to its detail page and pick up where you left off.
|
||||
- **In Your Library** — A grid of all titles you're tracking, regardless of status.
|
||||
- **Recommended for You** — Personalized suggestions based on titles you've watched and rated highly.
|
||||
|
||||
If your library is empty, the dashboard shows an invitation to start exploring.
|
||||
|
||||
## Tracking Movies & TV Shows
|
||||
|
||||
### The Status System
|
||||
|
||||
Every title in your library has one of three statuses:
|
||||
|
||||
| Status | Meaning |
|
||||
|---|---|
|
||||
| **Watchlist** | You plan to watch this |
|
||||
| **In Progress** | You're currently watching this |
|
||||
| **Completed** | You've finished watching this |
|
||||
|
||||
Set a status from any title's detail page by clicking the status button, or press <kbd>W</kbd> to cycle through statuses.
|
||||
|
||||
### Automatic Status Transitions
|
||||
|
||||
Sofa automatically updates statuses as you log watches so you don't have to manage them manually:
|
||||
|
||||
- **First episode watched** on a TV show → status changes to **In Progress** (if it was on your Watchlist or had no status)
|
||||
- **All episodes watched** on a TV show → status changes to **Completed**
|
||||
- **Unwatch an episode** on a completed show → status drops back to **In Progress**
|
||||
- **Mark a movie as watched** → status changes to **Completed**
|
||||
|
||||
These same transitions apply when watches are logged automatically via [webhook integrations](/docs/integrations) (Plex, Jellyfin, Emby).
|
||||
|
||||
### Logging Watches
|
||||
|
||||
**Movies**: Click the **Mark Watched** button on the title page (or press <kbd>M</kbd>) to log a watch and mark the movie as completed.
|
||||
|
||||
**TV episodes**: On the title page, expand a season and toggle individual episodes. You can also:
|
||||
|
||||
- **Mark an entire season** — Click the checkmark button on a season header to mark all its episodes as watched.
|
||||
- **Mark all episodes** — Mark the entire show as watched in one action.
|
||||
- **Catch up** — When you mark an episode as watched while earlier episodes are unwatched, a toast notification offers a "Catch up" button to batch-mark all prior episodes.
|
||||
|
||||
<Callout type="info">
|
||||
Duplicate watches within 5 minutes are automatically ignored. This prevents double-counting when webhook integrations send the same event multiple times.
|
||||
</Callout>
|
||||
|
||||
## Rating Titles
|
||||
|
||||
Rate any title from 1 to 5 stars on its detail page. Click a star to set the rating, or click the same star again to clear it. You can also press <kbd>1</kbd> through <kbd>5</kbd> on a title page to rate with the keyboard.
|
||||
|
||||
Ratings are independent of status — you can rate a title at any point, whether it's on your watchlist or completed. Highly-rated titles contribute to your personalized recommendations.
|
||||
|
||||
## Exploring & Discovering Content
|
||||
|
||||
The **Explore** page helps you find new things to watch:
|
||||
|
||||
- **Hero banner** — A featured trending title with backdrop, rating, and overview.
|
||||
- **Trending Today** — A scrollable row of titles currently trending on TMDB.
|
||||
- **Popular Movies** — Browse popular movies, filterable by genre using the dropdown.
|
||||
- **Popular TV Shows** — Browse popular TV shows, also filterable by genre.
|
||||
|
||||
Title cards show your tracking status as an overlay, so you can see at a glance which titles you've already added to your library.
|
||||
|
||||
## Search
|
||||
|
||||
Press <kbd>/</kbd> or <kbd>⌘K</kbd> (Mac) / <kbd>Ctrl+K</kbd> (Windows) to open the command palette. Start typing to search movies, TV shows, and people via TMDB.
|
||||
|
||||
Results show a poster or profile photo, type badge (Movie, TV, or Person), release year, and rating. Click a result to navigate to its detail page.
|
||||
|
||||
The command palette also shows:
|
||||
|
||||
- **Recent searches** — Your last few searches, with options to re-run or clear them.
|
||||
- **Quick actions** — Shortcuts to navigate to the dashboard or explore page.
|
||||
|
||||
## Streaming Availability
|
||||
|
||||
Title detail pages include a **Where to Watch** section showing which streaming services carry the title. Providers are grouped by offer type:
|
||||
|
||||
- **Stream** — Available with a subscription (Netflix, Disney+, etc.)
|
||||
- **Rent** — Available to rent digitally
|
||||
- **Buy** — Available to purchase digitally
|
||||
- **Free** — Available for free
|
||||
- **With Ads** — Available on ad-supported tiers
|
||||
|
||||
Click a provider's logo to open their page for that title in a new tab.
|
||||
|
||||
<Callout type="info">
|
||||
Streaming availability data is currently limited to the **United States**. Provider information is refreshed automatically every 6 hours for titles in your library.
|
||||
</Callout>
|
||||
|
||||
## Account Management
|
||||
|
||||
Manage your profile in **Settings**:
|
||||
|
||||
- **Display name** — Click your name to edit it inline.
|
||||
- **Profile picture** — Click your avatar to upload a new image (JPEG, PNG, WebP, or GIF) or remove the current one.
|
||||
- **Change password** — Available for email/password accounts. Not shown for OIDC/SSO-only accounts.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
Press <kbd>?</kbd> anywhere in the app to see the full shortcut reference. Here's a summary:
|
||||
|
||||
| Scope | Keys | Action |
|
||||
|---|---|---|
|
||||
| Global | `/` | Open search |
|
||||
| Global | `?` | Show keyboard shortcuts |
|
||||
| Navigation | `g` then `h` | Go to dashboard |
|
||||
| Navigation | `g` then `e` | Go to explore |
|
||||
| Title page | `w` | Cycle watch status |
|
||||
| Title page | `m` | Mark movie as watched |
|
||||
| Title page | `Escape` | Go back |
|
||||
| Title page | `1`–`5` | Rate 1–5 stars |
|
||||
@@ -10,6 +10,15 @@ const config = {
|
||||
// docs site is essentially self-contained from the rest of the monorepo
|
||||
root: import.meta.dirname,
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: "/docs/api",
|
||||
destination: "/docs/api/account",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
|
||||
+4588
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -9,18 +9,21 @@
|
||||
"lint": "biome check",
|
||||
"format": "biome format --write",
|
||||
"check-types": "fumadocs-mdx && next typegen && tsc --noEmit",
|
||||
"generate:api-docs": "bun scripts/generate-api-docs.ts",
|
||||
"postinstall": "fumadocs-mdx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@takumi-rs/image-response": "^0.73.0",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"fumadocs-core": "16.6.17",
|
||||
"fumadocs-mdx": "14.2.10",
|
||||
"fumadocs-ui": "16.6.17",
|
||||
"fumadocs-core": "^16.6.17",
|
||||
"fumadocs-mdx": "^14.2.10",
|
||||
"fumadocs-openapi": "^10.3.18",
|
||||
"fumadocs-ui": "^16.6.17",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "16.1.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"shiki": "^4.0.2",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { generateFiles } from "fumadocs-openapi";
|
||||
import { openapi } from "../src/lib/openapi";
|
||||
|
||||
void generateFiles({
|
||||
input: openapi,
|
||||
output: "./content/docs/api",
|
||||
per: "tag",
|
||||
addGeneratedComment: true,
|
||||
});
|
||||
@@ -77,7 +77,7 @@ const steps = [
|
||||
<pre className="mt-2 rounded-lg bg-fd-background/80 p-3 text-xs text-fd-muted-foreground ring-1 ring-white/[0.06]">
|
||||
<code>{`TMDB_API_READ_ACCESS_TOKEN=...
|
||||
BETTER_AUTH_SECRET=...
|
||||
BETTER_AUTH_URL=https://your-domain.com`}</code>
|
||||
BETTER_AUTH_URL=https://sofa.example.com`}</code>
|
||||
</pre>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
|
||||
<MarkdownCopyButton markdownUrl={`${page.url}.mdx`} />
|
||||
<ViewOptionsPopover
|
||||
markdownUrl={`${page.url}.mdx`}
|
||||
githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/content/docs/${page.path}`}
|
||||
githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/docs/content/docs/${page.path}`}
|
||||
/>
|
||||
</div>
|
||||
<DocsBody>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { source } from "@/lib/source";
|
||||
|
||||
export default function Layout({ children }: LayoutProps<"/docs">) {
|
||||
return (
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()} links={[]}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
@import "fumadocs-ui/css/neutral.css";
|
||||
@import "fumadocs-ui/css/preset.css";
|
||||
@import "fumadocs-openapi/css/preset.css";
|
||||
|
||||
@theme inline {
|
||||
--font-display: var(--font-dm-serif);
|
||||
@@ -47,6 +48,12 @@
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Text selection */
|
||||
::selection {
|
||||
background: oklch(0.8 0.14 65 / 70%);
|
||||
color: oklch(0.93 0.015 80);
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DM_Sans, DM_Serif_Display } from "next/font/google";
|
||||
import "./global.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://sofa.watch"),
|
||||
title: {
|
||||
default: "Sofa — Self-hosted movie & TV tracker",
|
||||
template: "%s | Sofa",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"use client";
|
||||
import { defineClientConfig } from "fumadocs-openapi/ui/client";
|
||||
|
||||
export default defineClientConfig({});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createAPIPage } from "fumadocs-openapi/ui";
|
||||
import { openapi } from "@/lib/openapi";
|
||||
import client from "./api-page.client";
|
||||
|
||||
export const APIPage = createAPIPage(openapi, { client });
|
||||
@@ -1,9 +1,11 @@
|
||||
import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
import { APIPage } from "@/components/api-page";
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents) {
|
||||
return {
|
||||
...defaultMdxComponents,
|
||||
APIPage,
|
||||
...components,
|
||||
} satisfies MDXComponents;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@ export function baseOptions(): BaseLayoutProps {
|
||||
<span className="font-display text-[17px] leading-none">Sofa</span>
|
||||
</>
|
||||
),
|
||||
transparentMode: "top",
|
||||
},
|
||||
links: [{ text: "Docs", url: "/docs" }],
|
||||
links: [
|
||||
{ text: "Docs", url: "/docs" },
|
||||
{ text: "API Reference", url: "/docs/api" },
|
||||
],
|
||||
githubUrl: "https://github.com/jakejarvis/sofa",
|
||||
themeSwitch: { enabled: false },
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createOpenAPI } from "fumadocs-openapi/server";
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
input: ["./openapi.json"],
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isMarkdownPreferred, rewritePath } from "fumadocs-core/negotiation";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const { rewrite: rewriteLLM } = rewritePath(
|
||||
"/docs{/*path}",
|
||||
"/llms.mdx/docs{/*path}",
|
||||
);
|
||||
|
||||
export default function proxy(request: NextRequest) {
|
||||
if (isMarkdownPreferred(request)) {
|
||||
const result = rewriteLLM(request.nextUrl.pathname);
|
||||
|
||||
if (result) {
|
||||
return NextResponse.rewrite(new URL(result, request.nextUrl));
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
||||
};
|
||||
Reference in New Issue
Block a user