Files
selfpost/internal/web/view/account.go
T
mixandClaude Opus 5.5 82a53e111c
test / test (push) Waiting to run
settings-save-row: the Settings form is the left column
The owner chose Variant 2 on 2026-10-16 ("форма в левой колонке, кнопка
под своими полями"); the mockup is f95c553. Server › Settings: the form
wraps only the left column - Log retention, Notifications and Save
settings under them - and the right column, outside the form, holds
Notification mail and the read-only Sending rate limits. The boxes are
renumbered column by column (Notifications 02, Notification mail 03,
Sending rate limits 04). The fields, their names and the POST are
unchanged.

Tests: the page's outline matches the mockup's (TestPanelOutlinesMatchMockups);
go vet + go test (26 packages) and make e2e on the test server for this
tree.

CHANGELOG [Unreleased]; roadmap row done, its section quotes the choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:04:06 +03:00

159 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package view
// The data of Account (templates/account.html, mockup account.html) and of
// Server › Settings (templates/settings.html, mockup settings.html): what used
// to be one Settings page, split by whose it is. Account is the signed-in
// user's own — who they are to the panel and their password — and every role
// has it. Settings is the instance's.
import "strconv"
// Account is the data of the Account page. Two forms, each its own POST: the
// profile (Username, Email) and the password. The fields hold what was stored,
// or what was just submitted when a form is shown again with an error.
type Account struct {
Meta
Head Head
Flash *Flash
Profile Box
Username string
Email string
// Notify is whether the panel writes to this user: the checkbox under the
// e-mail.
Notify bool
Password Box
}
// NewAccount builds the page for a user. role is "global" or "domain" and
// appears in the kicker.
func NewAccount(m Meta, role string) *Account {
m.Title, m.Section, m.Page = "Account", "user", "account"
return &Account{
Meta: m,
Head: Head{
Kicker: "Signed in as " + m.User + " · " + role,
Title: "Account",
Lead: Plain("Who you are to this panel. Applications keep their own logins and passwords and are not affected."),
},
Profile: Box{No: "01", Title: "Profile"},
Password: Box{No: "02", Title: "Password"},
}
}
// WithResult shows the result of the last action, or its refusal, between the
// head and the first box.
func (a *Account) WithResult(flash, formErr string) *Account {
a.Flash = resultFlash(flash, formErr)
return a
}
// Settings is the data of Server › Settings: one form with the instance's
// settings and one Save settings button, and the read-only level-1 rate limit
// the operator sets in .env.
type Settings struct {
Meta
Head Head
Flash *Flash
Retention Box
RetentionDays string
// Inbound is whether the inbound relay is on, which gives the retention box
// its second field; InboundRetentionDays is what that field shows.
Inbound bool
InboundRetentionDays string
RateLimits Box
RateFacts []Fact
// Notifications is the instance switch for notification e-mails; Notify is
// its state and NotifyNote the line under it, which says from where and to
// whom.
Notifications Box
Notify bool
NotifyNote Text
// Mail holds the DNS records of the server's own name, which notifications
// are signed and authorised with, drawn like a domain's.
Mail Box
MailRecords []Record
}
// NewSettings builds the page. retention is what the field shows (the stored
// value, or what was just submitted); level1 is the per-client-IP limit as
// FormatRate writes it.
func NewSettings(m Meta, retention, level1 string) *Settings {
m.Title, m.Section, m.Page = "Settings", "server", "settings"
return &Settings{
Meta: m,
Head: Head{
Kicker: "Server",
Title: "Settings",
Lead: Rich("What holds for the whole instance, whoever is signed in. Your own name, e-mail and password are under ",
Link("/account", "Account"), "."),
},
Retention: Box{No: "01", Title: "Log retention", Help: topicLink(HelpSettings, "How long rows are kept")},
RetentionDays: retention,
RateLimits: Box{No: "04", Title: "Sending rate limits", ID: "rate-limits", End: Plain("Read-only"), Help: topicLink(HelpLimits, "Levels 1 and 2")},
RateFacts: []Fact{
{Label: "Level 1 · per client IP", Value: Plain(level1), Big: true,
Note: Rich("Set in ", Code(".env"), "; restart to change. Nothing below may exceed it.")},
{Label: "Level 2 · domain, application",
Value: Rich("Set on each ", Link("/outbound/domains", "domain"), " and on each application of a domain.")},
},
Notifications: Box{No: "02", Title: "Notifications", ID: "notifications", Help: topicLink(HelpNotifications, "What is sent and when")},
Mail: Box{No: "03", Title: "Notification mail", ID: "notification-mail", End: Plain("DNS records of the server's own name"),
Help: topicLink(HelpNotifications, "What the panel sends and what makes it arrive")},
}
}
// WithNotifications sets the instance switch: on is whether notification
// e-mails are sent at all, from is the address they come from.
func (s *Settings) WithNotifications(on bool, from string) *Settings {
s.Notify = on
s.NotifyNote = Rich("From ", Mono(from), ", to every user who has an e-mail and has not turned them off under ",
Link("/account", "Account"), ".")
return s
}
// WithMail sets the records of the server's own name, in the order they are
// shown (ServerMailDKIM, ServerMailSPF).
func (s *Settings) WithMail(records []Record) *Settings {
s.MailRecords = records
return s
}
// WithInbound adds the retention of the Inbound log to the Log retention box.
// days is what the field shows, like retention in NewSettings. The field exists
// only while the inbound relay is on: with it off there is no journal to keep.
func (s *Settings) WithInbound(days string) *Settings {
s.Inbound, s.InboundRetentionDays = true, days
return s
}
// WithResult shows the result of the last action, or its refusal, between the
// head and the form.
func (s *Settings) WithResult(flash, formErr string) *Settings {
s.Flash = resultFlash(flash, formErr)
return s
}
// FormatRate writes a rate limit of messages per window the short way:
// "600 / h" for an hourly limit, "30 / min", "5 000 / day", and the window in
// seconds for any other.
func FormatRate(messages, windowSeconds int) string {
n := FormatCount(int64(messages))
switch {
case windowSeconds == 60:
return n + " / min"
case windowSeconds == 3600:
return n + " / h"
case windowSeconds == 86400:
return n + " / day"
case windowSeconds > 0 && windowSeconds%3600 == 0:
return n + " / " + strconv.Itoa(windowSeconds/3600) + " h"
}
return n + " / " + strconv.Itoa(windowSeconds) + " s"
}