Add CalVer versioning and rebuild the song page's fact line
Versions are YYYY.MM.DD-N, injected with -ldflags from a git tag, so no file in the repo carries the number and a local build honestly says dev. The version shows in the footer, the startup log and /healthz. The song page's metadata was five different kinds of fact — artist, genre, duration, provenance and a badge about the viewer — in one flat run with two competing pills. Now the artist has its own line in the display face, and the facts sit in four labelled cells like the spine of a cassette insert. The submitter links to their profile, "oma kappale" became "lähetti: sinä", and the clock time is gone: nobody needs the minute a song was published.
This commit is contained in:
+3
-1
@@ -1,9 +1,11 @@
|
||||
FROM golang:1.26-alpine AS build
|
||||
# CalVer, injected at build so no file needs bumping by hand: docker build --build-arg VERSION=…
|
||||
ARG VERSION=dev
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /levyraati .
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-X main.version=${VERSION}" -o /levyraati .
|
||||
|
||||
FROM alpine:3.24
|
||||
# yt-dlp rots against YouTube. Alpine's active branch tracks it closely (3.24 carries the current
|
||||
|
||||
@@ -19,13 +19,27 @@ Invite-only, no public registration. Built for about ten friends.
|
||||
| [docs/theme.md](docs/theme.md) | The visual language: tokens, type, and what differs from the theme handoff |
|
||||
| [docs/later.md](docs/later.md) | Deliberately not in v1, with the reasoning kept |
|
||||
|
||||
## Branches
|
||||
## Branches and releases
|
||||
|
||||
- **`main`** — released code only. Every commit on it is something that ran in production, or is
|
||||
meant to. Tagged at each release.
|
||||
- **`dev`** — current development, and whatever nightly builds get made. Work happens here and
|
||||
reaches `main` by merge at release time.
|
||||
|
||||
Versions are **CalVer: `YYYY.MM.DD-N`**, where `N` is the build number for that day, starting at 1.
|
||||
The version is injected at build time, so no file in the repo carries it:
|
||||
|
||||
```sh
|
||||
git switch main && git merge --no-ff dev
|
||||
git tag 2026.07.31-1
|
||||
VERSION=$(git describe --tags --exact-match) docker compose build app
|
||||
docker compose up -d app
|
||||
```
|
||||
|
||||
A plain `go build` reports `dev`, which is the honest answer for a local binary. The running
|
||||
version appears in the footer, in the startup log line, and in `GET /healthz` — so "what is
|
||||
actually deployed" is answerable without an SSH session.
|
||||
|
||||
The app never sends email — there is no verification, no password reset link, and no notifications.
|
||||
Members have an address because it is their login and because mail is a planned feature.
|
||||
|
||||
|
||||
+4
-1
@@ -17,7 +17,10 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
app:
|
||||
build: .
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
VERSION: ${VERSION:-dev}
|
||||
environment:
|
||||
DATABASE_URL: postgres://levyraati:${POSTGRES_PASSWORD}@postgres:5432/levyraati
|
||||
ADMIN_USER: ${ADMIN_USER:-admin}
|
||||
|
||||
@@ -185,3 +185,9 @@ says so.
|
||||
changing under a future client — and the first endpoint gets built the day something actually
|
||||
calls it. Both surfaces being thin adapters over one data function is already true of the page
|
||||
handlers, so adding the JSON side later stays a one-line-per-route job.
|
||||
44. **CalVer, `YYYY.MM.DD-N`, injected at build time.** The date is the useful part: this app ships
|
||||
when there is something to ship and gets rebuilt monthly for yt-dlp anyway, so a semantic
|
||||
version would communicate nothing a date does not. `N` is the build number within that day,
|
||||
starting at 1, for the second attempt at a release. The string lives in a git tag and reaches
|
||||
the binary through `-ldflags`, so no file in the repo has to be bumped and a local build
|
||||
honestly reports `dev`. It surfaces in the footer, the startup log and `/healthz`.
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -13,6 +14,9 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Set at build time with -ldflags "-X main.version=…". A local `go build` honestly says dev.
|
||||
var version = "dev"
|
||||
|
||||
type config struct {
|
||||
databaseURL string
|
||||
adminUser string
|
||||
@@ -67,6 +71,7 @@ type app struct {
|
||||
|
||||
func main() {
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
slog.Info("starting", "ctx", "startup", "version", version)
|
||||
cfg := loadConfig()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -126,7 +131,8 @@ func (a *app) memberMux() *http.ServeMux {
|
||||
http.Error(w, "db down", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("ok"))
|
||||
// The version answers "what is actually running out there" without an SSH session.
|
||||
fmt.Fprintf(w, "ok %s\n", version)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /login", a.loginPage)
|
||||
|
||||
@@ -16,6 +16,8 @@ var assetFS embed.FS
|
||||
|
||||
var funcs = template.FuncMap{
|
||||
"fidate": func(t time.Time) string { return t.Local().Format("2.1.2006 15:04") },
|
||||
// Date without the clock: the minute a song was published is noise.
|
||||
"fiday": func(t time.Time) string { return t.Local().Format("2.1.2006") },
|
||||
"score": func(f *float64) string { return strconv.FormatFloat(*f, 'f', 1, 64) },
|
||||
"value": func(f float64) string { return strconv.FormatFloat(f, 'f', 1, 64) },
|
||||
// Lets one board partial be called with a title and a list, instead of two near-identical
|
||||
@@ -58,6 +60,7 @@ type page struct {
|
||||
Path string
|
||||
Narrow bool // auth pages are a 420px column
|
||||
Queued int // songs still owed a review, shown in the nav
|
||||
Version string
|
||||
Data any
|
||||
}
|
||||
|
||||
@@ -70,6 +73,7 @@ func (a *app) render(w http.ResponseWriter, r *http.Request, status int, name st
|
||||
}
|
||||
p.Member = memberFrom(r.Context())
|
||||
p.Path = r.URL.Path
|
||||
p.Version = version
|
||||
if p.Member != nil {
|
||||
// The queue is a worklist, so its size belongs in the nav.
|
||||
a.pool.QueryRow(r.Context(), `
|
||||
|
||||
+45
-2
@@ -282,8 +282,48 @@ footer.sitefooter {
|
||||
|
||||
/* --- song page and reviews --- */
|
||||
|
||||
.songmeta { color: var(--muted); display: flex; flex-wrap: wrap; gap: var(--space-2);
|
||||
align-items: center; margin: calc(-1 * var(--space-2)) 0 var(--space-4); }
|
||||
/* The artist is the second most important thing on the page, so it gets the display face and a
|
||||
line of its own rather than being one bold word inside a run of metadata. */
|
||||
.by {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.35rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text);
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
|
||||
/* Four kinds of fact, four labelled cells — the spine of a cassette insert. */
|
||||
.spec {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 0 var(--space-5);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spec > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 6rem;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-right: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.spec > div:last-child { border-right: 0; }
|
||||
|
||||
.spec dt {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.spec dd { margin: 0; font-size: 0.95rem; }
|
||||
|
||||
.intro { white-space: pre-wrap; background: var(--surface); padding: var(--space-4);
|
||||
border-left: 3px solid var(--hairline); border-radius: 0 var(--radius) var(--radius) 0; }
|
||||
@@ -937,3 +977,6 @@ img.avatar { object-fit: cover; }
|
||||
|
||||
.copyright { color: var(--muted); }
|
||||
.copyright::before { content: "·"; margin: 0 var(--space-2); }
|
||||
|
||||
.version { color: var(--muted); font-family: var(--font-display); }
|
||||
.version::before { content: "·"; margin: 0 var(--space-2); }
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
{{end}}
|
||||
<span class="slogan">We know good music, baby!</span>
|
||||
<span class="copyright">© Kessinen</span>
|
||||
<span class="version" title="Käytössä oleva versio">v{{.Version}}</span>
|
||||
</footer>
|
||||
|
||||
{{with .Flash}}
|
||||
|
||||
+12
-7
@@ -1,13 +1,18 @@
|
||||
{{define "content"}}
|
||||
{{$s := .Data}}
|
||||
<h1>{{$s.Title}}</h1>
|
||||
<p class="songmeta">
|
||||
<strong>{{$s.Artist}}</strong>
|
||||
<span class="badge genre">{{$s.GenreLabel}}</span>
|
||||
<span>{{$s.Length}}</span>
|
||||
<span>lähetti {{$s.Submitter}} {{fidate $s.CreatedAt}}</span>
|
||||
{{if $s.Own}}<span class="badge admin">oma kappale</span>{{end}}
|
||||
</p>
|
||||
<p class="by">{{$s.Artist}}</p>
|
||||
|
||||
<!-- Four different kinds of fact, so four labelled cells rather than one run of text. "Oma
|
||||
kappale" belongs here as the submitter, not as a badge: it is a fact about you. -->
|
||||
<dl class="spec">
|
||||
<div><dt>Genre</dt><dd>{{$s.GenreLabel}}</dd></div>
|
||||
<div><dt>Kesto</dt><dd>{{$s.Length}}</dd></div>
|
||||
<div><dt>Lähetti</dt>
|
||||
<dd>{{if $s.Own}}sinä{{else}}<a href="/profile/{{$s.SubmitterID}}">{{$s.Submitter}}</a>{{end}}</dd>
|
||||
</div>
|
||||
<div><dt>Julkaistu</dt><dd>{{fiday $s.CreatedAt}}</dd></div>
|
||||
</dl>
|
||||
|
||||
{{if $s.CanEdit}}
|
||||
<details class="editbox">
|
||||
|
||||
Reference in New Issue
Block a user