@@ -11,6 +11,7 @@ import (
1111 "os"
1212 "time"
1313
14+ "github.com/nebari-dev/jhub-app-proxy/pkg/activity"
1415 "github.com/nebari-dev/jhub-app-proxy/pkg/logger"
1516)
1617
@@ -142,23 +143,79 @@ func (c *Client) NotifyActivity(ctx context.Context) error {
142143 return nil
143144}
144145
146+ // NotifyActivityWithTime notifies JupyterHub of activity with a specific timestamp
147+ // This is used when keepAlive=false to report actual last activity time
148+ func (c * Client ) NotifyActivityWithTime (ctx context.Context , timestamp time.Time ) error {
149+ endpoint := fmt .Sprintf ("%s/users/%s/activity" , c .baseURL , c .username )
150+
151+ payload := ActivityPayload {
152+ LastActivity : timestamp ,
153+ }
154+
155+ // Include server-specific activity if server name is set
156+ if c .servername != "" {
157+ payload .Servers = map [string ]ServerActivity {
158+ c .servername : {
159+ LastActivity : timestamp ,
160+ },
161+ }
162+ }
163+
164+ jsonData , err := json .Marshal (payload )
165+ if err != nil {
166+ return fmt .Errorf ("failed to marshal activity payload: %w" , err )
167+ }
168+
169+ req , err := http .NewRequestWithContext (ctx , http .MethodPost , endpoint , bytes .NewBuffer (jsonData ))
170+ if err != nil {
171+ return fmt .Errorf ("failed to create request: %w" , err )
172+ }
173+
174+ req .Header .Set ("Authorization" , fmt .Sprintf ("token %s" , c .apiToken ))
175+ req .Header .Set ("Content-Type" , "application/json" )
176+
177+ start := time .Now ()
178+ resp , err := c .httpClient .Do (req )
179+ duration := time .Since (start )
180+
181+ if err != nil {
182+ c .logger .HubAPICall ("POST" , endpoint , 0 , duration , err )
183+ return fmt .Errorf ("failed to notify activity: %w" , err )
184+ }
185+ defer resp .Body .Close ()
186+
187+ c .logger .HubAPICall ("POST" , endpoint , resp .StatusCode , duration , nil )
188+
189+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
190+ body , _ := io .ReadAll (resp .Body )
191+ return fmt .Errorf ("activity notification failed with status %d: %s" ,
192+ resp .StatusCode , string (body ))
193+ }
194+
195+ c .logger .Debug ("activity notification successful" , "timestamp" , timestamp )
196+ return nil
197+ }
198+
145199// StartActivityReporter starts a background goroutine that periodically reports activity
146200// Returns a cancel function to stop the reporter
147- func (c * Client ) StartActivityReporter (ctx context.Context , interval time.Duration , forceAlive bool ) context.CancelFunc {
201+ //
202+ // If keepAlive is true: Always report current time (prevent idle culling)
203+ // If keepAlive is false: Only report when there's actual activity tracked by activityTracker
204+ func (c * Client ) StartActivityReporter (ctx context.Context , interval time.Duration , keepAlive bool , activityTracker * activity.Tracker ) context.CancelFunc {
148205 ctx , cancel := context .WithCancel (ctx )
149206
150207 go func () {
151208 c .logger .Info ("starting activity reporter" ,
152209 "interval" , interval ,
153- "force_alive " , forceAlive ,
210+ "keep_alive " , keepAlive ,
154211 "username" , c .username ,
155212 "servername" , c .servername )
156213
157214 ticker := time .NewTicker (interval )
158215 defer ticker .Stop ()
159216
160- // Report activity immediately on start if force_alive is enabled
161- if forceAlive {
217+ // Report activity immediately on start if keepAlive is enabled
218+ if keepAlive {
162219 if err := c .NotifyActivity (ctx ); err != nil {
163220 c .logger .Error ("failed to notify activity on start" , err )
164221 }
@@ -170,13 +227,27 @@ func (c *Client) StartActivityReporter(ctx context.Context, interval time.Durati
170227 c .logger .Info ("activity reporter stopped" )
171228 return
172229 case <- ticker .C :
173- // In force_alive mode, always report activity
174- // In normal mode, only report if there was actual activity
175- // (for now, we always report - activity tracking can be added later)
176- if err := c .NotifyActivity (ctx ); err != nil {
177- c .logger .Error ("failed to notify activity" , err ,
178- "username" , c .username ,
179- "servername" , c .servername )
230+ if keepAlive {
231+ // Always report current time (keep alive forever)
232+ if err := c .NotifyActivity (ctx ); err != nil {
233+ c .logger .Error ("failed to notify activity" , err ,
234+ "username" , c .username ,
235+ "servername" , c .servername )
236+ }
237+ } else {
238+ // Only report if there was actual activity
239+ lastActivity := activityTracker .GetLastActivity ()
240+ if lastActivity != nil {
241+ if err := c .NotifyActivityWithTime (ctx , * lastActivity ); err != nil {
242+ c .logger .Error ("failed to notify activity" , err ,
243+ "username" , c .username ,
244+ "servername" , c .servername ,
245+ "last_activity" , lastActivity )
246+ }
247+ } else {
248+ // No activity yet, don't send notification
249+ c .logger .Debug ("no activity to report yet" )
250+ }
180251 }
181252 }
182253 }
0 commit comments