refactor(store): add error returns to all Store interface methods

Every Store method now returns an error. Callers handle errors
gracefully — TUI logs to event log, server returns HTTP 500,
monitor engine logs and retries. All rows.Scan() errors are now
checked in sqlstore.go instead of silently appending corrupt data.

- GetSites, GetAllAlerts, GetAllUsers return ([]T, error)
- GetAlert returns (AlertConfig, error) instead of (AlertConfig, bool)
- AddSite, UpdateSite, DeleteSite, etc. all return error
- SaveCheck, LoadAllHistory, ExportData return error
- ~25 caller sites updated across tui, server, monitor, main
This commit is contained in:
2026-05-15 00:37:20 -04:00
parent ab75f61c6b
commit d4f4012c8a
10 changed files with 185 additions and 93 deletions
+7 -3
View File
@@ -174,7 +174,8 @@ func startSSHServer(port int) {
}
func seedDemoData(s store.Store) {
if existing := s.GetSites(); len(existing) > 0 {
existing, _ := s.GetSites()
if len(existing) > 0 {
return
}
fmt.Println("Seeding demo data...")
@@ -187,7 +188,7 @@ func seedDemoData(s store.Store) {
"from": "oncall@example.com", "to": "team@example.com",
})
alerts := s.GetAllAlerts()
alerts, _ := s.GetAllAlerts()
alertID := 0
if len(alerts) > 0 {
alertID = alerts[0].ID
@@ -206,7 +207,10 @@ func seedDemoData(s store.Store) {
}
func isKeyAllowed(incomingKey ssh.PublicKey) bool {
users := store.Get().GetAllUsers()
users, err := store.Get().GetAllUsers()
if err != nil {
return false
}
for _, u := range users {
allowedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(u.PublicKey))
if err != nil {
+6 -2
View File
@@ -25,7 +25,11 @@ func InitHistoryFromStore() {
if s == nil {
return
}
all := s.LoadAllHistory(maxHistoryLen)
all, err := s.LoadAllHistory(maxHistoryLen)
if err != nil {
AddLog("Failed to load check history: " + err.Error())
return
}
historyMu.Lock()
defer historyMu.Unlock()
for siteID, records := range all {
@@ -71,7 +75,7 @@ func RecordCheck(siteID int, latency time.Duration, isUp bool) {
}
if s := store.Get(); s != nil {
go s.SaveCheck(siteID, latency.Nanoseconds(), isUp)
go func() { _ = s.SaveCheck(siteID, latency.Nanoseconds(), isUp) }()
}
}
+8 -3
View File
@@ -128,7 +128,12 @@ func StartEngine() {
continue
}
sites := s_instance.GetSites()
sites, err := s_instance.GetSites()
if err != nil {
AddLog(fmt.Sprintf("Failed to load sites: %v", err))
time.Sleep(5 * time.Second)
continue
}
for _, s := range sites {
Mutex.RLock()
_, exists := LiveState[s.ID]
@@ -406,8 +411,8 @@ func triggerAlert(alertID int, title, message string) {
if s_instance == nil {
return
}
cfg, ok := s_instance.GetAlert(alertID)
if !ok {
cfg, err := s_instance.GetAlert(alertID)
if err != nil {
return
}
provider := alert.GetProvider(cfg)
+6 -1
View File
@@ -185,7 +185,12 @@ func Start(cfg ServerConfig) {
http.Error(w, "Unauthorized: UPKEEP_CLUSTER_SECRET required", 401)
return
}
data := store.Get().ExportData()
data, err := store.Get().ExportData()
if err != nil {
log.Printf("Export failed: %v", err)
http.Error(w, "Export failed", 500)
return
}
json.NewEncoder(w).Encode(data)
})
+97 -49
View File
@@ -48,7 +48,7 @@ func (s *SQLStore) Init() error {
return nil
}
func (s *SQLStore) GetSites() []models.Site {
func (s *SQLStore) GetSites() ([]models.Site, error) {
bf := s.dialect.BoolFalse()
query := fmt.Sprintf(
"SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, %s), COALESCE(paused, %s) FROM sites",
@@ -56,107 +56,132 @@ func (s *SQLStore) GetSites() []models.Site {
)
rows, err := s.db.Query(query)
if err != nil {
return []models.Site{}
return nil, err
}
defer rows.Close()
var sites []models.Site
for rows.Next() {
var st models.Site
rows.Scan(&st.ID, &st.Name, &st.URL, &st.Type, &st.Token, &st.Interval, &st.AlertID,
if err := rows.Scan(&st.ID, &st.Name, &st.URL, &st.Type, &st.Token, &st.Interval, &st.AlertID,
&st.CheckSSL, &st.ExpiryThreshold, &st.MaxRetries, &st.Hostname, &st.Port, &st.Timeout,
&st.Method, &st.Description, &st.ParentID, &st.AcceptedCodes, &st.DNSResolveType,
&st.DNSServer, &st.IgnoreTLS, &st.Paused)
&st.DNSServer, &st.IgnoreTLS, &st.Paused); err != nil {
return sites, err
}
sites = append(sites, st)
}
return sites
return sites, rows.Err()
}
func (s *SQLStore) AddSite(site models.Site) {
func (s *SQLStore) AddSite(site models.Site) error {
token := ""
if site.Type == "push" {
token = generateToken()
}
s.db.Exec(s.q("INSERT INTO sites (name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries, hostname, port, timeout, method, description, parent_id, accepted_codes, dns_resolve_type, dns_server, ignore_tls, paused) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
_, err := s.db.Exec(s.q("INSERT INTO sites (name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries, hostname, port, timeout, method, description, parent_id, accepted_codes, dns_resolve_type, dns_server, ignore_tls, paused) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
site.Name, site.URL, site.Type, token, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries,
site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS, site.Paused)
return err
}
func (s *SQLStore) UpdateSite(site models.Site) {
func (s *SQLStore) UpdateSite(site models.Site) error {
var existingToken string
s.db.QueryRow(s.q("SELECT token FROM sites WHERE id=?"), site.ID).Scan(&existingToken)
if site.Type == "push" && existingToken == "" {
existingToken = generateToken()
}
s.db.Exec(s.q("UPDATE sites SET name=?, url=?, type=?, token=?, interval=?, alert_id=?, check_ssl=?, threshold=?, max_retries=?, hostname=?, port=?, timeout=?, method=?, description=?, parent_id=?, accepted_codes=?, dns_resolve_type=?, dns_server=?, ignore_tls=?, paused=? WHERE id=?"),
_, err := s.db.Exec(s.q("UPDATE sites SET name=?, url=?, type=?, token=?, interval=?, alert_id=?, check_ssl=?, threshold=?, max_retries=?, hostname=?, port=?, timeout=?, method=?, description=?, parent_id=?, accepted_codes=?, dns_resolve_type=?, dns_server=?, ignore_tls=?, paused=? WHERE id=?"),
site.Name, site.URL, site.Type, existingToken, site.Interval, site.AlertID, site.CheckSSL, site.ExpiryThreshold, site.MaxRetries,
site.Hostname, site.Port, site.Timeout, site.Method, site.Description, site.ParentID, site.AcceptedCodes, site.DNSResolveType, site.DNSServer, site.IgnoreTLS, site.Paused, site.ID)
return err
}
func (s *SQLStore) UpdateSitePaused(id int, paused bool) {
s.db.Exec(s.q("UPDATE sites SET paused=? WHERE id=?"), paused, id)
func (s *SQLStore) UpdateSitePaused(id int, paused bool) error {
_, err := s.db.Exec(s.q("UPDATE sites SET paused=? WHERE id=?"), paused, id)
return err
}
func (s *SQLStore) DeleteSite(id int) {
s.db.Exec(s.q("DELETE FROM sites WHERE id=?"), id)
func (s *SQLStore) DeleteSite(id int) error {
_, err := s.db.Exec(s.q("DELETE FROM sites WHERE id=?"), id)
if err != nil {
return err
}
s.dialect.ResetSequenceOnEmpty(s.db, "sites")
return nil
}
func (s *SQLStore) GetAllAlerts() []models.AlertConfig {
func (s *SQLStore) GetAllAlerts() ([]models.AlertConfig, error) {
rows, err := s.db.Query("SELECT id, name, type, settings FROM alerts")
if err != nil {
return []models.AlertConfig{}
return nil, err
}
defer rows.Close()
var alerts []models.AlertConfig
for rows.Next() {
var a models.AlertConfig
var settingsJSON string
rows.Scan(&a.ID, &a.Name, &a.Type, &settingsJSON)
if err := rows.Scan(&a.ID, &a.Name, &a.Type, &settingsJSON); err != nil {
return alerts, err
}
json.Unmarshal([]byte(settingsJSON), &a.Settings)
alerts = append(alerts, a)
}
return alerts
return alerts, rows.Err()
}
func (s *SQLStore) GetAlert(id int) (models.AlertConfig, bool) {
func (s *SQLStore) GetAlert(id int) (models.AlertConfig, error) {
var a models.AlertConfig
var settingsJSON string
err := s.db.QueryRow(s.q("SELECT id, name, type, settings FROM alerts WHERE id = ?"), id).Scan(&a.ID, &a.Name, &a.Type, &settingsJSON)
if err != nil {
return a, false
return a, err
}
json.Unmarshal([]byte(settingsJSON), &a.Settings)
return a, true
return a, nil
}
func (s *SQLStore) AddAlert(name, aType string, settings map[string]string) {
jsonBytes, _ := json.Marshal(settings)
s.db.Exec(s.q("INSERT INTO alerts (name, type, settings) VALUES (?, ?, ?)"), name, aType, string(jsonBytes))
func (s *SQLStore) AddAlert(name, aType string, settings map[string]string) error {
jsonBytes, err := json.Marshal(settings)
if err != nil {
return err
}
_, err = s.db.Exec(s.q("INSERT INTO alerts (name, type, settings) VALUES (?, ?, ?)"), name, aType, string(jsonBytes))
return err
}
func (s *SQLStore) UpdateAlert(id int, name, aType string, settings map[string]string) {
jsonBytes, _ := json.Marshal(settings)
s.db.Exec(s.q("UPDATE alerts SET name=?, type=?, settings=? WHERE id=?"), name, aType, string(jsonBytes), id)
func (s *SQLStore) UpdateAlert(id int, name, aType string, settings map[string]string) error {
jsonBytes, err := json.Marshal(settings)
if err != nil {
return err
}
_, err = s.db.Exec(s.q("UPDATE alerts SET name=?, type=?, settings=? WHERE id=?"), name, aType, string(jsonBytes), id)
return err
}
func (s *SQLStore) DeleteAlert(id int) {
s.db.Exec(s.q("DELETE FROM alerts WHERE id=?"), id)
func (s *SQLStore) DeleteAlert(id int) error {
_, err := s.db.Exec(s.q("DELETE FROM alerts WHERE id=?"), id)
if err != nil {
return err
}
s.dialect.ResetSequenceOnEmpty(s.db, "alerts")
return nil
}
func (s *SQLStore) GetAllUsers() []models.User {
func (s *SQLStore) GetAllUsers() ([]models.User, error) {
rows, err := s.db.Query("SELECT id, username, public_key, role FROM users")
if err != nil {
return []models.User{}
return nil, err
}
defer rows.Close()
var users []models.User
for rows.Next() {
var u models.User
rows.Scan(&u.ID, &u.Username, &u.PublicKey, &u.Role)
if err := rows.Scan(&u.ID, &u.Username, &u.PublicKey, &u.Role); err != nil {
return users, err
}
users = append(users, u)
}
return users
return users, rows.Err()
}
func (s *SQLStore) AddUser(username, publicKey, role string) error {
@@ -174,14 +199,18 @@ func (s *SQLStore) DeleteUser(id int) error {
return err
}
func (s *SQLStore) SaveCheck(siteID int, latencyNs int64, isUp bool) {
s.db.Exec(s.q("INSERT INTO check_history (site_id, latency_ns, is_up) VALUES (?, ?, ?)"), siteID, latencyNs, isUp)
s.db.Exec(s.q(`DELETE FROM check_history WHERE site_id = ? AND id NOT IN (
func (s *SQLStore) SaveCheck(siteID int, latencyNs int64, isUp bool) error {
_, err := s.db.Exec(s.q("INSERT INTO check_history (site_id, latency_ns, is_up) VALUES (?, ?, ?)"), siteID, latencyNs, isUp)
if err != nil {
return err
}
_, err = s.db.Exec(s.q(`DELETE FROM check_history WHERE site_id = ? AND id NOT IN (
SELECT id FROM check_history WHERE site_id = ? ORDER BY checked_at DESC LIMIT 1000
)`), siteID, siteID)
return err
}
func (s *SQLStore) LoadAllHistory(limit int) map[int][]models.CheckRecord {
func (s *SQLStore) LoadAllHistory(limit int) (map[int][]models.CheckRecord, error) {
result := make(map[int][]models.CheckRecord)
rows, err := s.db.Query(s.q(`
SELECT site_id, latency_ns, is_up FROM (
@@ -190,12 +219,14 @@ func (s *SQLStore) LoadAllHistory(limit int) map[int][]models.CheckRecord {
FROM check_history
) sub WHERE rn <= ?`), limit)
if err != nil {
return result
return result, err
}
defer rows.Close()
for rows.Next() {
var r models.CheckRecord
rows.Scan(&r.SiteID, &r.LatencyNs, &r.IsUp)
if err := rows.Scan(&r.SiteID, &r.LatencyNs, &r.IsUp); err != nil {
return result, err
}
result[r.SiteID] = append(result[r.SiteID], r)
}
for id, records := range result {
@@ -204,15 +235,23 @@ func (s *SQLStore) LoadAllHistory(limit int) map[int][]models.CheckRecord {
}
result[id] = records
}
return result
return result, rows.Err()
}
func (s *SQLStore) ExportData() models.Backup {
return models.Backup{
Sites: s.GetSites(),
Alerts: s.GetAllAlerts(),
Users: s.GetAllUsers(),
func (s *SQLStore) ExportData() (models.Backup, error) {
sites, err := s.GetSites()
if err != nil {
return models.Backup{}, err
}
alerts, err := s.GetAllAlerts()
if err != nil {
return models.Backup{}, err
}
users, err := s.GetAllUsers()
if err != nil {
return models.Backup{}, err
}
return models.Backup{Sites: sites, Alerts: alerts, Users: users}, nil
}
func (s *SQLStore) ImportData(data models.Backup) error {
@@ -225,16 +264,25 @@ func (s *SQLStore) ImportData(data models.Backup) error {
s.dialect.ImportWipe(tx)
for _, u := range data.Users {
tx.Exec(s.q("INSERT INTO users (username, public_key, role) VALUES (?, ?, ?)"), u.Username, u.PublicKey, u.Role)
if _, err := tx.Exec(s.q("INSERT INTO users (username, public_key, role) VALUES (?, ?, ?)"), u.Username, u.PublicKey, u.Role); err != nil {
return err
}
}
for _, a := range data.Alerts {
jsonBytes, _ := json.Marshal(a.Settings)
tx.Exec(s.q("INSERT INTO alerts (id, name, type, settings) VALUES (?, ?, ?, ?)"), a.ID, a.Name, a.Type, string(jsonBytes))
jsonBytes, err := json.Marshal(a.Settings)
if err != nil {
return err
}
if _, err := tx.Exec(s.q("INSERT INTO alerts (id, name, type, settings) VALUES (?, ?, ?, ?)"), a.ID, a.Name, a.Type, string(jsonBytes)); err != nil {
return err
}
}
for _, st := range data.Sites {
tx.Exec(s.q("INSERT INTO sites (id, name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries, hostname, port, timeout, method, description, parent_id, accepted_codes, dns_resolve_type, dns_server, ignore_tls, paused) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
if _, err := tx.Exec(s.q("INSERT INTO sites (id, name, url, type, token, interval, alert_id, check_ssl, threshold, max_retries, hostname, port, timeout, method, description, parent_id, accepted_codes, dns_resolve_type, dns_server, ignore_tls, paused) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
st.ID, st.Name, st.URL, st.Type, st.Token, st.Interval, st.AlertID, st.CheckSSL, st.ExpiryThreshold, st.MaxRetries,
st.Hostname, st.Port, st.Timeout, st.Method, st.Description, st.ParentID, st.AcceptedCodes, st.DNSResolveType, st.DNSServer, st.IgnoreTLS, st.Paused)
st.Hostname, st.Port, st.Timeout, st.Method, st.Description, st.ParentID, st.AcceptedCodes, st.DNSResolveType, st.DNSServer, st.IgnoreTLS, st.Paused); err != nil {
return err
}
}
s.dialect.ImportResetSequences(tx)
+14 -14
View File
@@ -8,31 +8,31 @@ type Store interface {
Init() error
// Sites
GetSites() []models.Site
AddSite(site models.Site)
UpdateSite(site models.Site)
UpdateSitePaused(id int, paused bool)
DeleteSite(id int)
GetSites() ([]models.Site, error)
AddSite(site models.Site) error
UpdateSite(site models.Site) error
UpdateSitePaused(id int, paused bool) error
DeleteSite(id int) error
// Alerts
GetAllAlerts() []models.AlertConfig
GetAlert(id int) (models.AlertConfig, bool)
AddAlert(name, aType string, settings map[string]string)
UpdateAlert(id int, name, aType string, settings map[string]string)
DeleteAlert(id int)
GetAllAlerts() ([]models.AlertConfig, error)
GetAlert(id int) (models.AlertConfig, error)
AddAlert(name, aType string, settings map[string]string) error
UpdateAlert(id int, name, aType string, settings map[string]string) error
DeleteAlert(id int) error
// Users
GetAllUsers() []models.User
GetAllUsers() ([]models.User, error)
AddUser(username, publicKey, role string) error
UpdateUser(id int, username, publicKey, role string) error
DeleteUser(id int) error
// History
SaveCheck(siteID int, latencyNs int64, isUp bool)
LoadAllHistory(limit int) map[int][]models.CheckRecord
SaveCheck(siteID int, latencyNs int64, isUp bool) error
LoadAllHistory(limit int) (map[int][]models.CheckRecord, error)
// Backup & Restore
ExportData() models.Backup
ExportData() (models.Backup, error)
ImportData(data models.Backup) error
}
+7 -2
View File
@@ -2,6 +2,7 @@ package tui
import (
"fmt"
"go-upkeep/internal/monitor"
"go-upkeep/internal/store"
tea "github.com/charmbracelet/bubbletea"
@@ -277,9 +278,13 @@ func (m *Model) submitAlertForm() {
}
if m.editID > 0 {
store.Get().UpdateAlert(m.editID, d.Name, d.AlertType, settings)
if err := store.Get().UpdateAlert(m.editID, d.Name, d.AlertType, settings); err != nil {
monitor.AddLog("Update alert failed: " + err.Error())
}
} else {
store.Get().AddAlert(d.Name, d.AlertType, settings)
if err := store.Get().AddAlert(d.Name, d.AlertType, settings); err != nil {
monitor.AddLog("Add alert failed: " + err.Error())
}
}
m.state = stateDashboard
}
+10 -4
View File
@@ -361,14 +361,16 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
}
alertOpts := []huh.Option[string]{huh.NewOption("None", "0")}
if store.Get() != nil {
for _, a := range store.Get().GetAllAlerts() {
if s := store.Get(); s != nil {
if alerts, err := s.GetAllAlerts(); err == nil {
for _, a := range alerts {
alertOpts = append(alertOpts, huh.NewOption(
fmt.Sprintf("%s (%s)", a.Name, a.Type),
strconv.Itoa(a.ID),
))
}
}
}
groupOpts := []huh.Option[string]{huh.NewOption("None", "0")}
for _, s := range m.sites {
@@ -558,10 +560,14 @@ func (m *Model) submitSiteForm() {
}
if m.editID > 0 {
store.Get().UpdateSite(site)
if err := store.Get().UpdateSite(site); err != nil {
monitor.AddLog("Update site failed: " + err.Error())
}
monitor.UpdateSiteConfig(site)
} else {
store.Get().AddSite(site)
if err := store.Get().AddSite(site); err != nil {
monitor.AddLog("Add site failed: " + err.Error())
}
}
m.state = stateDashboard
}
+7 -2
View File
@@ -2,6 +2,7 @@ package tui
import (
"fmt"
"go-upkeep/internal/monitor"
"go-upkeep/internal/store"
tea "github.com/charmbracelet/bubbletea"
@@ -145,9 +146,13 @@ func (m *Model) initUserHuhForm() tea.Cmd {
func (m *Model) submitUserForm() {
d := m.userFormData
if m.editID > 0 {
store.Get().UpdateUser(m.editID, d.Username, d.PublicKey, d.Role)
if err := store.Get().UpdateUser(m.editID, d.Username, d.PublicKey, d.Role); err != nil {
monitor.AddLog("Update user failed: " + err.Error())
}
} else {
store.Get().AddUser(d.Username, d.PublicKey, d.Role)
if err := store.Get().AddUser(d.Username, d.PublicKey, d.Role); err != nil {
monitor.AddLog("Add user failed: " + err.Error())
}
}
m.state = stateUsers
}
+19 -9
View File
@@ -107,17 +107,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if keyMsg, ok := msg.(tea.KeyMsg); ok {
switch keyMsg.String() {
case "y", "Y":
if store.Get() != nil {
if s := store.Get(); s != nil {
switch m.deleteTab {
case 0:
store.Get().DeleteSite(m.deleteID)
if err := s.DeleteSite(m.deleteID); err != nil {
monitor.AddLog("Delete site failed: " + err.Error())
}
monitor.RemoveSite(m.deleteID)
m.adjustCursor(len(m.sites) - 1)
case 1:
store.Get().DeleteAlert(m.deleteID)
if err := s.DeleteAlert(m.deleteID); err != nil {
monitor.AddLog("Delete alert failed: " + err.Error())
}
m.adjustCursor(len(m.alerts) - 1)
case 3:
store.Get().DeleteUser(m.deleteID)
if err := s.DeleteUser(m.deleteID); err != nil {
monitor.AddLog("Delete user failed: " + err.Error())
}
m.adjustCursor(len(m.users) - 1)
}
}
@@ -313,8 +319,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
site := m.sites[m.cursor]
monitor.ToggleSitePause(site.ID)
site.Paused = !site.Paused
if store.Get() != nil {
store.Get().UpdateSitePaused(site.ID, site.Paused)
if s := store.Get(); s != nil {
_ = s.UpdateSitePaused(site.ID, site.Paused)
}
m.refreshData()
}
@@ -464,10 +470,14 @@ func (m *Model) refreshData() {
}
ordered = append(ordered, ungrouped...)
m.sites = ordered
if store.Get() != nil {
m.alerts = store.Get().GetAllAlerts()
if s := store.Get(); s != nil {
if alerts, err := s.GetAllAlerts(); err == nil {
m.alerts = alerts
}
if m.isAdmin {
m.users = store.Get().GetAllUsers()
if users, err := s.GetAllUsers(); err == nil {
m.users = users
}
}
}
m.logViewport.SetContent(strings.Join(monitor.GetLogs(), "\n"))