From 74ef68daad9e05473f74e31e9a665e46485e79bb Mon Sep 17 00:00:00 2001 From: igor Date: Mon, 23 Feb 2026 19:19:39 +0100 Subject: [PATCH] added interactive setup of config.json --- AGENTS.md | 73 +++++++++++++++++++++++++++++ dbPrompt.go | 131 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f3611a2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,73 @@ +# AGENTS.md + +Tento subor je prakticky navod pre AI agentov a contributorov v projekte `dbPrompt`. + +## 1. Ciel projektu + +`dbPrompt` je jednoducha web aplikacia na spustanie SQL dotazov nad MySQL s historiou vykonanych dopytov. + +## 2. Struktura projektu + +- `dbPrompt.go`: backend server (Go, `net/http`), API endpointy aj praca s historiou. +- `index.html`: cely frontend (HTML + CSS + JavaScript v jednom subore). +- `static/`: staticke assety (napr. logo). +- `history/`: JSON subory s historiou dopytov. +- `config.json`: DB konfiguracia pre lokalny beh. +- `build.bat`: build pre Windows/Linux binarky do `dist/`. + +## 3. Lokalny beh a overenie + +1. Skontroluj `config.json` (lokalne MySQL udaje). +2. Spust aplikaciu: + - `go run dbPrompt.go` +3. Otvor: + - `http://localhost:8080` +4. Pred odovzdanim zmien spusti: + - `go test ./...` + +Poznamka: V projekte aktualne nie su unit testy, ale `go test ./...` overi aspon build. + +## 4. API kontrakt (nezlomit bez poziadavky) + +- `GET /history/`: vracia historiu ako JSON pole, zoradene od najstarsich. +- `POST /query`: prijme `{ id, query }`, vykona SQL, vrati `QueryResult`. +- `DELETE /history/{id}`: zmaze jeden zaznam historie. + +`QueryResult` klucove polia: +- `id`, `query`, `timestamp` +- `duration` (ms) +- `error` alebo vysledok (`columns`, `rows`) alebo `rowsAffected` + +Ak menis schema odpovede, zosulad backend aj frontend naraz. + +## 5. Pravidla pre zmeny + +- Zachovaj jednoduchu architekturu (single-file backend + single-file frontend), pokial nie je explicitna poziadavka na refactor. +- Preferuj male, cielene zmeny bez "cleanupu navyse". +- Pri zmenach endpointov vzdy uprav aj frontend render a nacitanie historie. +- Pri praci s cestami zachovaj sanitizaciu ID (`filepath.Base`) kvoli directory traversal. +- Nezavadzaj nove zavislosti bez dovodu. + +## 6. Bezpecnost a data + +- Nikdy necommituj realne prihlasovacie udaje do DB. +- `config.json` je lokalny runtime subor; citlive hodnoty maskuj v ukazkach. +- SQL sa vykonava priamo zo vstupu uzivatela. Ak by si pridaval funkcionalitu, zvaz oddelenie read-only rezimu alebo aspon jasne varovanie v UI. + +## 7. Frontend poznamky + +- UI je event-delegated cez `mainContainer.addEventListener('click', ...)`. +- Historicke bloky maju tlacidlo `Delete`; "new-query-block" ho nema, kym query neuspesne/neuspesne neprebehne. +- Collapsible tabulky pouzivaju `.collapsible-header` a `.collapsible-content`. + +Pri zmenach vizualu zachovaj funkcnost: +- run query +- delete history item +- load history po starte +- collapse/expand individualne aj globalne + +## 8. Definition of done pre kazdu upravu + +1. Zmena funguje funkcne v UI/API. +2. `go test ./...` prejde. +3. Nemas neplanovane zmeny mimo scope zadania. diff --git a/dbPrompt.go b/dbPrompt.go index ae8dc13..1378df2 100644 --- a/dbPrompt.go +++ b/dbPrompt.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "database/sql" "embed" "encoding/json" @@ -52,17 +53,9 @@ var db *sql.DB var historyDir = "./history" func main() { - // Load configuration - file, err := os.Open("config.json") + config, err := loadOrCreateConfig("config.json") if err != nil { - log.Fatalf("Failed to open config file: %v", err) - } - defer file.Close() - - var config Config - decoder := json.NewDecoder(file) - if err := decoder.Decode(&config); err != nil { - log.Fatalf("Failed to decode config: %v", err) + log.Fatalf("Failed to load configuration: %v", err) } // Connect to the database @@ -99,6 +92,124 @@ func main() { } } +func loadOrCreateConfig(path string) (Config, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + fmt.Printf("Configuration file %q was not found.\n", path) + fmt.Println("Please provide MySQL connection details to create it.") + + cfg, promptErr := promptConfigFromConsole() + if promptErr != nil { + return Config{}, promptErr + } + + data, marshalErr := json.MarshalIndent(cfg, "", " ") + if marshalErr != nil { + return Config{}, fmt.Errorf("failed to serialize config: %w", marshalErr) + } + + if writeErr := os.WriteFile(path, append(data, '\n'), 0600); writeErr != nil { + return Config{}, fmt.Errorf("failed to create %s: %w", path, writeErr) + } + + fmt.Printf("Created %s successfully.\n", path) + return cfg, nil + } else if err != nil { + return Config{}, fmt.Errorf("failed to check config file: %w", err) + } + + file, err := os.Open(path) + if err != nil { + return Config{}, fmt.Errorf("failed to open config file: %w", err) + } + defer file.Close() + + var cfg Config + decoder := json.NewDecoder(file) + if err := decoder.Decode(&cfg); err != nil { + return Config{}, fmt.Errorf("failed to decode config: %w", err) + } + + return cfg, nil +} + +func promptConfigFromConsole() (Config, error) { + reader := bufio.NewReader(os.Stdin) + + hostname, err := promptRequired(reader, "hostname") + if err != nil { + return Config{}, err + } + + port, err := promptPort(reader, 3306) + if err != nil { + return Config{}, err + } + + username, err := promptRequired(reader, "username") + if err != nil { + return Config{}, err + } + + password, err := promptRequired(reader, "password") + if err != nil { + return Config{}, err + } + + database, err := promptRequired(reader, "database") + if err != nil { + return Config{}, err + } + + return Config{ + Username: username, + Password: password, + Hostname: hostname, + Port: port, + Database: database, + }, nil +} + +func promptRequired(reader *bufio.Reader, field string) (string, error) { + for { + fmt.Printf("%s: ", field) + value, err := reader.ReadString('\n') + if err != nil { + return "", fmt.Errorf("failed to read %s: %w", field, err) + } + + value = strings.TrimSpace(value) + if value == "" { + fmt.Printf("%s is required.\n", field) + continue + } + + return value, nil + } +} + +func promptPort(reader *bufio.Reader, defaultPort int) (int, error) { + for { + fmt.Printf("port [%d]: ", defaultPort) + value, err := reader.ReadString('\n') + if err != nil { + return 0, fmt.Errorf("failed to read port: %w", err) + } + + value = strings.TrimSpace(value) + if value == "" { + return defaultPort, nil + } + + port, atoiErr := strconv.Atoi(value) + if atoiErr != nil || port <= 0 || port > 65535 { + fmt.Println("Port must be a number between 1 and 65535.") + continue + } + + return port, nil + } +} + func serveIndex(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "index.html") }