diff --git a/README.md b/README.md index c567f70..758576e 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,18 @@ Updates the numeric benchmarking metrics (`search_time` and `total_time`) for a - **Request Body:** JSON object containing floats: `{"search_time": 25.4, "total_time": 105.8}` - **Response:** JSON object `{"status": "success"}` +### `PUT /api/simulations/:id/hardware` +Updates the physical hardware strings tied directly to a specific simulation run to properly map system configurations to benchmark outcomes. +- **Request Body:** JSON object containing string fields (nullable): `{"cpu_info": "Intel i9", "gpu_info": "RTX 4090", "ram_info": "64GB DDR5"}` +- **Response:** JSON object `{"status": "success"}` + ### `PUT /api/simulations/:id/rename` Renames a specific simulation. Automatically updates the database and physically renames the matching directory path on the system filesystem to mirror structural links. - **Request Body:** JSON object containing the new underlying name: `{"name": "new_simulation_name"}` - **Response:** JSON object `{"status": "success", "new_name": "new_simulation_name"}` ### `POST /api/simulations/:id/upload` -Uploads a new media or data resource file directly into a completed simulation run. Supports both images (e.g., PNGs) and heavy video files (e.g., AVIs) out of the box with an initial multi-part allocation pool mapping of 500 MB. +Uploads a new media or data resource file directly into a completed simulation run. Supports both images (e.g., PNGs) and heavy video files (e.g., AVIs) out of the box with an initial multi-part memory bounds mapping of 32 MB (spilling to disk for larger files). - **Request Data:** `multipart/form-data` packet using the `file` keyword carrying the binary data blocks. - **Behavior:** The Go service validates the simulation ID exists, dynamically builds the memory pool, opens a stream writer caching the binary blob into the `../results/simulation_X/filename.ext` directory path, and records the resource in the primary SQLite instance. - **Response:** JSON object `{"status": "success", "filename": "example.png"}` diff --git a/server/db.go b/server/db.go index 5494ac4..8d195d6 100644 --- a/server/db.go +++ b/server/db.go @@ -34,6 +34,9 @@ func initDBConnection() { db.Exec("ALTER TABLE simulations ADD COLUMN config TEXT") db.Exec("ALTER TABLE simulations ADD COLUMN search_time REAL") db.Exec("ALTER TABLE simulations ADD COLUMN total_time REAL") + db.Exec("ALTER TABLE simulations ADD COLUMN cpu_info TEXT") + db.Exec("ALTER TABLE simulations ADD COLUMN gpu_info TEXT") + db.Exec("ALTER TABLE simulations ADD COLUMN ram_info TEXT") createResourcesTableSQL := `CREATE TABLE IF NOT EXISTS resources ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -65,7 +68,6 @@ func syncResults() { simName := entry.Name() simID := insertSimulation(simName) - // Read subfiles subFiles, err := os.ReadDir(filepath.Join(resultsDir, simName)) if err == nil { db.Exec("DELETE FROM resources WHERE simulation_id = ?", simID) diff --git a/server/routes/hardware.go b/server/routes/hardware.go new file mode 100644 index 0000000..a7d317f --- /dev/null +++ b/server/routes/hardware.go @@ -0,0 +1,34 @@ +package routes + +import ( + "encoding/json" + "log" + "net/http" +) + +func (rt *Router) UpdateSimulationHardware(w http.ResponseWriter, r *http.Request, idStr string) { + w.Header().Set("Access-Control-Allow-Origin", "*") + + var reqBody struct { + CPUInfo *string `json:"cpu_info"` + GPUInfo *string `json:"gpu_info"` + RAMInfo *string `json:"ram_info"` + } + + err := json.NewDecoder(r.Body).Decode(&reqBody) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + _, err = rt.DB.Exec("UPDATE simulations SET cpu_info = ?, gpu_info = ?, ram_info = ? WHERE id = ?", reqBody.CPUInfo, reqBody.GPUInfo, reqBody.RAMInfo, idStr) + if err != nil { + log.Println("Update simulation hardware error:", err) + http.Error(w, "Failed to update", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} diff --git a/server/routes/models.go b/server/routes/models.go index c24dae1..2838a3c 100644 --- a/server/routes/models.go +++ b/server/routes/models.go @@ -9,4 +9,7 @@ type Simulation struct { SearchTime *float64 `json:"search_time"` TotalTime *float64 `json:"total_time"` TotalSizeBytes int64 `json:"total_size_bytes"` + CPUInfo *string `json:"cpu_info"` + GPUInfo *string `json:"gpu_info"` + RAMInfo *string `json:"ram_info"` } diff --git a/server/routes/resources.go b/server/routes/resources.go index ffd904b..5dd194e 100644 --- a/server/routes/resources.go +++ b/server/routes/resources.go @@ -24,7 +24,7 @@ func (rt *Router) UploadSimulationResource(w http.ResponseWriter, r *http.Reques return } - err = r.ParseMultipartForm(32 << 20) // 32 MB max memory bounds, rest spills to disk + err = r.ParseMultipartForm(32 << 20) if err != nil { http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest) return diff --git a/server/routes/router.go b/server/routes/router.go index bfb5770..7a8514f 100644 --- a/server/routes/router.go +++ b/server/routes/router.go @@ -72,6 +72,10 @@ func (rt *Router) handleSimulationsPath(w http.ResponseWriter, r *http.Request) rt.RenameSimulation(w, r, idStr) return } + if r.Method == "PUT" && action == "hardware" { + rt.UpdateSimulationHardware(w, r, idStr) + return + } if r.Method == "POST" && action == "upload" { rt.UploadSimulationResource(w, r, idStr) return diff --git a/server/routes/simulations.go b/server/routes/simulations.go index 2944672..aa0cf91 100644 --- a/server/routes/simulations.go +++ b/server/routes/simulations.go @@ -57,17 +57,7 @@ func (rt *Router) CreateSimulation(w http.ResponseWriter, r *http.Request) { return } - var existingID int - var existingName string - err = rt.DB.QueryRow("SELECT id, name FROM simulations WHERE config = ?", configStr).Scan(&existingID, &existingName) - if err == nil { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "id": existingID, - "name": existingName, - }) - return - } + var count int rt.DB.QueryRow("SELECT COUNT(*) FROM simulations").Scan(&count) @@ -104,7 +94,7 @@ func (rt *Router) CreateSimulation(w http.ResponseWriter, r *http.Request) { func (rt *Router) GetSimulations(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") - rows, err := rt.DB.Query("SELECT id, name, config, created_at, search_time, total_time FROM simulations") + rows, err := rt.DB.Query("SELECT id, name, config, created_at, search_time, total_time, cpu_info, gpu_info, ram_info FROM simulations") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -114,7 +104,7 @@ func (rt *Router) GetSimulations(w http.ResponseWriter, r *http.Request) { var sims []Simulation for rows.Next() { var s Simulation - err = rows.Scan(&s.ID, &s.Name, &s.Config, &s.CreatedAt, &s.SearchTime, &s.TotalTime) + err = rows.Scan(&s.ID, &s.Name, &s.Config, &s.CreatedAt, &s.SearchTime, &s.TotalTime, &s.CPUInfo, &s.GPUInfo, &s.RAMInfo) if err != nil { log.Println("Row scan error:", err) continue @@ -134,7 +124,7 @@ func (rt *Router) GetSimulationDetails(w http.ResponseWriter, r *http.Request, i w.Header().Set("Access-Control-Allow-Origin", "*") var s Simulation - err := rt.DB.QueryRow("SELECT id, name, config, created_at, search_time, total_time FROM simulations WHERE id = ?", idStr).Scan(&s.ID, &s.Name, &s.Config, &s.CreatedAt, &s.SearchTime, &s.TotalTime) + err := rt.DB.QueryRow("SELECT id, name, config, created_at, search_time, total_time, cpu_info, gpu_info, ram_info FROM simulations WHERE id = ?", idStr).Scan(&s.ID, &s.Name, &s.Config, &s.CreatedAt, &s.SearchTime, &s.TotalTime, &s.CPUInfo, &s.GPUInfo, &s.RAMInfo) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Simulation not found", http.StatusNotFound) @@ -188,7 +178,6 @@ func (rt *Router) RenameSimulation(w http.ResponseWriter, r *http.Request, idStr return } - // Make sure the new name doesn't already exist var tempID int err = rt.DB.QueryRow("SELECT id FROM simulations WHERE name = ?", newName).Scan(&tempID) if err == nil { @@ -196,7 +185,6 @@ func (rt *Router) RenameSimulation(w http.ResponseWriter, r *http.Request, idStr return } - // Rename the physical storage folder to maintain link parity oldDirPath := filepath.Join("../results", oldName) newDirPath := filepath.Join("../results", newName) if _, statErr := os.Stat(oldDirPath); statErr == nil { @@ -207,13 +195,11 @@ func (rt *Router) RenameSimulation(w http.ResponseWriter, r *http.Request, idStr return } } else { - // Just ensure target dir exists os.MkdirAll(newDirPath, 0755) } _, err = rt.DB.Exec("UPDATE simulations SET name = ? WHERE id = ?", newName, idStr) if err != nil { - // Attempt revert if DB dies halfway os.Rename(newDirPath, oldDirPath) log.Println("Update simulation name error:", err) http.Error(w, "Failed to update database", http.StatusInternalServerError) @@ -238,7 +224,6 @@ func (rt *Router) DeleteSimulation(w http.ResponseWriter, r *http.Request, idStr return } - // Attempt dropping resource map hooks first _, err = rt.DB.Exec("DELETE FROM resources WHERE simulation_id = ?", idStr) if err != nil { log.Println("Warning: Error deleting resources map from DB:", err) @@ -251,7 +236,6 @@ func (rt *Router) DeleteSimulation(w http.ResponseWriter, r *http.Request, idStr return } - // Physically destroy data payload mapped onto it dirPath := filepath.Join("../results", name) if _, statErr := os.Stat(dirPath); statErr == nil { err = os.RemoveAll(dirPath) diff --git a/src/lib/css/simulation.css b/src/lib/css/simulation.css new file mode 100644 index 0000000..08789ce --- /dev/null +++ b/src/lib/css/simulation.css @@ -0,0 +1,224 @@ +:global(body) { + font-family: "JetBrains Mono", Courier, monospace; + 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; + border: 1px solid #000; + padding: 10px; + background-color: #f8f8f8; + max-width: 100%; + 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; +} +.table-label { + margin: 0 0 5px 0; + font-size: 16px; + text-decoration: underline; +} +.tables-container { + display: flex; + flex-wrap: wrap; + gap: 20px; + 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; + padding: 8px 10px; + 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, +.cancel-btn, +.delete-btn { + padding: 6px 15px; + font-size: 14px; + font-weight: bold; + cursor: pointer; + border: 1px solid #000; + 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; +} + +.edit-form { + border: 1px dashed #000; + padding: 15px; + 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; + border: 1px solid #000; + margin-top: 10px; + position: relative; + height: 25px; +} +.progress-bar { + height: 100%; + background-color: #4caf50; + transition: width 0.2s ease-in-out; +} +.progress-text { + position: absolute; + width: 100%; + text-align: center; + top: 0; + left: 0; + line-height: 25px; + 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; + font-weight: bold; + cursor: pointer; + border: 1px solid #000; + background-color: #e0e0e0; + font-family: inherit; + margin-right: 10px; +} +.form-group input[type="file"]::file-selector-button:hover { + background-color: #d0d0d0; +} +.form-actions { + display: flex; + gap: 10px; + margin-top: 10px; +} + +@media print { + :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; + } +} diff --git a/src/lib/simulationState.svelte.ts b/src/lib/simulationState.svelte.ts index 6130b62..eb91338 100644 --- a/src/lib/simulationState.svelte.ts +++ b/src/lib/simulationState.svelte.ts @@ -76,7 +76,6 @@ export class SimulationState { constructor() { $effect(() => { - // Reset to page 1 whenever filters change if ( this.searchQuery !== undefined || this.sortOrder !== undefined || @@ -90,7 +89,6 @@ export class SimulationState { this.filterUgvMax !== undefined || this.filterPattern !== undefined ) { - // Tracking any mutation will trigger this effect let triggerVar = this.searchQuery + this.sortOrder + this.filterDateFrom + this.filterDateTo + this.filterAltMin + this.filterAltMax + this.filterUavMin + this.filterUavMax + this.filterUgvMin + this.filterUgvMax + this.filterPattern; this.currentPage = 1; } @@ -104,7 +102,6 @@ export class SimulationState { let simDate = new Date(sim.created_at).getTime(); if (this.filterDateFrom && simDate < new Date(this.filterDateFrom).getTime()) return false; - // Provide +86400000 ms (1 day) to include the "To" date fully. if (this.filterDateTo && simDate > new Date(this.filterDateTo).getTime() + 86400000) return false; let p = parseConfig(sim.config); diff --git a/src/lib/ts/test.ts b/src/lib/ts/test.ts new file mode 100644 index 0000000..a0e4eca --- /dev/null +++ b/src/lib/ts/test.ts @@ -0,0 +1 @@ +export let count = 0; diff --git a/src/lib/ts/utils.ts b/src/lib/ts/utils.ts new file mode 100644 index 0000000..a71e599 --- /dev/null +++ b/src/lib/ts/utils.ts @@ -0,0 +1,74 @@ +export type SimulationDetails = { + id: number; + name: string; + created_at: string; + resources: string[]; + config: string | null; + search_time: number | null; + total_time: number | null; + total_size_bytes: number; + cpu_info: string | null; + gpu_info: string | null; + ram_info: string | null; +}; + +export function formatBytes(bytes: number) { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +} + +export function flattenJSON( + obj: any, + prefix = "", +): { key: string; value: string }[] { + let result: { key: string; value: string }[] = []; + if (!obj) return result; + for (let key in obj) { + if (typeof obj[key] === "object" && obj[key] !== null) { + if (Array.isArray(obj[key])) { + result.push({ + key: prefix + key, + value: JSON.stringify(obj[key]), + }); + } else { + result = result.concat( + flattenJSON(obj[key], prefix + key + "."), + ); + } + } else { + result.push({ key: prefix + key, value: String(obj[key]) }); + } + } + return result; +} + +export function isImage(fname: string) { + return ( + fname.toLowerCase().endsWith(".png") || + fname.toLowerCase().endsWith(".jpg") || + fname.toLowerCase().endsWith(".jpeg") + ); +} + +export function isVideo(fname: string) { + return ( + fname.toLowerCase().endsWith(".mp4") || + fname.toLowerCase().endsWith(".avi") || + fname.toLowerCase().endsWith(".webm") || + fname.toLowerCase().endsWith(".mkv") + ); +} + +export function getResourceUrl(simName: string, resourceName: string) { + return `/results/${simName}/${resourceName}`; +} + +export function formatTime(s: number) { + if (s == null || isNaN(s)) return "N/A"; + const mins = Math.floor(s / 60); + const secs = (s % 60).toFixed(2).padStart(5, "0"); + return `${mins.toString().padStart(2, "0")}:${secs}`; +} diff --git a/src/routes/simulation/[id]/+page.svelte b/src/routes/simulation/[id]/+page.svelte index 7da3a07..fd9aef1 100644 --- a/src/routes/simulation/[id]/+page.svelte +++ b/src/routes/simulation/[id]/+page.svelte @@ -1,4 +1,6 @@ -<< Back to Index +<< Back to Index {#if error}
Error: {error}
@@ -301,10 +290,16 @@Search Time: - {simulation.search_time.toFixed(2)}s | Total Time: - {simulation.total_time.toFixed(2)}s + {formatTime(simulation.search_time)} | Total Time: + {formatTime(simulation.total_time)}
{/if}@@ -368,6 +401,15 @@ {formatBytes(simulation.total_size_bytes || 0)}
+ {#if simulation.cpu_info || simulation.gpu_info || simulation.ram_info} +CPU: {simulation.cpu_info || "N/A"}
+GPU: {simulation.gpu_info || "N/A"}
+RAM: {simulation.ram_info || "N/A"}
+ID: {simulation.id}
++ Search Pattern: + + {parsedConfig?.find((c) => c.key === "search.spiral.max_legs") + ? "Spiral" + : parsedConfig?.find( + (c) => c.key === "search.lawnmower.width", + ) + ? "Lawnmower" + : parsedConfig?.find( + (c) => c.key === "search.levy.max_steps", + ) + ? "Levy" + : "Unknown"} + +
+Date Code: {formatDate(simulation.created_at)}
+ + {#if simulation.search_time !== null && simulation.total_time !== null} ++ Search Time: + {formatTime(simulation.search_time)} | + Total Time: + {formatTime(simulation.total_time)} +
+ {/if} + + {#if simulation.cpu_info || simulation.gpu_info || simulation.ram_info} +CPU: {simulation.cpu_info || "N/A"}
+GPU: {simulation.gpu_info || "N/A"}
+RAM: {simulation.ram_info || "N/A"}
+| {item.key} | {item.value} |
| {item.key} | {item.value} |
| {item.key} | {item.value} |
{image}
+No images found in this simulation.
+ {/if} +Loading printable report...
+{/if} + + diff --git a/src/routes/simulation/[id]/print/+page.ts b/src/routes/simulation/[id]/print/+page.ts new file mode 100644 index 0000000..6a69b49 --- /dev/null +++ b/src/routes/simulation/[id]/print/+page.ts @@ -0,0 +1,5 @@ +export function load({ params }) { + return { + id: params.id + }; +}