hateoas example1

This commit is contained in:
thomashamburg 2025-10-31 14:26:17 +01:00
parent 50e9c625be
commit bedfe1aa02
45 changed files with 18675 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
<template id="record1"></template>

View File

@ -0,0 +1 @@
<template id="record2"></template>

View File

@ -0,0 +1 @@
<template id="record3"></template>

View File

@ -0,0 +1,40 @@
<article id="record1">
<header>
<h3>Leanne Graham</h3>
<p>@Bret</p>
</header>
<form inert>
<div class="form-group">
<label for="email1">Email</label>
<input type="email" id="email1" value="Sincere@april.biz" >
</div>
<div class="form-group">
<label for="phone1">Phone</label>
<input type="tel" id="phone1" value="1-770-736-8031 x56442" >
</div>
<div class="form-group">
<label for="website1">Website</label>
<input type="text" id="website1" value="hildegard.org" >
</div>
<div class="form-group">
<label for="address1">Address</label>
<input type="text" id="address1" value="Apt. 556, Kulas Light" >
</div>
<div class="form-group">
<label for="city1">City</label>
<input type="text" id="city1" value="Gwenborough, 92998-3874" >
</div>
<div class="form-group">
<label for="company1">Company</label>
<input type="text" id="company1" value="Romaguera-Crona" >
</div>
<div class="form-group">
<label for="catchphrase1">Catchphrase</label>
<input type="text" id="catchphrase1" value="Multi-layered client-server neural-net" >
</div>
</form>
<footer class="grid">
<button class="primary" data-on:click="@get('edit-record1.html')">Edit</button>
<button class="secondary" data-on:click="@get('deleted-record1.html')">Delete</button>
</footer>
</article>

View File

@ -0,0 +1,40 @@
<article id="record2">
<header>
<h3>Ervin Howell</h3>
<p>@Antonette</p>
</header>
<form inert>
<div class="form-group">
<label for="email2">Email</label>
<input type="email" id="email2" value="Shanna@melissa.tv" >
</div>
<div class="form-group">
<label for="phone2">Phone</label>
<input type="tel" id="phone2" value="010-692-6593 x09125" >
</div>
<div class="form-group">
<label for="website2">Website</label>
<input type="text" id="website2" value="anastasia.net" >
</div>
<div class="form-group">
<label for="address2">Address</label>
<input type="text" id="address2" value="Suite 879, Victor Plains" >
</div>
<div class="form-group">
<label for="city2">City</label>
<input type="text" id="city2" value="Wisokyburgh, 90566-7771" >
</div>
<div class="form-group">
<label for="company2">Company</label>
<input type="text" id="company2" value="Deckow-Crist" >
</div>
<div class="form-group">
<label for="catchphrase2">Catchphrase</label>
<input type="text" id="catchphrase2" value="Proactive didactic contingency" >
</div>
</form>
<footer class="grid">
<button class="primary" data-on:click="@get('edit-record2.html')">Edit</button>
<button class="secondary" data-on:click="@get('deleted-record2.html')">Delete</button>
</footer>
</article>

View File

@ -0,0 +1,40 @@
<article id="record3">
<header>
<h3>Clementine Bauch</h3>
<p>@Samantha</p>
</header>
<form inert>
<div class="form-group">
<label for="email3">Email</label>
<input type="email" id="email3" value="Nathan@yesenia.net" >
</div>
<div class="form-group">
<label for="phone3">Phone</label>
<input type="tel" id="phone3" value="1-463-123-4447" >
</div>
<div class="form-group">
<label for="website3">Website</label>
<input type="text" id="website3" value="ramiro.info" >
</div>
<div class="form-group">
<label for="address3">Address</label>
<input type="text" id="address3" value="Suite 847, Douglas Extension" >
</div>
<div class="form-group">
<label for="city3">City</label>
<input type="text" id="city3" value="McKenziehaven, 59590-4157" >
</div>
<div class="form-group">
<label for="company3">Company</label>
<input type="text" id="company3" value="Romaguera-Jacobson" >
</div>
<div class="form-group">
<label for="catchphrase3">Catchphrase</label>
<input type="text" id="catchphrase3" value="Face to face bifurcated interface" >
</div>
</form>
<footer class="grid">
<button class="primary" data-on:click="@get('edit-record3.html')">Edit</button>
<button class="secondary" data-on:click="@get('deleted-record3.html')">Delete</button>
</footer>
</article>

View File

@ -0,0 +1,40 @@
<article id="record1">
<header>
<h3>Leanne Graham</h3>
<p>@Bret</p>
</header>
<form>
<div class="form-group">
<label for="email1">Email</label>
<input type="email" id="email1" value="Sincere@april.biz">
</div>
<div class="form-group">
<label for="phone1">Phone</label>
<input type="tel" id="phone1" value="1-770-736-8031 x56442">
</div>
<div class="form-group">
<label for="website1">Website</label>
<input type="text" id="website1" value="hildegard.org">
</div>
<div class="form-group">
<label for="address1">Address</label>
<input type="text" id="address1" value="Apt. 556, Kulas Light">
</div>
<div class="form-group">
<label for="city1">City</label>
<input type="text" id="city1" value="Gwenborough, 92998-3874">
</div>
<div class="form-group">
<label for="company1">Company</label>
<input type="text" id="company1" value="Romaguera-Crona">
</div>
<div class="form-group">
<label for="catchphrase1">Catchphrase</label>
<input type="text" id="catchphrase1" value="Multi-layered client-server neural-net">
</div>
</form>
<footer class="grid">
<button class="primary" data-on:click="@get('display-record1.html')">Save</button>
<button class="secondary" data-on:click="@get('display-record1.html')">Cancel</button>
</footer>
</article>

View File

@ -0,0 +1,40 @@
<article id="record2">
<header>
<h3>Ervin Howell</h3>
<p>@Antonette</p>
</header>
<form>
<div class="form-group">
<label for="email2">Email</label>
<input type="email" id="email2" value="Shanna@melissa.tv">
</div>
<div class="form-group">
<label for="phone2">Phone</label>
<input type="tel" id="phone2" value="010-692-6593 x09125">
</div>
<div class="form-group">
<label for="website2">Website</label>
<input type="text" id="website2" value="anastasia.net">
</div>
<div class="form-group">
<label for="address2">Address</label>
<input type="text" id="address2" value="Suite 879, Victor Plains">
</div>
<div class="form-group">
<label for="city2">City</label>
<input type="text" id="city2" value="Wisokyburgh, 90566-7771">
</div>
<div class="form-group">
<label for="company2">Company</label>
<input type="text" id="company2" value="Deckow-Crist">
</div>
<div class="form-group">
<label for="catchphrase2">Catchphrase</label>
<input type="text" id="catchphrase2" value="Proactive didactic contingency">
</div>
</form>
<footer class="grid">
<button class="primary" data-on:click="@get('display-record2.html')">Save</button>
<button class="secondary" data-on:click="@get('display-record2.html')">Cancel</button>
</footer>
</article>

View File

@ -0,0 +1,40 @@
<article id="record3">
<header>
<h3>Clementine Bauch</h3>
<p>@Samantha</p>
</header>
<form>
<div class="form-group">
<label for="email3">Email</label>
<input type="email" id="email3" value="Nathan@yesenia.net">
</div>
<div class="form-group">
<label for="phone3">Phone</label>
<input type="tel" id="phone3" value="1-463-123-4447">
</div>
<div class="form-group">
<label for="website3">Website</label>
<input type="text" id="website3" value="ramiro.info">
</div>
<div class="form-group">
<label for="address3">Address</label>
<input type="text" id="address3" value="Suite 847, Douglas Extension">
</div>
<div class="form-group">
<label for="city3">City</label>
<input type="text" id="city3" value="McKenziehaven, 59590-4157">
</div>
<div class="form-group">
<label for="company3">Company</label>
<input type="text" id="company3" value="Romaguera-Jacobson">
</div>
<div class="form-group">
<label for="catchphrase3">Catchphrase</label>
<input type="text" id="catchphrase3" value="Face to face bifurcated interface">
</div>
</form>
<footer class="grid">
<button class="primary" data-on:click="@get('display-record3.html')">Save</button>
<button class="secondary" data-on:click="@get('display-record3.html')">Cancel</button>
</footer>
</article>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hypermedia</title>
<link rel="stylesheet" href="pico.blue.css">
<script type="module" src="datastar.js"></script>
</head>
<body class="container">
<h1>Hypermedia as the Engine of Application State</h1>
<main class="grid">
<article id="record1" data-init="@get('display-record1.html')"></article>
<article id="record2" data-init="@get('display-record2.html')"></article>
<article id="record3" data-init="@get('display-record3.html')"></article>
</main>
</body>
</html>

File diff suppressed because it is too large Load Diff

2802
hypermedia/pico.blue.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
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
);
CREATE TABLE roster (
id INTEGER PRIMARY KEY AUTOINCREMENT,
crew_id INTEGER NOT NULL,
on_duty timestamp,
off_duty timestamp,
FOREIGN KEY (crew_id) REFERENCES crew_member(id)
);
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
event_type TEXT NOT NULL,
event_data JSON NOT NULL
);

95
lcars_v4/embed/crew.json Normal file
View 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_v4/embed/messages.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,310 @@
// modified version, see original:
// https://github.com/dtomasi/go-event-bus/tree/main
package eventbus
import (
"sync"
"sync/atomic"
)
type Data map[string]interface{}
// SafeCounter is a concurrency safe counter.
type SafeCounter struct {
v *uint64
}
// NewSafeCounter creates a new counter.
func NewSafeCounter() *SafeCounter {
return &SafeCounter{
v: new(uint64),
}
}
// Value returns the current value.
func (c *SafeCounter) Value() int {
return int(atomic.LoadUint64(c.v))
}
// IncBy increments the counter by given delta.
func (c *SafeCounter) IncBy(add uint) {
atomic.AddUint64(c.v, uint64(add))
}
// Inc increments the counter by 1.
func (c *SafeCounter) Inc() {
c.IncBy(1)
}
// DecBy decrements the counter by given delta.
func (c *SafeCounter) DecBy(dec uint) {
atomic.AddUint64(c.v, ^uint64(dec-1))
}
// Dec decrements the counter by 1.
func (c *SafeCounter) Dec() {
c.DecBy(1)
}
type TopicStats struct {
Name string
PublishedCount *SafeCounter
SubscriberCount *SafeCounter
}
type topicStatsMap map[string]*TopicStats
type Stats struct {
data topicStatsMap
}
func newStats() *Stats {
return &Stats{
data: map[string]*TopicStats{},
}
}
func (s *Stats) getOrCreateTopicStats(topicName string) *TopicStats {
_, ok := s.data[topicName]
if !ok {
s.data[topicName] = &TopicStats{
Name: topicName,
PublishedCount: NewSafeCounter(),
SubscriberCount: NewSafeCounter(),
}
}
return s.data[topicName]
}
func (s *Stats) incSubscriberCountByTopic(topicName string) {
s.getOrCreateTopicStats(topicName).SubscriberCount.Inc()
}
func (s *Stats) GetSubscriberCountByTopic(topicName string) int {
return s.getOrCreateTopicStats(topicName).SubscriberCount.Value()
}
func (s *Stats) incPublishedCountByTopic(topicName string) {
s.getOrCreateTopicStats(topicName).PublishedCount.Inc()
}
func (s *Stats) GetPublishedCountByTopic(topicName string) int {
return s.getOrCreateTopicStats(topicName).PublishedCount.Value()
}
func (s *Stats) GetTopicStats() []*TopicStats {
var tStatsSlice []*TopicStats
for _, tStats := range s.data {
tStatsSlice = append(tStatsSlice, tStats)
}
return tStatsSlice
}
func (s *Stats) GetTopicStatsByName(topicName string) *TopicStats {
return s.getOrCreateTopicStats(topicName)
}
// Event holds topic name and data.
type Event struct {
Data Data
Topic string
wg *sync.WaitGroup
}
// Done calls Done on sync.WaitGroup if set.
func (e *Event) Done() {
if e.wg != nil {
e.wg.Done()
}
}
// CallbackFunc Defines a CallbackFunc.
type CallbackFunc func(topic string, data Data)
// EventChannel is a channel which can accept an Event.
type EventChannel chan Event
// NewEventChannel Creates a new EventChannel.
func NewEventChannel() EventChannel {
return make(EventChannel)
}
// dataChannelSlice is a slice of DataChannels.
type eventChannelSlice []EventChannel
// EventBus stores the information about subscribers interested for a particular topic.
type EventBus struct {
mu sync.RWMutex
subscribers map[string]eventChannelSlice
stats *Stats
}
// NewEventBus returns a new EventBus instance.
func NewEventBus() *EventBus {
return &EventBus{ //nolint:exhaustivestruct
subscribers: map[string]eventChannelSlice{},
stats: newStats(),
}
}
// getSubscribingChannels returns all subscribing channels including wildcard matches.
func (eb *EventBus) getSubscribingChannels(topic string) eventChannelSlice {
subChannels := eventChannelSlice{}
for topicName := range eb.subscribers {
if topicName == topic || matchWildcard(topicName, topic) {
subChannels = append(subChannels, eb.subscribers[topicName]...)
}
}
return subChannels
}
// doPublish is publishing events to channels internally.
func (eb *EventBus) doPublish(channels eventChannelSlice, evt Event) {
eb.mu.RLock()
defer eb.mu.RUnlock()
go func(channels eventChannelSlice, evt Event) {
for _, ch := range channels {
ch <- evt
}
}(channels, evt)
}
// Code from https://github.com/minio/minio/blob/master/pkg/wildcard/match.go
func matchWildcard(pattern, name string) bool {
if pattern == "" {
return name == pattern
}
if pattern == "*" {
return true
}
// Does only wildcard '*' match.
return deepMatchRune([]rune(name), []rune(pattern), true)
}
// Code from https://github.com/minio/minio/blob/master/pkg/wildcard/match.go
func deepMatchRune(str, pattern []rune, simple bool) bool { //nolint:unparam
for len(pattern) > 0 {
switch pattern[0] {
default:
if len(str) == 0 || str[0] != pattern[0] {
return false
}
case '*':
return deepMatchRune(str, pattern[1:], simple) ||
(len(str) > 0 && deepMatchRune(str[1:], pattern, simple))
}
str = str[1:]
pattern = pattern[1:]
}
return len(str) == 0 && len(pattern) == 0
}
// PublishAsync data to a topic asynchronously
// This function returns a bool channel which indicates that all subscribers where called.
func (eb *EventBus) PublishAsync(topic string, data Data) {
eb.doPublish(
eb.getSubscribingChannels(topic),
Event{
Data: data,
Topic: topic,
wg: nil,
})
eb.stats.incPublishedCountByTopic(topic)
}
// PublishAsyncOnce same as PublishAsync but makes sure that topic is only published once.
func (eb *EventBus) PublishAsyncOnce(topic string, data Data) {
if eb.stats.GetPublishedCountByTopic(topic) > 0 {
return
}
eb.PublishAsync(topic, data)
}
// Publish data to a topic and wait for all subscribers to finish
// This function creates a waitGroup internally. All subscribers must call Done() function on Event.
func (eb *EventBus) Publish(topic string, data Data) interface{} {
wg := sync.WaitGroup{}
channels := eb.getSubscribingChannels(topic)
wg.Add(len(channels))
eb.doPublish(
channels,
Event{
Data: data,
Topic: topic,
wg: &wg,
})
wg.Wait()
eb.stats.incPublishedCountByTopic(topic)
return data
}
// PublishOnce same as Publish but makes sure only published once on topic.
func (eb *EventBus) PublishOnce(topic string, data Data) interface{} {
if eb.stats.GetPublishedCountByTopic(topic) > 0 {
return nil
}
return eb.Publish(topic, data)
}
// Subscribe to a topic passing a EventChannel.
func (eb *EventBus) Subscribe(topic string) EventChannel {
ch := make(EventChannel)
eb.SubscribeChannel(topic, ch)
eb.stats.incSubscriberCountByTopic(topic)
return ch
}
// SubscribeChannel subscribes to a given Channel.
func (eb *EventBus) SubscribeChannel(topic string, ch EventChannel) {
eb.mu.Lock()
defer eb.mu.Unlock()
if prev, found := eb.subscribers[topic]; found {
eb.subscribers[topic] = append(prev, ch)
} else {
eb.subscribers[topic] = append([]EventChannel{}, ch)
}
eb.stats.incSubscriberCountByTopic(topic)
}
// SubscribeCallback provides a simple wrapper that allows to directly register CallbackFunc instead of channels.
func (eb *EventBus) SubscribeCallback(topic string, callable CallbackFunc) {
ch := NewEventChannel()
eb.SubscribeChannel(topic, ch)
go func(callable CallbackFunc) {
evt := <-ch
callable(evt.Topic, evt.Data)
evt.Done()
}(callable)
eb.stats.incSubscriberCountByTopic(topic)
}
// HasSubscribers Check if a topic has subscribers.
func (eb *EventBus) HasSubscribers(topic string) bool {
return len(eb.getSubscribingChannels(topic)) > 0
}
// Stats returns the stats map.
func (eb *EventBus) Stats() *Stats {
return eb.stats
}

102
lcars_v4/experiments.go Normal file
View 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
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View 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();
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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 &#169; 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>

View 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 &#169; 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>

View 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_v4/go.mod Normal file
View 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_v4/go.sum Normal file
View 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=

View 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()
}()
}

221
lcars_v4/main.go Normal file
View File

@ -0,0 +1,221 @@
package main
import (
"embed"
"fmt"
"ld/eventbus"
"ld/interval"
"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()
ebus := eventbus.NewEventBus()
interval.SetInterval(func() {
fmt.Println("------------------")
}, 10)
interval.SetInterval(func() {
ebus.Publish("foo:baz", eventbus.Data{"value": "Hallo Welt"})
}, 100)
runEventbus(ebus)
run()
}
func runEventbus(ebus *eventbus.EventBus) {
// Create a new instance
eventChannel := eventbus.NewEventChannel()
// Subscribe to "foo:baz" - or use a wildcard like "foo:*"
ebus.SubscribeChannel("foo:baz", eventChannel)
ebus.SubscribeChannel("pups-klo", eventChannel)
ebus.SubscribeChannel("ömme*", eventChannel)
eventChannelTopic := ebus.Subscribe("ömmels")
// Subscribe with existing channel use
// eventbus.SubscribeChannel("foo:*", eventChannel)
// Wait for the incoming event on the channel
go func() {
for evt := range eventChannel {
fmt.Println("FIRST", evt.Topic, evt.Data)
evt.Done()
}
}()
go func() {
for evt := range eventChannel {
fmt.Println("SECOND", evt.Topic, evt.Data)
evt.Done()
}
}()
go func() {
for evt := range eventChannelTopic {
fmt.Println("ÖMMELS", evt.Topic, evt.Data)
evt.Done()
}
}()
for i := 0; i < 1000; i++ {
ebus.Publish("foo:baz", eventbus.Data{"value": i})
ebus.Publish("pups-klo", eventbus.Data{"value": i})
ebus.Publish("pups-klo", eventbus.Data{"value": i})
ebus.Publish("ömmels", eventbus.Data{"value": i})
ebus.Publish("ömmels", eventbus.Data{"value": i * 10})
ebus.Publish("ömmels", eventbus.Data{"value": i * 100})
}
}
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
}

2
lcars_v4/notes.txt Normal file
View File

@ -0,0 +1,2 @@
<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>
<a href="https://www.flaticon.com/free-icons/geek" title="geek icons">Geek icons created by Pixel perfect - Flaticon</a>

105
lcars_v4/server/server.go Normal file
View File

@ -0,0 +1,105 @@
// Copyright 2024 codeM GmbH
// Author: Thomas Hedeler
package server
import (
"embed"
"ld/eventbus"
"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
Ebus *eventbus.EventBus
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

View 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
}
}
}

440
lcars_v4/sqlite/database.go Normal file
View File

@ -0,0 +1,440 @@
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 {
query := `
PRAGMA analysis_limit = 400;
PRAGMA optimize;
`
_, err := d.DB().Exec(query)
if err != nil {
return err
}
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 synchronous = off;
PRAGMA foreign_keys = on;
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA cache_size = 2000;
PRAGMA temp_store = memory;
PRAGMA mmap_size = 30000000000;
PRAGMA page_size = 4096;
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
}

2
lcars_v4/state/events.go Normal file
View File

@ -0,0 +1,2 @@
package state