Add info page and version
This commit is contained in:
parent
e10d6112f5
commit
fa292154f6
12 changed files with 122 additions and 11 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,6 +1,20 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
This file keeps track of changes in more human-readable fashion
|
This file keeps track of changes in more human-readable fashion
|
||||||
|
|
||||||
|
## 7 May 2024 - v0.1.0
|
||||||
|
* Began move towards [semantic versioning](https://semver.org/)
|
||||||
|
* Current version is now 0.1.0
|
||||||
|
* Added `version` api method
|
||||||
|
* Versioned docker images may be provided in the future
|
||||||
|
* Added version to footer
|
||||||
|
* Added info page
|
||||||
|
* Accessed by clicking version number in footer
|
||||||
|
* Allows you to edit readme.txt
|
||||||
|
* Provides UI link for `export` api method
|
||||||
|
* Can be expanded with other functionality in the future (see [TODO](./TODO.md))
|
||||||
|
* Bug fixes
|
||||||
|
* Fixed export function failing
|
||||||
|
|
||||||
## 6 May 2024
|
## 6 May 2024
|
||||||
* Grace period is now non-inclusive (so `4h` means the switch will happen right at `4:00`, not `4:01`)
|
* Grace period is now non-inclusive (so `4h` means the switch will happen right at `4:00`, not `4:01`)
|
||||||
* Added API method to check if grace period is active
|
* Added API method to check if grace period is active
|
||||||
|
|
10
README.md
10
README.md
|
@ -15,8 +15,7 @@ As a result, I can't guarantee that it's either secure or stable.
|
||||||
* Optional Telegram notifications for failed login attempts
|
* Optional Telegram notifications for failed login attempts
|
||||||
|
|
||||||
## Technical details
|
## Technical details
|
||||||
Though Hibiscus.txt isn't versioned, [CHANGELOG.md](./CHANGELOG.md) can provide a good overview of changes.
|
[CHANGELOG.md](./CHANGELOG.md) provides a good overview of updates, and [TODO.md](./TODO.md) file shows what I will (or *may*) work on in the future.
|
||||||
And there is a [TODO.md](./TODO.md) file that shows what I will (or *may*) work on in the future.
|
|
||||||
|
|
||||||
You can read a relevant entry in my blog [here](https://a71.su/notes/hibiscus/).
|
You can read a relevant entry in my blog [here](https://a71.su/notes/hibiscus/).
|
||||||
It provides some useful information and context for why this app exists in the first place.
|
It provides some useful information and context for why this app exists in the first place.
|
||||||
|
@ -65,6 +64,8 @@ The [package](https://git.a71.su/Andrew71/hibiscus/packages) provided in this re
|
||||||
and there is a [Dockerfile](./Dockerfile) in case you want to compile for something rarer (like a Pi).
|
and there is a [Dockerfile](./Dockerfile) in case you want to compile for something rarer (like a Pi).
|
||||||
This repo contains the [compose.yml](./compose.yml) that I personally use.
|
This repo contains the [compose.yml](./compose.yml) that I personally use.
|
||||||
|
|
||||||
|
**Note**: versioned images may be provided in the future, possibly via dockerhub
|
||||||
|
|
||||||
### Executable flags
|
### Executable flags
|
||||||
If you for some reason decide to run plain executable instead of docker, it supports following flags:
|
If you for some reason decide to run plain executable instead of docker, it supports following flags:
|
||||||
```
|
```
|
||||||
|
@ -91,11 +92,10 @@ GET /day/<name> - get file contents for a specific day
|
||||||
GET /notes - get JSON list of all named notes
|
GET /notes - get JSON list of all named notes
|
||||||
GET /notes/<name> - get file contents for a specific note
|
GET /notes/<name> - get file contents for a specific note
|
||||||
POST /notes/<name> - save request body into a named note
|
POST /notes/<name> - save request body into a named note
|
||||||
|
|
||||||
GET /export - get .zip archive of entire data directory
|
|
||||||
|
|
||||||
GET /readme - get file contents for readme.txt in data dir's root
|
GET /readme - get file contents for readme.txt in data dir's root
|
||||||
POST /readme - save request body into readme.txt
|
POST /readme - save request body into readme.txt
|
||||||
|
|
||||||
|
GET /export - get .zip archive of entire data directory
|
||||||
GET /grace - "true" if grace period is active, otherwise "false"
|
GET /grace - "true" if grace period is active, otherwise "false"
|
||||||
|
GET /version - get app's version
|
||||||
```
|
```
|
15
TODO.md
15
TODO.md
|
@ -1,8 +1,21 @@
|
||||||
# TODO
|
# TODO
|
||||||
List of things to add to this project
|
List of things to add to this project
|
||||||
|
|
||||||
|
## Agenda
|
||||||
* a 512x logo so I can enable PWA
|
* a 512x logo so I can enable PWA
|
||||||
* CI/CD pipeline
|
* CI/CD pipeline
|
||||||
* Better docs in case others want to use this for some reason
|
* Better docs in case others want to use this for some reason
|
||||||
* Check export function for improvements
|
* Check export function for improvements
|
||||||
* *Go* dependency-less? <-- this is a terrible idea
|
|
||||||
|
## Brainstorming
|
||||||
|
Don't expect any of this, these are ideas floating inside my head
|
||||||
|
* Further info page functionality
|
||||||
|
* Edit config
|
||||||
|
* Statistics e.g. mb taken, number of day pages/notes
|
||||||
|
* Multi-user support through several username-pass keys
|
||||||
|
* `/data/<user>/...`
|
||||||
|
* This would be an *extremely* breaking change
|
||||||
|
* How to handle exporting *all*? Admin account?
|
||||||
|
* I don't need this, unless Hibiscus.txt somehow gets popular why bother?
|
||||||
|
Is this even a feature that fits the vision?
|
||||||
|
* *Go* dependency-less? <-- this is a terrible idea
|
||||||
|
|
6
api.go
6
api.go
|
@ -116,3 +116,9 @@ func GraceActiveApi(w http.ResponseWriter, r *http.Request) {
|
||||||
HandleWrite(w.Write([]byte(value)))
|
HandleWrite(w.Write([]byte(value)))
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetVersionApi returns current app version
|
||||||
|
func GetVersionApi(w http.ResponseWriter, r *http.Request) {
|
||||||
|
HandleWrite(w.Write([]byte(Info.Version)))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
|
@ -4,14 +4,21 @@
|
||||||
"title.today": "Your day so far",
|
"title.today": "Your day so far",
|
||||||
"title.days": "Previous days",
|
"title.days": "Previous days",
|
||||||
"title.notes": "Notes",
|
"title.notes": "Notes",
|
||||||
|
"title.info": "Info",
|
||||||
|
|
||||||
"link.today": "today",
|
"link.today": "today",
|
||||||
"link.tomorrow": "tomorrow",
|
"link.tomorrow": "tomorrow",
|
||||||
"link.days": "previous days",
|
"link.days": "previous days",
|
||||||
"link.notes": "notes",
|
"link.notes": "notes",
|
||||||
|
"link.info": "app information",
|
||||||
|
|
||||||
"description.notes": "/notes/<name> for a new note",
|
"description.notes": "/notes/<name> for a new note",
|
||||||
"time.date": "Today is",
|
"time.date": "Today is",
|
||||||
"time.grace": "grace period active",
|
"time.grace": "grace period active",
|
||||||
"button.save": "Save"
|
"button.save": "Save",
|
||||||
|
|
||||||
|
"info.version": "Version",
|
||||||
|
"info.version.link": "source and changelog",
|
||||||
|
"info.export": "Export data",
|
||||||
|
"info.readme": "Edit readme.txt"
|
||||||
}
|
}
|
|
@ -4,14 +4,21 @@
|
||||||
"title.today": "Сегодняшний день",
|
"title.today": "Сегодняшний день",
|
||||||
"title.days": "Предыдущие дни",
|
"title.days": "Предыдущие дни",
|
||||||
"title.notes": "Заметки",
|
"title.notes": "Заметки",
|
||||||
|
"title.info": "Информация",
|
||||||
|
|
||||||
"link.today": "сегодня",
|
"link.today": "сегодня",
|
||||||
"link.tomorrow": "завтра",
|
"link.tomorrow": "завтра",
|
||||||
"link.days": "раньше",
|
"link.days": "раньше",
|
||||||
"link.notes": "заметки",
|
"link.notes": "заметки",
|
||||||
|
"link.info": "системная информация",
|
||||||
|
|
||||||
"description.notes": "/notes/<название> для новой заметки",
|
"description.notes": "/notes/<название> для новой заметки",
|
||||||
"time.date": "Сегодня",
|
"time.date": "Сегодня",
|
||||||
"time.grace": "льготный период",
|
"time.grace": "льготный период",
|
||||||
"button.save": "Сохранить"
|
"button.save": "Сохранить",
|
||||||
|
|
||||||
|
"info.version": "Версия",
|
||||||
|
"info.version.link": "исходный код",
|
||||||
|
"info.export": "Экспорт данных",
|
||||||
|
"info.readme": "Редактировать readme.txt"
|
||||||
}
|
}
|
30
info.go
Normal file
30
info.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var infoTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/info.html"))
|
||||||
|
|
||||||
|
type HibiscusInfo struct {
|
||||||
|
Version string
|
||||||
|
SourceLink string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info contains app information
|
||||||
|
var Info = HibiscusInfo{
|
||||||
|
Version: "0.1.0",
|
||||||
|
SourceLink: "https://git.a71.su/Andrew71/hibiscus",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfo renders the info page
|
||||||
|
func GetInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := infoTemplate.ExecuteTemplate(w, "base", Info)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error executing template", "error", err)
|
||||||
|
InternalError(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,7 @@
|
||||||
|
|
||||||
{{define "footer"}}
|
{{define "footer"}}
|
||||||
<footer>
|
<footer>
|
||||||
<p><a href="/">{{ translatableText "link.today" }}</a> | <a href="/day">{{ translatableText "link.days" }}</a> | <a href="/notes">{{ translatableText "link.notes" }}</a></p>
|
<p><a href="/">{{ translatableText "link.today" }}</a> | <a href="/day">{{ translatableText "link.days" }}</a> | <a href="/notes">{{ translatableText "link.notes" }}</a>
|
||||||
|
<span style="float:right;"><a class="no-accent" href="/info" title="{{ translatableText "link.info" }}">{{ hibiscusVersion }}</a></span></p>
|
||||||
</footer>
|
</footer>
|
||||||
{{end}}
|
{{end}}
|
8
pages/info.html
Normal file
8
pages/info.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{{define "main"}}
|
||||||
|
<h2>{{ translatableText "title.info" }}</h2>
|
||||||
|
<ul>
|
||||||
|
<li>{{ translatableText "info.version" }} - {{ .Version }} (<a href="{{ .SourceLink }}">{{ translatableText "info.version.link" }}</a>)</li>
|
||||||
|
<li><a href="/api/export" download="hibiscus">{{ translatableText "info.export" }}</a></li>
|
||||||
|
<li><a href="/readme">{{ translatableText "info.readme" }}</a></li>
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
|
@ -13,6 +13,8 @@ body {
|
||||||
}
|
}
|
||||||
a, a:visited { color: #f85552; }
|
a, a:visited { color: #f85552; }
|
||||||
a:hover, a:visited:hover { color: #e66868; }
|
a:hover, a:visited:hover { color: #e66868; }
|
||||||
|
a.no-accent, a.no-accent:visited { color: #454545; }
|
||||||
|
a.no-accent:hover, a.no-accent:visited:hover { color: #656565; }
|
||||||
|
|
||||||
h2 { margin-bottom:12px; }
|
h2 { margin-bottom:12px; }
|
||||||
.list-title { margin-bottom: 0}
|
.list-title { margin-bottom: 0}
|
||||||
|
@ -65,4 +67,6 @@ header > h1, header > p {
|
||||||
color: #f5f0e1;
|
color: #f5f0e1;
|
||||||
background-color: #383030;
|
background-color: #383030;
|
||||||
}
|
}
|
||||||
|
a.no-accent, a.no-accent:visited { color: #f5f0e1; }
|
||||||
|
a.no-accent:hover, a.no-accent:visited:hover { color: #a9a8a4; }
|
||||||
}
|
}
|
21
routes.go
21
routes.go
|
@ -25,7 +25,9 @@ type Entry struct {
|
||||||
|
|
||||||
type formatEntries func([]string) []Entry
|
type formatEntries func([]string) []Entry
|
||||||
|
|
||||||
var templateFuncs = map[string]interface{}{"translatableText": TranslatableText}
|
var templateFuncs = map[string]interface{}{
|
||||||
|
"translatableText": TranslatableText,
|
||||||
|
"hibiscusVersion": func() string { return "v" + Info.Version }}
|
||||||
var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/edit.html"))
|
var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/edit.html"))
|
||||||
var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/entry.html"))
|
var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/entry.html"))
|
||||||
var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/list.html"))
|
var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/list.html"))
|
||||||
|
@ -104,7 +106,7 @@ func GetDays(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Fancy text for today and tomorrow
|
// Fancy text for today and tomorrow
|
||||||
// This looks bad, but strings.Title is deprecated, and I'm not importing a golang.org/x package for this...
|
// This looks bad, but strings.Title is deprecated, and I'm not importing a golang.org/x package for this...
|
||||||
// ( chances we ever run into tomorrow are really low)
|
// (chances we ever run into tomorrow are really low)
|
||||||
if v == TodayDate() {
|
if v == TodayDate() {
|
||||||
dayString = TranslatableText("link.today")
|
dayString = TranslatableText("link.today")
|
||||||
dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:])
|
dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:])
|
||||||
|
@ -130,6 +132,7 @@ func GetNotes(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEntry handles showing a single file, editable or otherwise
|
||||||
func GetEntry(w http.ResponseWriter, r *http.Request, title string, filename string, editable bool) {
|
func GetEntry(w http.ResponseWriter, r *http.Request, title string, filename string, editable bool) {
|
||||||
entry, err := ReadFile(filename)
|
entry, err := ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -208,3 +211,17 @@ func PostNote(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, r.Header.Get("Referer"), 302)
|
http.Redirect(w, r, r.Header.Get("Referer"), 302)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetReadme calls GetEntry for readme.txt
|
||||||
|
func GetReadme(w http.ResponseWriter, r *http.Request) {
|
||||||
|
GetEntry(w, r, "readme", "readme", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostReadme saves contents of readme.txt file
|
||||||
|
func PostReadme(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := SaveFile("readme", []byte(r.FormValue("text")))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error saving readme", "error", err)
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, r.Header.Get("Referer"), 302)
|
||||||
|
}
|
||||||
|
|
4
serve.go
4
serve.go
|
@ -25,6 +25,9 @@ func Serve() {
|
||||||
userRouter.Get("/notes", GetNotes)
|
userRouter.Get("/notes", GetNotes)
|
||||||
userRouter.Get("/notes/{note}", GetNote)
|
userRouter.Get("/notes/{note}", GetNote)
|
||||||
userRouter.Post("/notes/{note}", PostNote)
|
userRouter.Post("/notes/{note}", PostNote)
|
||||||
|
userRouter.Get("/info", GetInfo)
|
||||||
|
userRouter.Get("/readme", GetReadme)
|
||||||
|
userRouter.Post("/readme", PostReadme)
|
||||||
r.Mount("/", userRouter)
|
r.Mount("/", userRouter)
|
||||||
|
|
||||||
// API =============
|
// API =============
|
||||||
|
@ -41,6 +44,7 @@ func Serve() {
|
||||||
apiRouter.Post("/today", PostTodayApi)
|
apiRouter.Post("/today", PostTodayApi)
|
||||||
apiRouter.Get("/export", GetExport)
|
apiRouter.Get("/export", GetExport)
|
||||||
apiRouter.Get("/grace", GraceActiveApi)
|
apiRouter.Get("/grace", GraceActiveApi)
|
||||||
|
apiRouter.Get("/version", GetVersionApi)
|
||||||
r.Mount("/api", apiRouter)
|
r.Mount("/api", apiRouter)
|
||||||
|
|
||||||
// Static files
|
// Static files
|
||||||
|
|
Loading…
Reference in a new issue