diff --git a/bin/journal-server/assets/js/autosave-draft.js b/bin/journal-server/assets/js/autosave-draft.js index 65c91ca..1ebbd51 100644 --- a/bin/journal-server/assets/js/autosave-draft.js +++ b/bin/journal-server/assets/js/autosave-draft.js @@ -6,6 +6,7 @@ const AUTOSAVE_INTERVAL_MS = 2500; } const ipt_body = editform.querySelector("textarea"); + const ipt_project = editform.querySelector("#ipt-project") || document.createElement("input"); const ipt_draft_id = editform.querySelector("input[type=hidden][name=draft_id]"); if ( !ipt_body || !ipt_draft_id ) { return; @@ -34,6 +35,7 @@ const AUTOSAVE_INTERVAL_MS = 2500; let pb = new FormData(); pb.set("draft_id", draft_id); pb.set("body", ipt_body.value); + pb.set("project", ipt_project.value); let draft_emptied = (ipt_body.value.trim() === ''); let q = await fetch(draft_url, {method: "POST", body: pb}); q = await q.json(); @@ -58,14 +60,16 @@ const AUTOSAVE_INTERVAL_MS = 2500; }; let current_body = ipt_body.value; + let current_project = ipt_project.value; window.setInterval(async () => { - if ( ipt_body.value == current_body ) { + if ( ipt_body.value == current_body && ipt_project.value == current_project ) { return; } try { - await save_draft(); - current_body = ipt_body.value; + await save_draft(); + current_body = ipt_body.value; + current_project = ipt_project.value; } catch ( e ) { console.error("automatic save failed") } diff --git a/bin/journal-server/assets/scss/app.scss b/bin/journal-server/assets/scss/app.scss index 2566592..bc71708 100644 --- a/bin/journal-server/assets/scss/app.scss +++ b/bin/journal-server/assets/scss/app.scss @@ -34,7 +34,7 @@ main .journal-entry { - input[type=text], textarea, input[type=submit], button { + input[type=text], textarea, input[type=submit], button, select { width: 100%; &:focus-visible { outline: none; @@ -61,7 +61,7 @@ main padding-bottom: 120px; } - input[type=text], input[type=submit] { + input[type=text], input[type=submit], select { padding: 10px 20px; } } diff --git a/bin/journal-server/assets/templates/editor.html b/bin/journal-server/assets/templates/editor.html index bb5d1d4..0f21fdb 100644 --- a/bin/journal-server/assets/templates/editor.html +++ b/bin/journal-server/assets/templates/editor.html @@ -28,6 +28,16 @@
+ {{if .Projects}} ++ +
+ {{end}} {{if .CanAttachFiles}}diff --git a/bin/journal-server/journal-server.go b/bin/journal-server/journal-server.go index cb8e3d7..5f9f411 100644 --- a/bin/journal-server/journal-server.go +++ b/bin/journal-server/journal-server.go @@ -14,6 +14,7 @@ import ( "os" "os/signal" "path" + "sort" "strings" "sync" "time" @@ -29,6 +30,7 @@ var ( password_file = flag.String("password_file", ".htpasswd", "File containing passwords") secret_parameter = flag.String("secret_parameter", "apikey", "Parameter name containing the API key") attachments_dir = flag.String("attachments_dir", "", "Directory for storing attached files") + projects_dir = flag.String("projects_dir", "", "Directory with project log files") ) // DraftTimeout measures how long it takes for an unsaved draft to get added to the journal. @@ -41,6 +43,7 @@ type draftEntry struct { LastEdit time.Time Expires time.Time Body string + Project string } var ( @@ -138,7 +141,7 @@ func onShutdown() { draftsMutex.Lock() for draft_id, entry := range drafts { log.Printf("Add draft ID %s to journal: last saved at %s", draft_id, entry.LastEdit) - err := saveJournalEntry(entry.LastEdit, entry.Body, false) + err := saveJournalEntry(entry.LastEdit, entry.Body, entry.Project, false) if err != nil { log.Printf("Error saving journal entry: %v", err) } @@ -165,7 +168,7 @@ func autoAddDrafts(ctx context.Context) { } log.Printf("Draft ID %s expired at %s; saving it to journal", draft_id, entry.Expires) - err := saveJournalEntry(entry.LastEdit, entry.Body, false) + err := saveJournalEntry(entry.LastEdit, entry.Body, entry.Project, false) if err != nil { log.Printf("Error saving journal entry: %v", err) } else { @@ -213,21 +216,53 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) { executeTemplate(index, indexData, w, r) } +func listProjects(ctx context.Context) ([]string, error) { + if *projects_dir == "" { + return nil, nil + } + + d, err := os.Open(*projects_dir) + if err != nil { + return nil, err + } + + fis, err := d.ReadDir(-1) + if err != nil { + return nil, err + } + + var rv []string + for _, fi := range fis { + if fi.IsDir() { + continue + } + rv = append(rv, fi.Name()) + } + + sort.Strings(rv) + + return rv, nil +} + func WriterHandler(w http.ResponseWriter, r *http.Request) { getv := r.URL.Query() getv.Del("success") getv.Del("failure") + projects, _ := listProjects(r.Context()) + pageData := struct { Success, Failure bool Callback string CanAttachFiles bool + Projects []string }{ r.URL.Query().Get("success") != "", r.URL.Query().Get("failure") != "", "journal?" + getv.Encode(), *attachments_dir != "", + projects, } executeTemplate(editor, pageData, w, r) @@ -251,7 +286,19 @@ func DailyHandler(w http.ResponseWriter, r *http.Request) { executeTemplate(daily, pageData, w, r) } -func saveJournalEntry(timestamp time.Time, contents string, starred bool) error { +func saveJournalEntry(timestamp time.Time, contents string, project string, starred bool) error { + if project != "" && *projects_dir != "" { + prf := path.Join(*projects_dir, strings.Replace(strings.Replace(project, "/", "", -1), "\\", "", -1)) + //log.Printf("Also adding post to project %s → %s", project, prf) + if f, err := os.OpenFile(prf, os.O_APPEND|os.O_WRONLY, 0600); err == nil { + fmt.Fprintf(f, "\n=== %s ===\n%s\n", timestamp.Format("2006-01-02"), contents) + f.Close() + } + } + if project != "" { + contents = "@project " + formatProjectName(project) + "\n" + contents + } + e := &journal.Entry{ Date: timestamp, Starred: starred, @@ -265,6 +312,7 @@ func SaveHandler(w http.ResponseWriter, r *http.Request) { timestamp := journal.SmartTime(r.PostFormValue("ts")) starred := r.PostFormValue("star") != "" body := r.PostFormValue("body") + project := r.PostFormValue("project") // Remove carriage returns entirely. Why? Because it fits my use case, and because sod MS-DOS. body = strings.Replace(body, "\r", "", -1) @@ -298,7 +346,7 @@ func SaveHandler(w http.ResponseWriter, r *http.Request) { } } - err := saveJournalEntry(timestamp, body, starred) + err := saveJournalEntry(timestamp, body, project, starred) if err != nil { errorHandler(err, w, r) return @@ -337,6 +385,7 @@ func SaveDraftHandler(w http.ResponseWriter, r *http.Request) { } post_body := r.PostFormValue("body") + project := r.PostFormValue("project") draftsMutex.Lock() defer draftsMutex.Unlock() @@ -347,6 +396,7 @@ func SaveDraftHandler(w http.ResponseWriter, r *http.Request) { LastEdit: time.Now(), Expires: time.Now().Add(DraftTimeout), Body: post_body, + Project: project, } } diff --git a/bin/journal-server/plumbing.go b/bin/journal-server/plumbing.go index 7b5cb09..619690c 100644 --- a/bin/journal-server/plumbing.go +++ b/bin/journal-server/plumbing.go @@ -6,6 +6,7 @@ import ( "html/template" "log" "net/http" + "strings" "github.com/gorilla/context" ) @@ -16,10 +17,23 @@ var daily *template.Template var bwvlist *template.Template var tie *template.Template +func formatProjectName(name string) string { + if len(name) > 4 && name[len(name)-4:] == ".txt" { + name = name[:len(name)-5] + } else if len(name) > 5 && name[len(name)-5:] == ".wiki" { + name = name[:len(name)-5] + } + + name = strings.Replace(name, "_", " ", -1) + + return name +} + func init() { flag.Parse() funcs := template.FuncMap{} + funcs["ProjectName"] = formatProjectName b, err := Asset("assets/templates/editor.html") if err != nil {