From 6eef2d681c6df4d94b8f1a1934e4faee8dd0f2ab Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Sat, 15 Aug 2026 02:13:05 +0200 Subject: [PATCH] Multiple features. Admin page with verweeste items. Breadcrump working. --- README.md | 17 +- info.rkt | 2 +- private/attachment-references.rkt | 98 +++++++++ private/migrations.rkt | 27 ++- private/storage.rkt | 120 ++++++++++- server.rkt | 18 ++ static/css/wiki.css | 51 +++++ static/index.html | 9 + static/js/wiki.js | 340 ++++++++++++++++++++++++++++-- translate.rkt | 8 +- 10 files changed, 663 insertions(+), 27 deletions(-) create mode 100644 private/attachment-references.rkt diff --git a/README.md b/README.md index a820a77..b15dda6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # racket-wiki -Current development version: **0.2.19**. +Current development version: **0.2.23**. A small self-hosted wiki with a Racket backend and an HTML5/CSS/JavaScript frontend. @@ -365,3 +365,18 @@ Pages whose slug starts with `template-` are available in the editor Template dr ## 0.2.20 Inline `todo(...)` markers render as a yellow `Todo: text` link. Clicking the marker opens the Todo view at the matching item and highlights it. Todo markers inside fenced code blocks remain untouched. + + +## 0.2.23 + +Breadcrumbs now keep a per-browser-tab history of visited wiki pages. The start page remains the fixed root. Earlier pages in the trail are clickable; selecting one truncates the trail at that page, after which navigation continues from there. Returning to the wiki name/Home clears the trail and opens the start page. Special views such as Todo, Recent, Bookmarks, Graph and Admin do not become entries in the page-history breadcrumb. The trail is stored in browser session storage and is limited to the most recent eight non-home wiki pages. + + +## 0.2.23 + +CamelCase and normalized unlinked page mentions are rendered as automatic links to existing wiki pages. Section edit temporarily highlights the target source line. The EasyMDE toolbar contains a Raw Markdown toggle; the preference is kept in the browser. + + +## 0.2.24 + +Attachment references are tracked in database schema 6. Admin contains an Orphaned uploads page showing uploads no current page references, together with their last historical page/version when available. diff --git a/info.rkt b/info.rkt index 0911107..c114e34 100644 --- a/info.rkt +++ b/info.rkt @@ -1,7 +1,7 @@ #lang info (define pkg-authors '(hnmdijkema)) -(define version "0.2.21") +(define version "0.2.25") (define license 'MIT) (define collection "racket-wiki") (define pkg-desc diff --git a/private/attachment-references.rkt b/private/attachment-references.rkt new file mode 100644 index 0000000..24a4b16 --- /dev/null +++ b/private/attachment-references.rkt @@ -0,0 +1,98 @@ +#lang racket/base + +(require db + racket/string) + +(provide replace-current-attachment-references! + record-version-attachment-references! + rebuild-attachment-references!) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Supporting functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (attachment-rows db) + (query-rows db + #<6! db) + (query-exec db + #<5! db)) + (define after-todo-reindex (database-schema-version db)) + (when (= after-todo-reindex 5) + (migrate-5->6! db)) (define resulting-version (database-schema-version db)) (when (> resulting-version current-schema-version) (error 'migrate-database! diff --git a/private/storage.rkt b/private/storage.rkt index 299d02f..55d7195 100644 --- a/private/storage.rkt +++ b/private/storage.rkt @@ -6,6 +6,7 @@ racket/list racket/path racket/string + "attachment-references.rkt" "config.rkt" "database.rkt" "todo.rkt") @@ -26,6 +27,8 @@ list-bookmarks set-bookmark! delete-bookmark! + list-orphaned-uploads + delete-orphaned-upload! save-upload! uploaded-file) @@ -161,12 +164,13 @@ (hash-ref item 'text)))) (define (insert-version! db page-id version title markdown author action summary now tags) - (query-exec db - #<text tags) author action summary now)) + page-id version title markdown (tags->text tags) author action summary now)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; goal : Create a wiki page and its first version. @@ -193,8 +197,11 @@ VALUES ($1, $2, $3, $4, 1, $5, $5, $6, $6, RETURNING id SQL slug title markdown (tags->text tags) now author)) - (insert-version! db page-id 1 title markdown author "create" summary now tags) - (replace-todos! db page-id markdown))))) + (define page-version-id + (insert-version! db page-id 1 title markdown author "create" summary now tags)) + (replace-todos! db page-id markdown) + (replace-current-attachment-references! db page-id markdown now) + (record-version-attachment-references! db page-id page-version-id markdown now))))) (read-page config slug)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -237,8 +244,12 @@ SET title = $1, markdown = $2, tags = $3, current_version = $4, WHERE id = $7 SQL title markdown (tags->text page-tags) next-version now author (vector-ref row 0)) - (insert-version! db (vector-ref row 0) next-version title markdown author "edit" summary now page-tags) - (replace-todos! db (vector-ref row 0) markdown))))) + (define page-id (vector-ref row 0)) + (define page-version-id + (insert-version! db page-id next-version title markdown author "edit" summary now page-tags)) + (replace-todos! db page-id markdown) + (replace-current-attachment-references! db page-id markdown now) + (record-version-attachment-references! db page-id page-version-id markdown now))))) (read-page config slug)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -261,7 +272,10 @@ RETURNING id SQL (current-seconds) author slug)) (unless id - (error 'archive-page! "unknown page: ~a" slug)))) + (error 'archive-page! "unknown page: ~a" slug)) + (query-exec db + "DELETE FROM attachment_references WHERE page_id = $1 AND current_reference = TRUE" + id))) (void)) (define (page-id config slug) @@ -500,6 +514,96 @@ SQL user-id slug))) (void)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; goal : List uploads that no current page references. +; pre : PostgreSQL schema 6 or newer is initialized. +; post : Attachment and reference rows have only been read. +; result : A newest-first list with owner and last historical use metadata. +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(define (list-orphaned-uploads config) + (call-with-wiki-database + config + (λ (db) + (for/list ((row (in-list + (query-rows db + #< current-count 0) + (error 'delete-orphaned-upload! "attachment is still referenced by a current page")) + (define deleted-id + (query-maybe-value db + "DELETE FROM attachments WHERE id = $1 RETURNING id" + attachment-id)) + (unless deleted-id + (error 'delete-orphaned-upload! "unknown attachment: ~a" attachment-id)))))) + (void)) + (define (safe-file-name name) (define clean (regexp-replace* #px"[^A-Za-z0-9._ -]" name "_")) diff --git a/server.rkt b/server.rkt index 93d2240..cb872a4 100644 --- a/server.rkt +++ b/server.rkt @@ -394,6 +394,20 @@ CSS (upload-file-response attachment) (json-error 404 "File not found"))))) +(define (admin-orphaned-uploads-handler config req) + (require-role + config req 'admin + (λ (_session) + (json-response (hash 'uploads (list-orphaned-uploads config)))))) + +(define (admin-delete-orphaned-upload-handler config req id) + (require-write-role + config req 'admin + (λ (_session) + (with-handlers ((exn:fail? (λ (e) (json-error 400 (exn-message e))))) + (delete-orphaned-upload! config id) + (json-response (hash 'ok #t)))))) + (define (admin-users-handler config req) (require-role config req 'admin @@ -537,6 +551,10 @@ CSS (λ (req slug) (upload-handler config req slug))] [("uploads" (string-arg) (string-arg)) #:method "get" (λ (req slug stored-name) (upload-get-handler config req slug stored-name))] + [("api" "admin" "uploads" "orphaned") #:method "get" + (λ (req) (admin-orphaned-uploads-handler config req))] + [("api" "admin" "uploads" "orphaned" (integer-arg)) #:method "delete" + (λ (req id) (admin-delete-orphaned-upload-handler config req id))] [("api" "admin" "users") #:method "get" (λ (req) (admin-users-handler config req))] [("api" "admin" "users") #:method "post" diff --git a/static/css/wiki.css b/static/css/wiki.css index 1edc424..e30c4fb 100644 --- a/static/css/wiki.css +++ b/static/css/wiki.css @@ -1150,3 +1150,54 @@ body.editor-mode .editor-metadata-row { outline: 2px solid #e1c75a; outline-offset: 2px; } + +/* 0.2.23: source-mode and section editing */ +.EasyMDEContainer.raw-markdown .CodeMirror .cm-header-1, +.EasyMDEContainer.raw-markdown .CodeMirror .cm-header-2, +.EasyMDEContainer.raw-markdown .CodeMirror .cm-header-3, +.EasyMDEContainer.raw-markdown .CodeMirror .cm-header-4, +.EasyMDEContainer.raw-markdown .CodeMirror .cm-header-5, +.EasyMDEContainer.raw-markdown .CodeMirror .cm-header-6, +.EasyMDEContainer.raw-markdown .CodeMirror .cm-strong, +.EasyMDEContainer.raw-markdown .CodeMirror .cm-em { + font-size: inherit !important; + line-height: inherit !important; + font-weight: normal !important; + font-style: normal !important; +} + +.EasyMDEContainer .CodeMirror .section-edit-highlight { + background: #fff3a3; +} + +.wiki-auto-link { + text-decoration-style: dotted; + text-underline-offset: 2px; +} + +.orphaned-uploads-list { + display: grid; + gap: 12px; +} + +.orphaned-upload-row { + padding: 12px 0; + border-bottom: 1px solid var(--wiki-border); +} + +.orphaned-upload-actions { + display: flex; + align-items: center; + gap: 12px; + margin-top: 6px; +} + +.orphaned-upload-uses { + margin: 4px 0 0 18px; + padding: 0; +} + +.orphaned-upload-uses { + margin: 4px 0 0 18px; + padding: 0; +} diff --git a/static/index.html b/static/index.html index 4a2c6aa..e2db088 100644 --- a/static/index.html +++ b/static/index.html @@ -158,9 +158,18 @@ + +