feat(release): v0.2.0

commit 78d6c27c8940da32a6de8e64327c86f74fdaa2eb
Author: adminoo <git@kadath.corp>
Date:   Wed Feb 4 12:59:22 2026 +0100

    feat: freebsd log rotation config thingie

commit 55af4e6c70122e679272ed247c26e04b1247f694
Author: adminoo <git@kadath.corp>
Date:   Wed Feb 4 12:58:43 2026 +0100

    feat: embed templates, static resolution

commit 29c917f929a7378ec29c54315ee2e9f420747787
Author: adminoo <git@kadath.corp>
Date:   Wed Feb 4 10:44:34 2026 +0100

    feat: set log file path

commit 294fd3d1549979eab63587ceec6ff5d0978e9afc
Author: adminoo <git@kadath.corp>
Date:   Wed Feb 4 10:23:53 2026 +0100

    feat: logging HTTP request

commit c9ae80b240d58e1abed7ae3b7b2c3b283a31f1a1
Author: adminoo <git@kadath.corp>
Date:   Wed Feb 4 09:54:05 2026 +0100

    feat: freebsd-specific compile target and scripts

commit 86ca154dedd19aa1fe5f571c445dcf17a8396bfa
Author: adminoo <git@kadath.corp>
Date:   Wed Feb 4 09:25:16 2026 +0100

    feat: mobile friendly CSS

commit 199f4319e0b08a4b6d595d7eb3effb6db6c7beec
Author: adminoo <git@kadath.corp>
Date:   Wed Feb 4 09:25:03 2026 +0100

    feat: persisting rendered note

commit 865e258237e45d7c542685a4653bcad3c5af259d
Author: adminoo <git@kadath.corp>
Date:   Wed Feb 4 08:06:38 2026 +0100

    fix: grouping notes by folder

commit 242d1d074c92461f38212b033c7a9e383f9dc550
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 16:52:50 2026 +0100

    feat: storage layer logic

    - Prune notes from db not matching current folder structure at start
    - Detect file system deletion on start by comparing in-db notes
    - Prevent updating of in-db notes at start if modification time is not
      newer
    - Delete by path

commit d75d46bc1ab22bd990d0fdc307e571fe52f0dd99
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 15:27:07 2026 +0100

    feat: group notes by root folders

commit e1e25a938e717599332f7b40a449d9bb854b673a
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 14:24:37 2026 +0100

    feat: size in kilobytes

commit 61220272a2df2b66c2b8e356ba359ed01de3bd12
Author: adminoo <git@kadath.corp>
Date:   Tue Feb 3 14:19:40 2026 +0100

    feat: styling inputs
This commit is contained in:
2026-02-04 13:15:55 +01:00
parent 9d1254244f
commit b571588b15
23 changed files with 691 additions and 70 deletions

View File

@ -5,8 +5,11 @@ import (
"donniemarko/internal/render"
"donniemarko/internal/service"
"html/template"
"log"
"net/http"
"net/url"
"path/filepath"
"sort"
"strings"
)
@ -26,6 +29,7 @@ func NewHandler(ns *service.NotesService, tm *render.TemplateManager) *Handler {
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
log.Printf("request: method=%s path=%s host=%s remote=%s", r.Method, path, r.Host, r.RemoteAddr)
// Handle root and note list
if path == "/" {
@ -44,18 +48,27 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// Handle 404 for other paths
log.Printf("not found: method=%s path=%s host=%s remote=%s", r.Method, path, r.Host, r.RemoteAddr)
http.NotFound(w, r)
}
// ViewState is built per-request, not shared
type ViewState struct {
Notes []*note.Note
Groups []NoteGroup
Note *note.Note
RenderedNote template.HTML
SortBy string
SearchTerm string
TagFilter string
LastActive string
// BasePath is an optional URL prefix (e.g. "/donnie") when served behind a reverse proxy.
BasePath string
}
type NoteGroup struct {
Name string
Notes []*note.Note
}
func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
@ -66,8 +79,16 @@ func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
return
}
active := r.URL.Query().Get("active")
if active != "" {
_ = h.setActiveNote(state, active)
}
// Render with state
h.templates.Render(w, "index", state)
if err := h.templates.Render(w, "index", state); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Render error", http.StatusInternalServerError)
}
}
func (h *Handler) buildViewState(r *http.Request) (*ViewState, error) {
@ -81,6 +102,9 @@ func (h *Handler) buildViewState(r *http.Request) (*ViewState, error) {
searchTerm := query.Get("search")
tagFilter := query.Get("tag")
active := query.Get("active")
// Preserve proxy prefix so templates can build correct links and asset URLs.
basePath := basePathFromRequest(r)
// Get notes from service
var notes []*note.Note
@ -119,16 +143,19 @@ func (h *Handler) buildViewState(r *http.Request) (*ViewState, error) {
return &ViewState{
Notes: notes,
Groups: groupNotesByFolder(notes),
SortBy: sortBy,
SearchTerm: searchTerm,
TagFilter: tagFilter,
LastActive: active,
BasePath: basePath,
}, nil
}
func (h *Handler) SetupRoutes() {
// Set the handler as the main handler for http.DefaultServeMux
http.Handle("/", h)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("internal/web/static"))))
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(StaticFS()))))
}
func extractHash(path string) string {
@ -169,8 +196,30 @@ func (h *Handler) handleNotes(w http.ResponseWriter, r *http.Request) {
state.Note = note
state.RenderedNote = htmlContent
state.LastActive = hash
// Ensure note view carries proxy prefix for links/forms.
state.BasePath = basePathFromRequest(r)
h.templates.Render(w, "index", state)
if err := h.templates.Render(w, "index", state); err != nil {
log.Printf("render error: %v", err)
http.Error(w, "Render error", http.StatusInternalServerError)
}
}
func (h *Handler) setActiveNote(state *ViewState, noteID string) error {
note, err := h.notesService.GetNoteByHash(noteID)
if err != nil {
return err
}
htmlContent, err := render.RenderMarkdown([]byte(note.Content))
if err != nil {
return err
}
state.Note = note
state.RenderedNote = htmlContent
state.LastActive = noteID
return nil
}
func filterNotesByTag(notes []*note.Note, tag string) []*note.Note {
@ -191,6 +240,64 @@ func filterNotesByTag(notes []*note.Note, tag string) []*note.Note {
return filtered
}
func groupNotesByFolder(notes []*note.Note) []NoteGroup {
groups := make(map[string][]*note.Note)
for _, n := range notes {
name := rootFolderName(n.Path)
groups[name] = append(groups[name], n)
}
if len(groups) == 0 {
return nil
}
names := make([]string, 0, len(groups))
for name := range groups {
names = append(names, name)
}
sort.Strings(names)
result := make([]NoteGroup, 0, len(names))
for _, name := range names {
result = append(result, NoteGroup{
Name: name,
Notes: groups[name],
})
}
return result
}
func rootFolderName(path string) string {
if path == "" {
return "root"
}
clean := filepath.ToSlash(filepath.Clean(path))
if clean == "." || clean == "/" {
return "root"
}
clean = strings.TrimPrefix(clean, "/")
parts := strings.Split(clean, "/")
if len(parts) == 0 || parts[0] == "." {
return "root"
}
if len(parts) == 1 {
return "root"
}
return parts[0]
}
func basePathFromRequest(r *http.Request) string {
// X-Forwarded-Prefix is commonly set by reverse proxies for subpath mounts.
prefix := strings.TrimSpace(r.Header.Get("X-Forwarded-Prefix"))
if prefix == "" {
return ""
}
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
return strings.TrimRight(prefix, "/")
}
func (h *Handler) handleTags(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@ -228,7 +335,9 @@ func (h *Handler) handleTags(w http.ResponseWriter, r *http.Request) {
}
}
http.Redirect(w, r, "/notes/"+noteID, http.StatusSeeOther)
// Redirect using proxy prefix when present.
basePath := basePathFromRequest(r)
http.Redirect(w, r, basePath+"/notes/"+noteID, http.StatusSeeOther)
}
func parseTagRoute(path string) (noteID string, tag string, isRemove bool) {