@@ -103,6 +103,7 @@ func main() {
103103 legacyDB .EnsureEnergySnapshotsTable ()
104104 legacyDB .EnsureReadinessRedesignTables ()
105105 legacyDB .EnsureSubjectiveCheckinsTable ()
106+ legacyDB .EnsureContextPromptInteractionsTable ()
106107 legacyDB .EnsureAuthSessionsTable ()
107108
108109 passwordHash := ""
@@ -154,7 +155,7 @@ func main() {
154155 // One-time backfill of installation-wide Gemini config for installs
155156 // that pre-date PR #16 (where AI settings were per-tenant). When the
156157 // global table is empty but an admin tenant already has gemini_* rows,
157- // copy them up so non-admin tenants (Maria-style accounts) inherit.
158+ // copy them up so non-admin tenants inherit.
158159 if err := migrateGlobalAIIfNeeded (ctx , reg , mgr , users ); err != nil {
159160 log .Printf ("global AI migration: %v" , err )
160161 }
@@ -174,6 +175,7 @@ func main() {
174175 db .EnsureEnergySnapshotsTable ()
175176 db .EnsureReadinessRedesignTables ()
176177 db .EnsureSubjectiveCheckinsTable ()
178+ db .EnsureContextPromptInteractionsTable ()
177179 db .EnsureAuthSessionsTable ()
178180 startTenant (ctx , mgr , reg , db , u .SchemaName , envNotifyDefaults , envAIDefaults , baseURL )
179181 }
@@ -229,6 +231,7 @@ func main() {
229231 db .EnsureEnergySnapshotsTable ()
230232 db .EnsureReadinessRedesignTables ()
231233 db .EnsureSubjectiveCheckinsTable ()
234+ db .EnsureContextPromptInteractionsTable ()
232235 db .EnsureAuthSessionsTable ()
233236 startTenant (ctx , mgr , reg , db , schema , envNotifyDefaults , envAIDefaults , baseURL )
234237 })
@@ -667,6 +670,9 @@ type liveCheckinRouter struct {
667670func (r * liveCheckinRouter ) SaveAnswer (date , source , answer string , answeredAt time.Time ) (string , error ) {
668671 return r .db .SaveCheckinAnswer (date , source , answer , answeredAt )
669672}
673+ func (r * liveCheckinRouter ) SaveContextPromptAnswer (promptID , category , source string , answeredAt time.Time ) (string , error ) {
674+ return r .db .SaveContextPromptAnswer (promptID , category , source , answeredAt )
675+ }
670676func (r * liveCheckinRouter ) AnswerCallbackQuery (qid , text string ) error {
671677 return r .bot .AnswerCallbackQuery (qid , text )
672678}
@@ -706,20 +712,33 @@ func makeReportTrigger(mgr *tenants.Manager, schema string, defaults storage.Not
706712 sendMu := mgr .MorningSendMuFor (schema )
707713 if sendMu != nil {
708714 sendMu .Lock ()
709- defer sendMu .Unlock ()
710715 }
716+ sentReport := false
711717 if db .HasSentMorningReport (today ) {
718+ if sendMu != nil {
719+ sendMu .Unlock ()
720+ }
712721 return
713722 }
714723 sent , reason , err := notify .SendMorningSmart (bot , db , ncfg , false )
715724 if err != nil {
725+ if sendMu != nil {
726+ sendMu .Unlock ()
727+ }
716728 log .Printf ("checkin-trigger: send: %v" , err )
717729 return
718730 }
719731 if sent {
720732 if perr := db .MarkMorningReportSent (today ); perr != nil {
721733 log .Printf ("checkin-trigger: mark sent: %v" , perr )
722734 }
735+ sentReport = true
736+ }
737+ if sendMu != nil {
738+ sendMu .Unlock ()
739+ }
740+ if sentReport {
741+ trySendContextPromptAfterMorning (bot , db , ncfg , today , time .Now ().In (loc ))
723742 log .Printf ("checkin-trigger: sent (reason=%s) for %s" , reason , today )
724743 }
725744 }
@@ -832,25 +851,29 @@ func makeMorningTrigger(db *storage.DB, sendMu *sync.Mutex, mgr *tenants.Manager
832851 // both observe HasSent=false in the narrow window between
833852 // the outer check and the actual Telegram POST.
834853 sendMu .Lock ()
835- defer sendMu .Unlock ()
836854 if db .HasSentMorningReport (today ) {
855+ sendMu .Unlock ()
837856 return
838857 }
839858 sent , reason , err := notify .SendMorningSmartOpts (bot , db , ncfg , notify.MorningSendOpts {
840859 Force : force ,
841860 CheckinExpired : action == notify .MorningActionExpireAndForce ,
842861 })
843862 if err != nil {
863+ sendMu .Unlock ()
844864 log .Printf ("morning trigger: send telegram: %v" , err )
845865 return
846866 }
847867 if ! sent {
868+ sendMu .Unlock ()
848869 log .Printf ("morning trigger: deferring — %s" , reason )
849870 return
850871 }
851872 if err := db .MarkMorningReportSent (today ); err != nil {
852873 log .Printf ("morning trigger: mark sent: %v" , err )
853874 }
875+ sendMu .Unlock ()
876+ trySendContextPromptAfterMorning (bot , db , ncfg , today , now )
854877 log .Printf ("morning trigger: sent (reason=%s, forced=%v, action=%s)" , reason , force , action )
855878 }
856879 }
@@ -1090,6 +1113,7 @@ func runMorningSmartRetry(bot *notify.Bot, db *storage.DB, mgr *tenants.Manager,
10901113 }
10911114 if sent {
10921115 log .Printf ("morning smart-retry: sent (reason=%s, forced=%v, action=%s)" , reason , past , action )
1116+ trySendContextPromptAfterMorning (bot , db , ncfg , today , time .Now ().In (loc ))
10931117 return
10941118 }
10951119 if past {
@@ -1105,6 +1129,26 @@ func runMorningSmartRetry(bot *notify.Bot, db *storage.DB, mgr *tenants.Manager,
11051129 }
11061130}
11071131
1132+ func trySendContextPromptAfterMorning (bot * notify.Bot , db * storage.DB , cfg notify.Config , signalDate string , now time.Time ) {
1133+ if ! storage .IsContextPromptsEnabled (db ) {
1134+ return
1135+ }
1136+ promptDate := now .Format ("2006-01-02" )
1137+ prompt , reserved , err := db .ReserveLowSleepContextPrompt (signalDate , promptDate , now , now .Add (36 * time .Hour ))
1138+ if err != nil {
1139+ log .Printf ("context prompt: reserve low_sleep date=%s: %v" , signalDate , err )
1140+ return
1141+ }
1142+ if ! reserved || prompt == nil {
1143+ return
1144+ }
1145+ if err := notify .SendContextPrompt (bot , db , cfg .Lang , prompt , now ); err != nil {
1146+ log .Printf ("context prompt: send low_sleep prompt=%s date=%s: %v" , prompt .PromptID , signalDate , err )
1147+ return
1148+ }
1149+ log .Printf ("context prompt: sent low_sleep prompt=%s date=%s" , prompt .PromptID , signalDate )
1150+ }
1151+
11081152// runDailyQualityScan ticks once per day at 03:00 (REPORT_TZ or system local)
11091153// and calls MarkSuspectPoints to flag z-score outliers in the last 7 days of
11101154// autonomic metrics. Cheap (~one query per metric) and idempotent — re-running
@@ -1146,6 +1190,11 @@ func runDailyQualityScan(db *storage.DB, schema string, defaults storage.NotifyC
11461190 now = time .Now ().In (loc )
11471191 today := now .Format ("2006-01-02" )
11481192 db .RunReadinessRedesignBackfillForDatesAt ([]string {today }, now )
1193+ if deleted , err := db .PruneContextPromptInteractions (now ); err != nil {
1194+ log .Printf ("[%s] context prompt retention: %v" , schema , err )
1195+ } else if deleted > 0 {
1196+ log .Printf ("[%s] context prompt retention: pruned %d rows" , schema , deleted )
1197+ }
11491198 }
11501199}
11511200
0 commit comments