diff --git a/internal/webapp/shares.go b/internal/webapp/shares.go
index 247048b..dcd2761 100644
--- a/internal/webapp/shares.go
+++ b/internal/webapp/shares.go
@@ -370,7 +370,7 @@ func (s *Server) handleShared(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprintf(w, sharedMarkdownShell, html.EscapeString(path.Base(sh.Path)), body)
+ fmt.Fprintf(w, sharedMarkdownShell, html.EscapeString(path.Base(sh.Path)), updatedStamp(fi.Time), body)
case ".html", ".htm":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
io.Copy(w, rc)
@@ -381,7 +381,20 @@ func (s *Server) handleShared(w http.ResponseWriter, r *http.Request) {
}
}
+// updatedStamp renders the "how old is this?" line a share page owes its
+// reader — the link promises the latest version, so it has to say when latest
+// was. Zero time (a source that doesn't know) prints nothing rather than 1970.
+func updatedStamp(t time.Time) string {
+ if t.IsZero() {
+ return ""
+ }
+ t = t.UTC()
+ return fmt.Sprintf(`
Last updated %s
`,
+ html.EscapeString(t.Format(time.RFC3339)), html.EscapeString(t.Format("2 Jan 2006")))
+}
+
// sharedMarkdownShell wraps rendered markdown in a minimal readable page.
+// Verbs, in order: title, updated stamp, body.
const sharedMarkdownShell = `
%s
%s
+.updated{font-size:12.5px;color:#57606a;margin-bottom:28px}
+@media (prefers-color-scheme: dark){footer.bdrive{border-color:#3a3a44;color:#888}.updated{color:#888}}
+%s%s
`
diff --git a/internal/webapp/shares_test.go b/internal/webapp/shares_test.go
index 38dae92..072fafe 100644
--- a/internal/webapp/shares_test.go
+++ b/internal/webapp/shares_test.go
@@ -3,6 +3,7 @@ package webapp
import (
"bytes"
"encoding/json"
+ "fmt"
"net/http"
"net/http/httptest"
"path/filepath"
@@ -365,6 +366,73 @@ func TestShareListAPIsAreStable(t *testing.T) {
// helpers
+// TestShareLastUpdatedStamp: a share page promises the latest version, so a
+// markdown page says when latest was — and nothing else gets injected into.
+func TestShareLastUpdatedStamp(t *testing.T) {
+ srv, p, _, f, h := shareHub(t)
+ const rawHTML = "
Q3
"
+ const pdf = "%PDF-1.4 fake\n"
+ f.put("dev1", "wiki/deck.pdf", pdf)
+
+ // markdown: the stamp is the FILE's time, human date + precise title=
+ when := time.Date(2026, 3, 14, 9, 26, 53, 0, time.UTC)
+ f.putAt("dev1", "wiki/notes.md", "# Notes\n\nhello **team**", when)
+ token, _ := authedShare(t, srv, h, p.ID, "wiki/notes.md")
+ rec := do(t, h, "GET", "/s/"+token, nil)
+ body := rec.Body.String()
+ if !strings.Contains(body, "Last updated 14 Mar 2026") {
+ t.Fatalf("no last-updated stamp: %s", body)
+ }
+ if !strings.Contains(body, `title="2026-03-14T09:26:53Z"`) {
+ t.Fatalf("no precise timestamp: %s", body)
+ }
+ // ...and it sits above the content, not after it
+ if strings.Index(body, "Last updated") > strings.Index(body, "team") {
+ t.Fatal("stamp must render before the content")
+ }
+ // the sandbox + footer survive
+ if csp := rec.Header().Get("Content-Security-Policy"); csp != "sandbox allow-scripts allow-popups" {
+ t.Fatalf("CSP = %q", csp)
+ }
+ if rec.Header().Get("X-Content-Type-Options") != "nosniff" || rec.Header().Get("Referrer-Policy") != "no-referrer" {
+ t.Fatalf("share headers = %v", rec.Header())
+ }
+ if !strings.Contains(body, "Shared with