Review pass on the Lightwell redesign: layout, copy and behavior fixes
CI / test (push) Successful in 3m20s
Release / release (push) Successful in 5m6s

Dialog buttons are Cancel then the primary action at the bottom right, and each page has one primary
action. List pages no longer repeat their title. Dates have the month written out. Fields in a row
line up, hints sit under fields instead of inside labels, and long certificate errors are clamped.
Organizations show whether local access is on and name the action; setting a user's password uses a
dialog instead of a browser prompt. Server errors for sign-in and ranges are reworded with a next step,
and the stray scrollbar inside tables is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Opus 5.5 committed 2026-10-26 18:38:56 -06:00
1 parent d74e6c1e72
commit c585ddbf63
9 files changed
+130 -108

No files matched your search

+11 -11
View File
@@ -66,7 +66,7 @@ func (s *Server) withUser(fn func(http.ResponseWriter, *http.Request, *principal
return func(w http.ResponseWriter, r *http.Request) {
p := s.principalFrom(r)
if p == nil || p.User.ID == "" {
fail(w, 401, errors.New("not logged in"))
fail(w, 401, errors.New("Sign in to continue."))
return
}
fn(w, r, p)
@@ -96,7 +96,7 @@ func (s *Server) withOrg(needAdmin bool, fn func(http.ResponseWriter, *http.Requ
return func(w http.ResponseWriter, r *http.Request) {
p := s.principalFrom(r)
if p == nil {
fail(w, 401, errors.New("not logged in"))
fail(w, 401, errors.New("Sign in to continue."))
return
}
id := r.PathValue("org")
@@ -169,7 +169,7 @@ func (s *Server) createFirstAdmin(email, name, password, orgName string) error {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.st.Users) != 0 {
return errors.New("setup already done")
return errors.New("Hammerhead is already set up. Sign in instead.")
}
u := &User{ID: newID(), Email: email, Name: name, PassHash: hash, ServerAdmin: true, Created: time.Now()}
s.st.Users = append(s.st.Users, u)
@@ -240,7 +240,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
}
email := strings.ToLower(strings.TrimSpace(in.Email))
if !s.allow("login|"+clientIP(r)+"|"+email, 8, 5*time.Minute) {
fail(w, 429, errors.New("too many attempts, wait a few minutes"))
fail(w, 429, errors.New("Too many attempts. Wait a few minutes and try again."))
return
}
s.mu.Lock()
@@ -254,7 +254,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
if u == nil || !good || u.PassHash == "" {
s.audit("", email, "login.failed", clientIP(r))
time.Sleep(300 * time.Millisecond)
fail(w, 401, errors.New("wrong email or password"))
fail(w, 401, errors.New("That email and password do not match. Check them and try again."))
return
}
if u.TOTPEnabled {
@@ -276,7 +276,7 @@ func (s *Server) loginTOTP(w http.ResponseWriter, r *http.Request) {
return
}
if !s.allow("totp|"+clientIP(r), 10, 5*time.Minute) {
fail(w, 429, errors.New("too many attempts, wait a few minutes"))
fail(w, 429, errors.New("Too many attempts. Wait a few minutes and try again."))
return
}
s.mu.Lock()
@@ -307,7 +307,7 @@ func (s *Server) loginTOTP(w http.ResponseWriter, r *http.Request) {
}
s.mu.Unlock()
if !good {
fail(w, 401, errors.New("wrong code"))
fail(w, 401, errors.New("That code is not right. Check your authenticator and try again."))
return
}
s.startSession(w, r, u.ID)
@@ -413,7 +413,7 @@ func (s *Server) changePassword(w http.ResponseWriter, r *http.Request, p *princ
return
}
if p.User.PassHash != "" && bcrypt.CompareHashAndPassword([]byte(p.User.PassHash), []byte(in.Current)) != nil {
fail(w, 400, errors.New("current password is wrong"))
fail(w, 400, errors.New("Your current password is not right. Check it and try again."))
return
}
hash, err := hashPassword(in.New)
@@ -464,7 +464,7 @@ func (s *Server) totpEnable(w http.ResponseWriter, r *http.Request, p *principal
defer s.mu.Unlock()
u := s.findUser(p.User.ID)
if u.TOTPSecret == "" || !totpOK(u.TOTPSecret, strings.TrimSpace(in.Code)) {
fail(w, 400, errors.New("that code doesn't match; check your authenticator's clock"))
fail(w, 400, errors.New("That code does not match. Check that your authenticator's clock is right and try again."))
return
}
codes, hashes := make([]string, 8), make([]string, 8)
@@ -483,14 +483,14 @@ func (s *Server) totpDisable(w http.ResponseWriter, r *http.Request, p *principa
return
}
if p.User.PassHash != "" && bcrypt.CompareHashAndPassword([]byte(p.User.PassHash), []byte(in.Password)) != nil {
fail(w, 400, errors.New("password is wrong"))
fail(w, 400, errors.New("That password is not right. Check it and try again."))
return
}
s.mu.Lock()
defer s.mu.Unlock()
u := s.findUser(p.User.ID)
if !totpOK(u.TOTPSecret, strings.TrimSpace(in.Code)) {
fail(w, 400, errors.New("code is wrong"))
fail(w, 400, errors.New("That code is not right. Check your authenticator and try again."))
return
}
u.TOTPEnabled, u.TOTPSecret, u.BackupCodes = false, "", nil