diff --git a/crud/.server/api/board.go b/crud/.server/api/board.go index 321f29a..b6d354f 100644 --- a/crud/.server/api/board.go +++ b/crud/.server/api/board.go @@ -2,9 +2,9 @@ package api import ( "context" - "crud/sqlite" "fmt" "html/template" + "crud/sqlite" "net/http" "strconv" ) diff --git a/crud/.show-it b/crud/.show-it index e06118b..7803143 100644 --- a/crud/.show-it +++ b/crud/.show-it @@ -2,7 +2,8 @@ clear cp .user.db user.db ls -l echo "The CRUD Example" +echo "the user data is taken from https://jsonplaceholder.typicode.com/users" ---PAUSE--- -micro index.html board-content.html sample.go +micro index.html sample.go board-content.html xdg-open http://localhost:8080 ./.crud diff --git a/crud/users.json b/crud/users.json new file mode 100644 index 0000000..82a0056 --- /dev/null +++ b/crud/users.json @@ -0,0 +1,232 @@ +[ + { + "id": 1, + "name": "Leanne Graham", + "username": "Bret", + "email": "Sincere@april.biz", + "address": { + "street": "Kulas Light", + "suite": "Apt. 556", + "city": "Gwenborough", + "zipcode": "92998-3874", + "geo": { + "lat": "-37.3159", + "lng": "81.1496" + } + }, + "phone": "1-770-736-8031 x56442", + "website": "hildegard.org", + "company": { + "name": "Romaguera-Crona", + "catchPhrase": "Multi-layered client-server neural-net", + "bs": "harness real-time e-markets" + } + }, + { + "id": 2, + "name": "Ervin Howell", + "username": "Antonette", + "email": "Shanna@melissa.tv", + "address": { + "street": "Victor Plains", + "suite": "Suite 879", + "city": "Wisokyburgh", + "zipcode": "90566-7771", + "geo": { + "lat": "-43.9509", + "lng": "-34.4618" + } + }, + "phone": "010-692-6593 x09125", + "website": "anastasia.net", + "company": { + "name": "Deckow-Crist", + "catchPhrase": "Proactive didactic contingency", + "bs": "synergize scalable supply-chains" + } + }, + { + "id": 3, + "name": "Clementine Bauch", + "username": "Samantha", + "email": "Nathan@yesenia.net", + "address": { + "street": "Douglas Extension", + "suite": "Suite 847", + "city": "McKenziehaven", + "zipcode": "59590-4157", + "geo": { + "lat": "-68.6102", + "lng": "-47.0653" + } + }, + "phone": "1-463-123-4447", + "website": "ramiro.info", + "company": { + "name": "Romaguera-Jacobson", + "catchPhrase": "Face to face bifurcated interface", + "bs": "e-enable strategic applications" + } + }, + { + "id": 4, + "name": "Patricia Lebsack", + "username": "Karianne", + "email": "Julianne.OConner@kory.org", + "address": { + "street": "Hoeger Mall", + "suite": "Apt. 692", + "city": "South Elvis", + "zipcode": "53919-4257", + "geo": { + "lat": "29.4572", + "lng": "-164.2990" + } + }, + "phone": "493-170-9623 x156", + "website": "kale.biz", + "company": { + "name": "Robel-Corkery", + "catchPhrase": "Multi-tiered zero tolerance productivity", + "bs": "transition cutting-edge web services" + } + }, + { + "id": 5, + "name": "Chelsey Dietrich", + "username": "Kamren", + "email": "Lucio_Hettinger@annie.ca", + "address": { + "street": "Skiles Walks", + "suite": "Suite 351", + "city": "Roscoeview", + "zipcode": "33263", + "geo": { + "lat": "-31.8129", + "lng": "62.5342" + } + }, + "phone": "(254)954-1289", + "website": "demarco.info", + "company": { + "name": "Keebler LLC", + "catchPhrase": "User-centric fault-tolerant solution", + "bs": "revolutionize end-to-end systems" + } + }, + { + "id": 6, + "name": "Mrs. Dennis Schulist", + "username": "Leopoldo_Corkery", + "email": "Karley_Dach@jasper.info", + "address": { + "street": "Norberto Crossing", + "suite": "Apt. 950", + "city": "South Christy", + "zipcode": "23505-1337", + "geo": { + "lat": "-71.4197", + "lng": "71.7478" + } + }, + "phone": "1-477-935-8478 x6430", + "website": "ola.org", + "company": { + "name": "Considine-Lockman", + "catchPhrase": "Synchronised bottom-line interface", + "bs": "e-enable innovative applications" + } + }, + { + "id": 7, + "name": "Kurtis Weissnat", + "username": "Elwyn.Skiles", + "email": "Telly.Hoeger@billy.biz", + "address": { + "street": "Rex Trail", + "suite": "Suite 280", + "city": "Howemouth", + "zipcode": "58804-1099", + "geo": { + "lat": "24.8918", + "lng": "21.8984" + } + }, + "phone": "210.067.6132", + "website": "elvis.io", + "company": { + "name": "Johns Group", + "catchPhrase": "Configurable multimedia task-force", + "bs": "generate enterprise e-tailers" + } + }, + { + "id": 8, + "name": "Nicholas Runolfsdottir V", + "username": "Maxime_Nienow", + "email": "Sherwood@rosamond.me", + "address": { + "street": "Ellsworth Summit", + "suite": "Suite 729", + "city": "Aliyaview", + "zipcode": "45169", + "geo": { + "lat": "-14.3990", + "lng": "-120.7677" + } + }, + "phone": "586.493.6943 x140", + "website": "jacynthe.com", + "company": { + "name": "Abernathy Group", + "catchPhrase": "Implemented secondary concept", + "bs": "e-enable extensible e-tailers" + } + }, + { + "id": 9, + "name": "Glenna Reichert", + "username": "Delphine", + "email": "Chaim_McDermott@dana.io", + "address": { + "street": "Dayna Park", + "suite": "Suite 449", + "city": "Bartholomebury", + "zipcode": "76495-3109", + "geo": { + "lat": "24.6463", + "lng": "-168.8889" + } + }, + "phone": "(775)976-6794 x41206", + "website": "conrad.com", + "company": { + "name": "Yost and Sons", + "catchPhrase": "Switchable contextually-based project", + "bs": "aggregate real-time technologies" + } + }, + { + "id": 10, + "name": "Clementina DuBuque", + "username": "Moriah.Stanton", + "email": "Rey.Padberg@karina.biz", + "address": { + "street": "Kattie Turnpike", + "suite": "Suite 198", + "city": "Lebsackbury", + "zipcode": "31428-2261", + "geo": { + "lat": "-38.2386", + "lng": "57.2232" + } + }, + "phone": "024-648-3804", + "website": "ambrose.net", + "company": { + "name": "Hoeger LLC", + "catchPhrase": "Centralized empowering task-force", + "bs": "target end-to-end models" + } + } +] diff --git a/lcars/.log b/lcars/.log deleted file mode 100644 index 9a5aea1..0000000 --- a/lcars/.log +++ /dev/null @@ -1,3 +0,0 @@ -2025/09/30 11:18:25 Failed to create internal StateDB: SQL logic error: near ")": syntax error (1) -2025/09/30 11:20:30 Failed to create internal StateDB: SQL logic error: near ")": syntax error (1) - client_max_body_size 100m; diff --git a/lcars/embed/create_state_db.sql b/lcars/lcars_v0/embed/create_state_db.sql similarity index 100% rename from lcars/embed/create_state_db.sql rename to lcars/lcars_v0/embed/create_state_db.sql diff --git a/lcars/embed/crew.json b/lcars/lcars_v0/embed/crew.json similarity index 100% rename from lcars/embed/crew.json rename to lcars/lcars_v0/embed/crew.json diff --git a/lcars/embed/messages.json b/lcars/lcars_v0/embed/messages.json similarity index 100% rename from lcars/embed/messages.json rename to lcars/lcars_v0/embed/messages.json diff --git a/lcars/experiments.go b/lcars/lcars_v0/experiments.go similarity index 100% rename from lcars/experiments.go rename to lcars/lcars_v0/experiments.go diff --git a/lcars/frontend/assets/Antonio-Bold.woff b/lcars/lcars_v0/frontend/assets/Antonio-Bold.woff similarity index 100% rename from lcars/frontend/assets/Antonio-Bold.woff rename to lcars/lcars_v0/frontend/assets/Antonio-Bold.woff diff --git a/lcars/frontend/assets/Antonio-Bold.woff2 b/lcars/lcars_v0/frontend/assets/Antonio-Bold.woff2 similarity index 100% rename from lcars/frontend/assets/Antonio-Bold.woff2 rename to lcars/lcars_v0/frontend/assets/Antonio-Bold.woff2 diff --git a/lcars/frontend/assets/Antonio-Regular.woff b/lcars/lcars_v0/frontend/assets/Antonio-Regular.woff similarity index 100% rename from lcars/frontend/assets/Antonio-Regular.woff rename to lcars/lcars_v0/frontend/assets/Antonio-Regular.woff diff --git a/lcars/frontend/assets/Antonio-Regular.woff2 b/lcars/lcars_v0/frontend/assets/Antonio-Regular.woff2 similarity index 100% rename from lcars/frontend/assets/Antonio-Regular.woff2 rename to lcars/lcars_v0/frontend/assets/Antonio-Regular.woff2 diff --git a/lcars/frontend/assets/beep1.mp3 b/lcars/lcars_v0/frontend/assets/beep1.mp3 similarity index 100% rename from lcars/frontend/assets/beep1.mp3 rename to lcars/lcars_v0/frontend/assets/beep1.mp3 diff --git a/lcars/frontend/assets/beep2.mp3 b/lcars/lcars_v0/frontend/assets/beep2.mp3 similarity index 100% rename from lcars/frontend/assets/beep2.mp3 rename to lcars/lcars_v0/frontend/assets/beep2.mp3 diff --git a/lcars/frontend/assets/beep3.mp3 b/lcars/lcars_v0/frontend/assets/beep3.mp3 similarity index 100% rename from lcars/frontend/assets/beep3.mp3 rename to lcars/lcars_v0/frontend/assets/beep3.mp3 diff --git a/lcars/frontend/assets/beep4.mp3 b/lcars/lcars_v0/frontend/assets/beep4.mp3 similarity index 100% rename from lcars/frontend/assets/beep4.mp3 rename to lcars/lcars_v0/frontend/assets/beep4.mp3 diff --git a/lcars/frontend/assets/classic.css b/lcars/lcars_v0/frontend/assets/classic.css similarity index 100% rename from lcars/frontend/assets/classic.css rename to lcars/lcars_v0/frontend/assets/classic.css diff --git a/lcars/frontend/assets/lcars.js b/lcars/lcars_v0/frontend/assets/lcars.js similarity index 100% rename from lcars/frontend/assets/lcars.js rename to lcars/lcars_v0/frontend/assets/lcars.js diff --git a/lcars/frontend/assets/lower-decks-padd.css b/lcars/lcars_v0/frontend/assets/lower-decks-padd.css similarity index 100% rename from lcars/frontend/assets/lower-decks-padd.css rename to lcars/lcars_v0/frontend/assets/lower-decks-padd.css diff --git a/lcars/frontend/assets/lower-decks.css b/lcars/lcars_v0/frontend/assets/lower-decks.css similarity index 100% rename from lcars/frontend/assets/lower-decks.css rename to lcars/lcars_v0/frontend/assets/lower-decks.css diff --git a/lcars/frontend/assets/nemesis-blue.css b/lcars/lcars_v0/frontend/assets/nemesis-blue.css similarity index 100% rename from lcars/frontend/assets/nemesis-blue.css rename to lcars/lcars_v0/frontend/assets/nemesis-blue.css diff --git a/lcars/frontend/index copy 2.html b/lcars/lcars_v0/frontend/index copy 2.html similarity index 100% rename from lcars/frontend/index copy 2.html rename to lcars/lcars_v0/frontend/index copy 2.html diff --git a/lcars/frontend/index copy.html b/lcars/lcars_v0/frontend/index copy.html similarity index 100% rename from lcars/frontend/index copy.html rename to lcars/lcars_v0/frontend/index copy.html diff --git a/lcars/frontend/index.html b/lcars/lcars_v0/frontend/index.html similarity index 100% rename from lcars/frontend/index.html rename to lcars/lcars_v0/frontend/index.html diff --git a/lcars/go.mod b/lcars/lcars_v0/go.mod similarity index 100% rename from lcars/go.mod rename to lcars/lcars_v0/go.mod diff --git a/lcars/go.sum b/lcars/lcars_v0/go.sum similarity index 100% rename from lcars/go.sum rename to lcars/lcars_v0/go.sum diff --git a/lcars/interval/interval.go b/lcars/lcars_v0/interval/interval.go similarity index 100% rename from lcars/interval/interval.go rename to lcars/lcars_v0/interval/interval.go diff --git a/lcars/main.go b/lcars/lcars_v0/main.go similarity index 100% rename from lcars/main.go rename to lcars/lcars_v0/main.go diff --git a/lcars/server/server.go b/lcars/lcars_v0/server/server.go similarity index 100% rename from lcars/server/server.go rename to lcars/lcars_v0/server/server.go diff --git a/lcars/server/taskengine.go b/lcars/lcars_v0/server/taskengine.go similarity index 100% rename from lcars/server/taskengine.go rename to lcars/lcars_v0/server/taskengine.go diff --git a/lcars/sqlite/database.go b/lcars/lcars_v0/sqlite/database.go similarity index 100% rename from lcars/sqlite/database.go rename to lcars/lcars_v0/sqlite/database.go diff --git a/lcars/sqlite/description.md b/lcars/lcars_v0/sqlite/description.md similarity index 100% rename from lcars/sqlite/description.md rename to lcars/lcars_v0/sqlite/description.md diff --git a/lcars_v1/embed/create_state_db.sql b/lcars/lcars_v1/embed/create_state_db.sql similarity index 100% rename from lcars_v1/embed/create_state_db.sql rename to lcars/lcars_v1/embed/create_state_db.sql diff --git a/lcars_v1/embed/crew.json b/lcars/lcars_v1/embed/crew.json similarity index 100% rename from lcars_v1/embed/crew.json rename to lcars/lcars_v1/embed/crew.json diff --git a/lcars_v1/embed/messages.json b/lcars/lcars_v1/embed/messages.json similarity index 100% rename from lcars_v1/embed/messages.json rename to lcars/lcars_v1/embed/messages.json diff --git a/lcars_v1/experiments.go b/lcars/lcars_v1/experiments.go similarity index 100% rename from lcars_v1/experiments.go rename to lcars/lcars_v1/experiments.go diff --git a/lcars_v1/frontend/assets/Antonio-Bold.woff b/lcars/lcars_v1/frontend/assets/Antonio-Bold.woff similarity index 100% rename from lcars_v1/frontend/assets/Antonio-Bold.woff rename to lcars/lcars_v1/frontend/assets/Antonio-Bold.woff diff --git a/lcars_v1/frontend/assets/Antonio-Bold.woff2 b/lcars/lcars_v1/frontend/assets/Antonio-Bold.woff2 similarity index 100% rename from lcars_v1/frontend/assets/Antonio-Bold.woff2 rename to lcars/lcars_v1/frontend/assets/Antonio-Bold.woff2 diff --git a/lcars_v1/frontend/assets/Antonio-Regular.woff b/lcars/lcars_v1/frontend/assets/Antonio-Regular.woff similarity index 100% rename from lcars_v1/frontend/assets/Antonio-Regular.woff rename to lcars/lcars_v1/frontend/assets/Antonio-Regular.woff diff --git a/lcars_v1/frontend/assets/Antonio-Regular.woff2 b/lcars/lcars_v1/frontend/assets/Antonio-Regular.woff2 similarity index 100% rename from lcars_v1/frontend/assets/Antonio-Regular.woff2 rename to lcars/lcars_v1/frontend/assets/Antonio-Regular.woff2 diff --git a/lcars_v1/frontend/assets/alert.png b/lcars/lcars_v1/frontend/assets/alert.png similarity index 100% rename from lcars_v1/frontend/assets/alert.png rename to lcars/lcars_v1/frontend/assets/alert.png diff --git a/lcars_v1/frontend/assets/beep1.mp3 b/lcars/lcars_v1/frontend/assets/beep1.mp3 similarity index 100% rename from lcars_v1/frontend/assets/beep1.mp3 rename to lcars/lcars_v1/frontend/assets/beep1.mp3 diff --git a/lcars_v1/frontend/assets/beep2.mp3 b/lcars/lcars_v1/frontend/assets/beep2.mp3 similarity index 100% rename from lcars_v1/frontend/assets/beep2.mp3 rename to lcars/lcars_v1/frontend/assets/beep2.mp3 diff --git a/lcars_v1/frontend/assets/beep3.mp3 b/lcars/lcars_v1/frontend/assets/beep3.mp3 similarity index 100% rename from lcars_v1/frontend/assets/beep3.mp3 rename to lcars/lcars_v1/frontend/assets/beep3.mp3 diff --git a/lcars_v1/frontend/assets/beep4.mp3 b/lcars/lcars_v1/frontend/assets/beep4.mp3 similarity index 100% rename from lcars_v1/frontend/assets/beep4.mp3 rename to lcars/lcars_v1/frontend/assets/beep4.mp3 diff --git a/lcars_v1/frontend/assets/classic.css b/lcars/lcars_v1/frontend/assets/classic.css similarity index 100% rename from lcars_v1/frontend/assets/classic.css rename to lcars/lcars_v1/frontend/assets/classic.css diff --git a/lcars_v1/frontend/assets/lcars.js b/lcars/lcars_v1/frontend/assets/lcars.js similarity index 100% rename from lcars_v1/frontend/assets/lcars.js rename to lcars/lcars_v1/frontend/assets/lcars.js diff --git a/lcars_v1/frontend/assets/lower-decks-padd.css b/lcars/lcars_v1/frontend/assets/lower-decks-padd.css similarity index 100% rename from lcars_v1/frontend/assets/lower-decks-padd.css rename to lcars/lcars_v1/frontend/assets/lower-decks-padd.css diff --git a/lcars_v1/frontend/assets/lower-decks.css b/lcars/lcars_v1/frontend/assets/lower-decks.css similarity index 100% rename from lcars_v1/frontend/assets/lower-decks.css rename to lcars/lcars_v1/frontend/assets/lower-decks.css diff --git a/lcars_v1/frontend/assets/nemesis-blue.css b/lcars/lcars_v1/frontend/assets/nemesis-blue.css similarity index 100% rename from lcars_v1/frontend/assets/nemesis-blue.css rename to lcars/lcars_v1/frontend/assets/nemesis-blue.css diff --git a/lcars_v1/frontend/crew/index.html b/lcars/lcars_v1/frontend/crew/index.html similarity index 100% rename from lcars_v1/frontend/crew/index.html rename to lcars/lcars_v1/frontend/crew/index.html diff --git a/lcars_v1/frontend/index.html b/lcars/lcars_v1/frontend/index.html similarity index 100% rename from lcars_v1/frontend/index.html rename to lcars/lcars_v1/frontend/index.html diff --git a/lcars_v1/frontend/tinyserver.go b/lcars/lcars_v1/frontend/tinyserver.go similarity index 100% rename from lcars_v1/frontend/tinyserver.go rename to lcars/lcars_v1/frontend/tinyserver.go diff --git a/lcars_v1/go.mod b/lcars/lcars_v1/go.mod similarity index 100% rename from lcars_v1/go.mod rename to lcars/lcars_v1/go.mod diff --git a/lcars_v1/go.sum b/lcars/lcars_v1/go.sum similarity index 100% rename from lcars_v1/go.sum rename to lcars/lcars_v1/go.sum diff --git a/lcars_v1/interval/interval.go b/lcars/lcars_v1/interval/interval.go similarity index 100% rename from lcars_v1/interval/interval.go rename to lcars/lcars_v1/interval/interval.go diff --git a/lcars_v1/main.go b/lcars/lcars_v1/main.go similarity index 100% rename from lcars_v1/main.go rename to lcars/lcars_v1/main.go diff --git a/lcars_v1/notes.txt b/lcars/lcars_v1/notes.txt similarity index 100% rename from lcars_v1/notes.txt rename to lcars/lcars_v1/notes.txt diff --git a/lcars_v1/server/server.go b/lcars/lcars_v1/server/server.go similarity index 100% rename from lcars_v1/server/server.go rename to lcars/lcars_v1/server/server.go diff --git a/lcars_v1/server/taskengine.go b/lcars/lcars_v1/server/taskengine.go similarity index 100% rename from lcars_v1/server/taskengine.go rename to lcars/lcars_v1/server/taskengine.go diff --git a/lcars_v1/sqlite/database.go b/lcars/lcars_v1/sqlite/database.go similarity index 100% rename from lcars_v1/sqlite/database.go rename to lcars/lcars_v1/sqlite/database.go diff --git a/lcars_v2/embed/create_state_db.sql b/lcars/lcars_v2/embed/create_state_db.sql similarity index 100% rename from lcars_v2/embed/create_state_db.sql rename to lcars/lcars_v2/embed/create_state_db.sql diff --git a/lcars_v2/embed/crew.json b/lcars/lcars_v2/embed/crew.json similarity index 100% rename from lcars_v2/embed/crew.json rename to lcars/lcars_v2/embed/crew.json diff --git a/lcars_v2/embed/messages.json b/lcars/lcars_v2/embed/messages.json similarity index 100% rename from lcars_v2/embed/messages.json rename to lcars/lcars_v2/embed/messages.json diff --git a/lcars_v2/eventbus/eventbus.go b/lcars/lcars_v2/eventbus/eventbus.go similarity index 100% rename from lcars_v2/eventbus/eventbus.go rename to lcars/lcars_v2/eventbus/eventbus.go diff --git a/lcars_v2/experiments.go b/lcars/lcars_v2/experiments.go similarity index 100% rename from lcars_v2/experiments.go rename to lcars/lcars_v2/experiments.go diff --git a/lcars_v2/frontend/assets/Antonio-Bold.woff b/lcars/lcars_v2/frontend/assets/Antonio-Bold.woff similarity index 100% rename from lcars_v2/frontend/assets/Antonio-Bold.woff rename to lcars/lcars_v2/frontend/assets/Antonio-Bold.woff diff --git a/lcars_v2/frontend/assets/Antonio-Bold.woff2 b/lcars/lcars_v2/frontend/assets/Antonio-Bold.woff2 similarity index 100% rename from lcars_v2/frontend/assets/Antonio-Bold.woff2 rename to lcars/lcars_v2/frontend/assets/Antonio-Bold.woff2 diff --git a/lcars_v2/frontend/assets/Antonio-Regular.woff b/lcars/lcars_v2/frontend/assets/Antonio-Regular.woff similarity index 100% rename from lcars_v2/frontend/assets/Antonio-Regular.woff rename to lcars/lcars_v2/frontend/assets/Antonio-Regular.woff diff --git a/lcars_v2/frontend/assets/Antonio-Regular.woff2 b/lcars/lcars_v2/frontend/assets/Antonio-Regular.woff2 similarity index 100% rename from lcars_v2/frontend/assets/Antonio-Regular.woff2 rename to lcars/lcars_v2/frontend/assets/Antonio-Regular.woff2 diff --git a/lcars_v2/frontend/assets/alert.png b/lcars/lcars_v2/frontend/assets/alert.png similarity index 100% rename from lcars_v2/frontend/assets/alert.png rename to lcars/lcars_v2/frontend/assets/alert.png diff --git a/lcars_v2/frontend/assets/beep1.mp3 b/lcars/lcars_v2/frontend/assets/beep1.mp3 similarity index 100% rename from lcars_v2/frontend/assets/beep1.mp3 rename to lcars/lcars_v2/frontend/assets/beep1.mp3 diff --git a/lcars_v2/frontend/assets/beep2.mp3 b/lcars/lcars_v2/frontend/assets/beep2.mp3 similarity index 100% rename from lcars_v2/frontend/assets/beep2.mp3 rename to lcars/lcars_v2/frontend/assets/beep2.mp3 diff --git a/lcars_v2/frontend/assets/beep3.mp3 b/lcars/lcars_v2/frontend/assets/beep3.mp3 similarity index 100% rename from lcars_v2/frontend/assets/beep3.mp3 rename to lcars/lcars_v2/frontend/assets/beep3.mp3 diff --git a/lcars_v2/frontend/assets/beep4.mp3 b/lcars/lcars_v2/frontend/assets/beep4.mp3 similarity index 100% rename from lcars_v2/frontend/assets/beep4.mp3 rename to lcars/lcars_v2/frontend/assets/beep4.mp3 diff --git a/lcars_v2/frontend/assets/classic.css b/lcars/lcars_v2/frontend/assets/classic.css similarity index 100% rename from lcars_v2/frontend/assets/classic.css rename to lcars/lcars_v2/frontend/assets/classic.css diff --git a/lcars_v2/frontend/assets/lcars.js b/lcars/lcars_v2/frontend/assets/lcars.js similarity index 100% rename from lcars_v2/frontend/assets/lcars.js rename to lcars/lcars_v2/frontend/assets/lcars.js diff --git a/lcars_v2/frontend/assets/lower-decks-padd.css b/lcars/lcars_v2/frontend/assets/lower-decks-padd.css similarity index 100% rename from lcars_v2/frontend/assets/lower-decks-padd.css rename to lcars/lcars_v2/frontend/assets/lower-decks-padd.css diff --git a/lcars_v2/frontend/assets/lower-decks.css b/lcars/lcars_v2/frontend/assets/lower-decks.css similarity index 100% rename from lcars_v2/frontend/assets/lower-decks.css rename to lcars/lcars_v2/frontend/assets/lower-decks.css diff --git a/lcars_v2/frontend/assets/nemesis-blue.css b/lcars/lcars_v2/frontend/assets/nemesis-blue.css similarity index 100% rename from lcars_v2/frontend/assets/nemesis-blue.css rename to lcars/lcars_v2/frontend/assets/nemesis-blue.css diff --git a/lcars_v2/frontend/crew/index.html b/lcars/lcars_v2/frontend/crew/index.html similarity index 100% rename from lcars_v2/frontend/crew/index.html rename to lcars/lcars_v2/frontend/crew/index.html diff --git a/lcars_v2/frontend/index.html b/lcars/lcars_v2/frontend/index.html similarity index 100% rename from lcars_v2/frontend/index.html rename to lcars/lcars_v2/frontend/index.html diff --git a/lcars_v2/frontend/tinyserver.go b/lcars/lcars_v2/frontend/tinyserver.go similarity index 100% rename from lcars_v2/frontend/tinyserver.go rename to lcars/lcars_v2/frontend/tinyserver.go diff --git a/lcars_v2/go.mod b/lcars/lcars_v2/go.mod similarity index 100% rename from lcars_v2/go.mod rename to lcars/lcars_v2/go.mod diff --git a/lcars_v2/go.sum b/lcars/lcars_v2/go.sum similarity index 100% rename from lcars_v2/go.sum rename to lcars/lcars_v2/go.sum diff --git a/lcars_v2/interval/interval.go b/lcars/lcars_v2/interval/interval.go similarity index 100% rename from lcars_v2/interval/interval.go rename to lcars/lcars_v2/interval/interval.go diff --git a/lcars_v2/main.go b/lcars/lcars_v2/main.go similarity index 100% rename from lcars_v2/main.go rename to lcars/lcars_v2/main.go diff --git a/lcars_v2/notes.txt b/lcars/lcars_v2/notes.txt similarity index 100% rename from lcars_v2/notes.txt rename to lcars/lcars_v2/notes.txt diff --git a/lcars_v2/server/server.go b/lcars/lcars_v2/server/server.go similarity index 100% rename from lcars_v2/server/server.go rename to lcars/lcars_v2/server/server.go diff --git a/lcars_v2/server/taskengine.go b/lcars/lcars_v2/server/taskengine.go similarity index 100% rename from lcars_v2/server/taskengine.go rename to lcars/lcars_v2/server/taskengine.go diff --git a/lcars_v2/sqlite/database.go b/lcars/lcars_v2/sqlite/database.go similarity index 100% rename from lcars_v2/sqlite/database.go rename to lcars/lcars_v2/sqlite/database.go diff --git a/lcars_v3/embed/create_state_db.sql b/lcars/lcars_v3/embed/create_state_db.sql similarity index 100% rename from lcars_v3/embed/create_state_db.sql rename to lcars/lcars_v3/embed/create_state_db.sql diff --git a/lcars_v3/embed/crew.json b/lcars/lcars_v3/embed/crew.json similarity index 100% rename from lcars_v3/embed/crew.json rename to lcars/lcars_v3/embed/crew.json diff --git a/lcars_v3/embed/messages.json b/lcars/lcars_v3/embed/messages.json similarity index 100% rename from lcars_v3/embed/messages.json rename to lcars/lcars_v3/embed/messages.json diff --git a/lcars_v3/eventbus/eventbus.go b/lcars/lcars_v3/eventbus/eventbus.go similarity index 100% rename from lcars_v3/eventbus/eventbus.go rename to lcars/lcars_v3/eventbus/eventbus.go diff --git a/lcars_v3/experiments.go b/lcars/lcars_v3/experiments.go similarity index 100% rename from lcars_v3/experiments.go rename to lcars/lcars_v3/experiments.go diff --git a/lcars_v3/frontend/assets/Antonio-Bold.woff b/lcars/lcars_v3/frontend/assets/Antonio-Bold.woff similarity index 100% rename from lcars_v3/frontend/assets/Antonio-Bold.woff rename to lcars/lcars_v3/frontend/assets/Antonio-Bold.woff diff --git a/lcars_v3/frontend/assets/Antonio-Bold.woff2 b/lcars/lcars_v3/frontend/assets/Antonio-Bold.woff2 similarity index 100% rename from lcars_v3/frontend/assets/Antonio-Bold.woff2 rename to lcars/lcars_v3/frontend/assets/Antonio-Bold.woff2 diff --git a/lcars_v3/frontend/assets/Antonio-Regular.woff b/lcars/lcars_v3/frontend/assets/Antonio-Regular.woff similarity index 100% rename from lcars_v3/frontend/assets/Antonio-Regular.woff rename to lcars/lcars_v3/frontend/assets/Antonio-Regular.woff diff --git a/lcars_v3/frontend/assets/Antonio-Regular.woff2 b/lcars/lcars_v3/frontend/assets/Antonio-Regular.woff2 similarity index 100% rename from lcars_v3/frontend/assets/Antonio-Regular.woff2 rename to lcars/lcars_v3/frontend/assets/Antonio-Regular.woff2 diff --git a/lcars_v3/frontend/assets/alert.png b/lcars/lcars_v3/frontend/assets/alert.png similarity index 100% rename from lcars_v3/frontend/assets/alert.png rename to lcars/lcars_v3/frontend/assets/alert.png diff --git a/lcars_v3/frontend/assets/beep1.mp3 b/lcars/lcars_v3/frontend/assets/beep1.mp3 similarity index 100% rename from lcars_v3/frontend/assets/beep1.mp3 rename to lcars/lcars_v3/frontend/assets/beep1.mp3 diff --git a/lcars_v3/frontend/assets/beep2.mp3 b/lcars/lcars_v3/frontend/assets/beep2.mp3 similarity index 100% rename from lcars_v3/frontend/assets/beep2.mp3 rename to lcars/lcars_v3/frontend/assets/beep2.mp3 diff --git a/lcars_v3/frontend/assets/beep3.mp3 b/lcars/lcars_v3/frontend/assets/beep3.mp3 similarity index 100% rename from lcars_v3/frontend/assets/beep3.mp3 rename to lcars/lcars_v3/frontend/assets/beep3.mp3 diff --git a/lcars_v3/frontend/assets/beep4.mp3 b/lcars/lcars_v3/frontend/assets/beep4.mp3 similarity index 100% rename from lcars_v3/frontend/assets/beep4.mp3 rename to lcars/lcars_v3/frontend/assets/beep4.mp3 diff --git a/lcars_v3/frontend/assets/classic.css b/lcars/lcars_v3/frontend/assets/classic.css similarity index 100% rename from lcars_v3/frontend/assets/classic.css rename to lcars/lcars_v3/frontend/assets/classic.css diff --git a/lcars_v3/frontend/assets/lcars.js b/lcars/lcars_v3/frontend/assets/lcars.js similarity index 100% rename from lcars_v3/frontend/assets/lcars.js rename to lcars/lcars_v3/frontend/assets/lcars.js diff --git a/lcars_v3/frontend/assets/lower-decks-padd.css b/lcars/lcars_v3/frontend/assets/lower-decks-padd.css similarity index 100% rename from lcars_v3/frontend/assets/lower-decks-padd.css rename to lcars/lcars_v3/frontend/assets/lower-decks-padd.css diff --git a/lcars_v3/frontend/assets/lower-decks.css b/lcars/lcars_v3/frontend/assets/lower-decks.css similarity index 100% rename from lcars_v3/frontend/assets/lower-decks.css rename to lcars/lcars_v3/frontend/assets/lower-decks.css diff --git a/lcars_v3/frontend/assets/nemesis-blue.css b/lcars/lcars_v3/frontend/assets/nemesis-blue.css similarity index 100% rename from lcars_v3/frontend/assets/nemesis-blue.css rename to lcars/lcars_v3/frontend/assets/nemesis-blue.css diff --git a/lcars_v3/frontend/crew/index.html b/lcars/lcars_v3/frontend/crew/index.html similarity index 100% rename from lcars_v3/frontend/crew/index.html rename to lcars/lcars_v3/frontend/crew/index.html diff --git a/lcars_v3/frontend/index.html b/lcars/lcars_v3/frontend/index.html similarity index 100% rename from lcars_v3/frontend/index.html rename to lcars/lcars_v3/frontend/index.html diff --git a/lcars_v3/frontend/tinyserver.go b/lcars/lcars_v3/frontend/tinyserver.go similarity index 100% rename from lcars_v3/frontend/tinyserver.go rename to lcars/lcars_v3/frontend/tinyserver.go diff --git a/lcars_v3/go.mod b/lcars/lcars_v3/go.mod similarity index 100% rename from lcars_v3/go.mod rename to lcars/lcars_v3/go.mod diff --git a/lcars_v3/go.sum b/lcars/lcars_v3/go.sum similarity index 100% rename from lcars_v3/go.sum rename to lcars/lcars_v3/go.sum diff --git a/lcars_v3/interval/interval.go b/lcars/lcars_v3/interval/interval.go similarity index 100% rename from lcars_v3/interval/interval.go rename to lcars/lcars_v3/interval/interval.go diff --git a/lcars_v3/main.go b/lcars/lcars_v3/main.go similarity index 100% rename from lcars_v3/main.go rename to lcars/lcars_v3/main.go diff --git a/lcars_v3/notes.txt b/lcars/lcars_v3/notes.txt similarity index 100% rename from lcars_v3/notes.txt rename to lcars/lcars_v3/notes.txt diff --git a/lcars_v3/server/server.go b/lcars/lcars_v3/server/server.go similarity index 100% rename from lcars_v3/server/server.go rename to lcars/lcars_v3/server/server.go diff --git a/lcars_v3/server/taskengine.go b/lcars/lcars_v3/server/taskengine.go similarity index 100% rename from lcars_v3/server/taskengine.go rename to lcars/lcars_v3/server/taskengine.go diff --git a/lcars_v3/sqlite/database.go b/lcars/lcars_v3/sqlite/database.go similarity index 100% rename from lcars_v3/sqlite/database.go rename to lcars/lcars_v3/sqlite/database.go diff --git a/lcars_v3/state/events.go b/lcars/lcars_v3/state/events.go similarity index 100% rename from lcars_v3/state/events.go rename to lcars/lcars_v3/state/events.go diff --git a/lcars_v4/embed/create_state_db.sql b/lcars/lcars_v4/embed/create_state_db.sql similarity index 100% rename from lcars_v4/embed/create_state_db.sql rename to lcars/lcars_v4/embed/create_state_db.sql diff --git a/lcars_v4/embed/crew.json b/lcars/lcars_v4/embed/crew.json similarity index 100% rename from lcars_v4/embed/crew.json rename to lcars/lcars_v4/embed/crew.json diff --git a/lcars_v4/embed/messages.json b/lcars/lcars_v4/embed/messages.json similarity index 100% rename from lcars_v4/embed/messages.json rename to lcars/lcars_v4/embed/messages.json diff --git a/lcars_v4/eventbus/eventbus.go b/lcars/lcars_v4/eventbus/eventbus.go similarity index 100% rename from lcars_v4/eventbus/eventbus.go rename to lcars/lcars_v4/eventbus/eventbus.go diff --git a/lcars_v4/experiments.go b/lcars/lcars_v4/experiments.go similarity index 100% rename from lcars_v4/experiments.go rename to lcars/lcars_v4/experiments.go diff --git a/lcars_v4/frontend/assets/Antonio-Bold.woff b/lcars/lcars_v4/frontend/assets/Antonio-Bold.woff similarity index 100% rename from lcars_v4/frontend/assets/Antonio-Bold.woff rename to lcars/lcars_v4/frontend/assets/Antonio-Bold.woff diff --git a/lcars_v4/frontend/assets/Antonio-Bold.woff2 b/lcars/lcars_v4/frontend/assets/Antonio-Bold.woff2 similarity index 100% rename from lcars_v4/frontend/assets/Antonio-Bold.woff2 rename to lcars/lcars_v4/frontend/assets/Antonio-Bold.woff2 diff --git a/lcars_v4/frontend/assets/Antonio-Regular.woff b/lcars/lcars_v4/frontend/assets/Antonio-Regular.woff similarity index 100% rename from lcars_v4/frontend/assets/Antonio-Regular.woff rename to lcars/lcars_v4/frontend/assets/Antonio-Regular.woff diff --git a/lcars_v4/frontend/assets/Antonio-Regular.woff2 b/lcars/lcars_v4/frontend/assets/Antonio-Regular.woff2 similarity index 100% rename from lcars_v4/frontend/assets/Antonio-Regular.woff2 rename to lcars/lcars_v4/frontend/assets/Antonio-Regular.woff2 diff --git a/lcars_v4/frontend/assets/alert.png b/lcars/lcars_v4/frontend/assets/alert.png similarity index 100% rename from lcars_v4/frontend/assets/alert.png rename to lcars/lcars_v4/frontend/assets/alert.png diff --git a/lcars_v4/frontend/assets/beep1.mp3 b/lcars/lcars_v4/frontend/assets/beep1.mp3 similarity index 100% rename from lcars_v4/frontend/assets/beep1.mp3 rename to lcars/lcars_v4/frontend/assets/beep1.mp3 diff --git a/lcars_v4/frontend/assets/beep2.mp3 b/lcars/lcars_v4/frontend/assets/beep2.mp3 similarity index 100% rename from lcars_v4/frontend/assets/beep2.mp3 rename to lcars/lcars_v4/frontend/assets/beep2.mp3 diff --git a/lcars_v4/frontend/assets/beep3.mp3 b/lcars/lcars_v4/frontend/assets/beep3.mp3 similarity index 100% rename from lcars_v4/frontend/assets/beep3.mp3 rename to lcars/lcars_v4/frontend/assets/beep3.mp3 diff --git a/lcars_v4/frontend/assets/beep4.mp3 b/lcars/lcars_v4/frontend/assets/beep4.mp3 similarity index 100% rename from lcars_v4/frontend/assets/beep4.mp3 rename to lcars/lcars_v4/frontend/assets/beep4.mp3 diff --git a/lcars_v4/frontend/assets/classic.css b/lcars/lcars_v4/frontend/assets/classic.css similarity index 100% rename from lcars_v4/frontend/assets/classic.css rename to lcars/lcars_v4/frontend/assets/classic.css diff --git a/lcars_v4/frontend/assets/lcars.js b/lcars/lcars_v4/frontend/assets/lcars.js similarity index 100% rename from lcars_v4/frontend/assets/lcars.js rename to lcars/lcars_v4/frontend/assets/lcars.js diff --git a/lcars_v4/frontend/assets/lower-decks-padd.css b/lcars/lcars_v4/frontend/assets/lower-decks-padd.css similarity index 100% rename from lcars_v4/frontend/assets/lower-decks-padd.css rename to lcars/lcars_v4/frontend/assets/lower-decks-padd.css diff --git a/lcars_v4/frontend/assets/lower-decks.css b/lcars/lcars_v4/frontend/assets/lower-decks.css similarity index 100% rename from lcars_v4/frontend/assets/lower-decks.css rename to lcars/lcars_v4/frontend/assets/lower-decks.css diff --git a/lcars_v4/frontend/assets/nemesis-blue.css b/lcars/lcars_v4/frontend/assets/nemesis-blue.css similarity index 100% rename from lcars_v4/frontend/assets/nemesis-blue.css rename to lcars/lcars_v4/frontend/assets/nemesis-blue.css diff --git a/lcars_v4/frontend/crew/index.html b/lcars/lcars_v4/frontend/crew/index.html similarity index 100% rename from lcars_v4/frontend/crew/index.html rename to lcars/lcars_v4/frontend/crew/index.html diff --git a/lcars_v4/frontend/index.html b/lcars/lcars_v4/frontend/index.html similarity index 100% rename from lcars_v4/frontend/index.html rename to lcars/lcars_v4/frontend/index.html diff --git a/lcars_v4/frontend/tinyserver.go b/lcars/lcars_v4/frontend/tinyserver.go similarity index 100% rename from lcars_v4/frontend/tinyserver.go rename to lcars/lcars_v4/frontend/tinyserver.go diff --git a/lcars_v4/go.mod b/lcars/lcars_v4/go.mod similarity index 100% rename from lcars_v4/go.mod rename to lcars/lcars_v4/go.mod diff --git a/lcars_v4/go.sum b/lcars/lcars_v4/go.sum similarity index 100% rename from lcars_v4/go.sum rename to lcars/lcars_v4/go.sum diff --git a/lcars_v4/interval/interval.go b/lcars/lcars_v4/interval/interval.go similarity index 100% rename from lcars_v4/interval/interval.go rename to lcars/lcars_v4/interval/interval.go diff --git a/lcars_v4/main.go b/lcars/lcars_v4/main.go similarity index 100% rename from lcars_v4/main.go rename to lcars/lcars_v4/main.go diff --git a/lcars_v4/notes.txt b/lcars/lcars_v4/notes.txt similarity index 100% rename from lcars_v4/notes.txt rename to lcars/lcars_v4/notes.txt diff --git a/lcars_v4/server/server.go b/lcars/lcars_v4/server/server.go similarity index 100% rename from lcars_v4/server/server.go rename to lcars/lcars_v4/server/server.go diff --git a/lcars_v4/server/taskengine.go b/lcars/lcars_v4/server/taskengine.go similarity index 100% rename from lcars_v4/server/taskengine.go rename to lcars/lcars_v4/server/taskengine.go diff --git a/lcars_v4/sqlite/database.go b/lcars/lcars_v4/sqlite/database.go similarity index 100% rename from lcars_v4/sqlite/database.go rename to lcars/lcars_v4/sqlite/database.go diff --git a/lcars_v4/state/events.go b/lcars/lcars_v4/state/events.go similarity index 100% rename from lcars_v4/state/events.go rename to lcars/lcars_v4/state/events.go diff --git a/lcars/lcars_v5/api/board.go b/lcars/lcars_v5/api/board.go new file mode 100644 index 0000000..68b0c3e --- /dev/null +++ b/lcars/lcars_v5/api/board.go @@ -0,0 +1,125 @@ +package api + +import ( + "context" + "fmt" + "html/template" + "ld/sqlite" + "net/http" + "strconv" +) + +const baseQuery = `SELECT u.id, u.name, u.username, u.email, u.phone, u.website, a.street, a.suite, a.zipcode, a.city, c.name as company, c.catch_phrase, c.bs + FROM user u + JOIN company c ON u.company_id = c.id + JOIN address a ON u.address_id = a.id + ` + +func Board(ctx context.Context, db *sqlite.Database, templ *template.Template) http.Handler { + return http.HandlerFunc( + + func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("Content-Type", "text/html") + + records, err := sqlite.NoRowsOk(db.ReadRecords(ctx, baseQuery+";")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Execute template with proper error handling + if err := templ.ExecuteTemplate(w, "board", sqlite.Record{"records": records}); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }, + ) +} + +func DeleteRecord(ctx context.Context, db *sqlite.Database, templ *template.Template) http.Handler { + // Implementation of DeleteRecord handler + return http.HandlerFunc( + + func(w http.ResponseWriter, r *http.Request) { + + id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + } + + // fmt.Println("DeleteRecord handler called ", id) + + if err := db.DeleteRecord(ctx, "user", "id", id); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Redirect or respond with success + http.Redirect(w, r, "/board", http.StatusSeeOther) + }, + ) +} + +func EditRecord(ctx context.Context, db *sqlite.Database, templ *template.Template) http.Handler { + // Implementation of EditRecord handler + return http.HandlerFunc( + + func(w http.ResponseWriter, r *http.Request) { + + // fmt.Println("EditRecord handler called") + + id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + } + + records, err := db.ReadRecords(ctx, baseQuery+" WHERE u.id = ?;", id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + rec := records[0] + + // fmt.Printf("Record to edit: %+v\n", rec) + + w.Header().Set("Content-Type", "text/html") + + // Execute template with proper error handling + if err := templ.ExecuteTemplate(w, "edit-user", rec); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + }, + ) +} + +func PatchRecord(ctx context.Context, db *sqlite.Database, templ *template.Template) http.Handler { + // Implementation of PatchRecord handler + return http.HandlerFunc( + + func(w http.ResponseWriter, r *http.Request) { + + fmt.Println("PatchRecord handler called") + + // id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + // if err != nil { + // w.WriteHeader(http.StatusInternalServerError) + // w.Write([]byte(err.Error())) + // } + + // if err := db.DeleteRecord(ctx, "user", "id", id); err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + // } + + // Redirect or respond with success + http.Redirect(w, r, "/board", http.StatusSeeOther) + + }, + ) +} diff --git a/lcars/lcars_v5/datastar/consts.go b/lcars/lcars_v5/datastar/consts.go new file mode 100644 index 0000000..9b896fa --- /dev/null +++ b/lcars/lcars_v5/datastar/consts.go @@ -0,0 +1,115 @@ +// This is auto-generated by Datastar. DO NOT EDIT. + +package datastar + +import "time" + +const ( + DatastarKey = "datastar" + Version = "1.0.0-beta.11" + VersionClientByteSize = 40026 + VersionClientByteSizeGzip = 14900 + + //region Default durations + + // The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE. + DefaultSseRetryDuration = 1000 * time.Millisecond + + //endregion Default durations + + //region Default strings + + // The default attributes for +
+
+ + + \ No newline at end of file diff --git a/lcars/lcars_v5/frontend/index.html b/lcars/lcars_v5/frontend/index.html new file mode 100644 index 0000000..69f2980 --- /dev/null +++ b/lcars/lcars_v5/frontend/index.html @@ -0,0 +1,232 @@ + + + + + Lower Decks PADD + + + + + + + + + +
+
+
+ + +
02-262000
+
+
+ +
+
+
+
47
+
31
+
28
+
94
+
+
+
329
+
128
+
605
+
704
+
+
+
39725514862
+
51320259663
+
21857221984
+
40372566301
+
+
+
56
+
04
+
40
+
35
+
+
+
614
+
883
+
109
+
297
+
+
+
000
+
13
+
05
+
25
+
+
+
48
+
07
+
38
+
62
+
+
+
416
+
001
+
888
+
442
+
+
+
86225514862
+
31042009183
+
74882306985
+
54048523421
+
+
+
10
+
80
+
31
+
85
+
+
+
87
+
71
+
40
+
26
+
+
+
98
+
63
+
52
+
71
+
+
+
118
+
270
+
395
+
260
+
+
+
8675309
+
7952705
+
9282721
+
4981518
+
+
+
000
+
99
+
10
+
84
+
+
+
65821407321
+
54018820533
+
27174523016
+
38954062564
+
+
+
999
+
202
+
574
+
293
+
+
+
3872
+
1105
+
1106
+
7411
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
03-111968
+
04-041969
+
05-1701D
+
06-071984
+
+
+
07-081940
+
+
+
+
+
+
+
+
+
+
+
+

Lower Decks PADD

+ +
+ +
+
+
+ +
+
+ + + \ No newline at end of file diff --git a/lcars/lcars_v5/frontend/tinyserver.go b/lcars/lcars_v5/frontend/tinyserver.go new file mode 100644 index 0000000..fc97222 --- /dev/null +++ b/lcars/lcars_v5/frontend/tinyserver.go @@ -0,0 +1,39 @@ +package main + +import ( + "log" + "net/http" + "path/filepath" + "strings" +) + +func main() { + // Folder to serve + dir := "." + + // File server handler + fs := http.FileServer(http.Dir(dir)) + + // Wrap the file server to add caching headers + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ext := strings.ToLower(filepath.Ext(r.URL.Path)) + + // Set long caching for static assets + switch ext { + case ".css", ".js", ".woff", ".woff2", ".ttf", ".eot", ".svg", ".png", ".jpg", ".jpeg", ".gif": + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + default: + // Optional: short caching for HTML so you always get latest page + w.Header().Set("Cache-Control", "no-cache") + } + + fs.ServeHTTP(w, r) + }) + + addr := ":8080" + log.Printf("Serving %s on http://localhost%s\n", dir, addr) + err := http.ListenAndServe(addr, handler) + if err != nil { + log.Fatal(err) + } +} diff --git a/lcars/lcars_v5/go.mod b/lcars/lcars_v5/go.mod new file mode 100644 index 0000000..5a773da --- /dev/null +++ b/lcars/lcars_v5/go.mod @@ -0,0 +1,18 @@ +module ld + +go 1.25.0 + +require modernc.org/sqlite v1.39.0 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/sys v0.34.0 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/lcars/lcars_v5/go.sum b/lcars/lcars_v5/go.sum new file mode 100644 index 0000000..c46fb2d --- /dev/null +++ b/lcars/lcars_v5/go.sum @@ -0,0 +1,49 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= +modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/lcars/lcars_v5/interval/interval.go b/lcars/lcars_v5/interval/interval.go new file mode 100644 index 0000000..e755d5e --- /dev/null +++ b/lcars/lcars_v5/interval/interval.go @@ -0,0 +1,94 @@ +package interval + +import ( + "errors" + "math/rand" + "sync" + "time" +) + +type MutexMap[K comparable, V any] struct { + mutex sync.Mutex + m map[K]V +} + +func (m *MutexMap[K, V]) Get(key K) (V, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + v, ok := m.m[key] + if !ok { + return v, errors.New("unknown key") + } + return v, nil +} + +func (m *MutexMap[K, V]) Set(key K, value V) { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.m[key] = value +} + +func (m *MutexMap[K, V]) Delete(key K) { + m.mutex.Lock() + defer m.mutex.Unlock() + + delete(m.m, key) +} + +var stopChannels = MutexMap[int, chan bool]{ + m: make(map[int]chan bool), +} + +// SetInterval schedules a repeating task to be executed at a specified interval. +func SetInterval(f func(), milliseconds int) (id int) { + for { + id = rand.Int() + if _, err := stopChannels.Get(id); err == nil { + continue // ID collision, keep looking for another unique random value + } + break + } + + stop := make(chan bool) + stopChannels.Set(id, stop) + + ticker := time.NewTicker(time.Duration(milliseconds) * time.Millisecond) + + go func() { + for { + select { + case <-stop: + ticker.Stop() + return + case <-ticker.C: + f() + } + } + }() + + return +} + +// ClearInterval stops a scheduled interval identified by the specified interval ID. +func ClearInterval(id int) error { + stop, err := stopChannels.Get(id) + if err != nil { + return err + } + stop <- true + stopChannels.Delete(id) + + return nil +} + +// SetTimeout schedules a one-time task to be executed after a specified interval. +func SetTimeout(f func(), milliseconds int) { + timer := time.NewTimer(time.Duration(milliseconds) * time.Millisecond) + go func() { + <-timer.C + timer.Stop() + f() + }() +} diff --git a/lcars/lcars_v5/main.go b/lcars/lcars_v5/main.go new file mode 100644 index 0000000..16e3759 --- /dev/null +++ b/lcars/lcars_v5/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "embed" + "errors" + "fmt" + "ld/eventbus" + "ld/server" + "ld/sqlite" + "log" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" +) + +const ( + exitCodeErr = 1 + exitCodeInterrupt = 2 +) + +var AppRoot = "./" // path for supporting files that sit in app root folder in production + +//go:embed frontend/* +var frontend embed.FS + +//go:embed embed +var embedded embed.FS + +// main specific variables +var ExecutableName string + +func main() { + + fmt.Println("Developement mode") + + err := errors.New("") + + ExecutableName, err = getExecutableName() + if err != nil { + os.Exit(exitCodeErr) + } + + // setting up logging to file + logFileName := AppRoot + ExecutableName + ".log" + logFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Printf("error opening file: %v", err) + os.Exit(exitCodeErr) + } + defer logFile.Close() + log.SetOutput(logFile) + + err = run() + if err != nil { + os.Exit(exitCodeErr) + } + + run() + +} + +func run() error { + + ctx := context.Background() + + stateDB, err := createStateDB(true) + if err != nil { + log.Fatalf("Failed to create internal StateDB: %v", err) + } + + ebus := eventbus.NewEventBus() + + /* + type Server struct { + Ctx context.Context + StateDB *sqlite.Database + Static fs.FS + Embedded embed.FS + intervalID int + Ebus *eventbus.EventBus + Templ *template.Template + Mux *http.ServeMux + } + */ + + // setting up the server + + server, err := server.New( + ctx, + stateDB, + embedded, + ebus, + ) + + if err != nil { + log.Fatalf("server-app not created - error: %v", err) + } + + // tasks.SetupTasks(server) + + err = server.Start() + if err != nil { + fmt.Printf("server not started - error: %v \n", err) + log.Fatalf("server not started - error: %v", err) + } + + readCrew(server) + readMessages(server) + // return nil + + // listen for os shutdown events, report them into log file and exit application + chanOS := make(chan os.Signal, 2) + signal.Notify(chanOS, os.Interrupt, syscall.SIGTERM) + go func() { + <-chanOS + log.Println("shutting down request signal received") + server.Close() + os.Exit(exitCodeInterrupt) + }() + + // setting up the routes, hooking up API endpoints with backend functions + // routes.SetupRoutes(server) + + return nil + +} + +// some internal functions + +func getExecutableName() (string, error) { + name, err := os.Executable() + if err != nil { + return "", err + } + name = filepath.Base(name) + return strings.TrimSuffix(name, filepath.Ext(name)), nil +} + +func createStateDB(ctx context.Context) (*sqlite.Database, error) { + + fileName := "state.db" + + db := sqlite.New(fileName) + + err := db.Open(ctx) + if err != nil { + return nil, err + } + + query, err := embedded.ReadFile("embed/create_state_db.sql") + if err != nil { + return nil, err + } + + _, err = db.DB().Exec(string(query)) + if err != nil { + return nil, err + } + + return db, nil +} diff --git a/lcars/lcars_v5/notes.txt b/lcars/lcars_v5/notes.txt new file mode 100644 index 0000000..874fb6d --- /dev/null +++ b/lcars/lcars_v5/notes.txt @@ -0,0 +1,2 @@ +Nächste-Generation Abzeichen Icon von Icons8 +Geek icons created by Pixel perfect - Flaticon diff --git a/lcars/lcars_v5/routes.go b/lcars/lcars_v5/routes.go new file mode 100644 index 0000000..90ad15e --- /dev/null +++ b/lcars/lcars_v5/routes.go @@ -0,0 +1,20 @@ +package main + +// This file is the one place in your application where all routes are listed. + +import ( + "ld/server" + "net/http" +) + +// addRoutes combines the URL endpoints with the applications's services +// and dependencies and required middleware +func addRoutes( + s *server.Server, +) { + s.Mux.Handle("GET /", http.FileServer(http.FS(s.Static))) + // s.Mux.Handle("GET /board", api.Board(s.Ctx, s.StateDB, s.Templ)) + // s.Mux.Handle("GET /edit-record/{id}", api.EditRecord(s.Ctx, s.StateDB, s.Templ)) + // s.Mux.Handle("PATCH /patch-record/{id}", api.PatchRecord(s.Ctx, s.StateDB, s.Templ)) + // s.Mux.Handle("DELETE /delete-record/{id}", api.DeleteRecord(s.Ctx, s.StateDB, s.Templ)) +} diff --git a/lcars/lcars_v5/server/server.go b/lcars/lcars_v5/server/server.go new file mode 100644 index 0000000..2a89209 --- /dev/null +++ b/lcars/lcars_v5/server/server.go @@ -0,0 +1,63 @@ +// Copyright 2025 Thomas Hedeler +// Author: Thomas Hedeler +package server + +import ( + "context" + "embed" + "io/fs" + "ld/eventbus" + "ld/interval" + "ld/sqlite" + "log" + "net/http" + "text/template" +) + +type Server struct { + Ctx context.Context + StateDB *sqlite.Database + Static fs.FS + Embedded embed.FS + intervalID int + Ebus *eventbus.EventBus + Templ *template.Template + Mux *http.ServeMux +} + +func New( + stateDB *sqlite.Database, + embedded embed.FS, + ebus *eventbus.EventBus, + +) (*Server, error) { + + // creating the server + return &Server{ + StateDB: stateDB, + Embedded: embedded, + Ebus: ebus, + }, nil +} + +func (s *Server) Start() error { + + // // start the task engine + s.intervalID = interval.SetInterval(s.interval, 1000) // check for executable tasks every 1 second + return nil +} + +func (s *Server) Close() error { + + // stop the task engine + err := interval.ClearInterval(s.intervalID) + if err != nil { + log.Print(err) + } + + err = s.StateDB.Close() + if err != nil { + log.Print(err) + } + return err +} diff --git a/lcars/lcars_v5/server/taskengine.go b/lcars/lcars_v5/server/taskengine.go new file mode 100644 index 0000000..9041476 --- /dev/null +++ b/lcars/lcars_v5/server/taskengine.go @@ -0,0 +1,10 @@ +package server + +import "fmt" + +// this function schedules the tasks and will be called periodically, see server.Start() +func (s *Server) interval() { + + fmt.Println("taskengine was called") + +} diff --git a/lcars/lcars_v5/sqlite/database.go b/lcars/lcars_v5/sqlite/database.go new file mode 100644 index 0000000..73de4e3 --- /dev/null +++ b/lcars/lcars_v5/sqlite/database.go @@ -0,0 +1,614 @@ +package sqlite // name the package as you see fit, it is intended to be vendored + +import ( + "bytes" + "database/sql" + "errors" + "context" + "fmt" + "os" + "strconv" + "text/template" + + _ "modernc.org/sqlite" +) + +/* +Package sqlite provides a simplified wrapper around the modernc.org/sqlite driver. + +It aims to provide a convenient, developer-friendly interface for common database +operations, prioritizing ease of use with a map-based data exchange format (Record). + +Key Concepts: + +- Database Instance: A single `Database` struct instance manages the connection to + a specific database file or an in-memory database. +- Lifecycle: Use `New()` to create an instance, `Open()` or `OpenInMemory()` + to establish the connection, and `defer Close()` to release resources. +- Record Type: `type Record = map[string]any` is the primary type for exchanging + data with the database. Column names become map keys. +- Underlying DB Access: The `DB()` method provides access to the raw `*sql.DB` + object for operations not covered by the wrapper. + +Features: + +- Reading Data: + - `ReadTable(tablename string)`: Reads all rows and columns from a specified table. + - `ReadRecords(query string, args ...any)`: Executes a custom SQL SELECT query + with parameterized arguments and returns multiple records. + - `GetRecord(tablename string, idfield string, key any)`: Retrieves a single + record from a table based on a unique identifier. +- Writing Data: + - `UpsertRecord(tablename string, idfield string, record Record)`: Inserts a new + record or updates an existing one based on the value of the `idfield`. + Uses SQLite's `ON CONFLICT` clause. + - Supports partial updates: Only include fields you want to insert/update in the `Record`. + - Returns the full resulting record (including auto-generated IDs) using `RETURNING *`. +- Deleting Data: + - `DeleteRecord(tablename string, idfield string, id any)`: Deletes a single + record from a table based on its identifier. +- Metadata: + - `TableList()`: Lists all tables in the database. + - `Version()`: Gets the SQLite library version. + - `UserVersion()`: Gets the database's user_version PRAGMA. + +Transaction Handling: + +- `Begin()`: Starts a new database transaction, returning a `*Transaction` object. +- Chaining: Transaction methods (`GetRecord`, `UpsertRecord`, `DeleteRecord`, `Next`) + return the `*Transaction` object, allowing operations to be chained. +- Error Propagation: If any operation within a transaction chain fails, the error + is stored in the `Transaction` object (`tx.Err()`), and subsequent chained + operations become no-ops. +- `Next(action Action)`: Allows executing custom logic within the transaction + by providing a function that receives the raw `*sql.Tx`. +- `End()`: Finalizes the transaction. If `tx.Err()` is non-nil, it performs a + ROLLBACK; otherwise, it performs a COMMIT. Returns the accumulated error. + +Helper Functions: + +- `ValueT any`: A generic helper to safely extract + and type-assert a value from a `Record` map. +- `NoRowsOk([]Record, error)`: A helper to wrap calls that might return + `sql.ErrNoRows` and treat that specific error as a non-error case, returning + nil records and a nil error. + +Prerequisites: + +- For `UpsertRecord` to function correctly, the target table must have a unique + index defined on the specified `idfield`. +- It is highly recommended that the `idfield` is an `INTEGER PRIMARY KEY AUTOINCREMENT` + to leverage SQLite's built-in ID generation and efficient lookups. + +Shortcomings and Important Considerations: + +- SQL Injection Risk: + - Identifiers: Table names, field names, and record keys (used as field names) + are validated to contain only alphanumeric characters and underscores. They are + also quoted by the library. This significantly mitigates SQL injection risks + through identifiers. However, the caller MUST still ensure that these identifiers + refer to the *intended* database objects. + - Query Structure: For `ReadRecords` and `Transaction.Next` actions, if the raw + SQL query string itself is constructed from untrusted user input, it remains a + potential SQL injection vector. Parameterization is used by this library (and + `database/sql`) only for *values*, not for the query structure or identifiers + within a user-provided query string. + +- Simplicity over Edge Cases: This is a simplified layer. More complex scenarios + or advanced SQLite features might require using the underlying `*sql.DB` object + via the `DB()` method. +- Room for Improvement: As a fresh implementation, there is potential for + further optimization and refinement. + +Implementation Details: + +- Uses the `modernc.org/sqlite` driver. +- SQL commands for `UpsertRecord` are dynamically generated using Go's `text/template`. +- Internal interfaces (`iquery`, `iExec`) are used to allow functions like `upsert` + and `deleteRecord` to work seamlessly with both `*sql.DB` and `*sql.Tx`. + +Unit Tests: + +- The package includes unit tests (`database_test.go`, `transaction_test.go`, `helpers_test.go`) + covering core functionality and transaction handling. +*/ + +// ErrInvalidIdentifier is returned when a table or column name contains disallowed characters. +var ErrInvalidIdentifier = errors.New("invalid identifier: contains disallowed characters") + +// This is the data type to exchange data with the database +type Record = map[string]any + +type Database struct { + databaseName string + database *sql.DB +} + +func New(DBName string) *Database { + return &Database{databaseName: DBName} +} + +func (d *Database) Close() error { + return d.database.Close() +} + +// provides access to the internal database object +func (d *Database) DB() *sql.DB { + return d.database +} + +func (d *Database) Name() string { + return d.databaseName +} + +// basePragmas returns a string of common PRAGMA settings for SQLite. +// It excludes user_version, which is typically managed by schema migrations. +func basePragmas() string { + return ` + PRAGMA page_size = 4096; + PRAGMA synchronous = NORMAL; + PRAGMA foreign_keys = ON; + PRAGMA journal_mode = WAL; + ` +} + +func (d *Database) Open(ctx context.Context) (err error) { + d.database, err = openSqliteDB(ctx, d.databaseName) + return err +} + +func (d *Database) OpenInMemory(ctx context.Context) (err error) { + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + return err + } + // Apply base PRAGMAs for consistency in in-memory databases. + _, err = db.ExecContext(ctx, basePragmas()) + d.database = db + return err +} + +func openSqliteDB(ctx context.Context, databasefilename string) (*sql.DB, error) { + + _, err := os.Stat(databasefilename) + if errors.Is(err, os.ErrNotExist) { + return createDB(ctx, databasefilename) + } + if err != nil { + return nil, err + } + return sql.Open("sqlite", databasefilename) + +} + +func createDB(ctx context.Context, dbfileName string) (*sql.DB, error) { + // Apply base pragmas and set initial user_version for new database files. + query := basePragmas() + "PRAGMA user_version = 1;\n" + db, err := sql.Open("sqlite", dbfileName) + if err != nil { + return nil, err + } + _, err = db.ExecContext(ctx, query) + if err != nil { + db.Close() // Best effort to close if ExecContext fails + os.Remove(dbfileName) // Best effort to remove partially created file + return nil, err + } + return db, nil +} + +func (d *Database) TableList(ctx context.Context) (result []Record, err error) { + return d.ReadRecords(ctx, "select name from sqlite_master where type='table';") +} + +func (d *Database) ReadTable(ctx context.Context, tablename string) (result []Record, err error) { + if !isValidIdentifier(tablename) { + return nil, fmt.Errorf("ReadTable: %w: table name '%s'", ErrInvalidIdentifier, tablename) + } + return d.ReadRecords(ctx, fmt.Sprintf("select * from \"%s\";", tablename)) // Use double quotes for identifiers +} + +func (d *Database) ReadRecords(ctx context.Context, query string, args ...any) (result []Record, err error) { + // Note: For ReadRecords, the query string itself is provided by the caller. + // The library cannot validate the structure of this query beyond what the driver does. + // The SQL injection caveat for arbitrary query strings remains critical here. + rows, err := d.DB().QueryContext(ctx, query, args...) + if err != nil { + return result, err + } + defer rows.Close() + return Rows2records(rows) +} + +func (d *Database) GetRecord(ctx context.Context, tablename string, idfield string, key any) (result Record, err error) { + if !isValidIdentifier(tablename) { + return nil, fmt.Errorf("GetRecord: %w: table name '%s'", ErrInvalidIdentifier, tablename) + } + if !isValidIdentifier(idfield) { + return nil, fmt.Errorf("GetRecord: %w: id field '%s'", ErrInvalidIdentifier, idfield) + } + + query := fmt.Sprintf("select * from \"%s\" where \"%s\" = ?;", tablename, idfield) // Quote identifiers + res, err := d.DB().QueryContext(ctx, query, key) + if err != nil { + return result, err + } + defer res.Close() + return Rows2record(res) + +} + +func (d *Database) UpsertRecord(ctx context.Context, tablename string, idfield string, record Record) (result Record, err error) { + if !isValidIdentifier(tablename) { + return nil, fmt.Errorf("UpsertRecord: %w: table name '%s'", ErrInvalidIdentifier, tablename) + } + if !isValidIdentifier(idfield) { + return nil, fmt.Errorf("UpsertRecord: %w: id field '%s'", ErrInvalidIdentifier, idfield) + } + return upsert(ctx, d.DB(), tablename, idfield, record) + +} + +func (d *Database) DeleteRecord(ctx context.Context, tablename string, idfield string, id any) (err error) { + // Validation for tablename and idfield will be done by deleteRecord internal helper + // to ensure consistency for both Database and Transaction calls. + return deleteRecord(ctx, d.DB(), tablename, idfield, id) + +} + +// *sql.DB and *sql.Tx both have a method named 'Query', +// this way they can both be passed into upsert and deleteRecord function +type iqueryContext interface { + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) +} + +// iExec is an interface satisfied by both *sql.DB and *sql.Tx for Exec method +type iExecContext interface { + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) +} + +func upsert(ctx context.Context, q iqueryContext, tablename string, idfield string, record Record) (result Record, err error) { + // tablename and idfield are assumed to be validated by the public-facing methods (Database.UpsertRecord, Transaction.UpsertRecord) + + fields := []string{} + data := []any{} + for k, v := range record { + if !isValidIdentifier(k) { + return nil, fmt.Errorf("upsert: %w: field name '%s'", ErrInvalidIdentifier, k) + } + fields = append(fields, k) + data = append(data, v) + } + // Ensure idfield is part of the record if it's used for conflict and update, + // or handle cases where it might only be for conflict target and not in SET. + // The current buildUpsertCommand uses all fields from the record for the SET clause. + if _, present := record[idfield]; !present && len(record) > 0 { + // This situation is complex: if idfield is not in the record, + // it implies it might be auto-generated on INSERT, but for UPDATE, + // it's needed to identify the row. The ON CONFLICT target uses idfield. + // The current template includes all record fields in the SET clause. + // If idfield is not in record, it won't be in the SET clause unless explicitly added. + // For simplicity and current template, we assume if idfield is for update, it should be in the record. + } + if len(fields) == 0 { + return nil, errors.New("UpsertRecord: input record cannot be empty") + } + query, err := buildUpsertCommand(tablename, idfield, fields) + if err != nil { + return result, err + } + res, err := q.QueryContext(ctx, query, data...) // res contains the full record - see SQLite: RETURNING * + if err != nil { + return result, err + } + defer res.Close() + return Rows2record(res) +} + +func deleteRecord(ctx context.Context, e iExecContext, tablename string, idfield string, id any) (err error) { + if !isValidIdentifier(tablename) { + return fmt.Errorf("deleteRecord: %w: table name '%s'", ErrInvalidIdentifier, tablename) + } + if !isValidIdentifier(idfield) { + return fmt.Errorf("deleteRecord: %w: id field '%s'", ErrInvalidIdentifier, idfield) + } + + query := fmt.Sprintf("DELETE FROM \"%s\" WHERE \"%s\" = ?;", tablename, idfield) + _, err = e.ExecContext(ctx, query, id) + // Note: err could be sql.ErrNoRows if the driver/db supports it for Exec, + // or nil if delete affected 0 rows. Caller might want to check result.RowsAffected(). + // For simplicity here, we just return the error from Exec. + return err + +} + +func buildUpsertCommand(tablename string, idfield string, fields []string) (result string, err error) { + // Assumes tablename, idfield, and all elements in fields are already validated + // by the calling function (e.g., upsert). + // And that fields is not empty. + + pname := map[string]string{} // assign correct index for parameter name + // parameter position, starts at 1 in sql! So it needs to be calculated by function pname inside template + + for i, k := range fields { + pname[k] = strconv.Itoa(i + 1) + } + funcMap := template.FuncMap{ + "pname": func(fieldname string) string { + return pname[fieldname] + }, + } + tableDef := struct { + Tablename string + KeyField string + LastField int + FieldNames []string + }{ + Tablename: tablename, + KeyField: idfield, + LastField: len(fields) - 1, + FieldNames: fields, + } + var templString = `{{$last := .LastField}}INSERT INTO "{{ .Tablename }}"({{ range $i,$el := .FieldNames }} "{{$el}}"{{if ne $i $last}},{{end}}{{end}}) + VALUES({{ range $i,$el := .FieldNames }} ?{{pname $el}}{{if ne $i $last}},{{end}}{{end}}) + ON CONFLICT("{{ .Tablename }}"."{{.KeyField}}") + DO UPDATE SET {{ range $i,$el := .FieldNames }}"{{$el}}"= ?{{pname $el}}{{if ne $i $last}},{{end}}{{end}} + RETURNING *;` + + dbTempl, err := template.New("upsertDB").Funcs(funcMap).Parse(templString) + if err != nil { + return result, err + } + var templBytes bytes.Buffer + err = dbTempl.Execute(&templBytes, tableDef) + if err != nil { + return result, err + } + return templBytes.String(), nil +} + +func Rows2record(rows *sql.Rows) (Record, error) { + columns, err := rows.Columns() + if err != nil { + return nil, err + } + values := make([]any, len(columns)) + valuePtrs := make([]any, len(columns)) + for i := range values { + valuePtrs[i] = &values[i] + } + result := Record{} + + if !rows.Next() { + if err := rows.Err(); err != nil { // Check for errors during iteration attempt + return nil, err + } + return nil, sql.ErrNoRows // Standard error for no rows + } + + if err := rows.Scan(valuePtrs...); err != nil { + return nil, err + } + for i, col := range columns { + result[col] = values[i] + } + + // Check for errors encountered during iteration (e.g., if Next() was called multiple times). + if err := rows.Err(); err != nil { + return nil, err + } + + return result, nil +} + +func Rows2records(rows *sql.Rows) ([]Record, error) { + columns, err := rows.Columns() + if err != nil { + return nil, err + } + recLength := len(columns) + results := []Record{} + for rows.Next() { + valuePtrs := make([]any, recLength) + values := make([]any, recLength) + for i := range values { + valuePtrs[i] = &values[i] + } + record := Record{} + if err := rows.Scan(valuePtrs...); err != nil { + return nil, err + } + for i, col := range columns { + record[col] = values[i] + } + results = append(results, record) + } + // Check for errors encountered during iteration. + if err := rows.Err(); err != nil { + return nil, err + } + + if len(results) == 0 { + // For a function returning a slice, an empty slice and nil error is often preferred for "no rows". + // However, if the expectation is that Rows2records is used where rows *should* exist, sql.ErrNoRows is appropriate. + return nil, sql.ErrNoRows // Or: return []Record{}, nil if empty slice is the desired "no rows" outcome + } + return results, nil +} + +func (d *Database) Version(ctx context.Context) (string, error) { + var version string + err := d.DB().QueryRowContext(ctx, "SELECT sqlite_version();").Scan(&version) + return version, err +} + +func (d *Database) UserVersion(ctx context.Context) (int64, error) { + var result int64 + // PRAGMA user_version; returns a single row with a single column named "user_version". + // QueryRow().Scan() is appropriate here. + err := d.DB().QueryRowContext(ctx, "PRAGMA user_version;").Scan(&result) + return result, err +} + +func (d *Database) BeginTx(ctx context.Context, opts *sql.TxOptions) *Transaction { + tx, err := d.database.BeginTx(ctx, opts) + return &Transaction{tx, err} +} + +type Transaction struct { + tx *sql.Tx + err error +} + +// Err returns the current error state of the transaction. +func (t *Transaction) Err() error { + return t.err +} + +type Action func(ctx context.Context, tx *sql.Tx) error + +func (t *Transaction) Next(ctx context.Context, action Action) *Transaction { + if t.err != nil { + return t + } + t.err = action(ctx, t.tx) + return t +} + +func (t *Transaction) End() error { + if t.tx == nil { // Transaction was never begun or already ended + return t.err // Return any prior error + } + if t.err != nil { + err := t.tx.Rollback() // Rollback does not take context + if err != nil { + t.err = errors.Join(t.err, err) + } + return t.err + } + t.err = t.tx.Commit() + return t.err +} + +func (t *Transaction) GetRecord(ctx context.Context, tablename string, idfield string, key any, output Record) *Transaction { + if !isValidIdentifier(tablename) { + t.err = fmt.Errorf("Transaction.GetRecord: %w: table name '%s'", ErrInvalidIdentifier, tablename) + return t + } + if !isValidIdentifier(idfield) { + t.err = fmt.Errorf("Transaction.GetRecord: %w: id field '%s'", ErrInvalidIdentifier, idfield) + return t + } + + if t.err != nil { + return t + } + query := fmt.Sprintf("select * from \"%s\" where \"%s\" = ?;", tablename, idfield) // Quote identifiers + res, err := t.tx.QueryContext(ctx, query, key) + if err != nil { + t.err = err + return t + } + defer res.Close() + result, err := Rows2record(res) + if err != nil { + t.err = err + return t + } + for k := range output { + delete(output, k) + } + for k, v := range result { + output[k] = v + } + return t +} + +func (t *Transaction) UpsertRecord(ctx context.Context, tablename string, idfield string, record Record, output Record) *Transaction { + if !isValidIdentifier(tablename) { + t.err = fmt.Errorf("Transaction.UpsertRecord: %w: table name '%s'", ErrInvalidIdentifier, tablename) + return t + } + if !isValidIdentifier(idfield) { + t.err = fmt.Errorf("Transaction.UpsertRecord: %w: id field '%s'", ErrInvalidIdentifier, idfield) + return t + } + + if t.err != nil { + return t + } + result, err := upsert(ctx, t.tx, tablename, idfield, record) + if err != nil { + t.err = err + return t + } + for k := range output { + delete(output, k) + } + for k, v := range result { + output[k] = v + } + return t +} + +func (t *Transaction) DeleteRecord(ctx context.Context, tablename string, idfield string, id any) *Transaction { + // Validation will be done by the internal deleteRecord helper + // if !isValidIdentifier(tablename) { + // t.err = fmt.Errorf("Transaction.DeleteRecord: %w: table name '%s'", ErrInvalidIdentifier, tablename) + // return t + // } + // if !isValidIdentifier(idfield) { + // t.err = fmt.Errorf("Transaction.DeleteRecord: %w: id field '%s'", ErrInvalidIdentifier, idfield) + // return t + // } + + if t.err != nil { + return t + } + err := deleteRecord(ctx, t.tx, tablename, idfield, id) // t.tx satisfies iExecContext + if err != nil { + t.err = err + } + return t +} + +// returns a value of the provided type, if the field exist and if it can be cast into the provided type parameter +func Value[T any](rec Record, field string) (value T, ok bool) { + var v any + // No validation for 'field' here as it's used to access a map key from an existing Record, + // not to construct SQL. + if v, ok = rec[field]; ok { + value, ok = v.(T) + } + return +} + +// don't report an error if there are simply just 'no rows found' +func NoRowsOk(recs []Record, err error) ([]Record, error) { + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // Return an empty, non-nil slice and nil error to indicate "no rows found, but that's okay". + // This makes it safer for callers to immediately use len() or range over the result. + return []Record{}, nil + } + return recs, err + } + return recs, nil +} + +// isValidIdentifier checks if the given string is a safe identifier. +// Allows alphanumeric characters and underscores. Must not be empty. +func isValidIdentifier(identifier string) bool { + if len(identifier) == 0 { + return false + } + for _, r := range identifier { + if !((r >= 'a' && r <= 'z') || + (r >= 'A' && r <= 'Z') || + (r >= '0' && r <= '9') || + r == '_') { + return false + } + } + return true +} diff --git a/lcars/lcars_v5/state/events.go b/lcars/lcars_v5/state/events.go new file mode 100644 index 0000000..c3df2a6 --- /dev/null +++ b/lcars/lcars_v5/state/events.go @@ -0,0 +1,2 @@ +package state +