Compare commits

...
19 Commits
Author SHA1 Message Date
github-actions[bot] 4058b99680 chore: bump version to 0.22.2 2026-03-17 22:40:08 +00:00
Fred Cabassut f94ad8e479 Revert "fix: set ghcr.io package visibility to public after Docker push"
This reverts commit 49cc7e0317.
2026-03-17 23:37:16 +01:00
github-actions[bot] f4fbb05bfc chore: bump version to 0.22.1 2026-03-17 22:13:20 +00:00
Claude f2825aef59 fix: set ghcr.io package visibility to public after Docker push
Users pulling the image without authentication get "unauthorized" because
ghcr.io packages default to private under an organization. Add a step in
both release workflows to explicitly set the container package to public.

https://claude.ai/code/session_01QovQeW26KfEEmU1KdFfCPU
2026-03-17 23:12:59 +01:00
Frederic CabassutandClaude Opus 4.6 1d3e7bc159 fix: use npx tsx for openapi generation in build script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 06:58:42 +01:00
github-actions[bot] b52ff822dc chore: pin docker-compose to v0.22.0 [skip ci] 2026-03-12 23:05:13 +00:00
github-actions[bot] ba73ccde1c chore: bump version to 0.22.0 2026-03-12 23:00:57 +00:00
Frederic CabassutandClaude Opus 4.6 6973b0c24c feat: auto-generate discoverable OpenAPI spec from route tree
Adds a script that scans src/routes/api/ for exported HTTP methods and
generates an OpenAPI 3.1 spec at /api/openapi.json. Runs automatically
before each build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:59:52 +01:00
Frederic CabassutandClaude Opus 4.6 ed2cdaac6a chore: update about page wording to "bretzel.app universe"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:53:40 +01:00
github-actions[bot] 0a96faa5e3 chore: pin docker-compose to v0.21.9 [skip ci] 2026-03-12 22:31:07 +00:00
github-actions[bot] fb6ff18a16 chore: bump version to 0.21.9 2026-03-12 22:26:57 +00:00
Frederic CabassutandClaude Opus 4.6 de7780486f fix: replace emoji PWA icons with pixel-art pretzel from favicon.svg
The PWA manifest icons were a generic pretzel emoji on a dark background,
not matching the app's pixel-art pretzel favicon. Regenerated from the
favicon.svg source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:26:43 +01:00
github-actions[bot] e079a1e783 chore: pin docker-compose to v0.21.8 [skip ci] 2026-03-12 22:16:28 +00:00
github-actions[bot] 0329ee3052 chore: bump version to 0.21.8 2026-03-12 22:12:11 +00:00
Frederic CabassutandClaude Opus 4.6 f7652f2de3 fix: use absolute paths for service worker registration
SvelteKit sets Vite's base to './' for client builds, causing the PWA
plugin to generate relative SW paths (./sw.js). On sub-routes like
/settings or /tag/foo, this resolves to /settings/sw.js which returns
HTML, breaking SW registration. Force scope and buildBase to '/'.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:11:56 +01:00
github-actions[bot] 46a17232c4 chore: pin docker-compose to v0.21.7 [skip ci] 2026-03-12 21:16:06 +00:00
github-actions[bot] 1a7e214675 chore: bump version to 0.21.7 2026-03-12 21:11:35 +00:00
Frederic CabassutandClaude Opus 4.6 84e99a3497 fix: remove maskable icon purpose to prevent double-cropping
The maskable purpose caused Android to crop the already-round icon
into its adaptive shape, resulting in a square-inside-circle appearance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:11:08 +01:00
github-actions[bot] d1c48e7799 chore: pin docker-compose to v0.21.6 [skip ci] 2026-03-12 20:13:13 +00:00
9 changed files with 953 additions and 8 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
services:
crumbs:
image: ghcr.io/bretzel-app/crumbs:0.21.3
image: ghcr.io/bretzel-app/crumbs:0.22.0
container_name: crumbs
ports:
- "3000:3000"
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "crumbs",
"private": true,
"version": "0.21.6",
"version": "0.22.2",
"type": "module",
"license": "MIT",
"author": "Bretzel (https://bretzel.app)",
@@ -12,7 +12,8 @@
},
"scripts": {
"dev": "vite dev",
"build": "vite build",
"build": "npx tsx scripts/generate-openapi.ts && vite build",
"openapi": "npx tsx scripts/generate-openapi.ts",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+147
View File
@@ -0,0 +1,147 @@
/**
* Auto-generates an OpenAPI spec from the SvelteKit route tree.
*
* Scans all +server.ts files under src/routes/api/ for exported HTTP methods and
* builds a minimal OpenAPI 3.1 spec at static/.well-known/openapi.json.
*
* Usage: npx tsx scripts/generate-openapi.ts
*/
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
import { resolve, relative, dirname, join } from 'path';
const ROOT = process.cwd();
const ROUTES_DIR = resolve(ROOT, 'src/routes/api');
const OUTPUT = resolve(ROOT, 'static/api/openapi.json');
const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'));
function walkDir(dir: string): string[] {
const results: string[] = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
results.push(...walkDir(full));
} else if (entry === '+server.ts') {
results.push(full);
}
}
return results;
}
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
const METHOD_RE = new RegExp(`export\\s+const\\s+(${HTTP_METHODS.join('|')})`, 'g');
function svelteKitPathToOpenAPI(filePath: string): string {
const rel = relative(ROUTES_DIR, dirname(filePath));
const segments = rel === '' ? [] : rel.split('/');
const apiPath =
'/api/' +
segments
.map((s) => {
const match = s.match(/^\[(.+)\]$/);
return match ? `{${match[1]}}` : s;
})
.join('/');
return apiPath;
}
function extractMethods(filePath: string): string[] {
const content = readFileSync(filePath, 'utf-8');
const methods: string[] = [];
let match;
while ((match = METHOD_RE.exec(content)) !== null) {
methods.push(match[1].toLowerCase());
}
METHOD_RE.lastIndex = 0;
return methods;
}
function inferTag(path: string): string {
const first = path.replace('/api/', '').split('/')[0];
return first.charAt(0).toUpperCase() + first.slice(1);
}
function extractPathParams(path: string) {
const params: { name: string; in: string; required: boolean; schema: { type: string } }[] = [];
const re = /\{(\w+)\}/g;
let match;
while ((match = re.exec(path)) !== null) {
params.push({ name: match[1], in: 'path', required: true, schema: { type: 'string' } });
}
return params;
}
const files = walkDir(ROUTES_DIR);
const paths: Record<string, Record<string, unknown>> = {};
for (const fullPath of files) {
const apiPath = svelteKitPathToOpenAPI(fullPath);
const methods = extractMethods(fullPath);
if (methods.length === 0) continue;
const pathParams = extractPathParams(apiPath);
const pathEntry: Record<string, unknown> = {};
if (pathParams.length > 0) {
pathEntry.parameters = pathParams;
}
for (const method of methods) {
const operation: Record<string, unknown> = {
tags: [inferTag(apiPath)],
responses: {
'200': { description: 'Success' }
}
};
if (['post', 'put', 'patch'].includes(method)) {
operation.requestBody = {
content: {
'application/json': {
schema: { type: 'object' }
}
}
};
}
pathEntry[method] = operation;
}
paths[apiPath] = pathEntry;
}
const sortedPaths: Record<string, Record<string, unknown>> = {};
for (const key of Object.keys(paths).sort()) {
sortedPaths[key] = paths[key];
}
const spec = {
openapi: '3.1.0',
info: {
title: `${pkg.name.charAt(0).toUpperCase() + pkg.name.slice(1)} API`,
description: `Auto-generated API spec for ${pkg.name}`,
version: pkg.version,
contact: {
name: 'Bretzel',
url: 'https://bretzel.app'
}
},
servers: [{ url: '/', description: 'Current instance' }],
security: [{ sessionCookie: [] }],
paths: sortedPaths,
components: {
securitySchemes: {
sessionCookie: {
type: 'apiKey',
in: 'cookie',
name: 'session'
}
}
}
};
mkdirSync(dirname(OUTPUT), { recursive: true });
writeFileSync(OUTPUT, JSON.stringify(spec, null, 2) + '\n');
console.log(`Generated ${relative(ROOT, OUTPUT)} with ${Object.keys(paths).length} paths`);
+2 -2
View File
@@ -35,8 +35,8 @@
href="https://bretzel.app"
target="_blank"
rel="noopener noreferrer"
class="text-[var(--primary)] underline hover:text-[var(--primary-hover)]">Bretzel</a
> family of apps.
class="text-[var(--primary)] underline hover:text-[var(--primary-hover)]">bretzel.app</a
> universe.
</p>
</section>
+796
View File
@@ -0,0 +1,796 @@
{
"openapi": "3.1.0",
"info": {
"title": "Crumbs API",
"description": "Auto-generated API spec for crumbs",
"version": "0.21.9",
"contact": {
"name": "Bretzel",
"url": "https://bretzel.app"
}
},
"servers": [
{
"url": "/",
"description": "Current instance"
}
],
"security": [
{
"sessionCookie": []
}
],
"paths": {
"/api/admin/users": {
"get": {
"tags": [
"Admin"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"post": {
"tags": [
"Admin"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/admin/users/{id}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"patch": {
"tags": [
"Admin"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"delete": {
"tags": [
"Admin"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/auth/account": {
"delete": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/auth/login": {
"post": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/auth/logout": {
"post": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/auth/oauth/{provider}": {
"parameters": [
{
"name": "provider",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"get": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/auth/oauth/{provider}/callback": {
"parameters": [
{
"name": "provider",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"get": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/auth/password": {
"put": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/auth/profile": {
"put": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/auth/sessions": {
"get": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"delete": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/auth/setup": {
"post": {
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/mcp": {
"post": {
"tags": [
"Mcp"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"get": {
"tags": [
"Mcp"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"delete": {
"tags": [
"Mcp"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/notes": {
"get": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"post": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/notes/reorder": {
"post": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/notes/{id}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"get": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"patch": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"delete": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/notes/{id}/attachments": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"get": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"post": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"patch": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"delete": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/notes/{id}/collaborators": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"get": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"post": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"delete": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/notes/{id}/versions": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"get": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/notes/{id}/versions/{versionId}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "versionId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"get": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/notes/{id}/versions/{versionId}/restore": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "versionId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"post": {
"tags": [
"Notes"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/preferences": {
"get": {
"tags": [
"Preferences"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"put": {
"tags": [
"Preferences"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/search": {
"get": {
"tags": [
"Search"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/settings/api-keys": {
"get": {
"tags": [
"Settings"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"post": {
"tags": [
"Settings"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"/api/settings/api-keys/{id}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"delete": {
"tags": [
"Settings"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/sync": {
"post": {
"tags": [
"Sync"
],
"responses": {
"200": {
"description": "Success"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"get": {
"tags": [
"Sync"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/tags": {
"get": {
"tags": [
"Tags"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/api/users/search": {
"get": {
"tags": [
"Users"
],
"responses": {
"200": {
"description": "Success"
}
}
}
}
},
"components": {
"securitySchemes": {
"sessionCookie": {
"type": "apiKey",
"in": "cookie",
"name": "session"
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 299 KiB

+3 -2
View File
@@ -23,6 +23,8 @@ export default defineConfig({
]
: []),
SvelteKitPWA({
scope: '/',
buildBase: '/',
registerType: 'autoUpdate',
manifest: {
name: 'Crumbs by Bretzel',
@@ -34,8 +36,7 @@ export default defineConfig({
theme_color: '#C8860A',
icons: [
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' }
]
},
workbox: {
+1 -1
View File
@@ -280,7 +280,7 @@
<li><a href="https://github.com/bretzel-app/crumbs/blob/main/docs/DEPLOYMENT.md" target="_blank" rel="noopener">Docs</a></li>
<li><a href="https://github.com/bretzel-app/crumbs/blob/main/LICENSE" target="_blank" rel="noopener">License</a></li>
</ul>
<span class="footer-version">v0.21.6</span>
<span class="footer-version">v0.22.2</span>
</div>
</footer>