Make delivery and integration failures explicit

Persist reminder presentations and retry state, atomically complete collapsed deliveries, fall back across away reaches, and block permanent failures visibly (V-715, V-678). Fail closed when enabled integrations lack credentials and keep remote arms explicitly dark (V-691). Give mavweb one sanitized, request-correlated error contract (V-689). Owner explicitly requested direct commits to master.
This commit is contained in:
2026-08-13 02:50:59 +04:00
parent da9114b623
commit 35c6ff5a71
67 changed files with 3174 additions and 477 deletions
+24 -12
View File
@@ -332,12 +332,11 @@ func Load(path string) (*Config, error) {
// file committed to git.
expanded, missing := expandEnv(string(b))
if len(missing) > 0 {
// An unset variable expands to "", which every block reads as "not
// configured" and none of them complains about. That is the intended
// behaviour and it stays: CI parses this same file with no secrets
// present. What was missing is the line telling the operator which
// capability he just turned off by forgetting an env file.
log.Printf("config: %s references unset environment variables %v — those settings are empty, so whatever they configure is off", path, missing)
// Expansion happens before typed validation. A disabled block may carry
// empty placeholders; an enabled block that needs one of these values is
// rejected below. Log the names too so a failed deploy says which secret
// source was absent without ever printing a value.
log.Printf("config: %s references unset environment variables %v — expanded them to empty; enabled integrations will reject missing credentials", path, missing)
}
var c Config
if err := json.Unmarshal([]byte(expanded), &c); err != nil {
@@ -459,6 +458,12 @@ func (c *Config) validate() error {
if err := c.validateTelegram(); err != nil {
return err
}
if err := c.validateNtfy(); err != nil {
return err
}
if err := c.validateWorkstation(); err != nil {
return err
}
return nil
}
@@ -468,16 +473,23 @@ func (c *Config) validate() error {
// answers nothing — the failure is invisible from the chat. Same shape as
// validateNetScan: fail the config rather than the turn.
func (c *Config) validateTelegram() error {
if c.Telegram == nil || !c.Telegram.Intake {
if c.Telegram == nil || c.Telegram.Disabled {
return nil
}
// An unset ${TELEGRAM_*} expands to empty, and the daemon already reads an
// empty token or chat id as telegram not being wired at all. Validating a
// block that wires nothing would fail a box that merely has no bot.
if c.Telegram.BotToken == "" || c.Telegram.ChatID == "" {
if err := telegramsink.Validate(*c.Telegram); err != nil {
return err
}
if c.Telegram.Intake {
return telegramsink.ValidateIntakeChatID(c.Telegram.ChatID)
}
return nil
}
func (c *Config) validateNtfy() error {
if c.Ntfy == nil {
return nil
}
return telegramsink.ValidateIntakeChatID(c.Telegram.ChatID)
return ntfysink.Validate(*c.Ntfy)
}
// DBEncryptionKey resolves the at-rest encryption key: DBKeyEnv (if set) wins