Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func main() {
legacyDB.EnsureEnergySnapshotsTable()
legacyDB.EnsureReadinessRedesignTables()
legacyDB.EnsureSubjectiveCheckinsTable()
legacyDB.EnsureContextPromptInteractionsTable()
legacyDB.EnsureAuthSessionsTable()

passwordHash := ""
Expand Down Expand Up @@ -154,7 +155,7 @@ func main() {
// One-time backfill of installation-wide Gemini config for installs
// that pre-date PR #16 (where AI settings were per-tenant). When the
// global table is empty but an admin tenant already has gemini_* rows,
// copy them up so non-admin tenants (Maria-style accounts) inherit.
// copy them up so non-admin tenants inherit.
if err := migrateGlobalAIIfNeeded(ctx, reg, mgr, users); err != nil {
log.Printf("global AI migration: %v", err)
}
Expand All @@ -174,6 +175,7 @@ func main() {
db.EnsureEnergySnapshotsTable()
db.EnsureReadinessRedesignTables()
db.EnsureSubjectiveCheckinsTable()
db.EnsureContextPromptInteractionsTable()
db.EnsureAuthSessionsTable()
startTenant(ctx, mgr, reg, db, u.SchemaName, envNotifyDefaults, envAIDefaults, baseURL)
}
Expand Down Expand Up @@ -229,6 +231,7 @@ func main() {
db.EnsureEnergySnapshotsTable()
db.EnsureReadinessRedesignTables()
db.EnsureSubjectiveCheckinsTable()
db.EnsureContextPromptInteractionsTable()
db.EnsureAuthSessionsTable()
startTenant(ctx, mgr, reg, db, schema, envNotifyDefaults, envAIDefaults, baseURL)
})
Expand Down Expand Up @@ -667,6 +670,9 @@ type liveCheckinRouter struct {
func (r *liveCheckinRouter) SaveAnswer(date, source, answer string, answeredAt time.Time) (string, error) {
return r.db.SaveCheckinAnswer(date, source, answer, answeredAt)
}
func (r *liveCheckinRouter) SaveContextPromptAnswer(promptID, category, source string, answeredAt time.Time) (string, error) {
return r.db.SaveContextPromptAnswer(promptID, category, source, answeredAt)
}
func (r *liveCheckinRouter) AnswerCallbackQuery(qid, text string) error {
return r.bot.AnswerCallbackQuery(qid, text)
}
Expand Down Expand Up @@ -706,20 +712,33 @@ func makeReportTrigger(mgr *tenants.Manager, schema string, defaults storage.Not
sendMu := mgr.MorningSendMuFor(schema)
if sendMu != nil {
sendMu.Lock()
defer sendMu.Unlock()
}
sentReport := false
if db.HasSentMorningReport(today) {
if sendMu != nil {
sendMu.Unlock()
}
return
}
sent, reason, err := notify.SendMorningSmart(bot, db, ncfg, false)
if err != nil {
if sendMu != nil {
sendMu.Unlock()
}
log.Printf("checkin-trigger: send: %v", err)
return
}
if sent {
if perr := db.MarkMorningReportSent(today); perr != nil {
log.Printf("checkin-trigger: mark sent: %v", perr)
}
sentReport = true
}
if sendMu != nil {
sendMu.Unlock()
}
if sentReport {
trySendContextPromptAfterMorning(bot, db, ncfg, today, time.Now().In(loc))
log.Printf("checkin-trigger: sent (reason=%s) for %s", reason, today)
}
}
Expand Down Expand Up @@ -832,25 +851,29 @@ func makeMorningTrigger(db *storage.DB, sendMu *sync.Mutex, mgr *tenants.Manager
// both observe HasSent=false in the narrow window between
// the outer check and the actual Telegram POST.
sendMu.Lock()
defer sendMu.Unlock()
if db.HasSentMorningReport(today) {
sendMu.Unlock()
return
}
sent, reason, err := notify.SendMorningSmartOpts(bot, db, ncfg, notify.MorningSendOpts{
Force: force,
CheckinExpired: action == notify.MorningActionExpireAndForce,
})
if err != nil {
sendMu.Unlock()
log.Printf("morning trigger: send telegram: %v", err)
return
}
if !sent {
sendMu.Unlock()
log.Printf("morning trigger: deferring — %s", reason)
return
}
if err := db.MarkMorningReportSent(today); err != nil {
log.Printf("morning trigger: mark sent: %v", err)
}
sendMu.Unlock()
trySendContextPromptAfterMorning(bot, db, ncfg, today, now)
log.Printf("morning trigger: sent (reason=%s, forced=%v, action=%s)", reason, force, action)
}
}
Expand Down Expand Up @@ -1090,6 +1113,7 @@ func runMorningSmartRetry(bot *notify.Bot, db *storage.DB, mgr *tenants.Manager,
}
if sent {
log.Printf("morning smart-retry: sent (reason=%s, forced=%v, action=%s)", reason, past, action)
trySendContextPromptAfterMorning(bot, db, ncfg, today, time.Now().In(loc))
return
}
if past {
Expand All @@ -1105,6 +1129,26 @@ func runMorningSmartRetry(bot *notify.Bot, db *storage.DB, mgr *tenants.Manager,
}
}

func trySendContextPromptAfterMorning(bot *notify.Bot, db *storage.DB, cfg notify.Config, signalDate string, now time.Time) {
if !storage.IsContextPromptsEnabled(db) {
return
}
promptDate := now.Format("2006-01-02")
prompt, reserved, err := db.ReserveLowSleepContextPrompt(signalDate, promptDate, now, now.Add(36*time.Hour))
if err != nil {
log.Printf("context prompt: reserve low_sleep date=%s: %v", signalDate, err)
return
}
if !reserved || prompt == nil {
return
}
if err := notify.SendContextPrompt(bot, db, cfg.Lang, prompt, now); err != nil {
log.Printf("context prompt: send low_sleep prompt=%s date=%s: %v", prompt.PromptID, signalDate, err)
return
}
log.Printf("context prompt: sent low_sleep prompt=%s date=%s", prompt.PromptID, signalDate)
}

// runDailyQualityScan ticks once per day at 03:00 (REPORT_TZ or system local)
// and calls MarkSuspectPoints to flag z-score outliers in the last 7 days of
// autonomic metrics. Cheap (~one query per metric) and idempotent — re-running
Expand Down Expand Up @@ -1146,6 +1190,11 @@ func runDailyQualityScan(db *storage.DB, schema string, defaults storage.NotifyC
now = time.Now().In(loc)
today := now.Format("2006-01-02")
db.RunReadinessRedesignBackfillForDatesAt([]string{today}, now)
if deleted, err := db.PruneContextPromptInteractions(now); err != nil {
log.Printf("[%s] context prompt retention: %v", schema, err)
} else if deleted > 0 {
log.Printf("[%s] context prompt retention: pruned %d rows", schema, deleted)
}
}
}

Expand Down
7 changes: 3 additions & 4 deletions docs/ai-plans/2026-06-18-checkin-calendar-sla.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ <h2>Implemented Result</h2>
<section>
<h2>Checks Run</h2>
<ul>
<li><code>gofmt -w internal\storage\subjective_checkin.go internal\storage\subjective_checkin_test.go internal\ui\handler.go internal\ui\admin_checkin_coverage_test.go internal\ui\i18n_en.go internal\ui\i18n_ru.go internal\ui\i18n_sr.go</code></li>
<li><code>gofmt -w internal/storage/subjective_checkin.go internal/storage/subjective_checkin_test.go internal/ui/handler.go internal/ui/admin_checkin_coverage_test.go internal/ui/i18n_en.go internal/ui/i18n_ru.go internal/ui/i18n_sr.go</code></li>
<li><code>go test ./internal/storage -run "Test.*Checkin|Test.*CheckinCoverage" -count=1</code> - passed.</li>
<li><code>go test ./internal/ui -run TestAdminCheckinCoverage -count=1</code> - passed.</li>
</ul>
Expand All @@ -202,10 +202,9 @@ <h2>Known Limitations</h2>
</section>

<section class="approval">
<h2>Approval Gate</h2>
<h2>Approval Gate Status</h2>
<p>
Implementation must not start until the user explicitly approves this plan.
Suggested approval phrase: <code>погнали</code>.
This was the historical approval gate for the implementation plan. Approval was received and the implementation results above reflect the completed work.
</p>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</section>
</main>
Expand Down
Loading