[1.0.0]
Added:
- A **Delivery log** on each delivery's page (`/deliveries/{id}`): the
- A **History** block on the same page: the journal's two timestamps stated as
Fixed:
- Postfix config: copy TLS cert/key from the (often `:ro`, host-owned) mount
- Postfix config: allow `maillog_file` under `/data` via
- E2e: read `/data/setup-token` via `docker compose exec` (file is `0600`
- Entrypoint: check `SELFPOST_HOSTNAME` before `/data` setup (so a bad identity
- E2e gate: wait for host-published `/healthz` before panel setup, and stop
… (3 more in CHANGELOG)
Changed:
- Document what `.spbk` and `.spde` stand for (SelfPost backup / SelfPost domain
- Docs aligned with the code: setup URL is `/setup/<token>` (README and
- Full backups no longer carry `/data/log`. It is Postfix's raw log plus its
- Monitoring screens (status, mail queue, system log, deliveries) use adaptive
- Documentation package consolidated into `docs/development.md`: Documentation
… (4 more in CHANGELOG)
[1.0.1]
A documentation and packaging release: no change to the mail path, the database, or the on-disk layout. Upgrading is a tag bump.
Added:
- `SECURITY.md` — how to report a vulnerability privately (GitHub private
- [docs/plans/](docs/plans/) — one document per agreed extension: the optional
Changed:
- The panel's **Account** entry is now called **Settings** — nav link, page
- The signed-in name in the panel's nav is now labelled `User:`, so it reads as
- [docs/product.md](docs/product.md) reframes the future line: the inbound
- [docs/security.md](docs/security.md) is now in English, matching the rest of
- The README documentation table now points at `SECURITY.md` for reporting a
… (2 more in CHANGELOG)
Fixed:
- The panel's static assets are served with a content ETag and
Squashed CHANGELOG sections: [1.0.0], [1.0.1].
Full detail: CHANGELOG.md (1.0.0, 1.0.1).
60 lines
2.2 KiB
Go
60 lines
2.2 KiB
Go
package web
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"io/fs"
|
|
"net/http"
|
|
"path"
|
|
)
|
|
|
|
// staticETags maps each /static/ URL path to a strong ETag over the asset's
|
|
// bytes, computed once at startup from the embedded FS.
|
|
//
|
|
// The assets are baked into the binary, so their FS modification times are the
|
|
// zero value and http.FileServer sends no Last-Modified. Without an ETag either,
|
|
// a response carries no validator at all and the browser is free to guess how
|
|
// long to keep it — which is how a replaced favicon keeps showing the old mark
|
|
// long after a deploy. Hashing the content gives every asset a validator that
|
|
// changes exactly when the asset does.
|
|
var staticETags = buildStaticETags()
|
|
|
|
func buildStaticETags() map[string]string {
|
|
etags := make(map[string]string)
|
|
// An error here would mean the embed directive and this walk disagree, which
|
|
// is a build-time defect rather than a runtime condition; the assets still
|
|
// serve correctly without a validator, so skip what can't be read.
|
|
_ = fs.WalkDir(assetsFS, "static", func(name string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
b, err := fs.ReadFile(assetsFS, name)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
sum := sha256.Sum256(b)
|
|
etags["/"+name] = `"` + hex.EncodeToString(sum[:16]) + `"`
|
|
return nil
|
|
})
|
|
return etags
|
|
}
|
|
|
|
// staticHandler serves the embedded assets under /static/ with a content ETag.
|
|
//
|
|
// Cache-Control is no-cache rather than a max-age: it lets the browser keep the
|
|
// copy but requires it to revalidate, so an asset that changed is picked up on
|
|
// the next page load while an unchanged one costs a 304 with no body. For a
|
|
// handful of small files on a single-operator panel that trade is the right way
|
|
// round — correctness after a deploy matters more than saving the round trip.
|
|
// http.ServeContent answers the conditional request from the ETag we set here.
|
|
func staticHandler() http.Handler {
|
|
files := http.FileServer(http.FS(assetsFS))
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if etag, ok := staticETags[path.Clean(r.URL.Path)]; ok {
|
|
w.Header().Set("ETag", etag)
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
}
|
|
files.ServeHTTP(w, r)
|
|
})
|
|
}
|