Skip to content

Commit 2302b19

Browse files
committed
Feature releases
color-coded storage bar, user-controlled keep-awake timer, clip deletion snapshot sync
1 parent 8298553 commit 2302b19

File tree

13 files changed

+934
-64
lines changed

13 files changed

+934
-64
lines changed

server/api/drives.go

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,11 @@ func archiveLog(format string, args ...interface{}) {
199199
fmt.Fprintf(f, "%s: [drive-map] %s\n", time.Now().Format("Mon 02 Jan 15:04:05 MST 2006"), msg)
200200
}
201201

202-
// isArchiving returns true if the archiveloop is currently archiving files.
202+
// IsArchiving returns true if the archiveloop is currently archiving files.
203203
// The archive_progress_monitor updates the status file every 5s, so if it
204204
// hasn't been touched in over 120s, treat it as stale (archiveloop crashed
205205
// or forgot to clear it).
206-
func isArchiving() bool {
206+
func IsArchiving() bool {
207207
const statusFile = "/tmp/archive_status.json"
208208
info, err := os.Stat(statusFile)
209209
if err != nil {
@@ -262,7 +262,7 @@ func (dh *DriveHandlers) processFiles(w http.ResponseWriter, r *http.Request) {
262262
// post_archive=1 is set by post-archive-process.sh which runs after
263263
// archiving is complete. Skip the stale-file check in that case.
264264
postArchive := r.URL.Query().Get("post_archive") == "1"
265-
if !postArchive && isArchiving() {
265+
if !postArchive && IsArchiving() {
266266
writeError(w, http.StatusConflict, "archive is currently running — please wait until it finishes")
267267
return
268268
}
@@ -346,7 +346,7 @@ func (dh *DriveHandlers) reprocessAll(w http.ResponseWriter, r *http.Request) {
346346
writeError(w, http.StatusConflict, "processing already in progress")
347347
return
348348
}
349-
if isArchiving() {
349+
if IsArchiving() {
350350
writeError(w, http.StatusConflict, "archive is currently running — please wait until it finishes")
351351
return
352352
}
@@ -409,7 +409,7 @@ func (dh *DriveHandlers) processingStatus(w http.ResponseWriter, r *http.Request
409409
"running": dh.processor.IsRunning(),
410410
"routes_count": dh.store.RouteCount(),
411411
"processed_count": dh.store.ProcessedCount(),
412-
"archiving": isArchiving(),
412+
"archiving": IsArchiving(),
413413
}
414414

415415
if archive := readArchiveStatus(); archive != nil {
@@ -503,18 +503,18 @@ func (dh *DriveHandlers) driveStats(w http.ResponseWriter, r *http.Request) {
503503
}
504504

505505
writeJSON(w, http.StatusOK, map[string]interface{}{
506-
"drives_count": len(allDrives),
507-
"routes_count": len(routes),
508-
"processed_count": dh.store.ProcessedCount(),
509-
"total_distance_km": math.Round(totalDistKm*100) / 100,
510-
"total_distance_mi": math.Round(totalDistMi*100) / 100,
511-
"total_duration_ms": totalDurationMs,
512-
"fsd_engaged_ms": totalFSDEngagedMs,
513-
"fsd_distance_km": math.Round(totalFSDDistKm*100) / 100,
514-
"fsd_distance_mi": math.Round(totalFSDDistMi*100) / 100,
515-
"fsd_percent": fsdPercent,
516-
"fsd_disengagements": totalDisengagements,
517-
"fsd_accel_pushes": totalAccelPushes,
506+
"drives_count": len(allDrives),
507+
"routes_count": len(routes),
508+
"processed_count": dh.store.ProcessedCount(),
509+
"total_distance_km": math.Round(totalDistKm*100) / 100,
510+
"total_distance_mi": math.Round(totalDistMi*100) / 100,
511+
"total_duration_ms": totalDurationMs,
512+
"fsd_engaged_ms": totalFSDEngagedMs,
513+
"fsd_distance_km": math.Round(totalFSDDistKm*100) / 100,
514+
"fsd_distance_mi": math.Round(totalFSDDistMi*100) / 100,
515+
"fsd_percent": fsdPercent,
516+
"fsd_disengagements": totalDisengagements,
517+
"fsd_accel_pushes": totalAccelPushes,
518518
})
519519
}
520520

@@ -561,12 +561,12 @@ func (dh *DriveHandlers) fsdAnalytics(w http.ResponseWriter, r *http.Request) {
561561

562562
// Daily breakdown
563563
type dayStats struct {
564-
Date string `json:"date"`
565-
DayName string `json:"dayName"`
566-
Disengagements int `json:"disengagements"`
567-
AccelPushes int `json:"accelPushes"`
568-
FSDPercent float64 `json:"fsdPercent"`
569-
Drives int `json:"drives"`
564+
Date string `json:"date"`
565+
DayName string `json:"dayName"`
566+
Disengagements int `json:"disengagements"`
567+
AccelPushes int `json:"accelPushes"`
568+
FSDPercent float64 `json:"fsdPercent"`
569+
Drives int `json:"drives"`
570570
}
571571
dailyMap := make(map[string]*dayStats)
572572

server/api/files.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"io"
7+
"log"
78
"net/http"
89
"os"
910
"path/filepath"
@@ -225,6 +226,13 @@ func (h *handlers) deleteFile(w http.ResponseWriter, r *http.Request) {
225226
return
226227
}
227228

229+
// If deleted path was under SavedClips or SentryClips, clean up
230+
// matching symlinks in snapshot directories so those clips won't
231+
// be re-synced on next archive. RecentClips are left untouched.
232+
if strings.Contains(cleanPath, "/SavedClips/") || strings.Contains(cleanPath, "/SentryClips/") {
233+
go cleanupSnapshotSymlinks(cleanPath)
234+
}
235+
228236
writeOK(w)
229237
}
230238

@@ -330,3 +338,109 @@ func (h *handlers) downloadZip(w http.ResponseWriter, r *http.Request) {
330338

331339
w.Write([]byte(output))
332340
}
341+
342+
// cleanupSnapshotSymlinks removes symlinks in snapshot directories that
343+
// correspond to a deleted SavedClips or SentryClips path. This prevents
344+
// deleted clips from being re-archived on the next sync.
345+
//
346+
// The snapshot layout is:
347+
//
348+
// /backingfiles/snapshots/snap-NNNNNN/mnt/TeslaCam/{SavedClips,SentryClips}/<event>/<file>.mp4
349+
//
350+
// and the mutable layout is:
351+
//
352+
// /mutable/TeslaCam/{SavedClips,SentryClips}/<event>/<file>.mp4
353+
//
354+
// Both contain symlinks pointing into the snapshot mount.
355+
func cleanupSnapshotSymlinks(deletedPath string) {
356+
// Determine the clip type and event folder name from the deleted path.
357+
// Expected patterns:
358+
// /mutable/TeslaCam/SavedClips/<event>
359+
// /mutable/TeslaCam/SentryClips/<event>
360+
// /mutable/TeslaCam/SavedClips/<event>/<file>
361+
var clipType, eventName string
362+
for _, ct := range []string{"SavedClips", "SentryClips"} {
363+
marker := "/" + ct + "/"
364+
idx := strings.Index(deletedPath, marker)
365+
if idx >= 0 {
366+
clipType = ct
367+
rest := deletedPath[idx+len(marker):]
368+
parts := strings.SplitN(rest, "/", 2)
369+
if len(parts) > 0 {
370+
eventName = parts[0]
371+
}
372+
break
373+
}
374+
}
375+
376+
if clipType == "" || eventName == "" {
377+
return
378+
}
379+
380+
log.Printf("[files] Cleaning up snapshot symlinks for %s/%s", clipType, eventName)
381+
382+
// Walk all snapshot directories looking for matching event folders
383+
snapshotsBase := "/backingfiles/snapshots"
384+
entries, err := os.ReadDir(snapshotsBase)
385+
if err != nil {
386+
return
387+
}
388+
389+
for _, snapEntry := range entries {
390+
if !snapEntry.IsDir() || !strings.HasPrefix(snapEntry.Name(), "snap-") {
391+
continue
392+
}
393+
394+
// Check for the event folder in this snapshot's clip type directory
395+
eventDir := filepath.Join(snapshotsBase, snapEntry.Name(), "mnt", "TeslaCam", clipType, eventName)
396+
if _, err := os.Stat(eventDir); err != nil {
397+
continue
398+
}
399+
400+
// Remove all symlinks in this event directory
401+
clipEntries, err := os.ReadDir(eventDir)
402+
if err != nil {
403+
continue
404+
}
405+
for _, ce := range clipEntries {
406+
linkPath := filepath.Join(eventDir, ce.Name())
407+
fi, err := os.Lstat(linkPath)
408+
if err != nil {
409+
continue
410+
}
411+
if fi.Mode()&os.ModeSymlink != 0 {
412+
os.Remove(linkPath)
413+
}
414+
}
415+
416+
// Remove the event directory if now empty
417+
remaining, _ := os.ReadDir(eventDir)
418+
if len(remaining) == 0 {
419+
os.Remove(eventDir)
420+
}
421+
}
422+
423+
// Also clean up broken symlinks in /mutable/TeslaCam/<clipType>/<eventName>
424+
mutableEventDir := filepath.Join("/mutable/TeslaCam", clipType, eventName)
425+
if entries, err := os.ReadDir(mutableEventDir); err == nil {
426+
for _, e := range entries {
427+
linkPath := filepath.Join(mutableEventDir, e.Name())
428+
fi, err := os.Lstat(linkPath)
429+
if err != nil {
430+
continue
431+
}
432+
if fi.Mode()&os.ModeSymlink != 0 {
433+
// Check if target still exists
434+
if _, err := os.Stat(linkPath); err != nil {
435+
os.Remove(linkPath)
436+
}
437+
}
438+
}
439+
remaining, _ := os.ReadDir(mutableEventDir)
440+
if len(remaining) == 0 {
441+
os.Remove(mutableEventDir)
442+
}
443+
}
444+
445+
log.Printf("[files] Snapshot symlink cleanup complete for %s/%s", clipType, eventName)
446+
}

0 commit comments

Comments
 (0)