Compare commits
2 Commits
00d6da7b87
...
d3f682d360
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3f682d360 | ||
|
|
0e442ac460 |
BIN
lcars/state.db
BIN
lcars/state.db
Binary file not shown.
14
lcars_v1/embed/create_state_db.sql
Normal file
14
lcars_v1/embed/create_state_db.sql
Normal file
@ -0,0 +1,14 @@
|
||||
CREATE TABLE ship_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT NOT NULL,
|
||||
subsystem TEXT NOT NULL,
|
||||
severity TEXT CHECK(severity IN ('CRITICAL', 'ALERT', 'WARNING', 'NOTICE', 'INFO')) NOT NULL DEFAULT 'INFO',
|
||||
color TEXT NOT NULL,
|
||||
message TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE crew_member (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rank TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
95
lcars_v1/embed/crew.json
Normal file
95
lcars_v1/embed/crew.json
Normal file
@ -0,0 +1,95 @@
|
||||
[
|
||||
"Ensign Beckett Mariner",
|
||||
"Ensign Brad Boimler",
|
||||
"Captain Carol Freeman",
|
||||
"Commander Jack Ransom",
|
||||
"Ensign Barsa Orsino",
|
||||
"Lieutenant Commander D'Vana Tendi",
|
||||
"Lieutenant Commander Sam Rutherford",
|
||||
"Lieutenant Commander Shaxs",
|
||||
"Ensign Liora Vance",
|
||||
"Ensign Rylan Sato",
|
||||
"Lieutenant Jarek Torin",
|
||||
"Lieutenant Kira Dallin",
|
||||
"Lieutenant T'Lara Venn",
|
||||
"Lieutenant Shonnie Velar",
|
||||
"Lieutenant Commander Aric Thorne",
|
||||
"Lieutenant Commander Selene Marvik",
|
||||
"Lieutenant Commander Jovan Kreel",
|
||||
"Lieutenant Orin Kallis",
|
||||
"Ensign Mira Talon",
|
||||
"Ensign Fynn Darvik",
|
||||
"Lieutenant Commander Elara Voss",
|
||||
"Lieutenant Zev Ralyn",
|
||||
"Ensign Daxia Morn",
|
||||
"Lieutenant Varek Solis",
|
||||
"Ensign Tylen Kael",
|
||||
"Lieutenant Commander Nira Falco",
|
||||
"Lieutenant Kael Dorran",
|
||||
"Ensign Saren Vale",
|
||||
"Ensign Tova Lin",
|
||||
"Lieutenant Commander Ryn Talor",
|
||||
"Lieutenant Draven Korr",
|
||||
"Ensign Lyra Kenning",
|
||||
"Ensign Joren Pax",
|
||||
"Lieutenant Commander Calix Arden",
|
||||
"Lieutenant Selan Vey",
|
||||
"Ensign Aricel Taren",
|
||||
"Ensign Velin Daro",
|
||||
"Lieutenant Caris Vennor",
|
||||
"Lieutenant Kellen Dray",
|
||||
"Lieutenant Risa Talven",
|
||||
"Lieutenant Commander Thalen Voss",
|
||||
"Lieutenant Commander Sariah Quell",
|
||||
"Ensign Orin Talvik",
|
||||
"Ensign Lyric Selden",
|
||||
"Lieutenant Commander Varen Korr",
|
||||
"Lieutenant Elara Vynn",
|
||||
"Ensign Jax Talmar",
|
||||
"Lieutenant Commander Neris Vay",
|
||||
"Lieutenant Draven Solis",
|
||||
"Ensign Tavia Korlen",
|
||||
"Ensign Ryn Paxil",
|
||||
"Lieutenant Commander Kira Dalen",
|
||||
"Lieutenant Zev Ardin",
|
||||
"Ensign Lyra Taven",
|
||||
"Ensign Fynn Velar",
|
||||
"Lieutenant Commander Calen Rhos",
|
||||
"Lieutenant Selan Vaylen",
|
||||
"Ensign Aricel Dorran",
|
||||
"Ensign Tylen Korr",
|
||||
"Lieutenant Commander Nira Talos",
|
||||
"Lieutenant Kael Venn",
|
||||
"Ensign Saren Daro",
|
||||
"Ensign Tova Vennor",
|
||||
"Lieutenant Commander Ryn Arden",
|
||||
"Lieutenant Draven Voss",
|
||||
"Ensign Lyra Talin",
|
||||
"Ensign Joren Vay",
|
||||
"Lieutenant Commander Calix Talven",
|
||||
"Lieutenant Selan Korr",
|
||||
"Ensign Aricel Vynn",
|
||||
"Ensign Velin Talor",
|
||||
"Lieutenant Caris Vaylen",
|
||||
"Lieutenant Kellen Rhos",
|
||||
"Lieutenant Risa Vennor",
|
||||
"Lieutenant Commander Thalen Daro",
|
||||
"Lieutenant Commander Sariah Voss",
|
||||
"Ensign Orin Vaylen",
|
||||
"Ensign Lyric Talven",
|
||||
"Lieutenant Commander Varen Talos",
|
||||
"Lieutenant Elara Solis",
|
||||
"Ensign Jax Vennor",
|
||||
"Lieutenant Commander Neris Talor",
|
||||
"Lieutenant Draven Vaylen",
|
||||
"Ensign Tavia Dorran",
|
||||
"Ensign Ryn Voss",
|
||||
"Lieutenant Commander Kira Arden",
|
||||
"Lieutenant Zev Vay",
|
||||
"Ensign Lyra Daro",
|
||||
"Ensign Fynn Talor",
|
||||
"Lieutenant Commander Calen Venn",
|
||||
"Lieutenant Selan Talvik",
|
||||
"Ensign Aricel Rhos"
|
||||
]
|
||||
|
||||
1066
lcars_v1/embed/messages.json
Normal file
1066
lcars_v1/embed/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
102
lcars_v1/experiments.go
Normal file
102
lcars_v1/experiments.go
Normal file
@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ld/server"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func readCrew(server *server.Server) (string, error) {
|
||||
|
||||
content, err := embedded.ReadFile("embed/crew.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create a slice to hold the parsed names
|
||||
var crewNames []string
|
||||
|
||||
// Parse the JSON
|
||||
err = json.Unmarshal(content, &crewNames)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing JSON:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
const insertStmt = "INSERT INTO crew_member ( rank, name) VALUES (?, ?) ;"
|
||||
insstmt, err := server.StateDB.DB().Prepare(insertStmt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer insstmt.Close()
|
||||
|
||||
// Print the results
|
||||
for _, text := range crewNames {
|
||||
rank, name := splitRank(text)
|
||||
if rank == "" {
|
||||
rank = "ERROR"
|
||||
}
|
||||
// fmt.Printf("%d: rank: %s name: %s\n", i+1, rank, name)
|
||||
_, err = insstmt.Exec(rank, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
// fmt.Println(string(content))
|
||||
return "", nil
|
||||
|
||||
}
|
||||
|
||||
// splitRank separates the rank (all tokens except the last two) from the crewman's name (last two tokens)
|
||||
func splitRank(fullName string) (rank, name string) {
|
||||
tokens := strings.Fields(fullName)
|
||||
if len(tokens) < 2 {
|
||||
return fullName, "" // fallback if malformed
|
||||
}
|
||||
|
||||
nameTokens := tokens[len(tokens)-2:] // last 2 tokens as name
|
||||
rankTokens := tokens[:len(tokens)-2] // everything else as rank
|
||||
name = strings.Join(nameTokens, " ")
|
||||
rank = strings.Join(rankTokens, " ")
|
||||
|
||||
return rank, name
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
Severity string `json:"severity"`
|
||||
Color string `json:"color"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func readMessages(server *server.Server) error {
|
||||
content, err := embedded.ReadFile("embed/messages.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var messages []Message
|
||||
if err := json.Unmarshal(content, &messages); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const insertStmt = "INSERT INTO ship_messages ( timestamp, subsystem, severity, color, message) VALUES (?,?,?,?,?) ;"
|
||||
insstmt, err := server.StateDB.DB().Prepare(insertStmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer insstmt.Close()
|
||||
// For demonstration, print the parsed messages
|
||||
for _, m := range messages {
|
||||
// fmt.Printf("[%s] %s (%s) - %s\n", m.Timestamp, m.Subsystem, m.Severity, m.Message)
|
||||
_, err = insstmt.Exec(m.Timestamp, m.Subsystem, m.Severity, m.Color, m.Message)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
BIN
lcars_v1/frontend/assets/Antonio-Bold.woff
Normal file
BIN
lcars_v1/frontend/assets/Antonio-Bold.woff
Normal file
Binary file not shown.
BIN
lcars_v1/frontend/assets/Antonio-Bold.woff2
Normal file
BIN
lcars_v1/frontend/assets/Antonio-Bold.woff2
Normal file
Binary file not shown.
BIN
lcars_v1/frontend/assets/Antonio-Regular.woff
Normal file
BIN
lcars_v1/frontend/assets/Antonio-Regular.woff
Normal file
Binary file not shown.
BIN
lcars_v1/frontend/assets/Antonio-Regular.woff2
Normal file
BIN
lcars_v1/frontend/assets/Antonio-Regular.woff2
Normal file
Binary file not shown.
BIN
lcars_v1/frontend/assets/alert.png
Normal file
BIN
lcars_v1/frontend/assets/alert.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
BIN
lcars_v1/frontend/assets/beep1.mp3
Normal file
BIN
lcars_v1/frontend/assets/beep1.mp3
Normal file
Binary file not shown.
BIN
lcars_v1/frontend/assets/beep2.mp3
Normal file
BIN
lcars_v1/frontend/assets/beep2.mp3
Normal file
Binary file not shown.
BIN
lcars_v1/frontend/assets/beep3.mp3
Normal file
BIN
lcars_v1/frontend/assets/beep3.mp3
Normal file
Binary file not shown.
BIN
lcars_v1/frontend/assets/beep4.mp3
Normal file
BIN
lcars_v1/frontend/assets/beep4.mp3
Normal file
Binary file not shown.
3011
lcars_v1/frontend/assets/classic.css
Normal file
3011
lcars_v1/frontend/assets/classic.css
Normal file
File diff suppressed because it is too large
Load Diff
51
lcars_v1/frontend/assets/lcars.js
Normal file
51
lcars_v1/frontend/assets/lcars.js
Normal file
@ -0,0 +1,51 @@
|
||||
document.addEventListener("touchstart", function() {},false);
|
||||
let mybutton = document.getElementById("topBtn");
|
||||
window.onscroll = function() {scrollFunction()};
|
||||
function scrollFunction() {
|
||||
if (document.body.scrollTop > 200 || document.documentElement.scrollTop > 200) {
|
||||
mybutton.style.display = "block";
|
||||
} else {
|
||||
mybutton.style.display = "none";
|
||||
}
|
||||
}
|
||||
function topFunction() {
|
||||
document.body.scrollTop = 0;
|
||||
document.documentElement.scrollTop = 0;
|
||||
}
|
||||
function playSoundAndRedirect(audioId, url) {
|
||||
// var audio = document.getElementById(audioId);
|
||||
// audio.play();
|
||||
|
||||
// audio.onended = function() {
|
||||
// console.log("Audio has ended", url);
|
||||
window.location.href = url;
|
||||
// };
|
||||
}
|
||||
function goToAnchor(anchorId) {
|
||||
window.location.hash = anchorId;
|
||||
}
|
||||
// Accordion drop-down
|
||||
var acc = document.getElementsByClassName("accordion");
|
||||
var i;
|
||||
|
||||
for (i = 0; i < acc.length; i++) {
|
||||
acc[i].addEventListener("click", function() {
|
||||
this.classList.toggle("active");
|
||||
var accordionContent = this.nextElementSibling;
|
||||
if (accordionContent.style.maxHeight){
|
||||
accordionContent.style.maxHeight = null;
|
||||
} else {
|
||||
accordionContent.style.maxHeight = accordionContent.scrollHeight + "px";
|
||||
}
|
||||
});
|
||||
}
|
||||
// LCARS keystroke sound (not to be used with hyperlinks)
|
||||
const LCARSkeystroke = document.getElementById('LCARSkeystroke');
|
||||
const allPlaySoundButtons = document.querySelectorAll('.playSoundButton');
|
||||
allPlaySoundButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
LCARSkeystroke.pause();
|
||||
LCARSkeystroke.currentTime = 0; // Reset to the beginning of the sound
|
||||
LCARSkeystroke.play();
|
||||
});
|
||||
});
|
||||
1885
lcars_v1/frontend/assets/lower-decks-padd.css
Normal file
1885
lcars_v1/frontend/assets/lower-decks-padd.css
Normal file
File diff suppressed because it is too large
Load Diff
1856
lcars_v1/frontend/assets/lower-decks.css
Normal file
1856
lcars_v1/frontend/assets/lower-decks.css
Normal file
File diff suppressed because it is too large
Load Diff
2830
lcars_v1/frontend/assets/nemesis-blue.css
Normal file
2830
lcars_v1/frontend/assets/nemesis-blue.css
Normal file
File diff suppressed because it is too large
Load Diff
231
lcars_v1/frontend/crew/index.html
Normal file
231
lcars_v1/frontend/crew/index.html
Normal file
@ -0,0 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Lower Decks PADD</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="format-detection" content="date=no">
|
||||
<link rel="stylesheet" type="text/css" href="../assets/lower-decks-padd.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- <audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
|
||||
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
|
||||
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
|
||||
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio> -->
|
||||
<div class="wrap-all">
|
||||
<div class="wrap">
|
||||
<div class="left-frame-top">
|
||||
<!--
|
||||
*** LCARS PANEL BUTTON ***
|
||||
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
|
||||
|
||||
<div class="panel-1">
|
||||
<a href="#">LCARS</a>
|
||||
</div>
|
||||
-->
|
||||
<a href="/" class="panel-1-button">LCARS</a>
|
||||
<!-- <button onclick="playSoundAndRedirect('audio2', '/')" class="panel-1-button">LCARS</button> -->
|
||||
<div class="panel-2">02<span class="hop">-262000</span></div>
|
||||
</div>
|
||||
<div class="right-frame-top">
|
||||
<div class="banner">LCARS 57436.2</div>
|
||||
<div class="data-cascade-button-group">
|
||||
<div class="data-wrapper">
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">47</div>
|
||||
<div class="dc-row-2">31</div>
|
||||
<div class="dc-row-3">28</div>
|
||||
<div class="dc-row-4">94</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">329</div>
|
||||
<div class="dc-row-2 font-night-rain">128</div>
|
||||
<div class="dc-row-3">605</div>
|
||||
<div class="dc-row-4">704</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-night-rain">39725514862</div>
|
||||
<div class="dc-row-2 font-arctic-ice">51320259663</div>
|
||||
<div class="dc-row-3 font-alpha-blue">21857221984</div>
|
||||
<div class="dc-row-4">40372566301</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">56</div>
|
||||
<div class="dc-row-2 font-night-rain">04</div>
|
||||
<div class="dc-row-3 font-night-rain">40</div>
|
||||
<div class="dc-row-4 font-night-rain">35</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">614</div>
|
||||
<div class="dc-row-2 font-arctic-ice">883</div>
|
||||
<div class="dc-row-3 font-alpha-blue">109</div>
|
||||
<div class="dc-row-4">297</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||
<div class="dc-row-2 darkspace font-alpha-blue">13</div>
|
||||
<div class="dc-row-3 darkspace font-arctic-ice">05</div>
|
||||
<div class="dc-row-4 darkspace font-night-rain">25</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">48</div>
|
||||
<div class="dc-row-2 font-night-rain">07</div>
|
||||
<div class="dc-row-3">38</div>
|
||||
<div class="dc-row-4">62</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">416</div>
|
||||
<div class="dc-row-2 font-night-rain">001</div>
|
||||
<div class="dc-row-3">888</div>
|
||||
<div class="dc-row-4">442</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-night-rain">86225514862</div>
|
||||
<div class="dc-row-2 font-arctic-ice">31042009183</div>
|
||||
<div class="dc-row-3 font-alpha-blue">74882306985</div>
|
||||
<div class="dc-row-4">54048523421</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-alpha-blue">10</div>
|
||||
<div class="dc-row-2">80</div>
|
||||
<div class="dc-row-3 font-night-rain">31</div>
|
||||
<div class="dc-row-4 font-alpha-blue">85</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-alpha-blue">87</div>
|
||||
<div class="dc-row-2">71</div>
|
||||
<div class="dc-row-3 font-night-rain">40</div>
|
||||
<div class="dc-row-4 font-night-rain">26</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">98</div>
|
||||
<div class="dc-row-2">63</div>
|
||||
<div class="dc-row-3 font-night-rain">52</div>
|
||||
<div class="dc-row-4 font-alpha-blue">71</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">118</div>
|
||||
<div class="dc-row-2">270</div>
|
||||
<div class="dc-row-3">395</div>
|
||||
<div class="dc-row-4">260</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">8675309</div>
|
||||
<div class="dc-row-2 font-night-rain">7952705</div>
|
||||
<div class="dc-row-3">9282721</div>
|
||||
<div class="dc-row-4">4981518</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||
<div class="dc-row-2 darkspace font-alpha-blue">99</div>
|
||||
<div class="dc-row-3 darkspace font-arctic-ice">10</div>
|
||||
<div class="dc-row-4 darkspace font-night-rain">84</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">65821407321</div>
|
||||
<div class="dc-row-2 font-alpha-blue">54018820533</div>
|
||||
<div class="dc-row-3 font-night-rain">27174523016</div>
|
||||
<div class="dc-row-4">38954062564</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">999</div>
|
||||
<div class="dc-row-2 font-arctic-ice">202</div>
|
||||
<div class="dc-row-3 font-alpha-blue">574</div>
|
||||
<div class="dc-row-4">293</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">3872</div>
|
||||
<div class="dc-row-2 font-night-rain">1105</div>
|
||||
<div class="dc-row-3">1106</div>
|
||||
<div class="dc-row-4 font-alpha-blue">7411</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<!--
|
||||
*** MAIN NAVIGATION BUTTONS ***
|
||||
Replace the hashtag '#' with a real URL (or not).
|
||||
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
|
||||
<a href="#">01</a>
|
||||
<a href="#">02</a>
|
||||
<a href="#">03</a>
|
||||
<a href="#">04</a>
|
||||
-->
|
||||
<button onclick="playSoundAndRedirect('audio2', 'crew.html')">01</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="bar-panel first-bar-panel">
|
||||
<div class="bar-1"> </div>
|
||||
<div class="bar-2"> </div>
|
||||
<div class="bar-3"> </div>
|
||||
<div class="bar-4"> </div>
|
||||
<div class="bar-5"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider">
|
||||
<div class="block-left"> </div>
|
||||
<div class="block-right">
|
||||
<div class="block-row">
|
||||
<div class="bar-11"> </div>
|
||||
<div class="bar-12"> </div>
|
||||
<div class="bar-13"> </div>
|
||||
<div class="bar-14">
|
||||
<div class="blockhead"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="left-frame">
|
||||
<!--
|
||||
** SCROLL TO TOP OF PAGE BUTTON **
|
||||
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
|
||||
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
|
||||
-->
|
||||
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span
|
||||
class="hop">screen</span> top</button>
|
||||
<div>
|
||||
<div class="panel-3">03<span class="hop">-111968</span></div>
|
||||
<div class="panel-4">04<span class="hop">-041969</span></div>
|
||||
<div class="panel-5">05<span class="hop">-1701D</span></div>
|
||||
<div class="panel-6">06<span class="hop">-071984</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="panel-7">07<span class="hop">-081940</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-frame">
|
||||
<div class="bar-panel">
|
||||
<div class="bar-6"> </div>
|
||||
<div class="bar-7"> </div>
|
||||
<div class="bar-8"> </div>
|
||||
<div class="bar-9"> </div>
|
||||
<div class="bar-10"> </div>
|
||||
</div>
|
||||
<main>
|
||||
<h1>Crew Sheet</h1>
|
||||
|
||||
</main>
|
||||
<footer>
|
||||
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
|
||||
Content Copyright © 2025 ld.hedeler.com <br>
|
||||
|
||||
<!-- The following attribution must not be removed: -->
|
||||
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="assets/lcars.js"></script>
|
||||
<div class="headtrim"> </div>
|
||||
<div class="baseboard"> </div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
232
lcars_v1/frontend/index.html
Normal file
232
lcars_v1/frontend/index.html
Normal file
@ -0,0 +1,232 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Lower Decks PADD</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="format-detection" content="date=no">
|
||||
<link rel="stylesheet" type="text/css" href="../assets/lower-decks-padd.css">
|
||||
<script type="speculationrules">
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- <audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
|
||||
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
|
||||
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
|
||||
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio> -->
|
||||
<div class="wrap-all">
|
||||
<div class="wrap">
|
||||
<div class="left-frame-top">
|
||||
<!--
|
||||
*** LCARS PANEL BUTTON ***
|
||||
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
|
||||
|
||||
<div class="panel-1">
|
||||
<a href="#">LCARS</a>
|
||||
</div>
|
||||
-->
|
||||
<button class="panel-1-button">LCARS</button>
|
||||
<div class="panel-2">02<span class="hop">-262000</span></div>
|
||||
</div>
|
||||
<div class="right-frame-top">
|
||||
<div class="banner">LCARS 57436.2</div>
|
||||
<div class="data-cascade-button-group">
|
||||
<div class="data-wrapper">
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">47</div>
|
||||
<div class="dc-row-2">31</div>
|
||||
<div class="dc-row-3">28</div>
|
||||
<div class="dc-row-4">94</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">329</div>
|
||||
<div class="dc-row-2 font-night-rain">128</div>
|
||||
<div class="dc-row-3">605</div>
|
||||
<div class="dc-row-4">704</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-night-rain">39725514862</div>
|
||||
<div class="dc-row-2 font-arctic-ice">51320259663</div>
|
||||
<div class="dc-row-3 font-alpha-blue">21857221984</div>
|
||||
<div class="dc-row-4">40372566301</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">56</div>
|
||||
<div class="dc-row-2 font-night-rain">04</div>
|
||||
<div class="dc-row-3 font-night-rain">40</div>
|
||||
<div class="dc-row-4 font-night-rain">35</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">614</div>
|
||||
<div class="dc-row-2 font-arctic-ice">883</div>
|
||||
<div class="dc-row-3 font-alpha-blue">109</div>
|
||||
<div class="dc-row-4">297</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||
<div class="dc-row-2 darkspace font-alpha-blue">13</div>
|
||||
<div class="dc-row-3 darkspace font-arctic-ice">05</div>
|
||||
<div class="dc-row-4 darkspace font-night-rain">25</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">48</div>
|
||||
<div class="dc-row-2 font-night-rain">07</div>
|
||||
<div class="dc-row-3">38</div>
|
||||
<div class="dc-row-4">62</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">416</div>
|
||||
<div class="dc-row-2 font-night-rain">001</div>
|
||||
<div class="dc-row-3">888</div>
|
||||
<div class="dc-row-4">442</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-night-rain">86225514862</div>
|
||||
<div class="dc-row-2 font-arctic-ice">31042009183</div>
|
||||
<div class="dc-row-3 font-alpha-blue">74882306985</div>
|
||||
<div class="dc-row-4">54048523421</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-alpha-blue">10</div>
|
||||
<div class="dc-row-2">80</div>
|
||||
<div class="dc-row-3 font-night-rain">31</div>
|
||||
<div class="dc-row-4 font-alpha-blue">85</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-alpha-blue">87</div>
|
||||
<div class="dc-row-2">71</div>
|
||||
<div class="dc-row-3 font-night-rain">40</div>
|
||||
<div class="dc-row-4 font-night-rain">26</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">98</div>
|
||||
<div class="dc-row-2">63</div>
|
||||
<div class="dc-row-3 font-night-rain">52</div>
|
||||
<div class="dc-row-4 font-alpha-blue">71</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">118</div>
|
||||
<div class="dc-row-2">270</div>
|
||||
<div class="dc-row-3">395</div>
|
||||
<div class="dc-row-4">260</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">8675309</div>
|
||||
<div class="dc-row-2 font-night-rain">7952705</div>
|
||||
<div class="dc-row-3">9282721</div>
|
||||
<div class="dc-row-4">4981518</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||
<div class="dc-row-2 darkspace font-alpha-blue">99</div>
|
||||
<div class="dc-row-3 darkspace font-arctic-ice">10</div>
|
||||
<div class="dc-row-4 darkspace font-night-rain">84</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">65821407321</div>
|
||||
<div class="dc-row-2 font-alpha-blue">54018820533</div>
|
||||
<div class="dc-row-3 font-night-rain">27174523016</div>
|
||||
<div class="dc-row-4">38954062564</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">999</div>
|
||||
<div class="dc-row-2 font-arctic-ice">202</div>
|
||||
<div class="dc-row-3 font-alpha-blue">574</div>
|
||||
<div class="dc-row-4">293</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">3872</div>
|
||||
<div class="dc-row-2 font-night-rain">1105</div>
|
||||
<div class="dc-row-3">1106</div>
|
||||
<div class="dc-row-4 font-alpha-blue">7411</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<!--
|
||||
*** MAIN NAVIGATION BUTTONS ***
|
||||
Replace the hashtag '#' with a real URL (or not).
|
||||
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
|
||||
<a href="#">01</a>
|
||||
<a href="#">02</a>
|
||||
<a href="#">03</a>
|
||||
<a href="#">04</a>
|
||||
-->
|
||||
<a href="crew">01</a>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="bar-panel first-bar-panel">
|
||||
<div class="bar-1"> </div>
|
||||
<div class="bar-2"> </div>
|
||||
<div class="bar-3"> </div>
|
||||
<div class="bar-4"> </div>
|
||||
<div class="bar-5"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider">
|
||||
<div class="block-left"> </div>
|
||||
<div class="block-right">
|
||||
<div class="block-row">
|
||||
<div class="bar-11"> </div>
|
||||
<div class="bar-12"> </div>
|
||||
<div class="bar-13"> </div>
|
||||
<div class="bar-14">
|
||||
<div class="blockhead"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="left-frame">
|
||||
<!--
|
||||
** SCROLL TO TOP OF PAGE BUTTON **
|
||||
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
|
||||
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
|
||||
-->
|
||||
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span
|
||||
class="hop">screen</span> top</button>
|
||||
<div>
|
||||
<div class="panel-3">03<span class="hop">-111968</span></div>
|
||||
<div class="panel-4">04<span class="hop">-041969</span></div>
|
||||
<div class="panel-5">05<span class="hop">-1701D</span></div>
|
||||
<div class="panel-6">06<span class="hop">-071984</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="panel-7">07<span class="hop">-081940</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-frame">
|
||||
<div class="bar-panel">
|
||||
<div class="bar-6"> </div>
|
||||
<div class="bar-7"> </div>
|
||||
<div class="bar-8"> </div>
|
||||
<div class="bar-9"> </div>
|
||||
<div class="bar-10"> </div>
|
||||
</div>
|
||||
<main>
|
||||
<h1>Lower Decks PADD</h1>
|
||||
|
||||
</main>
|
||||
<footer>
|
||||
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
|
||||
Content Copyright © 2025 ld.hedeler.com <br>
|
||||
|
||||
<!-- The following attribution must not be removed: -->
|
||||
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="assets/lcars.js"></script>
|
||||
<div class="headtrim"> </div>
|
||||
<div class="baseboard"> </div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
39
lcars_v1/frontend/tinyserver.go
Normal file
39
lcars_v1/frontend/tinyserver.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
18
lcars_v1/go.mod
Normal file
18
lcars_v1/go.mod
Normal file
@ -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
|
||||
)
|
||||
49
lcars_v1/go.sum
Normal file
49
lcars_v1/go.sum
Normal file
@ -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=
|
||||
94
lcars_v1/interval/interval.go
Normal file
94
lcars_v1/interval/interval.go
Normal file
@ -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()
|
||||
}()
|
||||
}
|
||||
157
lcars_v1/main.go
Normal file
157
lcars_v1/main.go
Normal file
@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"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("Here")
|
||||
err := run()
|
||||
if err != nil {
|
||||
os.Exit(exitCodeErr)
|
||||
}
|
||||
ExecutableName, err = getExecutableName()
|
||||
if err != nil {
|
||||
os.Exit(exitCodeErr)
|
||||
}
|
||||
|
||||
// readCrew()
|
||||
|
||||
// readMessages()
|
||||
|
||||
run()
|
||||
|
||||
}
|
||||
|
||||
func run() error {
|
||||
|
||||
// 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)
|
||||
|
||||
stateDB, err := createStateDB(true)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create internal StateDB: %v", err)
|
||||
}
|
||||
|
||||
// setting up the server
|
||||
server, err := server.New(
|
||||
logFileName,
|
||||
stateDB,
|
||||
embedded,
|
||||
)
|
||||
|
||||
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(StateDBDelete bool) (*sqlite.Database, error) {
|
||||
|
||||
// fileName := fmt.Sprintf("state-%s.db", ulid.Make())
|
||||
|
||||
fileName := "state.db"
|
||||
|
||||
if StateDBDelete {
|
||||
_, err := os.Stat(fileName)
|
||||
if err == nil {
|
||||
err := os.Remove(fileName)
|
||||
if err != nil {
|
||||
log.Fatal("error deleting statedb-file:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db, err := sqlite.New(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.Open()
|
||||
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
|
||||
}
|
||||
1
lcars_v1/notes.txt
Normal file
1
lcars_v1/notes.txt
Normal file
@ -0,0 +1 @@
|
||||
<a target="_blank" href="https://icons8.com/icon/21039/star-trek">Nächste-Generation Abzeichen</a> Icon von <a target="_blank" href="https://icons8.com">Icons8</a>
|
||||
103
lcars_v1/server/server.go
Normal file
103
lcars_v1/server/server.go
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright 2024 codeM GmbH
|
||||
// Author: Thomas Hedeler
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"ld/interval"
|
||||
"ld/sqlite"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
SQLiteVersion string
|
||||
ServerInfo map[string]any
|
||||
StateDB *sqlite.Database
|
||||
Embedded embed.FS
|
||||
LogFileName string
|
||||
TokenDuration int // TODO einbauen
|
||||
Secret []byte
|
||||
Header string
|
||||
intervalID int
|
||||
Tasks map[string]TaskFunc
|
||||
}
|
||||
|
||||
func New(
|
||||
logfilename string,
|
||||
StateDB *sqlite.Database,
|
||||
embedded embed.FS,
|
||||
|
||||
) (*Server, error) {
|
||||
|
||||
// creating the server
|
||||
return &Server{
|
||||
LogFileName: logfilename,
|
||||
StateDB: StateDB,
|
||||
Embedded: embedded,
|
||||
Tasks: make(map[string]func(s *Server) error),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
|
||||
// query, err := s.Embedded.ReadFile("embed/win/server_info.sql")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// res, err := s.StundenDB.ReadRecords(string(query))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// s.ServerInfo = res[0]
|
||||
|
||||
// err = inits.LoadLogins(s.StundenDB, s.StateDB)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// err = inits.LoadTasks(s.StundenDB, s.StateDB)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // start the task engine
|
||||
// if s.Production {
|
||||
// s.intervalID = interval.SetInterval(s.interval, 60000) // check for executable tasks every 60 seconds
|
||||
// } else {
|
||||
// s.intervalID = interval.SetInterval(s.interval, 30000) // check for executable tasks every 30 seconds
|
||||
// }
|
||||
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
|
||||
}
|
||||
|
||||
// func to dispatch routes to all parts of the application:
|
||||
// they receive references to the server and the current fiber context via closures
|
||||
// this way all functions have access to server properties and can handle the
|
||||
// incoming requests themselves.
|
||||
|
||||
// type HandlerFunc = func(s *Server, c *fiber.Ctx) error
|
||||
|
||||
// func (s *Server) Handler(handler HandlerFunc) func(c *fiber.Ctx) error {
|
||||
// return func(c *fiber.Ctx) error {
|
||||
// return handler(s, c)
|
||||
// }
|
||||
// }
|
||||
|
||||
// signature for internal tasks
|
||||
type TaskFunc = func(s *Server) error
|
||||
118
lcars_v1/server/taskengine.go
Normal file
118
lcars_v1/server/taskengine.go
Normal file
@ -0,0 +1,118 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// this function schedules the tasks and will be called periodically, see server.Start()
|
||||
func (s *Server) interval() {
|
||||
|
||||
// read scheduled task list from stateDB
|
||||
// check for next executable task:
|
||||
// - if there is one or more tasks ready for execution then select one of them.
|
||||
// - if there is a selected task, update its next execution field and execute the task
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// fmt.Println("Recovered from panic in worker:", r)
|
||||
log.Printf("recovered from panic in taskengine: %v ", r)
|
||||
}
|
||||
}()
|
||||
|
||||
tasks, err := s.StateDB.ReadRecords("SELECT * FROM tasks ORDER by next_execution limit 1;")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("error in taskengine: %s ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(tasks) < 1 {
|
||||
log.Printf("error in taskengine: %s ", "found no task for execution")
|
||||
return
|
||||
}
|
||||
|
||||
task := tasks[0] // pick the one task with the smallest next execution time, see previous sql statement
|
||||
|
||||
task_name, haveTask := task["task_name"].(string)
|
||||
if !haveTask {
|
||||
log.Printf("error in taskengine: task %s is of wrong type", task["task_name"])
|
||||
return
|
||||
}
|
||||
|
||||
nextExecution := task["next_execution"].(int64)
|
||||
startTime := task["start_time"].(int64)
|
||||
execInterval := task["interval"].(int64)
|
||||
nowSeconds := time.Now().Unix()
|
||||
|
||||
if nowSeconds < nextExecution { // task execution is not yet due
|
||||
return
|
||||
}
|
||||
|
||||
// calculate next execution time
|
||||
for nextExecution = startTime; nowSeconds > nextExecution; nextExecution += execInterval {
|
||||
// add as many intervals to the starttime until the next execution lies in the future
|
||||
}
|
||||
|
||||
task["start_time"] = task["next_execution"]
|
||||
task["next_execution"] = nextExecution
|
||||
|
||||
/*
|
||||
no_executions INTEGER, -- how often executed
|
||||
duration INTEGER, -- duration of the last exec in ms
|
||||
no_errors INTEGER, -- error count
|
||||
last_error_text TEXT,
|
||||
|
||||
*/
|
||||
|
||||
if count, ok := task["no_executions"].(int64); ok {
|
||||
task["no_executions"] = count + 1
|
||||
}
|
||||
|
||||
// update next_execution in state database
|
||||
_, err = s.StateDB.UpsertRecord("tasks", "task_id", task)
|
||||
if err != nil {
|
||||
log.Printf("error in taskengine: cannot update task record - before execution %s ", err)
|
||||
return
|
||||
}
|
||||
|
||||
task_func, haveTask := s.Tasks[task_name] // select the function with the matching name
|
||||
|
||||
if !haveTask {
|
||||
log.Printf("error in taskengine: task %s is not defined", task_name)
|
||||
}
|
||||
|
||||
if haveTask {
|
||||
|
||||
start := time.Now()
|
||||
// if !s.Production {
|
||||
// fmt.Println("Taskengine: executing task:", task_name, start)
|
||||
// }
|
||||
err = task_func(s) // finally execute the task; attention: a task that panics will kill the server!
|
||||
|
||||
task["duration"] = int(time.Since(start).Milliseconds())
|
||||
|
||||
if err != nil {
|
||||
log.Printf("taskengine: execution task: %s failed with error: %s ", task_name, err)
|
||||
task["last_error_text"] = err.Error()
|
||||
if count, ok := task["no_errors"].(int64); ok {
|
||||
task["no_errors"] = count + 1
|
||||
}
|
||||
// if !s.Production {
|
||||
// fmt.Println("Taskengine: failed task:", task_name, err)
|
||||
// }
|
||||
} else {
|
||||
// if !s.Production {
|
||||
// fmt.Println("Taskengine: successfully completed task:", task_name, time.Now())
|
||||
// }
|
||||
}
|
||||
|
||||
_, err = s.StateDB.UpsertRecord("tasks", "task_id", task)
|
||||
if err != nil {
|
||||
log.Printf("error in taskengine: cannot update task record - after execution %s ", err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
428
lcars_v1/sqlite/database.go
Normal file
428
lcars_v1/sqlite/database.go
Normal file
@ -0,0 +1,428 @@
|
||||
package sqlite // name the package as you see fit
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// This is the data type to exchange data with the database
|
||||
type Record = map[string]any
|
||||
|
||||
type Database struct {
|
||||
databaseName string
|
||||
database *sql.DB
|
||||
}
|
||||
|
||||
type Transaction struct {
|
||||
tx *sql.Tx
|
||||
err error
|
||||
}
|
||||
|
||||
type Action func(tx *sql.Tx) error
|
||||
|
||||
func New(DBName string) (*Database, error) {
|
||||
return &Database{databaseName: DBName}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (d *Database) Open() (err error) {
|
||||
d.database, err = openSqliteDB(d.databaseName)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) OpenInMemory() (err error) {
|
||||
d.database, err = sql.Open("sqlite", ":memory:")
|
||||
return err
|
||||
}
|
||||
|
||||
func openSqliteDB(databasefilename string) (*sql.DB, error) {
|
||||
|
||||
_, err := os.Stat(databasefilename)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return createDB(databasefilename)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sql.Open("sqlite", databasefilename)
|
||||
|
||||
}
|
||||
|
||||
func createDB(dbfileName string) (*sql.DB, error) {
|
||||
|
||||
query := `
|
||||
PRAGMA page_size = 4096;
|
||||
PRAGMA synchronous = off;
|
||||
PRAGMA foreign_keys = off;
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA user_version = 1;
|
||||
`
|
||||
db, err := sql.Open("sqlite", dbfileName)
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
_, err = db.Exec(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (d *Database) TableList() (result []Record, err error) {
|
||||
return d.ReadRecords("select name from sqlite_master where type='table';")
|
||||
}
|
||||
|
||||
func (d *Database) ReadTable(tablename string) (result []Record, err error) {
|
||||
|
||||
return d.ReadRecords(fmt.Sprintf("select * from '%s';", tablename))
|
||||
}
|
||||
|
||||
func (d *Database) ReadRecords(query string, args ...any) (result []Record, err error) {
|
||||
|
||||
rows, err := d.DB().Query(query, args...)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return Rows2records(rows)
|
||||
|
||||
}
|
||||
|
||||
func (d *Database) GetRecord(tablename string, idfield string, key any) (result Record, err error) {
|
||||
|
||||
query := fmt.Sprintf("select * from %s where %s = ?;", tablename, idfield)
|
||||
res, err := d.DB().Query(query, key)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer res.Close()
|
||||
return Rows2record(res)
|
||||
|
||||
}
|
||||
|
||||
func (d *Database) UpsertRecord(tablename string, idfield string, record Record) (result Record, err error) {
|
||||
|
||||
return upsert(d.DB(), tablename, idfield, record)
|
||||
|
||||
}
|
||||
|
||||
func (d *Database) DeleteRecord(tablename string, idfield string, id any) (err error) {
|
||||
|
||||
return deleteRecord(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 iquery interface {
|
||||
Query(query string, args ...any) (*sql.Rows, error)
|
||||
}
|
||||
|
||||
func upsert(t iquery, tablename string, idfield string, record Record) (result Record, err error) {
|
||||
|
||||
fields := []string{}
|
||||
data := []any{}
|
||||
for k, v := range record {
|
||||
fields = append(fields, k)
|
||||
data = append(data, v)
|
||||
}
|
||||
query, err := buildUpsertCommand(tablename, idfield, fields)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
res, err := t.Query(query, data...) // res contains the full record - see SQLite: RETURNING *
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer res.Close()
|
||||
return Rows2record(res)
|
||||
|
||||
}
|
||||
|
||||
func deleteRecord(t iquery, tablename string, idfield string, id any) (err error) {
|
||||
|
||||
query := fmt.Sprintf("DELETE FROM \"%s\" WHERE \"%s\" = ?;", tablename, idfield)
|
||||
_, err = t.Query(query, id)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func buildUpsertCommand(tablename string, idfield string, fields []string) (string, error) {
|
||||
var sb strings.Builder
|
||||
sb.Grow(256 + len(fields)*20) // rough preallocation
|
||||
|
||||
// INSERT INTO
|
||||
sb.WriteString(`INSERT INTO "`)
|
||||
sb.WriteString(tablename)
|
||||
sb.WriteString(`"(`)
|
||||
for i, f := range fields {
|
||||
sb.WriteString(` "`)
|
||||
sb.WriteString(f)
|
||||
sb.WriteByte('"')
|
||||
if i < len(fields)-1 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
}
|
||||
sb.WriteString(")\n\tVALUES(")
|
||||
|
||||
// VALUES
|
||||
for i := 0; i < len(fields); i++ {
|
||||
sb.WriteString(" ?")
|
||||
sb.Write(strconv.AppendInt(nil, int64(i+1), 10))
|
||||
if i < len(fields)-1 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
}
|
||||
sb.WriteString(")\n\tON CONFLICT(\"")
|
||||
sb.WriteString(tablename)
|
||||
sb.WriteString(`"."`)
|
||||
sb.WriteString(idfield)
|
||||
sb.WriteString("\")\n\tDO UPDATE SET ")
|
||||
|
||||
// UPDATE SET
|
||||
for i, f := range fields {
|
||||
sb.WriteByte('"')
|
||||
sb.WriteString(f)
|
||||
sb.WriteString(`"= ?`)
|
||||
sb.Write(strconv.AppendInt(nil, int64(i+1), 10))
|
||||
if i < len(fields)-1 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n\tRETURNING *;")
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// func buildUpsertCommand(tablename string, idfield string, fields []string) (result string, err error) {
|
||||
|
||||
// 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{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, col := range columns {
|
||||
result[col] = values[i]
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, errors.New("no rows found")
|
||||
}
|
||||
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() {
|
||||
values := make([]any, recLength)
|
||||
valuePtrs := 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)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return nil, errors.New("no rows found")
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (d *Database) Version() (string, error) {
|
||||
result := ""
|
||||
sqliteversion, err := d.ReadRecords("SELECT sqlite_version();")
|
||||
if len(sqliteversion) == 1 {
|
||||
result = sqliteversion[0]["sqlite_version()"].(string)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *Database) UserVersion() (int64, error) {
|
||||
|
||||
var result int64
|
||||
userversion, err := d.ReadRecords("PRAGMA user_version;")
|
||||
if len(userversion) == 1 {
|
||||
result = userversion[0]["user_version"].(int64)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *Database) Begin() *Transaction {
|
||||
tx, err := d.database.Begin()
|
||||
return &Transaction{tx, err}
|
||||
}
|
||||
|
||||
func (t *Transaction) Next(action Action) *Transaction {
|
||||
if t.err != nil {
|
||||
return t
|
||||
}
|
||||
t.err = action(t.tx)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Transaction) End() error {
|
||||
|
||||
if t.err != nil {
|
||||
err := t.tx.Rollback()
|
||||
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(tablename string, idfield string, key any, output Record) *Transaction {
|
||||
|
||||
if t.err != nil {
|
||||
return t
|
||||
}
|
||||
query := fmt.Sprintf("select * from %s where %s = ?;", tablename, idfield)
|
||||
res, err := t.tx.Query(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(tablename string, idfield string, record Record, output Record) *Transaction {
|
||||
|
||||
if t.err != nil {
|
||||
return t
|
||||
}
|
||||
result, err := upsert(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(tablename string, idfield string, id any) *Transaction {
|
||||
|
||||
if t.err != nil {
|
||||
return t
|
||||
}
|
||||
err := deleteRecord(t.tx, tablename, idfield, id)
|
||||
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
|
||||
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 && err.Error() != "no rows found" {
|
||||
return recs, err
|
||||
}
|
||||
return recs, nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user