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:
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user