Compare commits

...

4 Commits

Author SHA1 Message Date
luxick f78ebe7d5d Remove auto new tab for external links 2026-08-05 18:26:09 +02:00
luxick d943fc5596 Update CLAUDE.md 2026-08-05 18:25:52 +02:00
luxick e9061a221c Hide console on windows
The companion does no longer show a terminal window pop up
2026-08-03 19:29:27 +02:00
luxick 5a5305dd42 Update CLAUDE.md 2026-08-03 19:28:03 +02:00
7 changed files with 34 additions and 197 deletions
+3 -137
View File
@@ -1,141 +1,7 @@
# CLAUDE.md # CLAUDE.md
## Project Overview I want to understand every line of code that goes into this project. Never create, edit, move, rename, or delete project files unless I explicitly ask you to do so. Instead, show me every proposed edit in the chat so I can type it in manually.
`datascape` is a minimal personal wiki where **the folder structure is the wiki**. Do not run commands that modify project files, install dependencies, or change repository state unless I explicitly request that action. Instead, show me those commands in the chat so I can run them manually.
No database, no CMS, no abstraction layer — every folder is a page, and `index.md`
in a folder is that page's content.
## Build & Deploy I'm an experienced developer. Do not explain syntax, APIs, programming concepts, or implementation details unless explicitly asked.
```bash
# Local build (host architecture)
go build .
# Deploy to NAS
make deploy
```
### Editor bundle (the one build-pipeline exception)
The page editor uses CodeMirror 6, vendored as a single pre-built IIFE at
`assets/editor/vendor/codemirror.bundle.js` and embedded via `embed.FS`. This is
the **only** deliberate exception to the "no build pipeline" rule below — it is a
one-time, committed artifact, not a runtime build. `go build` / `make deploy`
never touch Node and only consume the committed bundle.
Regenerate the bundle **only** when upgrading the `@codemirror/*` versions:
```bash
# bump versions in editor-build/package.json first, then:
make editor # runs `npm ci && npm run build` in editor-build/, rewrites the vendored bundle
```
Commit the regenerated `codemirror.bundle.js` and the updated
`editor-build/package-lock.json`. `editor-build/node_modules/` is gitignored.
The bundle is served immutable under a stable filename, so the edit template
appends `?v=<content-hash>` to its `<script>` src (`editorBundleVersion` in
`main.go`). The hash changes whenever the bundle bytes change, so a rebuilt
bundle busts client caches automatically — no manual version bump needed.
## HTTP API Surface
| Method | Path | Behaviour |
|--------|------|-----------|
| GET | `/{path}/` | If folder exists: render `index.md` + list contents. If not: show empty create prompt. |
| GET | `/{path}/?edit` | CodeMirror 6 editor initialized with `index.md` content |
| POST | `/{path}` | Write `index.md` to disk; creates the folder if it does not exist yet |
Non-existent paths without a trailing slash redirect to the slash form (GET only — POSTs
are not redirected because `path.Clean` strips the trailing slash from `PostURL` and the
content would be lost).
## Code Structure
When adding a new special folder type, create a new `.go` file. Do not add type-specific logic to `main.go` or `render.go`.
Prefer separate, human-readable `.html` files over inlined HTML strings in Go. Embed them via `embed.FS` if needed.
## Architecture Rules
- **Single binary** — no installer, no runtime dependencies, no Docker
- **Go stdlib `net/http`** only — no web framework
- **`goldmark`** for Markdown rendering — no other Markdown libraries
- **`embed.FS`** for all assets — no external serving, no CDN
- **No database** of any kind
- **No indexing or caching** unless explicitly requested and justified
- Keep dependencies to an absolute minimum; if stdlib can do it, use stdlib
## Frontend Rules
- Vanilla JS only — no frameworks, no build pipeline (the single exception is the vendored CodeMirror editor bundle; see Build & Deploy)
- Each feature gets its own JS file; global behaviour goes in `global-shortcuts.js`
- Do not inline JS in templates or merge unrelated features into one file
- `ALT+SHIFT` is the modifier for all keyboard shortcuts — do not introduce others
- Editor toolbar buttons use `data-action` + `data-key`; adding `data-key` auto-registers the shortcut
- The editor is a *mode* of a page. Opening it pushes a normal history entry so
the browser/Android Back button cancels the edit and returns to the page — this
"back to cancel" gesture takes priority. `history-nav.js` only rewrites the way
*out*: leaving the editor via the same-page CANCEL link uses `location.replace`
so the editor entry is collapsed rather than sandwiched between two page entries
(which would let Back walk back into the editor). SAVE does the equivalent — it
POSTs via fetch and then `replaceState`s the editor entry with the saved page,
so the editor never lingers in history once you leave it (the pre-save page
snapshot one step back is the accepted cost). Links to a *different* page's
editor (new page / new child) push normally. The save POST answers `204` +
`X-Target` when the request carries `X-Save-Mode: replace`, because the target
may hold a `#section` anchor only the server can compute and fetch drops
fragments from followed redirects.
- For mutating modals (anything that POSTs and then navigates), call `closeModal()` and then `postReplace(action, body, target)` from `page/actions.js`. Do NOT use `<form>.submit()`. Two reasons:
1. The modal must be removed from the DOM before navigation, or the browser's bfcache snapshots it open and back-nav restores the modal.
2. `postReplace` uses `window.location.replace` so the action + result occupy a single history entry. A naive POST → 303 → GET creates two entries, and back-nav lands on a stale pre-mutation snapshot of the same page.
## CSS
Follow **SMACSS** conventions (Scalable and Modular Architecture for CSS). The stylesheet is organized into five categories:
- **Base** — element resets and global defaults only. Never style `header`, `textarea`, `input`, `aside`, `footer`, etc. directly for visual treatment — always via a class.
- **Layout** — `.row`, `.col`, `.page-wrap`. Use these for flex layout; do not inline `display: flex` on feature classes.
- **Modules** — reusable components: `.panel`, `.panel-header`, `.menu-row`, `.btn`, `.input`, `.muted`, `.truncate`, etc. New visual patterns should reuse these. Before adding a new module, check whether an existing one + a modifier already covers the case.
- **State** — `.is-*` prefix only (`.is-open`, `.is-selected`, `.is-active`, `.is-disabled`, `.is-empty`). State is the only place a class describes a moment in time rather than a structural role.
- **Theme** — colors, borders, spacing, and font sizes come from CSS variables defined in `:root` (`--bg`, `--secondary`, `--border`, `--border-dashed`, `--space-*`, `--font-*`). No hardcoded `1px solid #...`, no hardcoded rem spacing in component rules.
Naming: flat-dash (`.panel-header`, `.btn-small`), not BEM (`.panel__header--small`). Modifiers attach as additional classes (`<div class="btn btn-small">`), not as new standalone classes.
Anti-patterns to reject:
- One-off classes that duplicate an existing module (`.save-button` when `.btn` exists, `.form-name-input` when `.input` exists).
- Element selectors (`textarea { ... }`, `header { ... }`) for visual treatment — add a class instead.
- Inlining `display: flex; gap: X` on a feature class instead of composing with `.row` / `.col`.
- Adding a new module for a single use site — prefer a modifier on an existing module first.
- Hardcoded colors, border widths, or spacing values inside component rules — pull a variable, or add one to `:root` if it's missing.
## Development Priorities
When building features, apply this order:
1. Correctness on the filesystem — never corrupt or lose files
2. Mobile usability (primary editing device is Android over Wireguard VPN)
3. Simplicity of implementation, adhere to KISS
4. Performance
## Date Formatting
- General UI dates (file listings, metadata): ISO `YYYY-MM-DD`
- Diary headings (year/month/day) are also ISO short form: `# 2026`, `## 2026-05`, `### 2026-05-28`. No long-form rendering.
- Calendar widget month names are German; the `germanMonths` map in `diary.go` keeps the labels keyed by `time.Month` since Go's `time.Format` is English-only.
## What to Avoid
- Any parallel folder structure (e.g. a separate `media/` tree mirroring `pages/`)
- Over-engineering auth — Basic auth is sufficient for a personal VPN tool
- Heavy payloads or expensive rendering (target CPU: ARMv7 32-bit NAS)
- Suggesting Docker (plain binary is preferred)
## Out of Scope (do not implement unless explicitly asked)
- Full-text search
- Browser-based file upload
- Version history / git integration
- Multi-user support
- Tagging or metadata beyond `index.md` content
+3 -1
View File
@@ -57,7 +57,9 @@ func runOpenCommand(template, path string) error {
if !sawPath { if !sawPath {
tokens = append(tokens, path) tokens = append(tokens, path)
} }
return exec.Command(tokens[0], tokens[1:]...).Start() cmd := exec.Command(tokens[0], tokens[1:]...)
hideConsole(cmd)
return cmd.Start()
} }
// tokenizeCommand splits a command-line string into argv tokens, honouring // tokenizeCommand splits a command-line string into argv tokens, honouring
+8
View File
@@ -0,0 +1,8 @@
//go:build !windows
package main
import "os/exec"
// Allows running commands without showing a terminal window on startup when not running on windows
func hideConsole(*exec.Cmd) {}
+18
View File
@@ -0,0 +1,18 @@
package main
import (
"os/exec"
"syscall"
)
// This is the windows CREATE_NO_WINDOW syscall.
// We have no golang.org/x/sys dependency, so this will suffice
const createNoWindow = 0x08000000
// Allows running commands without showing a terminal window on startup on windows
func hideConsole(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: createNoWindow,
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"path/filepath" "path/filepath"
) )
const version = "1" const version = "2"
func main() { func main() {
flag.Parse() flag.Parse()
-57
View File
@@ -1,57 +0,0 @@
package main
import (
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
type extLinksTransformer struct{}
func (extLinksTransformer) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
link, ok := n.(*ast.Link)
if !ok {
return ast.WalkContinue, nil
}
if isExternalURL(string(link.Destination)) {
link.SetAttribute([]byte("target"), []byte("_blank"))
link.SetAttribute([]byte("rel"), []byte("noopener noreferrer"))
}
return ast.WalkContinue, nil
})
}
func isExternalURL(dest string) bool {
if strings.HasPrefix(dest, "//") {
return true
}
i := strings.Index(dest, ":")
if i <= 0 {
return false
}
for _, c := range dest[:i] {
if !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') &&
!(c >= '0' && c <= '9') && c != '+' && c != '-' && c != '.' {
return false
}
}
return true
}
type extLinksExt struct{}
func newExtLinksExt() goldmark.Extender { return &extLinksExt{} }
func (e *extLinksExt) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithASTTransformers(
util.Prioritized(extLinksTransformer{}, 999),
))
}
+1 -1
View File
@@ -24,7 +24,7 @@ var md goldmark.Markdown
// targets against the filesystem. // targets against the filesystem.
func initMarkdown(root string) { func initMarkdown(root string) {
md = goldmark.New( md = goldmark.New(
goldmark.WithExtensions(extension.GFM, extension.Table, newWikiLinkExt(root), newWikiEmbedExt(root), newExtLinksExt()), goldmark.WithExtensions(extension.GFM, extension.Table, newWikiLinkExt(root), newWikiEmbedExt(root)),
goldmark.WithParserOptions(parser.WithAutoHeadingID()), goldmark.WithParserOptions(parser.WithAutoHeadingID()),
goldmark.WithRendererOptions(html.WithUnsafe(), html.WithHardWraps()), goldmark.WithRendererOptions(html.WithUnsafe(), html.WithHardWraps()),
) )