Compare commits

..
26 Commits
Author SHA1 Message Date
cupcakearmy f6ea6376e1 Merge pull request #221 from cupcakearmy/fix/spa-fallback-status-code
Fix/spa fallback status code
2026-09-26 13:33:33 +02:00
cupcakearmy 19e7899309 docs: add changelog note for SPA fallback + api 404 fix 2026-09-21 21:57:48 +02:00
cupcakearmy 9e69730bfd fix: keep SPA fallback for non-api paths but 404 unmatched /api routes 2026-09-21 21:39:09 +02:00
cupcakearmy b5e1c4b42a chore: cleanup 2026-09-21 21:27:50 +02:00
cupcakearmy 7660dd7c30 remove uncessary commens 2026-09-21 21:18:59 +02:00
cupcakearmy 685784c360 Merge branch 'main' into fix/spa-fallback-status-code 2026-09-21 21:16:57 +02:00
cupcakearmy cc2c45221b Merge pull request #219 from cupcakearmy/v3
v3
2026-09-21 21:08:14 +02:00
cupcakearmy b113d253a9 chore: bump to 3.0.0 2026-09-21 20:51:51 +02:00
cupcakearmy c99c62cf00 chore: clean up compose file 2026-09-21 20:50:48 +02:00
cupcakearmy 7ef162bd83 chore: bump to 3.0.0-rc.4 2026-09-19 16:02:55 +02:00
cupcakearmy 282fdb97d1 docs: add LLM usage disclosure to readmes 2026-09-19 16:00:04 +02:00
cupcakearmy 4dfed492bc docs: correct frontend bullet in v3 changelog (was already SvelteKit) 2026-09-19 15:51:37 +02:00
cupcakearmy 5084ed59f0 docs: fix incorrect pg note wording in v3 changelog 2026-09-19 15:47:20 +02:00
cupcakearmy 055a48a38d fix(web): stop transferring nested file buffers in worker unpack (comlink multi-buffer failure) 2026-09-19 15:13:34 +02:00
cupcakearmy d1126ace82 fix(web): don't run file downloads as a $derived side effect 2026-09-19 14:44:35 +02:00
cupcakearmy b58bf8af6d feat: move calculation intensive part into a web worker 2026-09-13 21:51:13 +02:00
cupcakearmy a57ead61b9 fix(web): snapshot files before worker transfer (Svelte proxy not cloneable) 2026-09-13 21:17:24 +02:00
cupcakearmy 0d597edfee fix(web): re-add FileDTO import; run svelte-check in CI 2026-09-13 16:19:41 +02:00
cupcakearmy 7653409473 chore: bump to 3.0.0-rc.3 2026-09-12 16:13:41 +02:00
cupcakearmy f05707e033 update deps 2026-09-12 16:13:02 +02:00
cupcakearmy 8df6b6c7b5 docs: use v3 image tag instead of latest in compose 2026-09-12 15:56:00 +02:00
cupcakearmy 2dc9d6aa16 chore: bump to 3.0.0-rc.2 2026-09-12 15:32:32 +02:00
cupcakearmy 98c56f6fb9 fix(cli): replace removed run-s in prepublishOnly 2026-09-12 15:32:02 +02:00
cupcakearmy 3f7bb6f32e chore: bump to 3.0.0-rc.1 2026-09-12 15:29:49 +02:00
cupcakearmy 6de97039d3 ci: publish with --no-git-checks and drop done cli-deps todo 2026-09-12 15:27:29 +02:00
Matheus Leal 8034b1a501 fix: serve SPA fallback with 200 instead of 404
`ServeDir::not_found_service` wraps the fallback in `SetStatus`, which forces
every response to `404 Not Found`. Client side routes such as `/note/<id>` and
`/about` were therefore served the correct `index.html` but with a 404 status.

Use `ServeDir::fallback` instead, which leaves the status untouched. A note that
genuinely does not exist is still reported as 404 by `/api/notes/<id>`.

Behind a reverse proxy this made effectively every document request show up as a
4xx, skewing error rate dashboards and triggering false alerts.

Fixes #217
2026-09-03 16:09:40 -03:00
23 changed files with 655 additions and 560 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
if [[ "${GITHUB_REF_NAME}" == *"-"* ]]; then if [[ "${GITHUB_REF_NAME}" == *"-"* ]]; then
DIST_TAG=rc DIST_TAG=rc
fi fi
pnpm publish --filter cryptgeon --tag "${DIST_TAG}" pnpm publish --filter cryptgeon --tag "${DIST_TAG}" --no-git-checks
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+3
View File
@@ -34,6 +34,9 @@ jobs:
- name: Shared tests - name: Shared tests
run: pnpm --filter @cryptgeon/shared test run: pnpm --filter @cryptgeon/shared test
- name: Frontend type check
run: pnpm --filter @cryptgeon/web check
- name: Run your tests - name: Run your tests
run: pnpm test run: pnpm test
+6 -2
View File
@@ -11,15 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New shared TypeScript package `@cryptgeon/shared` as single source of truth for crypto, content codec and API client (crypto + compression + payload + types). - New shared TypeScript package `@cryptgeon/shared` as single source of truth for crypto, content codec and API client (crypto + compression + payload + types).
- Shared payload codec: `packContent` / `unpackContent` (encode → LZ4 → XChaCha20-Poly1305 and reverse). - Shared payload codec: `packContent` / `unpackContent` (encode → LZ4 → XChaCha20-Poly1305 and reverse).
- New `pg`-backend storage of note hashes in the cache. - Cache-backed note storage using hashes (valkey/redis) with atomic view counting.
### Changed ### Changed
- Encryption from AES to **XChaCha20-Poly1305** (client-side); dropped `occulto`. - Encryption from AES to **XChaCha20-Poly1305** (client-side); dropped `occulto`.
- All API bodies switched to **MessagePack**. - All API bodies switched to **MessagePack**.
- Frontend migrated to SvelteKit + `@cryptgeon/shared`. - Frontend uses `@cryptgeon/shared` for crypto + payload codec (replacing the previous local `cryptgeon/shared`); heavy pack/unpack runs in a web worker.
- CLI rebuilt with `vite-plus` (bundles all deps) and imports from `@cryptgeon/shared`. - CLI rebuilt with `vite-plus` (bundles all deps) and imports from `@cryptgeon/shared`.
### Fixed
- SPA fallback now serves the app (200) for client-side routes such as `/about` and `/note/<id>`, while unmatched `/api/*` paths still return 404.
### Breaking changes ### Breaking changes
- Endpoints moved to `/api/v3/notes/` and `/api/v3/status`; health check to `/healthz`. - Endpoints moved to `/api/v3/notes/` and `/api/v3/status`; health check to `/healthz`.
+5 -1
View File
@@ -115,7 +115,7 @@ services:
- /data - /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:v3
depends_on: depends_on:
- cache - cache
environment: environment:
@@ -169,6 +169,10 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md).
Please refer to the security section [here](./SECURITY.md). Please refer to the security section [here](./SECURITY.md).
## Usage of LLMs
Starting from V3, I used LLMs heavily to implement _my own ideas_. This means that the direction and architecture choices are human. A lot of the implementation that derives from that, is automated with an LLM.
--- ---
_Attributions_ _Attributions_
+5 -1
View File
@@ -103,7 +103,7 @@ services:
- /data - /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:v3
depends_on: depends_on:
- cache - cache
environment: environment:
@@ -151,6 +151,10 @@ Ver [CONTRIBUTING.md](./CONTRIBUTING.md).
Por favor dirígete a la sección de seguridad [aquí](./SECURITY.md). Por favor dirígete a la sección de seguridad [aquí](./SECURITY.md).
## Uso de LLMs
A partir de la V3, utilicé LLMs de forma intensiva para implementar _mis propias ideas_. Esto significa que la dirección y las decisiones de arquitectura son humanas. Gran parte de la implementación que deriva de eso está automatizada con un LLM.
--- ---
_Atribuciones_ _Atribuciones_
+6 -2
View File
@@ -91,7 +91,7 @@ services:
- /data - /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:v3
depends_on: depends_on:
- cache - cache
environment: environment:
@@ -131,7 +131,7 @@ services:
- /data - /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:v3
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- cache - cache
@@ -149,6 +149,10 @@ services:
参见 [CONTRIBUTING.md](./CONTRIBUTING.md)。 参见 [CONTRIBUTING.md](./CONTRIBUTING.md)。
## LLM 的使用
从 V3 开始,我大量使用 LLM 来实现_我自己的想法_。这意味着项目的方向和架构选择均由人类决定。由此衍生的大部分实现由 LLM 自动完成。
###### Attributions ###### Attributions
- 测试数据: - 测试数据:
+10 -12
View File
@@ -1,17 +1,17 @@
services: services:
cache: cache:
image: valkey/valkey:7-alpine image: valkey/valkey:9-alpine
# This is required to stay in RAM only. # This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no command: valkey-server --save "" --appendonly no
# Set a size limit. See link below on how to customise. # Set a size limit. See link below on how to customise.
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/ # https://valkey.io/topics/lru-cache/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine # --maxmemory 1gb --maxmemory-policy allkeys-lru
# This prevents the creation of an anonymous volume. # This prevents the creation of an anonymous volume.
tmpfs: tmpfs:
- /data - /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:3
depends_on: depends_on:
- cache - cache
environment: environment:
@@ -19,11 +19,9 @@ services:
SIZE_LIMIT: 4 MiB SIZE_LIMIT: 4 MiB
ports: ports:
- 80:8000 - 80:8000
healthcheck:
# Optional health checks test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/healthz"]
# healthcheck: interval: 1m
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/healthz"] timeout: 3s
# interval: 1m retries: 2
# timeout: 3s start_period: 5s
# retries: 2
# start_period: 5s
-2
View File
@@ -2,11 +2,9 @@
## Todo ## Todo
- Localize i18n readmes: `README_ES.md`, `README_zh-CN.md` still reference `REDIS` / `/api/v1`-era endpoint names — align to `CACHE` / `/healthz` (also sweep `CONTRIBUTING.md`, `examples/*`, postman collection).
- Add remaining shared tooling to the pnpm catalog (`vite`, `tsdown`). - Add remaining shared tooling to the pnpm catalog (`vite`, `tsdown`).
- Move formatting, linting and type-checking + git hooks onto `vite-plus` (oxlint, oxfmt, vitest). - Move formatting, linting and type-checking + git hooks onto `vite-plus` (oxlint, oxfmt, vitest).
- Re-add CSP (`Content-Security-Policy`) wired into the axum router (was in `csp.rs`, removed as unused). - Re-add CSP (`Content-Security-Policy`) wired into the axum router (was in `csp.rs`, removed as unused).
- Move all CLI deps to `devDependencies` — they bundle into the single output file anyway.
## Unified payload (drop the text/file union) ## Unified payload (drop the text/file union)
+1 -1
View File
@@ -12,7 +12,7 @@ services:
- /data - /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:v3
depends_on: depends_on:
- cache - cache
+1 -1
View File
@@ -116,7 +116,7 @@ services:
- /data - /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:v3
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- cache - cache
+2 -2
View File
@@ -26,7 +26,7 @@ services:
- /data - /data
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:v3
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- cache - cache
@@ -70,7 +70,7 @@ services:
- /data - /data
cryptgeon: cryptgeon:
image: cupcakearmy/cryptgeon image: cupcakearmy/cryptgeon:v3
depends_on: depends_on:
- cache - cache
labels: labels:
+1 -1
View File
@@ -252,7 +252,7 @@ dependencies = [
[[package]] [[package]]
name = "cryptgeon" name = "cryptgeon"
version = "3.0.0-rc.0" version = "3.0.0"
dependencies = [ dependencies = [
"axum", "axum",
"bs62", "bs62",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "cryptgeon" name = "cryptgeon"
version = "3.0.0-rc.0" version = "3.0.0"
authors = ["cupcakearmy <hi@nicco.io>"] authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2024" edition = "2024"
rust-version = "1.95" rust-version = "1.95"
+8 -2
View File
@@ -20,6 +20,10 @@ mod note;
mod status; mod status;
mod store; mod store;
async fn api_not_found() -> axum::http::StatusCode {
axum::http::StatusCode::NOT_FOUND
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
dotenv().ok(); dotenv().ok();
@@ -39,11 +43,13 @@ async fn main() {
.nest("/notes", notes_routes) .nest("/notes", notes_routes)
.merge(status_routes); .merge(status_routes);
let api_routes = Router::new().nest("/v3", v3_routes); let api_routes = Router::new()
.nest("/v3", v3_routes)
.fallback(api_not_found);
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html"); let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
let serve_dir = let serve_dir =
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index)); ServeDir::new(config::FRONTEND_PATH.to_string()).fallback(ServeFile::new(index));
let app = Router::new() let app = Router::new()
.nest("/api", api_routes) .nest("/api", api_routes)
.merge(health_routes) .merge(health_routes)
+8 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "cryptgeon", "name": "cryptgeon",
"version": "3.0.0-rc.0", "version": "3.0.0",
"homepage": "https://github.com/cupcakearmy/cryptgeon", "homepage": "https://github.com/cupcakearmy/cryptgeon",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -21,19 +21,19 @@
"scripts": { "scripts": {
"build": "vp pack", "build": "vp pack",
"dev": "vp pack --watch", "dev": "vp pack --watch",
"prepublishOnly": "run-s build" "prepublishOnly": "pnpm run build"
}, },
"devDependencies": { "devDependencies": {
"@commander-js/extra-typings": "^15.0.0",
"@cryptgeon/shared": "workspace:*", "@cryptgeon/shared": "workspace:*",
"@msgpack/msgpack": "^3.1.3", "@msgpack/msgpack": "^3.1.3",
"@commander-js/extra-typings": "^12.1.0",
"@tsconfig/strictest": "catalog:", "@tsconfig/strictest": "catalog:",
"@types/inquirer": "^9.0.9", "@types/inquirer": "^9.0.10",
"@types/node": "^22.15.3", "@types/node": "^22.20.1",
"commander": "^12.1.0", "commander": "^15.0.0",
"inquirer": "^9.3.8", "inquirer": "^14.2.1",
"mime": "^4.1.0", "mime": "^4.1.0",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^7.1.3",
"typescript": "catalog:", "typescript": "catalog:",
"vite-plus": "catalog:" "vite-plus": "catalog:"
}, },
+8 -7
View File
@@ -13,21 +13,22 @@
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.61.1", "@sveltejs/kit": "^2.70.3",
"@sveltejs/vite-plugin-svelte": "^7.1.2", "@sveltejs/vite-plugin-svelte": "^7.3.0",
"@zerodevx/svelte-toast": "^0.9.6", "@zerodevx/svelte-toast": "^0.9.6",
"license-checker-rseidelsohn": "^5.0.1", "license-checker-rseidelsohn": "^5.0.1",
"svelte": "^5.55.9", "svelte": "^5.57.0",
"svelte-check": "^4.4.8", "svelte-check": "^4.7.6",
"svelte-intl-precompile": "^0.12.3", "svelte-intl-precompile": "^0.12.3",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vite": "^8.0.14" "vite": "^8.2.2"
}, },
"dependencies": { "dependencies": {
"@cryptgeon/shared": "workspace:*", "@cryptgeon/shared": "workspace:*",
"@fontsource/fira-mono": "^5.2.7", "@fontsource/fira-mono": "^5.3.0",
"pretty-bytes": "^7.1.0", "comlink": "^4.4.2",
"pretty-bytes": "^7.1.3",
"uqr": "^0.1.3" "uqr": "^0.1.3"
} }
} }
+3 -3
View File
@@ -46,11 +46,11 @@ async function downloadFile(file: FileDTO) {
files = note.contents files = note.contents
} }
}) })
let download = $derived(() => { function downloadAll() {
for (const file of files) { for (const file of files) {
downloadFile(file) downloadFile(file)
} }
}) }
let links = $derived(typeof note.contents === 'string' ? note.contents.match(RE_URL) : []) let links = $derived(typeof note.contents === 'string' ? note.contents.match(RE_URL) : [])
</script> </script>
@@ -92,7 +92,7 @@ async function downloadFile(file: FileDTO) {
{/key} {/key}
{/if} {/if}
{/each} {/each}
<Button onclick={download}>{$t('show.download_all')}</Button> <Button onclick={downloadAll}>{$t('show.download_all')}</Button>
{/if} {/if}
</div> </div>
+23 -8
View File
@@ -1,12 +1,15 @@
<script lang="ts"> <script lang="ts">
import { import {
bytesToHex,
create as apiCreate, create as apiCreate,
bytesToHex,
packContent, packContent,
type ServerNote type FileDTO,
type NoteInput,
type ServerNote,
} from '@cryptgeon/shared' } from '@cryptgeon/shared'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
import { blur } from 'svelte/transition' import { blur } from 'svelte/transition'
import { transfer } from 'comlink'
import { status } from '$lib/stores/status' import { status } from '$lib/stores/status'
import { notify } from '$lib/toast' import { notify } from '$lib/toast'
@@ -15,10 +18,12 @@
import FileUpload from '$lib/ui/FileUpload.svelte' import FileUpload from '$lib/ui/FileUpload.svelte'
import Loader from '$lib/ui/Loader.svelte' import Loader from '$lib/ui/Loader.svelte'
import MaxSize from '$lib/ui/MaxSize.svelte' import MaxSize from '$lib/ui/MaxSize.svelte'
import PastedFilesPreview from '$lib/ui/PastedFilesPreview.svelte'
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte' import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
import PastedFilesPreview from '$lib/ui/PastedFilesPreview.svelte'
import Switch from '$lib/ui/Switch.svelte' import Switch from '$lib/ui/Switch.svelte'
import TextArea from '$lib/ui/TextArea.svelte' import TextArea from '$lib/ui/TextArea.svelte'
import { createWorker } from '$lib/worker'
import { onMount } from 'svelte'
let note: { views: number; expiration: number } = $state({ views: 1, expiration: 60 }) let note: { views: number; expiration: number } = $state({ views: 1, expiration: 60 })
let files: FileDTO[] = $state([]) let files: FileDTO[] = $state([])
@@ -53,6 +58,8 @@
if (!isFile) textContent = '' if (!isFile) textContent = ''
}) })
const worker = createWorker()
async function handlePaste(e: ClipboardEvent) { async function handlePaste(e: ClipboardEvent) {
const data = e.clipboardData const data = e.clipboardData
if (!data) return if (!data) return
@@ -123,13 +130,21 @@
throw new EmptyContentError() throw new EmptyContentError()
} }
const payload = packContent( const noteInput: NoteInput = isFile
isFile ? { type: 'files', files } : { type: 'text', text: textContent }, ? transfer(
customPassword || undefined {
) type: 'files',
files: $state.snapshot(files),
},
files.map((f) => f.data.buffer)
)
: { type: 'text', text: textContent }
const payload = await worker.pack(noteInput, customPassword || undefined)
const serverNote: ServerNote = { const serverNote: ServerNote = {
meta: { meta: {
...(timeExpiration ? { expiration: parseInt(note.expiration as any) } : { views: parseInt(note.views as any) }), ...(timeExpiration
? { expiration: parseInt(note.expiration as any) }
: { views: parseInt(note.views as any) }),
extra: payload.extra, extra: payload.extra,
}, },
data: payload.data, data: payload.data,
+14
View File
@@ -0,0 +1,14 @@
import { wrap } from 'comlink'
import CryptWorker from './worker?worker'
import type { packContent, unpackContent } from '@cryptgeon/shared'
export function createWorker() {
const worker = new CryptWorker()
return wrap<WorkerConract>(worker)
}
export type WorkerConract = {
pack: typeof packContent
unpack: typeof unpackContent
}
@@ -0,0 +1,15 @@
import type { WorkerConract } from '$lib/worker'
import { packContent, unpackContent } from '@cryptgeon/shared'
import { expose, transfer } from 'comlink'
const contract: WorkerConract = {
pack(input, password) {
const content = packContent(input, password)
return transfer(content, [content.data.buffer, content.extra.buffer, content.key.buffer])
},
unpack(data, key) {
return unpackContent(data, key)
},
}
expose(contract)
@@ -1,5 +1,13 @@
<script lang="ts"> <script lang="ts">
import { deriveKey, hexToBytes, decode, info, get as apiGet, unpackContent, type FileDTO } from '@cryptgeon/shared' import {
deriveKey,
hexToBytes,
decode,
info,
get as apiGet,
unpackContent,
type FileDTO,
} from '@cryptgeon/shared'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { t } from 'svelte-intl-precompile' import { t } from 'svelte-intl-precompile'
@@ -8,6 +16,7 @@
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte' import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import type { PageData } from './$types' import type { PageData } from './$types'
import { createWorker } from '$lib/worker'
interface Props { interface Props {
data: PageData data: PageData
@@ -44,6 +53,8 @@
} }
}) })
const worker = createWorker()
async function show(e: SubmitEvent) { async function show(e: SubmitEvent) {
e.preventDefault() e.preventDefault()
try { try {
@@ -69,7 +80,7 @@
key = hexToBytes(password!) key = hexToBytes(password!)
} }
const content = unpackContent(serverNote.data, key) const content = await worker.unpack(serverNote.data, key)
switch (content.type) { switch (content.type) {
case 'text': case 'text':
@@ -78,16 +89,16 @@
contents: content.data, contents: content.data,
} }
break break
case 'files': case 'files':
const files = (content.data as any[]).map((f: any) => ({ const files = (content.data as any[]).map((f: any) => ({
...f, ...f,
data: f.data instanceof Uint8Array ? f.data : new Uint8Array(f.data as any), data: f.data instanceof Uint8Array ? f.data : new Uint8Array(f.data as any),
})) }))
note = { note = {
meta: { type: 'file' }, meta: { type: 'file' },
contents: files, contents: files,
} }
break break
default: default:
error = $t('show.errors.unsupported_type') error = $t('show.errors.unsupported_type')
return return
+494 -491
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
import { expect, test } from "@playwright/test";
test.describe("@web", () => {
for (const path of ["/", "/about", "/note/does-not-exist"]) {
test(`serves ${path} with status 200`, async ({ request }) => {
const response = await request.get(path);
expect(response.status()).toBe(200);
});
}
test("api still reports a missing note as 404", async ({ request }) => {
const response = await request.get("/api/notes/does-not-exist");
expect(response.status()).toBe(404);
});
});