=setup: add heats
=priority: load heats
This commit is contained in:
Miki 2023-12-14 12:17:23 +01:00
parent 50a1d74e65
commit c6f4800273
16 changed files with 436 additions and 146 deletions

View file

@ -8,14 +8,15 @@ func (w *Webapp) initApi() {
http_api := w.Engine.Group("/api")
http_api.GET("/priority", w.GetPriority)
http_api.GET("/load", w.LoadHeats)
http_api.POST("/priority", w.SetPriority)
http_api.POST("/start", w.StartTimer)
// SSE
http_api.GET("/sse", HeadersMiddleware(), stream.serveHTTP(), stream.retvalSSE())
http_api.POST("/setup", w.SetupHeat)
http_api.POST("/startheat", w.StartHeat)
http_api.POST("/saveheat", w.SaveHeat)
http_api.POST("/deleteheat", w.DeleteHeat)
http_api.GET("/loadheats", w.LoadHeats)
// // Surfers
// http_api.GET("/surfers", w.GetSurfers)

Binary file not shown.

179
backend/heats.go Normal file
View file

@ -0,0 +1,179 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
scribble "github.com/nanobox-io/golang-scribble"
)
type Surfer struct {
Name string `json:"name"`
Color string `json:"color"`
Priority string `json:"priority"`
Score string `json:"score"`
}
type Heat struct {
Name string `json:"name"`
Category string `json:"category"`
Number int `json:"number"`
Timer int `json:"timer"`
Status string `json:"status"`
Surfers []Surfer `json:"surfers"`
}
func heatName(heat Heat) string {
return fmt.Sprintf("%s.%d.%s", heat.Name, heat.Number, heat.Category)
}
func (w *Webapp) SaveHeat(c *gin.Context) {
var heat Heat
err := c.ShouldBind(&heat)
if err != nil {
log.Printf("req error: %+v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("heat: %+v", heat)
heat.Status = "idle"
err = w.DB.Write("Heat", heatName(heat), heat)
if err != nil {
log.Printf("set error: %+v", err)
c.JSON(http.StatusInternalServerError, gin.H{"status": fmt.Sprintf("Error: %+v", err)})
return
}
c.JSON(http.StatusOK, gin.H{"status": "saved"})
}
func (w *Webapp) DeleteHeat(c *gin.Context) {
var heat Heat
err := c.ShouldBind(&heat)
if err != nil {
log.Printf("req error: %+v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("heat: %+v", heat)
err = w.DB.Delete("Heat", heatName(heat))
if err != nil {
log.Printf("set error: %+v", err)
c.JSON(http.StatusInternalServerError, gin.H{"status": fmt.Sprintf("Error: %+v", err)})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}
func (w *Webapp) LoadRunning(c *gin.Context) {
heats := loadHeats(w.DB)
for _, heat := range heats {
if heat.Status == "running" {
c.JSON(http.StatusOK, heat)
return
}
}
}
func (w *Webapp) LoadHeats(c *gin.Context) {
heats := loadHeats(w.DB)
c.JSON(http.StatusOK, heats)
log.Printf("heats: %+v", heats)
}
func (w *Webapp) StartHeatTimer(c *gin.Context) {
var msg Message
var err error
var timer time.Duration
if w.Stream.Start {
c.JSON(http.StatusOK, w.Stream.Duration)
return
}
err = c.ShouldBind(&msg)
if err != nil {
log.Printf("req error: %+v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
timer, err = time.ParseDuration(msg.Duration)
if err != nil {
log.Printf("req error: %+v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
w.Stream.Duration = timer
w.Stream.Start = true
startHeat(w.DB, *w.Stream.Heat)
log.Printf("start timer %s - received %s", w.Stream.Duration, msg.Duration)
c.JSON(http.StatusOK, w.Stream.Duration)
}
func loadHeats(db *scribble.Driver) []Heat {
records, err := db.ReadAll("Heat")
if err != nil {
fmt.Printf("read error: %+v", err)
}
heats := make([]Heat, 0)
for _, record := range records {
var heat Heat
err = json.Unmarshal([]byte(record), &heat)
if err != nil {
fmt.Printf("decode error: %+v", err)
}
heats = append(heats, heat)
}
return heats
}
func startHeat(db *scribble.Driver, heat Heat) error {
log.Printf("heat: %+v", heat)
heat.Status = "running"
err := db.Write("Heat", heatName(heat), heat)
if err != nil {
log.Printf("set error: %+v", err)
return err
}
return nil
}
func stopHeat(db *scribble.Driver, heat Heat) error {
log.Printf("heat: %+v", heat)
heat.Status = "ended"
err := db.Write("Heat", heatName(heat), heat)
if err != nil {
log.Printf("set error: %+v", err)
return err
}
return nil
}

View file

@ -1,8 +1,6 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
@ -37,27 +35,6 @@ func (w *Webapp) StartTimer(c *gin.Context) {
c.JSON(http.StatusOK, w.Stream.Duration)
}
func (w *Webapp) LoadHeats(c *gin.Context) {
records, err := w.DB.ReadAll("Heat")
if err != nil {
fmt.Printf("read error: %+v", err)
}
heats := make([]Heat, 0)
for _, record := range records {
var heat Heat
err = json.Unmarshal([]byte(record), &heat)
if err != nil {
fmt.Printf("decode error: %+v", err)
}
heats = append(heats, heat)
}
c.JSON(http.StatusOK, heats)
log.Printf("heats: %+v", heats)
}
func (w *Webapp) GetPriority(c *gin.Context) {
log.Printf("send priority %s", w.Stream.StatusPriority)

View file

@ -1,45 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
type Surfer struct {
Name string `json:"name"`
Color string `json:"color"`
Priority string `json:"priority"`
Score string `json:"score"`
}
type Heat struct {
Name string `json:"name"`
Category string `json:"category"`
Number int `json:"number"`
Timer int `json:"timer"`
Surfers []Surfer `json:"surfers"`
}
func (w *Webapp) SetupHeat(c *gin.Context) {
var heat Heat
err := c.ShouldBind(&heat)
if err != nil {
log.Printf("req error: %+v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("heat: %+v", heat)
c.JSON(http.StatusOK, gin.H{"status": "added"})
err = w.DB.Write("Heat", fmt.Sprintf("%s.%s.%d", heat.Name, heat.Category, heat.Number), heat)
if err != nil {
log.Printf("set error: %+v", err)
}
}

View file

@ -20,6 +20,7 @@ type SurferLive struct {
type Message struct {
Surfers []SurferLive `json:"surfers"`
Priority []string `json:"priority"`
Heat Heat `json:"heat"`
Duration string `json:"duration"`
Source string `json:"source"`
Msg string `json:"msg"`
@ -36,7 +37,7 @@ type ClientChan chan Message
type IPAddress string
type PriorityStream struct {
type SseStream struct {
// Events are pushed to this channel by the main events-gathering routine
Message chan Message
@ -52,16 +53,18 @@ type PriorityStream struct {
StatusPriority []string
Duration time.Duration
Start bool
Heat *Heat
}
// Initialize event and Start procnteessing requests
func NewServer() (sse *PriorityStream) {
sse = &PriorityStream{
func NewServer() (sse *SseStream) {
sse = &SseStream{
Message: make(chan Message),
NewClients: make(chan Client),
ClosedClients: make(chan ClientChan),
TotalClients: make(map[ClientChan]IPAddress),
Start: false,
Heat: &Heat{},
}
go sse.listen()
@ -73,7 +76,7 @@ func NewServer() (sse *PriorityStream) {
// It Listens all incoming requests from clients.
// Handles addition and removal of clients and broadcast messages to clients.
func (stream *PriorityStream) listen() {
func (stream *SseStream) listen() {
for {
select {
// Add new available client
@ -108,7 +111,7 @@ func HeadersMiddleware() gin.HandlerFunc {
}
}
func (stream *PriorityStream) serveHTTP() gin.HandlerFunc {
func (stream *SseStream) serveHTTP() gin.HandlerFunc {
return func(c *gin.Context) {
// Initialize client channel
clientChan := make(ClientChan)
@ -132,7 +135,7 @@ func (stream *PriorityStream) serveHTTP() gin.HandlerFunc {
}
}
func (stream *PriorityStream) retvalSSE() gin.HandlerFunc {
func (stream *SseStream) retvalSSE() gin.HandlerFunc {
return func(c *gin.Context) {
if len(stream.StatusPriority) > 0 {
@ -159,14 +162,14 @@ func (stream *PriorityStream) retvalSSE() gin.HandlerFunc {
}
}
func (stream *PriorityStream) SendPriority(pri []string) {
func (stream *SseStream) SendPriority(pri []string) {
stream.Message <- Message{
Priority: pri,
Mode: Priority.String(),
}
}
func (stream *PriorityStream) timer() {
func (stream *SseStream) timer() {
for {
if stream.Start {
timer := time.NewTimer(stream.Duration)
@ -180,6 +183,7 @@ func (stream *PriorityStream) timer() {
}
stream.Message <- msg
log.Printf("stop timer %+v", stream.Duration)
stream.Heat.Status = "ended"
continue
default:
if len(stream.TotalClients) > 0 {

View file

@ -1 +1 @@
{"version":"1702477632110"}
{"version":"1702551352225"}

Binary file not shown.

Binary file not shown.

View file

@ -5,17 +5,17 @@
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="modulepreload" href="/_app/immutable/entry/start.f2f23bb5.js">
<link rel="modulepreload" href="/_app/immutable/entry/start.99d6dd70.js">
<link rel="modulepreload" href="/_app/immutable/chunks/scheduler.d1a939e8.js">
<link rel="modulepreload" href="/_app/immutable/chunks/singletons.f3cb4e26.js">
<link rel="modulepreload" href="/_app/immutable/entry/app.c3a1467b.js">
<link rel="modulepreload" href="/_app/immutable/chunks/singletons.2b3a0a27.js">
<link rel="modulepreload" href="/_app/immutable/entry/app.3faab5fc.js">
<link rel="modulepreload" href="/_app/immutable/chunks/index.994dd985.js">
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">
<script>
{
__sveltekit_1cx8eia = {
__sveltekit_1lbm7l = {
base: "",
env: {}
};
@ -23,8 +23,8 @@
const element = document.currentScript.parentElement;
Promise.all([
import("/_app/immutable/entry/start.f2f23bb5.js"),
import("/_app/immutable/entry/app.c3a1467b.js")
import("/_app/immutable/entry/start.99d6dd70.js"),
import("/_app/immutable/entry/app.3faab5fc.js")
]).then(([kit, app]) => {
kit.start(app, element);
});

Binary file not shown.

Binary file not shown.

View file

@ -10,7 +10,7 @@ import (
type Webapp struct {
Engine *gin.Engine
Stream *PriorityStream
Stream *SseStream
DB *scribble.Driver
}
@ -44,6 +44,9 @@ func InitHttp(d *scribble.Driver) *Webapp {
setup := router.Group("/setup")
setup.Static("/", "./static/setup")
draws := router.Group("/draws")
draws.Static("/", "./static/draws")
sapp := router.Group("/_app")
sapp.Static("/", "./static/_app")

View file

@ -0,0 +1,97 @@
<script>
import Logo from "$lib/img/topscorer_logo_web.png"
$: heats = [];
loadHeats();
async function loadHeats() {
const res = await fetch(`/api/loadheats`);
const data = await res.json();
for (let i in data) {
heats = [...heats, data[i]];
console.log(`${i} retval: ${JSON.stringify(data[i])}`);
}
}
</script>
<div class="header">
<img class="img" src={Logo} alt="logo">
<span class="title" style="color: aliceblue;">Heat Draws</span>
</div>
<hr>
{#each heats as heat}
<table>
<tr>
{#if heat.status === 'running'}
<th colspan="2" class="running">
{heat.name} ({heat.number}) {heat.category}
</th>
{:else if heat.status === 'ended'}
<th colspan="2" class="ended">
{heat.name} ({heat.number}) {heat.category}
</th>
{:else}
<th colspan="2">
{heat.name} ({heat.number}) {heat.category}
</th>
{/if}
</tr>
{#each heat.surfers as surfer }
<tr>
<td>
{surfer.name}
</td>
<td style="background-color: {surfer.color};">
{surfer.color}
</td>
</tr>
{/each}
</table>
<hr>
{/each}
<style>
.header {
background-color: black;
display: flex;
width: 100%;
justify-content: space-between;
text-align: center;
align-items: center;
}
.header .img {
height: 3rem;
}
.header .title {
font-size: 3rem;
margin: 0 auto;
}
table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
th.running {
background-color: green;
animation: blinker 2s linear infinite;
}
th.ended {
background-color: red;
text-decoration: line-through 2px yellow;
}
@keyframes blinker {
50% {
opacity: 0;
}
}
</style>

View file

@ -30,6 +30,8 @@
let end = false;
let start = false;
loadHeats();
function Subscribe() {
const sse = new EventSource(`/api/sse`);
console.log('subscribe');
@ -71,7 +73,7 @@
}
}
async function startCounter() {
async function startHeat() {
const res = await fetch(`/api/start`, {
method: 'POST',
body: JSON.stringify({
@ -88,7 +90,7 @@
}
async function loadHeats() {
const res = await fetch(`/api/load`);
const res = await fetch(`/api/loadheats`);
const data = await res.json();
for (let i in data) {
heats[i] = data[i];
@ -98,7 +100,9 @@
function setHeat(id) {
console.log(`setHeat: ${id}`);
if (id === 99) {
if (id === "99") {
min = 0;
surfers = []
return;
}
min = heats[id].timer;
@ -170,11 +174,11 @@
<svelte:window bind:innerWidth={width} />
<div class="header">
<button class="button" on:click={() => loadHeats()} disabled={start}>Load</button>
<!-- <button class="button" on:click={() => loadHeats()} disabled={start}>Load</button> -->
<select name="heats" id="heats" bind:value={heat_number} on:change={() => setHeat(heat_number)}>
<option value=99>Select</option>
<option value="99">Select Heat</option>
{#each heats as heat, id}
<option value={id}>{heat.name} {heat.category} {heat.number}</option>
<option value={id}>{heat.name} {heat.number} {heat.category}</option>
{/each}
</select>
{#if !end}
@ -182,7 +186,7 @@
{:else}
<div class="timer" style="color: red">{pad2(min)}:{pad2(sec)}</div>
{/if}
<button class="button" on:click={() => startCounter()} disabled={start}>Start</button>
<button class="button" on:click={() => startHeat()} disabled={start}>Start</button>
</div>
<div class="container">

View file

@ -1,45 +1,91 @@
<script>
import { onMount } from 'svelte';
import Logo from "$lib/img/topscorer_logo_web.png"
$: surfers = 2;
let surfer_list = new Array();
$: heats = [];
let surfer_list = [];
$: heat = {};
surfer_list.push({
name: '',
color: '#000000',
score: '',
priority: ''
});
resetHeat();
loadHeats();
surfer_list.push({
name: '',
color: '#ffffff',
score: '',
priority: ''
});
function resetHeat() {
surfers = 2;
surfer_list = new Array();
surfer_list.push({
name: '',
color: '',
score: '',
priority: ''
});
surfer_list.push({
name: '',
color: '',
score: '',
priority: ''
});
let heat = {
number: '',
name: '',
category: '',
timer: '',
surfers: surfer_list
heat = {
number: '',
name: '',
category: '',
timer: '',
surfers: surfer_list
}
}
async function loadHeats() {
const res = await fetch(`/api/loadheats`);
const data = await res.json();
for (let i in data) {
heats[i] = data[i];
console.log(`${i} retval: ${JSON.stringify(data[i])}`);
}
}
async function deleteHeat(id) {
const res = await fetch(`/api/deleteheat`, {
method: 'POST',
body: JSON.stringify(heats[id]),
headers: {
'Content-Type': 'application/json'
}
});
console.log(`retval: ${JSON.stringify(res)}`);
console.log(JSON.stringify(heats[id]));
resetHeat();
loadHeats();
}
function setHeat(id) {
resetHeat();
console.log(`setHeat: ${id}`);
console.log(heats[id]);
heat.number = heats[id].number;
heat.name = heats[id].name;
heat.category = heats[id].category;
heat.timer = heats[id].timer;
surfer_list = heats[id].surfers;
surfers = surfer_list.length;
}
function addSurfers() {
surfers++;
surfer_list.push({
name: '',
color: '#000000',
color: '',
score: '',
priority: ''
});
});
}
function removeSurfers() {
surfers--;
if (surfers < 2) surfers = 2;
surfer_list.pop();
}
async function save() {
@ -73,7 +119,9 @@
return;
}
const res = await fetch(`/api/setup`, {
heat.surfers = surfer_list;
const res = await fetch(`/api/saveheat`, {
method: 'POST',
body: JSON.stringify(heat),
headers: {
@ -83,6 +131,9 @@
console.log(`retval: ${JSON.stringify(res)}`);
console.log(JSON.stringify(heat));
resetHeat();
loadHeats();
}
function hasDuplicateColors(arr) {
@ -109,40 +160,69 @@
console.log(`element: ${element[elementName]}`);
}
onMount(() => {
resetHeat();
loadHeats();
});
</script>
<div class="header">
<img class="img" src={Logo} alt="logo">
<span class="title" >Heat setup</span>
<span class="title" style="color: aliceblue;">Heat setup</span>
</div>
<div class="container">
<div class="heat">
<label class="label" for="name">Name</label>
<input bind:value={heat.name} on:change={capitalize(heat, "name")} id="name" type="text">
<label class="label" for="number">Number</label>
<input bind:value={heat.number} id="number" type="number" min="1" max="20">
<label class="label" for="category">Category</label>
<input bind:value={heat.category} on:change={capitalize(heat, "category")} id="category" type="text">
<label class="label" for="number">Number</label>
<input bind:value={heat.number} id="number" on:keydown={(event) => {event.preventDefault()}} type="number" min="1" max="20">
<label class="label" for="timer">Timer</label>
<input bind:value={heat.timer} id="timer" on:keydown={(event) => {event.preventDefault()}} type="number" min="5" max="60" step="5">
</div>
<hr>
<button class="plus" on:click={() => {addSurfers();}}>+</button>
<button class="plus" on:click={() => {removeSurfers();}}>-</button>
<span class="surfers">{surfers}</span>
{#each Array(surfers) as _, surfer}
<div class="surfer">
<label class="label" for="name{surfer}">Name</label>
<input bind:value={surfer_list[surfer].name} on:change={capitalize(heat.surfers[surfer], "name")} id="name{surfer}" type="text">
<input bind:value={surfer_list[surfer].name} on:change={capitalize(surfer_list[surfer], "name")} id="name{surfer}" type="text">
<label class="label" for="color{surfer}">Color</label>
<input bind:value={surfer_list[surfer].color} type="color" id="color{surfer}">
<select name="color" id="color{surfer}" bind:value={surfer_list[surfer].color} style="background-color: {surfer_list[surfer].color};">
<!-- <option value="red" style="background-color: red;">Select color</option> -->
<option value="red" style="background-color: red;">Red</option>
<option value="blue" style="background-color: blue;">Blue</option>
<option value="green" style="background-color: green;">Green</option>
<option value="yellow" style="background-color: yellow;">Yellow</option>
<option value="orange" style="background-color: orange;">Orange</option>
<option value="violet" style="background-color: violet;">Violet</option>
</select>
<!-- <input bind:value={surfer_list[surfer].color} type="color" id="color{surfer}"> -->
</div>
{/each}
</div>
<button class="plus" on:click={() => {save();}}>SET</button>
<button class="plus" on:click={() => {resetHeat();}}>RESET</button>
<hr>
{#each heats as h, id}
<div class="surfer">
<button on:click={() => {setHeat(id);}}>{h.name} {h.number} {h.category}</button>
<button class="plus" on:click={() => {deleteHeat(id);}}>X</button>
</div>
{/each}
<!-- <hr> -->
<!-- <h2>{JSON.stringify(surfer_list)}</h2> -->
<style>
@ -152,7 +232,6 @@
.header {
background-color: black;
color: aliceblue;
display: flex;
width: 100%;
justify-content: space-between;
@ -170,19 +249,20 @@
}
.label {
border: 2px solid #111;
background-color: #111;
border: 2px solid #555;
background-color: gray;
border-radius: 8px;
padding: 2px;
}
.heat {
font-size: 1.3rem;
margin-top: 2px;
margin-top: 8px;
margin-bottom: 2px;
width: 100%;
margin-left: auto;
margin-right: auto;
color: lightcyan;
/* color: lightcyan; */
display: inline-block;
}
@ -199,7 +279,7 @@
font-size: 1.3rem;
margin-top: 2px;
margin-bottom: 2px;
color: lightcyan;
/* color: lightcyan; */
display: inline-flex;
width: 100%;
margin-left: auto;
@ -217,29 +297,19 @@
}
/* select {
font-size: 1.2rem;
border-radius: 6px;
margin-left: 0.1rem;
margin-right: 0.1rem;
padding-top: 2px;
padding-bottom: 2px;
width: 5rem;
}
option {
width: 1rem;
padding-left: 25px;
background-size: 1.2rem 100%;
background-repeat: no-repeat;
background-position: 0px 0px;
transition: border 0.3s
} */
.plus {
border-radius: 8px;
margin-left: 0.2rem;
margin-right: 0.2rem;
margin-bottom: 8px;
margin-top: 8px;
}
.surfers {
border: 1px solid #111;
background-color: yellow;
border-radius: 8px;
padding: 5px;
}
</style>