Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

goreleaser-snapshot:
name: Goreleaser snapshot build
runs-on: ubuntu-latest
needs: [backend, frontend]
if: "!startsWith(github.ref, 'refs/tags/')"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: web/package-lock.json
- uses: goreleaser/goreleaser-action@v6
with:
version: ~> v2
args: build --snapshot --clean

goreleaser:
name: Build + publish release binaries
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/helm
web/dist
web/node_modules
dist/

# Runtime data (never commit)
data/
Expand Down
9 changes: 9 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ builds:
goarch:
- amd64
- arm64
- arm
goarm:
- "7"
# Raspberry Pi Zero (original) uses armv6 — skipped; v2/Zero 2 W and later
# are all armv7+. Matrix-level exclusion so darwin and linux get the full
# set without emitting darwin/arm.
ignore:
- goos: darwin
goarch: arm
main: ./cmd/helm
binary: helm
ldflags:
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ cd web && npm run dev
| `notes-folders` | Folder picker for notes |
| `notes-editor` | Note list + editor with markdown view/edit toggle, tags, pin |
| `tags` | Tag management — create, delete, view all |
| `custom-api` | Fetch JSON from any URL server-side, render via `{{.dot.path}}` template. Secrets stay in `config.yml` |
| `iframe` | Embed an external dashboard. Gated by `iframe_allowed_hosts` + CSP frame-src |
| `docker-status` | Live container list with state color-coding. Opt-in; requires `/var/run/docker.sock` mount |

All widgets support global search from the header bar. Tags attach/detach from every entity type.

Expand Down Expand Up @@ -134,6 +137,25 @@ pages:

Column sizes: `small` (~25%), `medium` (~33%), `large` (~50%).

### Starter configs

Copy any of the ready-made layouts in [`config-examples/`](config-examples) to `config.yml` and edit:

- **`minimal.yml`** — one page, memos + todos. Smallest working config.
- **`developer.yml`** — notes + todos + clipboard + bookmarks + `custom-api` + `docker-status`.
- **`personal.yml`** — calendar + memos + todos + notes + an `iframe` embedding Home Assistant.
- **`gtd.yml`** — three pages modelled on the Getting Things Done flow (inbox → triage → archive).

### Themes

Three built-in themes: `noir` (default, Terminal-Noir aesthetic), `light`, `gruvbox`. Toggle in the top-right of the header; choice persists in `localStorage`.

### Integrations

- **`custom-api`** — declares `url`, `refresh` (≥10s), optional `headers` (e.g. `Authorization: Bearer …`) in `config.yml`. The backend fetches through an SSRF-hardened client; the browser only ever sees the rendered template. Never puts URLs or auth tokens in the DOM.
- **`iframe`** — declare embeddable hosts under `iframe_allowed_hosts`. The backend validates widget URLs against this list at boot *and* sets a matching `Content-Security-Policy: frame-src`. An un-allowlisted host fails fast at startup.
- **`docker-status`** — opt-in. Set `docker.enabled: true` and mount `/var/run/docker.sock:/var/run/docker.sock:ro`. Mounting the docker socket is a real security surface — a container with r/w on that socket can escape; `:ro` limits the blast radius to read-only engine queries.

---

## Upgrading
Expand Down
52 changes: 52 additions & 0 deletions config-examples/developer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Developer config — notes + todos + clipboard + bookmarks, plus a
# custom-api widget hitting a public endpoint (GitHub repo metadata) and a
# docker-status widget for a local engine.
#
# Requires docker-compose socket mount + docker.enabled: true for the
# docker-status widget. See docker-compose.yml.

server:
host: 0.0.0.0
port: 8080

auth:
password: changeme
secret: replace-with-a-long-random-string-at-least-32-chars

storage:
db_path: ./data/helm.db
attachments_path: ./data/attachments

docker:
enabled: true
socket: /var/run/docker.sock

pages:
- name: Code
columns:
- size: small
widgets:
- type: todos
config:
filter: today
- type: clipboard
config:
limit: 10
- size: large
widgets:
- type: notes-editor
- type: notes-folders
- size: small
widgets:
- type: bookmarks
- type: custom-api
config:
url: https://api.github.com/repos/lerko96/helm
refresh: 10m
template: |
{{.name}} — ☆ {{.stargazers_count}}
{{.description}}
open issues: {{.open_issues_count}}
- type: docker-status
config:
refresh: 30s
47 changes: 47 additions & 0 deletions config-examples/gtd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# GTD config — inbox (memos) → triage (todos) → archive (notes).
# Three pages mirroring the classic capture/clarify/review flow.

server:
host: 0.0.0.0
port: 8080

auth:
password: changeme
secret: replace-with-a-long-random-string-at-least-32-chars

storage:
db_path: ./data/helm.db
attachments_path: ./data/attachments

pages:
- name: Inbox
columns:
- size: full
widgets:
- type: memos

- name: Triage
columns:
- size: small
widgets:
- type: task-lists
- size: large
widgets:
- type: task-board
- size: small
widgets:
- type: todos
config:
filter: today

- name: Archive
columns:
- size: small
widgets:
- type: notes-folders
- size: large
widgets:
- type: notes-editor
- size: small
widgets:
- type: tags
24 changes: 24 additions & 0 deletions config-examples/minimal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Minimal config — one page, memos + todos.
# Good starting point; copy to config.yml and edit auth.password + auth.secret.

server:
host: 0.0.0.0
port: 8080

auth:
password: changeme
secret: replace-with-a-long-random-string-at-least-32-chars

storage:
db_path: ./data/helm.db
attachments_path: ./data/attachments

pages:
- name: Home
columns:
- size: small
widgets:
- type: memos
- size: small
widgets:
- type: todos
44 changes: 44 additions & 0 deletions config-examples/personal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Personal config — calendar + memos + todos + notes, with an iframe widget
# for an external dashboard (Home Assistant in this example).
#
# The iframe host MUST be declared in iframe_allowed_hosts. Swap the values
# below for your own and leave the rest alone.

server:
host: 0.0.0.0
port: 8080

auth:
password: changeme
secret: replace-with-a-long-random-string-at-least-32-chars

storage:
db_path: ./data/helm.db
attachments_path: ./data/attachments

iframe_allowed_hosts:
- ha.example.com

pages:
- name: Home
columns:
- size: small
widgets:
- type: memos
- type: todos
- size: large
widgets:
- type: cal-view
- size: small
widgets:
- type: notes-editor
- type: cal-sources

- name: House
columns:
- size: full
widgets:
- type: iframe
config:
url: https://ha.example.com/lovelace/home
height: 720px
29 changes: 29 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config

import (
"os"
"path/filepath"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -161,3 +163,30 @@ func TestWidgetID_Stable(t *testing.T) {
t.Errorf("WidgetID = %q, want %q", got, want)
}
}

// TestConfigExamples_AllLoad keeps config-examples/ honest — every example
// must pass the same Load() validation as a real config. Catches drift when
// we tighten validation or rename a field.
func TestConfigExamples_AllLoad(t *testing.T) {
dir := filepath.Join("..", "..", "config-examples")
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("read config-examples dir: %v", err)
}

if len(entries) == 0 {
t.Fatal("no examples found in config-examples/")
}

for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".yml") {
continue
}
path := filepath.Join(dir, e.Name())
t.Run(e.Name(), func(t *testing.T) {
if _, err := Load(path); err != nil {
t.Errorf("example %s failed to load: %v", path, err)
}
})
}
}
Loading