commit 4c24b141feb42afffd40775afe58fc6cd092072b Author: default Date: Sun Feb 22 00:50:51 2026 +0000 Inital Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af8ece --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +#bun +bun.lock +.vscode +results/ +*.db \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8aabb43 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Stage 1: Build SvelteKit static frontend +FROM oven/bun:alpine AS frontend-builder +WORKDIR /app +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile || bun install +COPY . . +RUN bun run build + +# Stage 2: Build Golang server +FROM golang:1.23-alpine AS backend-builder +# Enable CGO for SQLite3 compiling +RUN apk add --no-cache gcc musl-dev +WORKDIR /app +COPY server/go.mod ./ +# Copy go.sum if it exists +COPY server/go.su[m] ./ +RUN go mod download +COPY server/ ./ +RUN CGO_ENABLED=1 GOOS=linux go build -o sim-link-server main.go + +# Stage 3: Final lightweight runtime image +FROM alpine:latest +WORKDIR /app + +# Install sqlite-libs and tzdata for go-sqlite3 runtime +RUN apk add --no-cache sqlite-libs tzdata + +# Copy built frontend from Stage 1 +COPY --from=frontend-builder /app/build ./build + +# Copy built Golang binary to system bin so we can run it from any directory +COPY --from=backend-builder /app/sim-link-server /usr/local/bin/sim-link-server + +# Change to a server sub-directory so relative paths like "../build" +# and "../results" still resolve properly just as they did in dev. +WORKDIR /app/server + +EXPOSE 8080 + +CMD ["sim-link-server"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f44379 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Sim-Link + +Sim-Link is a web application designed to take the results of Gazebo simulations, save them, rank the best-performing simulations, and display the detailed results in a clean, easy-to-understand format. + +The project is intentionally styled without colors to mimic a legacy system, prioritizing highly readable, pure-text tables over modern visual flair. + +## Tech Stack +- **Frontend:** SvelteKit (built into a completely Static Single Page Application using Bun). +- **Backend:** Golang. +- **Database:** SQLite3. +- **Containerization:** Docker & Docker Compose. + +## Running with Docker (Recommended) + +The easiest way to run Sim-Link is by using the provided `docker-compose` environment. This encapsulates the build steps for both the SvelteKit frontend and the Go backend. + +```bash +# Build and start the container in the background +docker compose up -d --build +``` +*Note: The `results/` folder and `server/` folder are native volume mounts in Docker Compose. This ensures your simulation logs update live, and the `sim-link.db` SQLite file persists on your host machine.* + +You can then access the interface at: [http://localhost:8080](http://localhost:8080) + +## Running Locally Manually + +If you prefer to run the system directly on your OS without Docker, you will need `Bun` and `Go` (with CGO enabled) installed. + +### 1. Build the Frontend +Navigate to the root directory and use Bun to render the static HTML/JS payload. +```bash +bun install +bun run build +``` +*(This produces a `build/` directory that the Go backend expects).* + +### 2. Run the Backend Server +Because the backend uses `go-sqlite3`, it requires CGO to compile. Navigate completely into the `server` directory so the relative paths for serving the `../build` and `../results` folders remain accurate. +```bash +cd server +go run . +``` + +The server will automatically scan the `../results` folder on startup, index the simulations into the SQLite database, and host the web interface at [http://localhost:8080](http://localhost:8080). + +## API Endpoints + +The Go backend provides a simple REST API to interact with the simulations: + +### `GET /api/simulations` +Returns a JSON array of all indexed simulations, including their ID, name, created date, and configuration string. + +### `POST /api/simulations/create` +Creates a new simulation entry. +- **Request Body:** Raw text containing the YAML configuration block. +- **Behavior:** Checks if an identical config exists. If so, it returns the existing simulation ID/Name for overwriting. If not, it allocates a new `simulation_X` incrementally, inserts it into the database, builds the file directory in `../results`, and returns the new ID/Name. +- **Response:** JSON object `{"id": , "name": }` + +### `GET /api/simulations/:id` +Provides detailed JSON metadata for a specific simulation ID, including its name, raw text config, creation timestamp, and a dynamically fetched array of all the internal resources (images, logs, videos) located within its folder. + +### `PUT /api/simulations/:id/time` +Updates the numeric benchmarking metrics (`search_time` and `total_time`) for a specific simulation run. +- **Request Body:** JSON object containing floats: `{"search_time": 25.4, "total_time": 105.8}` +- **Response:** JSON object `{"status": "success"}` + +### `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. +- **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"}` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..decc1e2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: sim-link-server + ports: + - "8080:8080" + volumes: + # Mount results explicitly to view simulation outcomes and process new files. + - ./results:/app/results + # Mount the DB directory to retain data. Because our Docker WORKDIR is /app/server, + # the sim-link.db will be saved in /app/server/sim-link.db, matching this volume mapping. + - ./server:/app/server + restart: unless-stopped + environment: + - PORT=8080 diff --git a/package.json b/package.json new file mode 100644 index 0000000..d1e5629 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "sim-link", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev --host", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.1", + "@sveltejs/kit": "^2.53.0", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@tailwindcss/forms": "^0.5.11", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.2.0", + "svelte": "^5.53.2", + "svelte-check": "^4.4.3", + "tailwindcss": "^4.2.0", + "typescript": "^5.9.3", + "vite": "^7.3.1" + }, + "dependencies": { + "@sveltejs/adapter-static": "^3.0.10" + } +} diff --git a/server/api.go b/server/api.go new file mode 100644 index 0000000..e905d81 --- /dev/null +++ b/server/api.go @@ -0,0 +1,265 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" +) + +func createSimulation(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", "POST") + w.WriteHeader(http.StatusOK) + return + } + + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + configStr := string(bodyBytes) + + if configStr == "" { + http.Error(w, "Config cannot be empty", http.StatusBadRequest) + return + } + + // Check if this config already exists + var existingID int + var existingName string + err = db.QueryRow("SELECT id, name FROM simulations WHERE config = ?", configStr).Scan(&existingID, &existingName) + if err == nil { + // Found exact config, return the existing run info + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": existingID, + "name": existingName, + }) + return + } + + var count int + db.QueryRow("SELECT COUNT(*) FROM simulations").Scan(&count) + newName := fmt.Sprintf("simulation_%d", count+1) + + // ensure name doesn't exist + for { + var temp int + err := db.QueryRow("SELECT id FROM simulations WHERE name = ?", newName).Scan(&temp) + if err == sql.ErrNoRows { + break + } + count++ + newName = fmt.Sprintf("simulation_%d", count+1) + } + + res, err := db.Exec("INSERT INTO simulations(name, config) VALUES(?, ?)", newName, configStr) + if err != nil { + log.Println("Insert simulation error:", err) + http.Error(w, "Failed to create", http.StatusInternalServerError) + return + } + newID, _ := res.LastInsertId() + + // Ensure the directory exists + resultsDir := filepath.Join("../results", newName) + os.MkdirAll(resultsDir, 0755) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": newID, + "name": newName, + }) +} + +func getSimulations(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", "GET") + w.WriteHeader(http.StatusOK) + return + } + + rows, err := db.Query("SELECT id, name, config, created_at, search_time, total_time FROM simulations") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + var sims []Simulation + for rows.Next() { + var s Simulation + err = rows.Scan(&s.ID, &s.Name, &s.Config, &s.CreatedAt, &s.SearchTime, &s.TotalTime) + if err != nil { + log.Println("Row scan error:", err) + continue + } + sims = append(sims, s) + } + + if sims == nil { + sims = []Simulation{} + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(sims) +} + +func getSimulationDetails(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, OPTIONS") + w.WriteHeader(http.StatusOK) + return + } + + pathParts := strings.Split(strings.Trim(r.URL.Path[len("/api/simulations/"):], "/"), "/") + idStr := pathParts[0] + + if idStr == "" || idStr == "create" { + http.Error(w, "Not found", http.StatusNotFound) + return + } + + if r.Method == "PUT" && len(pathParts) > 1 && pathParts[1] == "time" { + updateSimulationTime(w, r, idStr) + return + } + + if r.Method == "POST" && len(pathParts) > 1 && pathParts[1] == "upload" { + uploadSimulationResource(w, r, idStr) + return + } + + if r.Method != "GET" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var s Simulation + err := 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) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Simulation not found", http.StatusNotFound) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + rows, err := db.Query("SELECT filename FROM resources WHERE simulation_id = ?", s.ID) + if err == nil { + defer rows.Close() + for rows.Next() { + var fname string + if err := rows.Scan(&fname); err == nil { + s.Resources = append(s.Resources, fname) + } + } + } + if s.Resources == nil { + s.Resources = []string{} + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(s) +} + +func updateSimulationTime(w http.ResponseWriter, r *http.Request, idStr string) { + var reqBody struct { + SearchTime *float64 `json:"search_time"` + TotalTime *float64 `json:"total_time"` + } + + err := json.NewDecoder(r.Body).Decode(&reqBody) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + if reqBody.SearchTime == nil || reqBody.TotalTime == nil { + http.Error(w, "Missing search_time or total_time", http.StatusBadRequest) + return + } + + _, err = db.Exec("UPDATE simulations SET search_time = ?, total_time = ? WHERE id = ?", *reqBody.SearchTime, *reqBody.TotalTime, idStr) + if err != nil { + log.Println("Update simulation 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"}) +} + +func uploadSimulationResource(w http.ResponseWriter, r *http.Request, idStr string) { + var simName string + err := db.QueryRow("SELECT name FROM simulations WHERE id = ?", idStr).Scan(&simName) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Simulation not found", http.StatusNotFound) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + err = r.ParseMultipartForm(500 << 20) // 500 MB max memory/file bounds + if err != nil { + http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest) + return + } + + file, handler, err := r.FormFile("file") + if err != nil { + http.Error(w, "Error retrieving the file", http.StatusBadRequest) + return + } + defer file.Close() + + destPath := filepath.Join("../results", simName, handler.Filename) + dst, err := os.Create(destPath) + if err != nil { + http.Error(w, "Error creating destination file: "+err.Error(), http.StatusInternalServerError) + return + } + defer dst.Close() + + _, err = io.Copy(dst, file) + if err != nil { + http.Error(w, "Error saving file: "+err.Error(), http.StatusInternalServerError) + return + } + + var resID int + err = db.QueryRow("SELECT id FROM resources WHERE simulation_id = ? AND filename = ?", idStr, handler.Filename).Scan(&resID) + if err == sql.ErrNoRows { + _, err = db.Exec("INSERT INTO resources(simulation_id, filename) VALUES(?, ?)", idStr, handler.Filename) + if err != nil { + log.Println("Error inserting uploaded resource into db:", err) + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": "success", + "filename": handler.Filename, + }) +} diff --git a/server/db.go b/server/db.go new file mode 100644 index 0000000..1ee422b --- /dev/null +++ b/server/db.go @@ -0,0 +1,118 @@ +package main + +import ( + "database/sql" + "log" + "os" + "path/filepath" + + _ "github.com/mattn/go-sqlite3" +) + +type Simulation struct { + ID int `json:"id"` + Name string `json:"name"` + Resources []string `json:"resources"` + CreatedAt string `json:"created_at"` + Config *string `json:"config"` + SearchTime *float64 `json:"search_time"` + TotalTime *float64 `json:"total_time"` +} + +var db *sql.DB + +func initDBConnection() { + var err error + db, err = sql.Open("sqlite3", "./sim-link.db") + if err != nil { + log.Fatal(err) + } + + createTableSQL := `CREATE TABLE IF NOT EXISTS simulations ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT UNIQUE, + "config" TEXT, + "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP + );` + _, err = db.Exec(createTableSQL) + if err != nil { + log.Fatal(err) + } + + db.Exec("ALTER TABLE simulations ADD COLUMN config TEXT") // Ignore error if exists + db.Exec("ALTER TABLE simulations ADD COLUMN search_time REAL") // Ignore error if exists + db.Exec("ALTER TABLE simulations ADD COLUMN total_time REAL") // Ignore error if exists + + createResourcesTableSQL := `CREATE TABLE IF NOT EXISTS resources ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "simulation_id" INTEGER, + "filename" TEXT, + FOREIGN KEY(simulation_id) REFERENCES simulations(id) + );` + _, err = db.Exec(createResourcesTableSQL) + if err != nil { + log.Fatal(err) + } +} + +func syncResults() { + resultsDir := "../results" + if _, err := os.Stat(resultsDir); os.IsNotExist(err) { + log.Println("Results directory does not exist, skipping sync") + return + } + + entries, err := os.ReadDir(resultsDir) + if err != nil { + log.Println("Error reading results directory:", err) + return + } + + for _, entry := range entries { + if entry.IsDir() { + simName := entry.Name() + simID := insertSimulation(simName) + + // Read subfiles + subFiles, err := os.ReadDir(filepath.Join(resultsDir, simName)) + if err == nil { + // Clear old resources for this simulation + db.Exec("DELETE FROM resources WHERE simulation_id = ?", simID) + + for _, sf := range subFiles { + if !sf.IsDir() { + insertResource(simID, sf.Name()) + } + } + } + } + } + log.Println("Database synchronized with results directory") +} + +func insertSimulation(name string) int64 { + var id int64 + err := db.QueryRow("SELECT id FROM simulations WHERE name = ?", name).Scan(&id) + if err == sql.ErrNoRows { + res, err := db.Exec("INSERT INTO simulations(name) VALUES(?)", name) + if err != nil { + log.Println("Error inserting simulation:", err) + return 0 + } + id, err = res.LastInsertId() + if err != nil { + return 0 + } + } else if err != nil { + log.Println("Error selecting simulation:", err) + return 0 + } + return id +} + +func insertResource(simID int64, filename string) { + _, err := db.Exec("INSERT INTO resources(simulation_id, filename) VALUES(?, ?)", simID, filename) + if err != nil { + log.Println("Error inserting resource:", err) + } +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..cc6b3b2 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,5 @@ +module sim-link-server + +go 1.22.2 + +require github.com/mattn/go-sqlite3 v1.14.34 // indirect diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..684933a --- /dev/null +++ b/server/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..5257754 --- /dev/null +++ b/server/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strings" + + _ "github.com/mattn/go-sqlite3" +) + +func main() { + initDBConnection() + defer db.Close() + + syncResults() + + // Handle API routes + http.HandleFunc("/api/simulations", getSimulations) + http.HandleFunc("/api/simulations/create", createSimulation) + http.HandleFunc("/api/simulations/", getSimulationDetails) + + // Serve the static files from results directory + resultsDir := "../results" + fsResults := http.FileServer(http.Dir(resultsDir)) + http.Handle("/results/", http.StripPrefix("/results/", fsResults)) + + // Serve the static frontend with SPA fallback + frontendDir := "../build" + fsFrontend := http.FileServer(http.Dir(frontendDir)) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + path := filepath.Join(frontendDir, r.URL.Path) + _, err := os.Stat(path) + if os.IsNotExist(err) { + http.ServeFile(w, r, filepath.Join(frontendDir, "index.html")) + return + } + fsFrontend.ServeHTTP(w, r) + }) + + fmt.Println("Server listening on port 5173") + + // Wrap the default ServeMux with our logging middleware + loggedMux := loggingMiddleware(http.DefaultServeMux) + log.Fatal(http.ListenAndServe("0.0.0.0:5173", loggedMux)) +} + +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/api/") { + log.Printf("[%s] %s %s", r.RemoteAddr, r.Method, r.URL.Path) + } + next.ServeHTTP(w, r) + }) +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/lib/MediaItem.svelte b/src/lib/MediaItem.svelte new file mode 100644 index 0000000..41acb6b --- /dev/null +++ b/src/lib/MediaItem.svelte @@ -0,0 +1,76 @@ + + +
+
{resourceName}
+ {#if isImage(resourceName)} + {resourceName} + {:else if isVideo(resourceName)} + + {:else} + + {/if} +
+ + diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..2244c6c --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,49 @@ + + +

SYSTEM ERROR

+ +
+
Error Code: {$page.status}
+
+ Description: {$page.error?.message || "An unknown error occurred."} +
+ +

+ Please contact your system administrator or check the simulation logs. +

+
+ +<< Return to Main Menu + + diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..3c70bc0 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,21 @@ + + + + +
+ {@render children()} +
+ + \ No newline at end of file diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..08c5b63 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,3 @@ +export const ssr = false; +export const prerender = false; +export const trailingSlash = "always"; \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..9d53e89 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,471 @@ + + +

Sim-Link Simulation Results

+ +{#if error} +

Error: {error}

+{/if} + +
+ + +
+ + + + + + + + + + + {#each paginatedSimulations as sim} + + + + + + + {/each} + +
IDSim NameDate RunLink
{sim.id}{sim.name}{sim.created_at}View Details
+ + +
+
+ + diff --git a/src/routes/layout.css b/src/routes/layout.css new file mode 100644 index 0000000..39e5223 --- /dev/null +++ b/src/routes/layout.css @@ -0,0 +1,14 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/forms'; +@plugin '@tailwindcss/typography'; + +@font-face { + font-family: 'JetBrains Mono'; + src: url('/fonts/JetBrainsMono-SemiBold.ttf') format('truetype'); + font-weight: 600; + font-style: normal; +} + +body { + font-family: 'JetBrains Mono', 'Courier New', Courier, monospace; +} diff --git a/src/routes/simulation/[id]/+page.svelte b/src/routes/simulation/[id]/+page.svelte new file mode 100644 index 0000000..c191bd5 --- /dev/null +++ b/src/routes/simulation/[id]/+page.svelte @@ -0,0 +1,115 @@ + + +<< Back to Index + +{#if error} +

Error: {error}

+{:else if !simulation} +

Loading...

+{:else} +

Simulation Detail: {simulation.name}

+

ID: {simulation.id}

+

Date Code: {simulation.created_at}

+ + {#if simulation.search_time !== null && simulation.total_time !== null} +

+ Search Time: + {simulation.search_time}s | Total Time: + {simulation.total_time}s +

+ {/if} + + {#if simulation.config} +
+ Configuration: +
{simulation.config}
+
+ {/if} + +
+ +

Media & Results

+ {#if simulation.resources && simulation.resources.length > 0} +
+ {#each simulation.resources as res} + + {/each} +
+ {:else} +

No resources found for this simulation.

+ {/if} +{/if} + + diff --git a/src/routes/simulation/[id]/+page.ts b/src/routes/simulation/[id]/+page.ts new file mode 100644 index 0000000..6a69b49 --- /dev/null +++ b/src/routes/simulation/[id]/+page.ts @@ -0,0 +1,5 @@ +export function load({ params }) { + return { + id: params.id + }; +} diff --git a/static/fonts/JetBrainsMono-SemiBold.ttf b/static/fonts/JetBrainsMono-SemiBold.ttf new file mode 100644 index 0000000..a70e69b Binary files /dev/null and b/static/fonts/JetBrainsMono-SemiBold.ttf differ diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..c56f7eb --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,16 @@ +import adapter from '@sveltejs/adapter-static'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + strict: true + }) + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..56f40c7 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,5 @@ +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });