diff --git a/README.md b/README.md index 758576e..c7f4366 100644 --- a/README.md +++ b/README.md @@ -86,4 +86,12 @@ Permanently deletes a complete simulation from the platform. ### `DELETE /api/simulations/:id/resources/:filename` Deletes a specific user-uploaded media asset or log file dynamically out of a simulation without dropping the core simulation entity itself. - **Behavior:** Detaches the file association string from SQLite and forces an OS-level file drop directly onto the specific asset file stored within the simulation's results directory. -- **Response:** JSON object `{"status": "success"}` \ No newline at end of file +- **Response:** JSON object `{"status": "success"}` + +### `DELETE /api/simulations/clear-fails` +Purges all failed tracking simulations globally from the database and physically drops their filesystem result nodes off the host system. A simulation is identified natively as "failed" when its `total_time` tracking variable maps to a `0.0` or numeric `null` value during extraction. +- **Response:** JSON object `{"status": "success", "cleared": }` + +### `GET /api/stats` +Retrieves globally aggregated system statistics across the entire database resolving UI constraints. +- **Response:** JSON object `{"total_simulations": , "fastest_sim_id": , "total_storage_bytes": }` \ No newline at end of file diff --git a/server/routes/router.go b/server/routes/router.go index 7a8514f..bf899f8 100644 --- a/server/routes/router.go +++ b/server/routes/router.go @@ -18,6 +18,17 @@ func NewRouter(db *sql.DB) *Router { func (rt *Router) Register(mux *http.ServeMux) { mux.HandleFunc("/api/simulations", rt.handleSimulationsBase) mux.HandleFunc("/api/simulations/", rt.handleSimulationsPath) + mux.HandleFunc("/api/stats", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + rt.OptionsHandler(w, r) + return + } + if r.Method == "GET" { + rt.GetStats(w, r) + } else { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + }) } func (rt *Router) handleSimulationsBase(w http.ResponseWriter, r *http.Request) { @@ -49,6 +60,11 @@ func (rt *Router) handleSimulationsPath(w http.ResponseWriter, r *http.Request) return } + if pathParts[0] == "clear-fails" && r.Method == "DELETE" { + rt.ClearFailedSimulations(w, r) + return + } + idStr := pathParts[0] if len(pathParts) == 1 { diff --git a/server/routes/simulations.go b/server/routes/simulations.go index aa0cf91..a9ca387 100644 --- a/server/routes/simulations.go +++ b/server/routes/simulations.go @@ -247,3 +247,44 @@ func (rt *Router) DeleteSimulation(w http.ResponseWriter, r *http.Request, idStr w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } + +func (rt *Router) ClearFailedSimulations(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + + rows, err := rt.DB.Query(` + SELECT id, name FROM simulations + WHERE search_time IS NULL + AND total_time IS NULL + AND NOT EXISTS (SELECT 1 FROM resources WHERE simulation_id = simulations.id) + `) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + var deleteIDs []string + var deleteDirs []string + + for rows.Next() { + var id, name string + if err := rows.Scan(&id, &name); err == nil { + deleteIDs = append(deleteIDs, id) + deleteDirs = append(deleteDirs, name) + } + } + rows.Close() + + for i, idStr := range deleteIDs { + rt.DB.Exec("DELETE FROM resources WHERE simulation_id = ?", idStr) + rt.DB.Exec("DELETE FROM simulations WHERE id = ?", idStr) + + dirPath := filepath.Join("../results", deleteDirs[i]) + if _, statErr := os.Stat(dirPath); statErr == nil { + os.RemoveAll(dirPath) + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"status": "success", "deleted_count": len(deleteIDs)}) +} diff --git a/server/routes/stats.go b/server/routes/stats.go new file mode 100644 index 0000000..4325eed --- /dev/null +++ b/server/routes/stats.go @@ -0,0 +1,43 @@ +package routes + +import ( + "database/sql" + "encoding/json" + "net/http" + "os" + "path/filepath" +) + +func (rt *Router) GetStats(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + + var totalSims int + err := rt.DB.QueryRow("SELECT COUNT(*) FROM simulations").Scan(&totalSims) + if err != nil { + totalSims = 0 + } + + var fastestSimID sql.NullInt64 + err = rt.DB.QueryRow("SELECT id FROM simulations WHERE total_time IS NOT NULL ORDER BY total_time ASC LIMIT 1").Scan(&fastestSimID) + + var totalSize int64 = 0 + filepath.Walk("../results", func(path string, info os.FileInfo, err error) error { + if err == nil && !info.IsDir() { + totalSize += info.Size() + } + return nil + }) + + var fastest *int + if fastestSimID.Valid { + f := int(fastestSimID.Int64) + fastest = &f + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "total_simulations": totalSims, + "fastest_sim_id": fastest, + "total_storage_bytes": totalSize, + }) +} diff --git a/src/lib/SimulationSidebar.svelte b/src/lib/SimulationSidebar.svelte index 053570a..83f0709 100644 --- a/src/lib/SimulationSidebar.svelte +++ b/src/lib/SimulationSidebar.svelte @@ -2,6 +2,33 @@ import type { SimulationState } from "./simulationState.svelte"; let { state }: { state: SimulationState } = $props(); + + async function clearFails() { + if ( + !confirm( + "Are you sure you want to permanently delete all failed simulations?", + ) + ) { + return; + } + try { + const res = await fetch("/api/simulations/clear-fails", { + method: "DELETE", + }); + if (res.ok) { + const data = await res.json(); + alert( + `Successfully cleared ${data.deleted_count} failed runs.`, + ); + window.location.reload(); + } else { + alert("Failed to clear simulations."); + } + } catch (e) { + console.error("Error clearing fails:", e); + alert("Network error clearing fails."); + } + } diff --git a/src/lib/SimulationTable.svelte b/src/lib/SimulationTable.svelte index 97119ae..fb87a17 100644 --- a/src/lib/SimulationTable.svelte +++ b/src/lib/SimulationTable.svelte @@ -4,6 +4,7 @@ formatDate, type SimulationState, } from "./simulationState.svelte"; + import { formatTime } from "$lib/ts/utils"; let { state }: { state: SimulationState } = $props(); @@ -15,6 +16,8 @@ ID Sim Name Search Pattern + Search Time + Total Time Date Run Link @@ -27,6 +30,8 @@ {parseConfig(sim.config).pattern || "Unknown"} + {formatTime(sim.search_time || NaN)} + {formatTime(sim.total_time || NaN)} {formatDate(sim.created_at)} View Details diff --git a/src/lib/css/simulation.css b/src/lib/css/simulation.css index 08789ce..515b67f 100644 --- a/src/lib/css/simulation.css +++ b/src/lib/css/simulation.css @@ -3,34 +3,42 @@ background-color: #ffffff; color: #000000; } + h1 { font-size: 24px; } + h2 { font-size: 20px; margin-top: 30px; border-bottom: 1px solid #000; } + a { color: #0000ff; text-decoration: underline; } + a:visited { color: #800080; } + .back-link { display: inline-block; margin-bottom: 20px; } + .error { color: red; font-weight: bold; } + hr { border: 0; border-top: 1px solid #000; margin: 20px 0; } + .config-box, .hardware-box { margin-top: 20px; @@ -41,23 +49,52 @@ hr { box-sizing: border-box; overflow-x: auto; } + .hardware-box h3 { margin-top: 0; margin-bottom: 10px; text-decoration: underline; } + .hardware-box p { margin: 5px 0; } + pre { margin: 10px 0 0 0; white-space: pre-wrap; } + +.super-container { + display: flex; + flex-direction: column; + gap: 20px; + margin-top: 15px; +} + +.config-category { + padding: 15px; + background: #ffffff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.config-category h2 { + margin-top: 0; + margin-bottom: 15px; + font-size: 20px; + color: #222; + border-bottom: 2px solid #ccc; + padding-bottom: 5px; +} + .table-label { margin: 0 0 5px 0; font-size: 16px; text-decoration: underline; } + .tables-container { display: flex; flex-wrap: wrap; @@ -65,17 +102,20 @@ pre { align-items: flex-start; margin-top: 10px; } + .table-wrapper { flex: 1 1 300px; max-width: 100%; overflow-x: auto; } + .config-table { margin-top: 10px; width: 100%; border-collapse: collapse; background-color: #ffffff; } + .config-table th, .config-table td { border: 1px solid #000; @@ -83,18 +123,22 @@ pre { text-align: left; word-break: break-all; } + .config-table th { background-color: #e0e0e0; } + .header-container { display: flex; align-items: center; gap: 15px; margin-bottom: 20px; } + .header-container h1 { margin: 0; } + .edit-btn, .export-btn, .save-btn, @@ -108,22 +152,26 @@ pre { background-color: #e0e0e0; font-family: inherit; } + .edit-btn:hover:not(:disabled), .export-btn:hover:not(:disabled), .save-btn:hover:not(:disabled), .cancel-btn:hover:not(:disabled) { background-color: #d0d0d0; } + button:disabled { opacity: 0.5; cursor: not-allowed; } + .delete-btn { background-color: #ffcccc; color: #990000; border-color: #990000; margin-left: auto; } + .delete-btn:hover { background-color: #ff9999; } @@ -134,27 +182,32 @@ button:disabled { background-color: #f5f5f5; width: 100%; } + .edit-form h2 { margin-top: 0; border-bottom: none; } + .form-group { margin-bottom: 15px; display: flex; align-items: center; gap: 10px; } + .edit-form label { display: inline-block; width: 100px; font-weight: bold; } + .edit-form input[type="text"] { flex: 1; padding: 5px; border: 1px solid #000; font-family: inherit; } + .progress-bar-container { width: 100%; background-color: #ddd; @@ -163,11 +216,13 @@ button:disabled { position: relative; height: 25px; } + .progress-bar { height: 100%; background-color: #4caf50; transition: width 0.2s ease-in-out; } + .progress-text { position: absolute; width: 100%; @@ -178,11 +233,13 @@ button:disabled { font-weight: bold; color: #000; } + .form-group input[type="file"] { font-family: inherit; font-size: 14px; cursor: pointer; } + .form-group input[type="file"]::file-selector-button { padding: 6px 15px; font-size: 14px; @@ -193,9 +250,11 @@ button:disabled { font-family: inherit; margin-right: 10px; } + .form-group input[type="file"]::file-selector-button:hover { background-color: #d0d0d0; } + .form-actions { display: flex; gap: 10px; @@ -206,19 +265,23 @@ button:disabled { :global(.no-print) { display: none !important; } + :global(body) { background-color: #fff !important; color: #000 !important; padding: 0 !important; } + :global(*) { box-shadow: none !important; } + table { border: 1px solid #000; } + th, td { border: 1px solid #000; } -} +} \ No newline at end of file diff --git a/src/lib/simulationState.svelte.ts b/src/lib/simulationState.svelte.ts index eb91338..40021e0 100644 --- a/src/lib/simulationState.svelte.ts +++ b/src/lib/simulationState.svelte.ts @@ -3,6 +3,8 @@ export type Simulation = { name: string; created_at: string; config: string; + search_time: number | null; + total_time: number | null; }; export function parseConfig(config: string) { @@ -123,12 +125,11 @@ export class SimulationState { if (this.sortOrder === "newest") return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); else if (this.sortOrder === "oldest") return new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); else if (this.sortOrder === "fastest" || this.sortOrder === "slowest") { - let pA = parseConfig(a.config); - let pB = parseConfig(b.config); - let speedA = (pA.uavMax || 0) + (pA.ugvMax || 0); - let speedB = (pB.uavMax || 0) + (pB.ugvMax || 0); - if (this.sortOrder === "fastest") return speedB - speedA; - else return speedA - speedB; + let timeA = a.total_time ?? Infinity; + let timeB = b.total_time ?? Infinity; + + if (this.sortOrder === "fastest") return timeA - timeB; + else return timeB - timeA; } return 0; }); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a4ce3cf..aba26c0 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,6 +1,26 @@ @@ -15,8 +35,24 @@
-

Sim-Link for Gazebo Simulations

-

GMU RDC TEAM

+ + +
diff --git a/src/routes/simulation/[id]/+page.svelte b/src/routes/simulation/[id]/+page.svelte index fd9aef1..4b3c1ff 100644 --- a/src/routes/simulation/[id]/+page.svelte +++ b/src/routes/simulation/[id]/+page.svelte @@ -1,6 +1,6 @@