upd wiki on chachy

This commit is contained in:
d-chrka 2026-03-15 11:25:35 +01:00
parent 64b5a82498
commit f28dc954e8
11 changed files with 133 additions and 97 deletions

View File

@ -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<input\\\\n\\\\t\\\\t\\\\t\\\\t\\\\tclass=\"flex-1 bg-transparent text-2xl font-bold text-white outline-none placeholder:text-muted\"'\nnew = '\\\\t\\\\t\\\\t\\\\t<input\\\\n\\\\t\\\\t\\\\t\\\\t\\\\tbind:this={titleInputEl}\\\\n\\\\t\\\\t\\\\t\\\\t\\\\tclass=\"flex-1 bg-transparent text-2xl font-bold text-white outline-none placeholder:text-muted\"'\n\nif old in content:\n content = content.replace\\(old, new, 1\\)\n with open\\(path, 'w'\\) as f:\n f.write\\(content\\)\n print\\(\"replaced\"\\)\nelse:\n print\\(\"NOT FOUND\"\\)\n # debug: show actual bytes around the area\n idx = content.find\\('<input'\\)\n print\\(repr\\(content[idx:idx+100]\\)\\)\nEOF)"
]
}
}

View File

@ -1 +1 @@
1.2.37
1.2.47

View File

@ -2,7 +2,7 @@
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { db } from '$lib/db/schema';
import { upsertPage, softDeletePage, togglePageFavorite, getNotebooksForPage, getBacklinksForPage, assignPageToNotebook, removePageFromNotebook, getAllNotebooks } from '$lib/db/repositories';
import { upsertPage, softDeletePage, togglePageFavorite, getNotebooksForPage, getBacklinksForPage, assignPageToNotebook, removePageFromNotebook, getAllNotebooks, createPage } from '$lib/db/repositories';
import MarkdownEditor from '$lib/components/MarkdownEditor.svelte';
import RenderedMarkdown from '$lib/components/RenderedMarkdown.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
@ -23,6 +23,7 @@
let autosaveTimer: ReturnType<typeof setTimeout> | undefined;
let titleInput = $state('');
let titleInputEl = $state<HTMLInputElement | undefined>();
let editorRef: MarkdownEditor | undefined = $state();
let notebooks = $state<Notebook[]>([]);
let backlinks = $state<Page[]>([]);
@ -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 @@
<div class="flex items-center gap-3">
{#if editing}
<input
bind:this={titleInputEl}
class="flex-1 bg-transparent text-2xl font-bold text-white outline-none placeholder:text-muted"
bind:value={titleInput}
onblur={saveTitle}
@ -153,6 +168,13 @@
>
{editing ? '✓ Fertig' : '✎ Bearbeiten'}
</button>
{#if !editing}
<button
class="rounded px-3 py-1.5 text-sm font-medium bg-white/10 text-[#ccc] hover:bg-white/20 hover:text-white transition-colors"
onclick={addPage}
title="Neue Seite mit gleichem Scope und Notizbüchern anlegen"
>+ Neue Seite</button>
{/if}
{#if editing}
<span class="text-xs {savedState === 'saved' ? 'text-muted' : savedState === 'saving' ? 'text-muted animate-pulse' : 'text-warning'}">
{savedState === 'saved' ? '✓ gespeichert' : savedState === 'saving' ? '…' : '● ungespeichert'}
@ -204,7 +226,8 @@
<!-- Content: read or edit -->
{#if editing}
<div class="rounded border border-accent/40 bg-sidebar/50 p-3 min-h-[300px]">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="rounded border border-accent/40 bg-sidebar/50 p-3 min-h-[300px]" ondblclick={(e) => { if (!(e.target as Element).closest('.cm-editor')) switchToRead(); }}>
<MarkdownEditor
bind:this={editorRef}
content={currentPage.body}

View File

@ -29,9 +29,9 @@ export default defineConfig({
})
],
server: {
host: '127.0.0.1',
host: 'localhost',
proxy: {
'/api': 'http://127.0.0.1:9000'
'/api': 'http://localhost:9000'
},
headers: {
'Cache-Control': 'no-store'

View File

@ -1,8 +1,10 @@
<#
.PARAMETER CheckOnly
Download and verify the prod DB without building or deploying.
.PARAMETER SkipDbBackup
Skip the DB download/verification step (use when DB hasn't changed since last run).
#>
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" } `

View File

@ -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`);
PRAGMA foreign_keys=ON;

Binary file not shown.

Binary file not shown.

View File

@ -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)

View File

@ -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) {

View File

@ -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);