diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 66f2359..b537b9f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -40,7 +40,8 @@ "Bash(paru -S --noconfirm visual-studio-code-bin)", "Bash(code --version)", "Bash(code --install-extension svelte.svelte-vscode --install-extension bradlc.vscode-tailwindcss --install-extension esbenp.prettier-vscode --install-extension dbaeumer.vscode-eslint)", - "Bash(git config:*)" + "Bash(git config:*)", + "Bash(FILE=\"/home/d-chrka@internal.lan/Projekte/chrka/Ka-Note/ka-note/client/src/routes/wiki/[id]/+page.svelte\"\npython3 - \"$FILE\" <<'EOF'\nimport sys\npath = sys.argv[1]\nwith open\\(path, 'r'\\) as f:\n content = f.read\\(\\)\n\nold = '\\\\t\\\\t\\\\t\\\\t | undefined; let titleInput = $state(''); + let titleInputEl = $state(); let editorRef: MarkdownEditor | undefined = $state(); let notebooks = $state([]); let backlinks = $state([]); @@ -37,9 +38,10 @@ // Re-initialize whenever pageId changes (SvelteKit reuses component on param navigation) $effect(() => { const id = pageId; // track reactively + const openInEdit = page.url.searchParams.get('edit') === '1'; currentPage = undefined; titleInitialized = false; - editing = false; + editing = openInEdit; notebooks = []; backlinks = []; db.pages.get(id).then(p => { @@ -58,6 +60,10 @@ if (currentPage && !titleInitialized) { titleInput = currentPage.title; titleInitialized = true; + if (editing) { + // focus title on next tick so the input is rendered + setTimeout(() => titleInputEl?.focus(), 0); + } } }); @@ -88,6 +94,14 @@ currentPage = { ...currentPage!, title }; } + async function addPage() { + const newPage = await createPage('Neue Seite', scope === 'private'); + for (const nb of notebooks) { + await assignPageToNotebook(newPage.id, nb.id); + } + goto(`/wiki/${newPage.id}?edit=1`); + } + async function handleDelete() { await softDeletePage(pageId); goto('/wiki'); @@ -118,6 +132,7 @@
{#if editing} {editing ? '✓ Fertig' : '✎ Bearbeiten'} + {#if !editing} + + {/if} {#if editing} {savedState === 'saved' ? '✓ gespeichert' : savedState === 'saving' ? '…' : '● ungespeichert'} @@ -204,7 +226,8 @@ {#if editing} -
+ +
{ if (!(e.target as Element).closest('.cm-editor')) switchToRead(); }}> -param([switch]$CheckOnly) +param([switch]$CheckOnly, [switch]$SkipDbBackup) $ErrorActionPreference = "Stop" @@ -34,60 +36,66 @@ if ($CheckOnly) { $checkLabel = if ($CheckOnly) { " [check-only]" } else { "" } Write-Host "=== Version: $VERSION$checkLabel ===" -ForegroundColor Yellow -Write-Host "=== Backup DB from Prod ===" -ForegroundColor Cyan -$KuduToken = az account get-access-token --resource "https://management.azure.com" --query accessToken -o tsv -if ($LASTEXITCODE -ne 0) { throw "Failed to get Azure access token" } $KuduDbUrl = "https://${APP}.scm.azurewebsites.net/api/vfs/data/ka-note.db" +$AppUrl = "https://$APP.azurewebsites.net" $BackupDir = Join-Path $PSScriptRoot "backups" $null = New-Item -ItemType Directory -Path $BackupDir -Force $BackupFile = Join-Path $BackupDir ("ka-note-pre-deploy-$VERSION.db") -# Flush WAL before download so the snapshot is consistent (WAL file not included in Kudu download). -$AppUrl = "https://$APP.azurewebsites.net" -if ($env:KA_NOTE_DEPLOY_API_KEY) { - Write-Host " Flushing WAL checkpoint before download..." -ForegroundColor DarkGray - try { - $cpResult = Invoke-RestMethod -Uri "$AppUrl/api/admin/checkpoint" -Method POST ` - -Headers @{ Authorization = "Bearer $env:KA_NOTE_DEPLOY_API_KEY" } ` - -UseBasicParsing -TimeoutSec 30 - Write-Host " Checkpoint: $($cpResult.checkpointed)/$($cpResult.log) pages" -ForegroundColor DarkGray - } catch { - Write-Host " Checkpoint call failed (continuing anyway): $_" -ForegroundColor Yellow - } +if ($SkipDbBackup) { + Write-Host "=== Skipping DB Backup (-SkipDbBackup) ===" -ForegroundColor Yellow + $KuduToken = $null } else { - Write-Host " KA_NOTE_DEPLOY_API_KEY not set, skipping WAL checkpoint" -ForegroundColor Yellow -} + Write-Host "=== Backup DB from Prod ===" -ForegroundColor Cyan + $KuduToken = az account get-access-token --resource "https://management.azure.com" --query accessToken -o tsv + if ($LASTEXITCODE -ne 0) { throw "Failed to get Azure access token" } -Write-Host " Downloading ka-note.db from prod..." -ForegroundColor DarkGray -try { - Invoke-WebRequest -Uri $KuduDbUrl ` - -Headers @{ Authorization = "Bearer $KuduToken" } ` - -OutFile $BackupFile -UseBasicParsing -} catch { - throw "Kudu DB download failed: $_" -} + # Flush WAL before download so the snapshot is consistent (WAL file not included in Kudu download). + if ($env:KA_NOTE_DEPLOY_API_KEY) { + Write-Host " Flushing WAL checkpoint before download..." -ForegroundColor DarkGray + try { + $cpResult = Invoke-RestMethod -Uri "$AppUrl/api/admin/checkpoint" -Method POST ` + -Headers @{ Authorization = "Bearer $env:KA_NOTE_DEPLOY_API_KEY" } ` + -UseBasicParsing -TimeoutSec 30 + Write-Host " Checkpoint: $($cpResult.checkpointed)/$($cpResult.log) pages" -ForegroundColor DarkGray + } catch { + Write-Host " Checkpoint call failed (continuing anyway): $_" -ForegroundColor Yellow + } + } else { + Write-Host " KA_NOTE_DEPLOY_API_KEY not set, skipping WAL checkpoint" -ForegroundColor Yellow + } -$DbSize = (Get-Item $BackupFile).Length -Write-Host " Downloaded: $([math]::Round($DbSize/1KB,1)) KB" -ForegroundColor DarkGray + Write-Host " Downloading ka-note.db from prod..." -ForegroundColor DarkGray + try { + Invoke-WebRequest -Uri $KuduDbUrl ` + -Headers @{ Authorization = "Bearer $KuduToken" } ` + -OutFile $BackupFile -UseBasicParsing + } catch { + throw "Kudu DB download failed: $_" + } -if ($DbSize -lt 4096) { - throw "DB backup is suspiciously small ($DbSize bytes) - aborting deploy" -} + $DbSize = (Get-Item $BackupFile).Length + Write-Host " Downloaded: $([math]::Round($DbSize/1KB,1)) KB" -ForegroundColor DarkGray -# Verify with sqlite3 (must be in PATH) -$IntegrityResult = & sqlite3 $BackupFile "PRAGMA integrity_check;" 2>&1 -if ($LASTEXITCODE -ne 0 -or $IntegrityResult -ne 'ok') { - throw "DB integrity_check FAILED: $IntegrityResult - aborting deploy. Backup saved at $BackupFile" -} + if ($DbSize -lt 4096) { + throw "DB backup is suspiciously small ($DbSize bytes) - aborting deploy" + } -Write-Host " integrity_check: ok ($([math]::Round($DbSize/1KB,1)) KB)" -ForegroundColor Green + # Verify with sqlite3 (must be in PATH) + $IntegrityResult = & sqlite3 $BackupFile "PRAGMA integrity_check;" 2>&1 + if ($LASTEXITCODE -ne 0 -or $IntegrityResult -ne 'ok') { + throw "DB integrity_check FAILED: $IntegrityResult - aborting deploy. Backup saved at $BackupFile" + } -if ($CheckOnly) { - Write-Host "" - Write-Host "=== Check passed -- no deploy performed ===" -ForegroundColor Green - Write-Host " Backup: $BackupFile" -ForegroundColor DarkGray - Write-Host "" - exit 0 + Write-Host " integrity_check: ok ($([math]::Round($DbSize/1KB,1)) KB)" -ForegroundColor Green + + if ($CheckOnly) { + Write-Host "" + Write-Host "=== Check passed -- no deploy performed ===" -ForegroundColor Green + Write-Host " Backup: $BackupFile" -ForegroundColor DarkGray + Write-Host "" + exit 0 + } } # Bump version now that DB check passed @@ -209,6 +217,10 @@ while ($keepValidating) { $keepValidating = $true } elseif ($answer -eq 'yes') { Write-Host " Uploading backup to prod..." -ForegroundColor Cyan + if (-not $KuduToken) { + $KuduToken = az account get-access-token --resource "https://management.azure.com" --query accessToken -o tsv + if ($LASTEXITCODE -ne 0) { throw "Failed to get Azure access token for restore" } + } Invoke-WebRequest -Uri $KuduDbUrl ` -Method PUT ` -Headers @{ Authorization = "Bearer $KuduToken" } ` diff --git a/ka-note/server/drizzle/0002_ambiguous_night_nurse.sql b/ka-note/server/drizzle/0002_ambiguous_night_nurse.sql index a68317a..72907a1 100644 --- a/ka-note/server/drizzle/0002_ambiguous_night_nurse.sql +++ b/ka-note/server/drizzle/0002_ambiguous_night_nurse.sql @@ -1,4 +1,46 @@ PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_contexts` ( + `id` text NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + `meta` text, + `archived_at` text, + `is_favorite` integer DEFAULT false NOT NULL, + `updated_at` text NOT NULL, + `deleted_at` text, + `version` integer DEFAULT 1 NOT NULL, + PRIMARY KEY(`id`, `user_id`) +); +--> statement-breakpoint +INSERT INTO `__new_contexts`("id", "user_id", "name", "type", "sort_order", "meta", "archived_at", "is_favorite", "updated_at", "deleted_at", "version") SELECT "id", '', "name", "type", "sort_order", "meta", "archived_at", "is_favorite", "updated_at", "deleted_at", "version" FROM `contexts`;--> statement-breakpoint +DROP TABLE `contexts`;--> statement-breakpoint +ALTER TABLE `__new_contexts` RENAME TO `contexts`;--> statement-breakpoint +CREATE INDEX `contexts_updated_at_idx` ON `contexts` (`updated_at`);--> statement-breakpoint +CREATE INDEX `contexts_user_id_idx` ON `contexts` (`user_id`);--> statement-breakpoint +CREATE TABLE `__new_topics` ( + `id` text NOT NULL, + `user_id` text NOT NULL, + `context_id` text NOT NULL, + `title` text NOT NULL, + `status` text DEFAULT 'active' NOT NULL, + `snooze_until` text, + `sort_order` integer DEFAULT 0 NOT NULL, + `is_new` integer DEFAULT true NOT NULL, + `updated_at` text NOT NULL, + `deleted_at` text, + `version` integer DEFAULT 1 NOT NULL, + PRIMARY KEY(`id`, `user_id`), + FOREIGN KEY (`context_id`,`user_id`) REFERENCES `contexts`(`id`,`user_id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +INSERT INTO `__new_topics`("id", "user_id", "context_id", "title", "status", "snooze_until", "sort_order", "is_new", "updated_at", "deleted_at", "version") SELECT "id", '', "context_id", "title", "status", "snooze_until", "sort_order", "is_new", "updated_at", "deleted_at", "version" FROM `topics`;--> statement-breakpoint +DROP TABLE `topics`;--> statement-breakpoint +ALTER TABLE `__new_topics` RENAME TO `topics`;--> statement-breakpoint +CREATE INDEX `topics_updated_at_idx` ON `topics` (`updated_at`);--> statement-breakpoint +CREATE INDEX `topics_context_id_idx` ON `topics` (`context_id`);--> statement-breakpoint +CREATE INDEX `topics_user_id_idx` ON `topics` (`user_id`);--> statement-breakpoint CREATE TABLE `__new_history_entries` ( `id` text NOT NULL, `user_id` text NOT NULL, @@ -18,7 +60,6 @@ CREATE TABLE `__new_history_entries` ( INSERT INTO `__new_history_entries`("id", "user_id", "topic_id", "date", "text", "sort_order", "linked_context_id", "done_at", "updated_at", "deleted_at", "version") SELECT "id", '', "topic_id", "date", "text", "sort_order", "linked_context_id", "done_at", "updated_at", "deleted_at", "version" FROM `history_entries`;--> statement-breakpoint DROP TABLE `history_entries`;--> statement-breakpoint ALTER TABLE `__new_history_entries` RENAME TO `history_entries`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint CREATE INDEX `history_entries_updated_at_idx` ON `history_entries` (`updated_at`);--> statement-breakpoint CREATE INDEX `history_entries_topic_id_idx` ON `history_entries` (`topic_id`);--> statement-breakpoint CREATE INDEX `history_entries_user_id_idx` ON `history_entries` (`user_id`);--> statement-breakpoint @@ -44,45 +85,4 @@ ALTER TABLE `__new_ratings` RENAME TO `ratings`;--> statement-breakpoint CREATE INDEX `ratings_updated_at_idx` ON `ratings` (`updated_at`);--> statement-breakpoint CREATE INDEX `ratings_topic_id_idx` ON `ratings` (`topic_id`);--> statement-breakpoint CREATE INDEX `ratings_user_id_idx` ON `ratings` (`user_id`);--> statement-breakpoint -CREATE TABLE `__new_topics` ( - `id` text NOT NULL, - `user_id` text NOT NULL, - `context_id` text NOT NULL, - `title` text NOT NULL, - `status` text DEFAULT 'active' NOT NULL, - `snooze_until` text, - `sort_order` integer DEFAULT 0 NOT NULL, - `is_new` integer DEFAULT true NOT NULL, - `updated_at` text NOT NULL, - `deleted_at` text, - `version` integer DEFAULT 1 NOT NULL, - PRIMARY KEY(`id`, `user_id`), - FOREIGN KEY (`context_id`,`user_id`) REFERENCES `contexts`(`id`,`user_id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -INSERT INTO `__new_topics`("id", "user_id", "context_id", "title", "status", "snooze_until", "sort_order", "is_new", "updated_at", "deleted_at", "version") SELECT "id", '', "context_id", "title", "status", "snooze_until", "sort_order", "is_new", "updated_at", "deleted_at", "version" FROM `topics`;--> statement-breakpoint -DROP TABLE `topics`;--> statement-breakpoint -ALTER TABLE `__new_topics` RENAME TO `topics`;--> statement-breakpoint -CREATE INDEX `topics_updated_at_idx` ON `topics` (`updated_at`);--> statement-breakpoint -CREATE INDEX `topics_context_id_idx` ON `topics` (`context_id`);--> statement-breakpoint -CREATE INDEX `topics_user_id_idx` ON `topics` (`user_id`);--> statement-breakpoint -CREATE TABLE `__new_contexts` ( - `id` text NOT NULL, - `user_id` text NOT NULL, - `name` text NOT NULL, - `type` text NOT NULL, - `sort_order` integer DEFAULT 0 NOT NULL, - `meta` text, - `archived_at` text, - `is_favorite` integer DEFAULT false NOT NULL, - `updated_at` text NOT NULL, - `deleted_at` text, - `version` integer DEFAULT 1 NOT NULL, - PRIMARY KEY(`id`, `user_id`) -); ---> statement-breakpoint -INSERT INTO `__new_contexts`("id", "user_id", "name", "type", "sort_order", "meta", "archived_at", "is_favorite", "updated_at", "deleted_at", "version") SELECT "id", '', "name", "type", "sort_order", "meta", "archived_at", "is_favorite", "updated_at", "deleted_at", "version" FROM `contexts`;--> statement-breakpoint -DROP TABLE `contexts`;--> statement-breakpoint -ALTER TABLE `__new_contexts` RENAME TO `contexts`;--> statement-breakpoint -CREATE INDEX `contexts_updated_at_idx` ON `contexts` (`updated_at`);--> statement-breakpoint -CREATE INDEX `contexts_user_id_idx` ON `contexts` (`user_id`); \ No newline at end of file +PRAGMA foreign_keys=ON; diff --git a/ka-note/server/ka-note.db-shm b/ka-note/server/ka-note.db-shm index 40674fb..d3f4527 100644 Binary files a/ka-note/server/ka-note.db-shm and b/ka-note/server/ka-note.db-shm differ diff --git a/ka-note/server/ka-note.db-wal b/ka-note/server/ka-note.db-wal index fc57f22..5cbe80b 100644 Binary files a/ka-note/server/ka-note.db-wal and b/ka-note/server/ka-note.db-wal differ diff --git a/ka-note/server/src/routes/api-keys.ts b/ka-note/server/src/routes/api-keys.ts index d80a151..0d7acd3 100644 --- a/ka-note/server/src/routes/api-keys.ts +++ b/ka-note/server/src/routes/api-keys.ts @@ -48,7 +48,7 @@ router.post('/', handle('api-keys/create', async (c) => { router.delete('/:id', handle('api-keys/delete', async (c) => { const { userId } = c.get('auth'); - const id = c.req.param('id'); + const id = c.req.param('id')!; const result = await db .update(apiKeys) diff --git a/ka-note/server/src/routes/backup.ts b/ka-note/server/src/routes/backup.ts index 6987545..e249f69 100644 --- a/ka-note/server/src/routes/backup.ts +++ b/ka-note/server/src/routes/backup.ts @@ -67,7 +67,7 @@ backup.get('/list', handle('backup/list', async (c) => { // Download a stored backup file backup.get('/download/:filename', handle('backup/download', async (c) => { - const filename = c.req.param('filename'); + const filename = c.req.param('filename')!; let filePath: string; try { filePath = backupFilePath(filename); @@ -85,7 +85,7 @@ backup.get('/download/:filename', handle('backup/download', async (c) => { // Delete a stored backup backup.delete('/:filename', handle('backup/delete', async (c) => { - const filename = c.req.param('filename'); + const filename = c.req.param('filename')!; try { await deleteBackup(filename); } catch (e: unknown) { diff --git a/ka-note/server/src/routes/sync.ts b/ka-note/server/src/routes/sync.ts index 65b92c7..4928996 100644 --- a/ka-note/server/src/routes/sync.ts +++ b/ka-note/server/src/routes/sync.ts @@ -56,7 +56,7 @@ sync.get('/pull', handle('sync/pull', async (c) => { sync.get('/blob/:id', handle('sync/blob', async (c) => { const { userId } = c.get('auth'); - const id = c.req.param('id'); + const id = c.req.param('id')!; const blob = await fetchBlob(id, userId); if (!blob) return c.json({ error: 'Not found' }, 404); return c.json(blob);