-from flask import Flask, render_template, send_from_directory
+from flask import Flask, render_template, send_from_directory, request
from flask_socketio import SocketIO, emit
+import time
import os
+from .utils import deindex
+
from director.database.read import read as database_read
+from director.database.read import read_all_current as database_read_all_current
+from director.database.read import read_indexes as database_read_indexes
+from director.database.read import read_views as database_read_views
+from director.database.write import write as database_write
app = Flask(__name__, instance_relative_config=False)
-socketio = SocketIO(app, logger=True, engineio_logger=True)
+socketio = SocketIO(app, logger=False, engineio_logger=False)
@app.route("/", methods=['get'])
def index():
return render_template("index.html")
-@app.route("/admin", methods=['get'])
+@app.route("/info/<string:screen>", methods=["get"])
+def info(screen):
+ return render_template(f"info/{screen}.html")
+
+@app.route("/admin", methods=["get"])
def admin():
- indexes, current = database_read()
- return render_template("admin.html", indexes=indexes, current=current)
+ return render_template("admin.html", indexes=database_read_indexes())
+
+@app.route("/admin/<string:table_name>", methods=["get", "post"])
+def admin_table(table_name):
+ start = time.time()
+
+ if request.method == "POST":
+ database_write(request.form)
+
+ emit(
+ "update",
+ database_read_views(table_name),
+ json=True, namespace="/", broadcast=True
+ )
+
+ end = time.time()
+
+ return render_template(
+ "table.html",
+ indexes=database_read_indexes(),
+ table=database_read(table_name),
+ table_name=table_name,
+ time_taken = end - start
+ )
@app.route("/script/<path:filename>", methods=["get"])
def script(filename):
return send_from_directory(os.path.join(app.root_path, "build"), filename)
-@app.route("/test")
-def test():
- return [database_read()]
- #emit("update", ["data__product", {"data":{"product":{"currentPrice":100}}}], json=True, namespace="/", broadcast=True)
-
- return ""
+@socketio.on('connect')
+def connecting():
+ emit("update", database_read_views(), json=True, namespace="/", broadcast=True)
check if file exists and pragam check the database on init
"""
-from .utils import integrity_check
+from .utils import integrity_check, optimize
import sqlite3
import os
assert os.path.exists("./data/main.db"), "Database missing"
assert integrity_check(), "Database integrity error"
+
+optimize()
import sqlite3
-from .utils import list_tables
+from .utils import list_all, list_views
-def read():
- tables = list_tables()
+def read_all_current():
+ tables = list_all()
+ indexes = read_indexes()
with sqlite3.connect("./data/main.db") as connection:
cursor = connection.cursor()
data = {}
for table in tables:
+ if table == "indexes": continue
+
+ if "__view" in table:
+ index_table = table.split("__view")[0]
+ else: index_table = table
+
cursor.execute(f"SELECT * FROM {table};")
- names = list(map(lambda x: x[0], cursor.description))
- rows = [dict(zip(names, row)) for row in cursor.fetchall()]
+ names = list(map(lambda x: f"{table}~~{x[0]}", cursor.description))
+ row = cursor.fetchall()[indexes[f"int--{index_table}"]]
+
+ data.update(dict(zip(names, row)))
+
+ return data
+
+def read(table):
+ """
+ reads a table by name
+ """
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+
+ cursor.execute(f"SELECT * FROM {table};")
+
+ names = list(map(lambda x: x[0], cursor.description))
+ rows = [dict(zip(names, row)) for row in cursor.fetchall()]
+
+ return rows
+
+def read_views(table=None):
+ """
+ Reads views. If a table is specified, it reads all views related to that
+ table. If not it reads all views.
+ """
+ if table is None:
+ related = list_views()
+ else:
+ related = [view for view in list_views() if view.split("__view")[0] == table]
+
+ data = {}
+ for view in related:
+ for key, value in read(view)[0].items():
+ data.update({f"{view.replace('__view', '')}~~{key}": value})
+
+ return data
+
+def read_indexes():
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+
+ cursor.execute("SELECT * FROM indexes;")
- if table == "indexes":
- indexes = rows[0]
- else:
- data.update({table: rows})
+ names = list(map(lambda x: x[0], cursor.description))
+ indexes = [dict(zip(names, row)) for row in cursor.fetchall()][0]
- return indexes, data
+ return indexes
return integrity.fetchone()[0] == "ok"
-def list_tables():
+def optimize():
"""
- get a list of all the tables in the database
+ check the database is ok and return true if it is
+ """
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+ integrity = cursor.execute('PRAGMA optimize')
+
+def list_all():
+ """
+ get a list of all the tables and views in the database
+ """
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+ cursor.execute("SELECT name FROM sqlite_master WHERE type IN ('table', 'view');")
+ return [table[0] for table in cursor.fetchall()]
+
+def list_views():
+ """
+ get a list of all the views in the database
"""
with sqlite3.connect("./data/main.db") as connection:
cursor = connection.cursor()
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='view';")
return [table[0] for table in cursor.fetchall()]
--- /dev/null
+import sqlite3
+
+from .utils import list_all
+from .read import read
+
+def write(data):
+
+ with sqlite3.connect("./data/main.db") as connection:
+ cursor = connection.cursor()
+
+ for key in data:
+ table, index, feild = key.split("~~")
+
+ cursor.execute(f"""
+ UPDATE {table}
+ SET '{feild}' = '{data[key]}'
+ WHERE ROWID = {int(index) + 1};
+ """)
+
+ connection.commit()
-interface Body {
- [index: string]: number;
-}
+const outlineStyle: string = "4px solid green";
+const outlineTime: number = 500; // ms
export function setupTrigger() {
// setup all trigger buttons to send post request on click
for (let e of el) {
e.addEventListener("click", (event) => {
- let time: number = Math.round(Date.now() / 1000);
- let body: Body = {};
+ if (event.target instanceof HTMLElement) {
+ let time: number = Math.round(Date.now() / 1000);
+ let body: FormData = new FormData();
+ let delay = document.getElementById(`${event.target.getAttribute('name')}+delay`);
+
+ if (delay instanceof HTMLInputElement) {
+ body.set(`${event.target.getAttribute('name')}`, `${time}+${delay.value}`);
+
+ fetch("", {
+ method: "POST",
+ body: body
+ });
- if (event.target instanceof Element) {
- body[`${event.target.getAttribute('name')}`] = time;
+ // trigger the button to turn green for approx same time (or 1s)
+ let timeout: number = delay.value as unknown as number * 1000;
+ if (timeout < outlineTime) { timeout = outlineTime; }
- fetch(".", {
- method: "POST",
- body: JSON.stringify(body)
- });
+ if ( event.target instanceof HTMLElement) {
+ event.target.style.outline = outlineStyle;
+ setTimeout(function () {
+ if ( event.target instanceof HTMLElement) {
+ event.target.style.outline = "none"
+ }
+ }, timeout);
+ }
+ }
}
})
}
--- /dev/null
+export function renderDiscount(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("discount");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ e.innerHTML = `${e.dataset.raw}%`;
+ }
+ }
+ }
+}
+
+export function renderReverseDiscount(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("reverseDiscount");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ e.innerHTML = `${100 - Number(e.dataset.raw)}%`;
+ }
+ }
+ }
+}
-const unstablePriceChance: number = 0.7;
-
-export function renderPrice(): void {
+export default function renderPrice(): void {
const el: HTMLCollectionOf<Element> =
document.getElementsByClassName("price");
}
}
}
-
-export function renderUnstablePrice(): void {
- const el: HTMLCollectionOf<Element> =
- document.getElementsByClassName("unstablePrice");
-
- for (let e of el) {
- if (e instanceof HTMLElement) {
- if (typeof e.dataset.raw === "string") {
- if (Math.random() < unstablePriceChance) {
- e.innerHTML = String(parseFloat(e.dataset.raw).toFixed(2));
- } else {
- e.innerHTML = String(parseFloat(e.dataset.raw));
- }
- }
- }
- }
-}
--- /dev/null
+export default function renderShowUntil(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("showUntil");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ let duration: number = (eval(e.dataset.raw) * 1000) - Date.now();
+ if (duration > 0) {
+ e.classList.add("showUntil-showing")
+ setTimeout(function () {e.classList.remove("showUntil-showing")}, duration)
+ }
+ }
+ }
+ }
+}
--- /dev/null
+export function setupSounds() {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("sound");
+
+ const context = new AudioContext();
+ if (el.length > 0) {
+ for (let e of el) {
+ if (e instanceof HTMLMediaElement) {
+ const track = context.createMediaElementSource(e);
+ track.connect(context.destination);
+ }
+ }
+ }
+}
+
+export function playSounds() {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("sound");
+
+ for (let e of el) {
+ if (e instanceof HTMLMediaElement) {
+ if (typeof e.dataset.raw === "string") {
+ if (e.dataset.rawLast != e.dataset.raw) {
+ e.play();
+ }
+ e.dataset.rawLast = e.dataset.raw;
+ }
+ }
+ }
+}
--- /dev/null
+export default function renderState(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("state");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ for (let className of e.classList) {
+ if (className.startsWith("state-")) {
+ e.classList.remove(className);
+ }
+ }
+
+ e.classList.add(`state-${e.dataset.raw}`);
+ }
+ }
+ }
+}
--- /dev/null
+export function setupStyle(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("style");
+
+ for (let i = 0; i < el.length; i++) {
+ const stylesheet = new CSSStyleSheet();
+ document.adoptedStyleSheets.push(stylesheet);
+ }
+}
+
+export function renderStyle(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("style");
+
+ for (let [i, e] of Array.from(el).entries()) {
+ if (e instanceof HTMLElement) {
+ let variable;
+ for (let className of e.classList) {
+ if (className.includes("--")) {
+ variable = className.split("--")[1];
+ }
+ }
+
+ if (typeof e.dataset.raw === "string") {
+ document.adoptedStyleSheets[i].replace(`
+ :root {--${variable}: ${e.dataset.raw};}
+ `);
+ }
+ }
+ }
+}
-export function renderText(): void {
+export default function renderText(): void {
const el = document.querySelectorAll(".text,.title");
for (let e of el) {
if (e instanceof HTMLElement) {
--- /dev/null
+import { timeSince, timeUntil, timeFormatBroadcast } from "./utils.js";
+
+const timeUpdate: number = 1000; // ms
+
+export function setupTimeSince(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("since");
+
+ if (el.length > 0) {
+ setInterval(
+ function () {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("since");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ let [hours, minutes, seconds] = timeSince(eval(e.dataset.raw))
+ e.innerHTML = timeFormatBroadcast(hours, minutes, seconds);
+ }
+ }
+ }
+ },
+ timeUpdate
+ );
+ }
+}
+
+export function setupTimeUntil(): void {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("until");
+
+ if (el.length > 0) {
+ setInterval(
+ function () {
+ const el: HTMLCollectionOf<Element> =
+ document.getElementsByClassName("until");
+
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ if (typeof e.dataset.raw === "string") {
+ let [hours, minutes, seconds] = timeUntil(eval(e.dataset.raw))
+ e.innerHTML = timeFormatBroadcast(hours, minutes, seconds);
+ }
+ }
+ }
+ },
+ timeUpdate
+ );
+ }
+}
-import { renderText } from "./text.js";
-import { renderPrice, renderUnstablePrice } from "./price.js"
import { io } from "socket.io-client";
-// apply a number of sequential keys to an object
-function objectPath(obj: any, path: string[]): any {
- let key: string|undefined = path.shift();
- if (typeof key === "undefined") {
- return obj
- } else {
- return objectPath(obj[key], path)
- }
-}
+import { renderDiscount, renderReverseDiscount } from "./discount.js";
+import { setupTimeSince, setupTimeUntil } from "./time.js";
+import { setupSounds, playSounds } from "./sound.js";
+import { setupStyle, renderStyle } from "./style.js";
+import renderShowUntil from "./show_until.js";
+import renderState from "./state.js";
+import renderPrice from "./price.js"
+import renderText from "./text.js";
export default function setupUpdate(): void {
- const socket = io();
- socket.on("update", ([namespace, data]) => {
- const el = document.querySelectorAll(`.update[class*='${namespace}']`);
+ setupTimeSince();
+ setupTimeUntil();
+ setupSounds();
+ setupStyle();
- for (let e of el) {
- let c: string|undefined = Array.from(e.classList)
- .find((c) => c.startsWith(namespace))
-
- if (typeof c === "string") {
- let path = c.split("__");
- let result = objectPath(data, path);
+ const socket = io();
+ socket.on("update", (data) => {
+ for (const [key, value] of Object.entries(data)) {
+ const el = document.querySelectorAll(`.update[class~='${key}']`);
- if (typeof result !== "undefined" && e instanceof HTMLElement) {
- e.dataset.raw = result;
+ for (let e of el) {
+ if (e instanceof HTMLElement) {
+ e.dataset.raw = value as unknown as string;
}
}
}
+ renderReverseDiscount();
+ renderShowUntil();
+ renderDiscount();
+ renderState();
renderPrice();
- renderUnstablePrice();
- })
+ renderStyle();
+ renderText();
+ playSounds();
+ });
}
--- /dev/null
+export function timeUntil(end: number): number[] {
+ let current: number = Date.now() / 1000;
+ var time: number = end - current;
+
+ if (Math.sign(time) == -1) {time = 0;}
+
+ let hours: number = Math.floor(time / 3600);
+ time %= 3600;
+ let minutes: number = Math.floor(time / 60);
+ let seconds: number = Math.floor(time % 60);
+
+ return [hours, minutes, seconds]
+}
+
+export function timeSince(end: number): number[] {
+ let current: number = Date.now() / 1000;
+ var time: number = current - end;
+
+ if (Math.sign(time) == -1) {time = 0;}
+
+ let hours: number = Math.floor(time / 3600);
+ time %= 3600;
+ let minutes: number = Math.floor(time / 60);
+ let seconds: number = Math.floor(time % 60);
+
+ return [hours, minutes, seconds]
+}
+
+export function timeFormatBroadcast(hours: number, minutes: number, seconds: number): string {
+ var hoursString = hours.toString().padStart(2, "0");
+ var minutesString = minutes.toString().padStart(2, "0");
+ var secondsString = seconds.toString().padStart(2, "0");
+
+ return `${hoursString}h ${minutesString}' ${secondsString}"`
+}
--- /dev/null
+/* PAGE POSITIONING */
+
+:root {
+ --nav-background: lightgray;
+}
+
+body > * {
+ padding: 15px;
+ overflow: scroll
+}
+nav {
+ position: absolute;
+ top: 0; bottom: 0; left: 0;
+ right: max(80%, calc(100vw - 350px));
+ background-color: var(--nav-background);
+}
+main {
+ position: absolute;
+ top: 0; bottom: 0; right: 0;
+ left: min(20%, calc(350px));
+}
+
+
+/* THE OPTIONS GRID */
+
+.optionsGrid {
+ display: grid;
+ grid-template-columns: 50px auto auto auto;
+}
+.optionsGrid > * {
+ padding: 10px;
+ border: 1px solid gray;
+ margin-top: -1px;
+ margin-right: -1px;
+}
+.optionsGridSpacer { border: none; }
+
+
+:target {
+ outline: 4px solid blue;
+ z-index: 1;
+}
+.currentValue { color: red; }
+.currentValue[type="radio"] { background-color: red; }
--- /dev/null
+:root {
+ font-family: sans-serif;
+ color: white;
+ background-color: black;
+}
+
+.infoA { font-size: var(--aFontSize); }
+.infoB { font-size: var(--bFontSize); }
+.infoC { font-size: var(--cFontSize); }
+
+.title { text-transform: capitalize; }
+
+/* INFO A */
+.infoA div > * { padding: 20px; }
+.infoA div {
+ position: absolute;
+ top: 0; left: 0; bottom: 0; right: 0;
+ display: grid;
+ grid: none / 1fr;
+}
+
+/* INFO B */
+.infoB div > * { padding: 20px; }
+.infoB div {
+ position: absolute;
+ top: 0; left: 0; bottom: 0; right: 0;
+ display: grid;
+ grid: none / 1fr 1fr;
+}
+
+/* INFO C */
+.infoC div > * { padding: 20px; text-align: center; }
+.infoC div {
+ position: absolute;
+ top: 0; left: 0; bottom: 0; right: 0;
+ display: grid;
+ grid: none / 6fr 1fr 6fr;
+}
+
--- /dev/null
+.showUntil {
+ opacity: 0;
+ transition: opacity 0.5s;
+}
+.showUntil.showUntil-showing { opacity: 1; }
builds the admin page
#}
+<!DOCTYPE html>
<html>
<head>
+ <link rel="stylesheet" href="/static/admin.css">
+
<script type="module" defer>
import { setupTrigger } from "/script/admin.js";
</script>
</head>
<body>
- {% for table in current %}
- <details>
- <summary>{{ table }}</summary>
- <form>
- {% for row in current[table] %}
-
- {# CREATE RADIO BUTTON FOR ROW #}
-
- {% if loop.index0 == indexes["int--" ~ table] %}
- <input type="radio" name="{{ "indexes~~int--" ~ table }}" value="{{ loop.index0 }}" checked="checked" >
- {% else %}
- <input type="radio" name="{{ "indexes~~int--" ~ table }}" value="{{ loop.index0 }}" >
- {% endif %}
-
-
- {# CREATE FEILDS #}
-
- <div style="display: inline-block; vertical-align: middle; border: 1px solid gray;">
- {% for feild in row %}
-
- {# SELECT ELEMENT FROM TYPE #}
-
- {% set type, label = feild.split('--') %}
- {% set value = row[feild] %}
- {% set name = table ~ "~~" ~ feild %}
-
- {% if type == "textarea" %}
- {% include "admin/textarea.html" %}
- {% elif type == "text" %}
- {% include "admin/text.html" %}
- {% elif type == "number" %}
- {% include "admin/number.html" %}
- {% elif type == "float" %}
- {% include "admin/float.html" %}
- {% elif type == "trigger" %}
- {% include "admin/trigger.html" %}
- {% endif %}
-
- <br>
- {% endfor %}
- </div><br>
-
- {% endfor %}
- </form>
- </details>
- {% endfor %}
+ <nav>
+ <p><b>Tables</b></p>
+ <ul style="border: 1px solid gray">
+ {% for table in indexes %}
+ {% set table_name = table.split("--")[1] %}
+ <li><a href="/admin/{{ table_name }}">{{ table_name }}</a></li>
+ {% endfor %}
+ </ul>
+ </nav>
+ <main>
+ <h1>Admin</h1>
+ <ul>
+ <li><a href="/info/a">Info page A</a></li>
+ <li><a href="/info/b">Info page B</a></li>
+ <li><a href="/info/c">Info page C</a></li>
+ </ul>
+ </main>
</body>
</html>
--- /dev/null
+<label>{{ label }}</label>
+<span>
+ <input id="{{ name }}" type="color" name="{{ name }}" value="{{ value }}" />
+ {{ value }}
+</span>
+<span class="currentValue" style="background-color: {{ value }};"></span>
-<label>{{ label }}</label><br>
-<input type="number" name="{{ name }}" value="{{ value }}" step="0.01" />
+<label>{{ label }}</label>
+<input id="{{ name }}" type="number" name="{{ name }}" value="{{ value }}" step="0.01" />
+<span class="currentValue">{{ value }}</span>
-<label>{{ label }}</label><br>
-<input type="number" name="{{ name }}" value="{{ value }}" />
+<label>{{ label }}</label>
+<input id="{{ name }}" type="number" name="{{ name }}" value="{{ value }}" />
+<span class="currentValue">{{ value }}</span>
-<label>{{ label }}</label><br>
-<input type="text" name="{{ name }}" value="{{ value }}" />
+<label>{{ label }}</label>
+<input id="{{ name }}" type="text" name="{{ name }}" value="{{ value }}" />
+<span class="currentValue">{{ value }}</span>
-<label>{{ label }}</label><br>
-<textarea name="{{ name }}">
+<label>{{ label }}</label>
+<textarea id="{{ name }}" style="grid-column: 3 / span 2;" name="{{ name }}">
{{- value -}}
</textarea>
-<noscript style="color: red;">Trigger will not function without JS</noscript><br>
-<button type="button" name="{{ name }}" value="{{ label }}" class="trigger">
- {{ label }}
-</button>
+<span id="{{ name }}" style="grid-column: 2 / span 3;">
+ <noscript style="color: red;">Trigger will not function without JS<br></noscript>
+ <button type="button" name="{{ name }}" value="{{ label }}" class="trigger">
+ {{ label }}
+ </button>
+ <input type="number" id="{{ name }}+delay" value="{{ value.split('+')[1] }}"/><span> seconds delay <em>(This does not update the triggered data.)</em></span>
+</span>
<html>
<head>
<link rel="stylesheet" href="/static/style.css">
+ <link rel="stylesheet" href="/static/show_until.css">
<script type="importmap">
{
"imports": {
</script>
</head>
- <body>
- <span class="update data__product__currentPrice unstablePrice">This is some text</span>
+ <body class="update data__state~~text--state state">
+
+ {# DYNAMIC STYLING ELEMENTS #}
+ <span class="update style__colorscheme~~color--text style"></span>
+ <span class="update style__colorscheme~~color--textAccent style"></span>
+
+ {# SFX ELEMENTS #}
+ <audio src="/static/media/sfx/clock.wav" class="update data__clock~~number--positionDegrees sound"></audio>
+ <audio src="/static/media/sfx/shout.wav" class="update data~~trigger--shoutTrigger sound"></audio>
+
+ {# BEGINS #}
+
+ <div class="update data~~trigger--shoutTrigger showUntil">
+ <span class="update data~~text--shoutText title"></span>
+ </div>
</body>
</html>
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/info.css">
+ <script type="importmap">
+ {
+ "imports": {
+ "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
+ }
+ }
+ </script>
+ <script type="module" defer>
+
+import setupUpdate from "/script/update.js";
+setupUpdate();
+
+ </script>
+ </head>
+ <body class="infoA">
+ <span class="update style~~text--aFontSize style"></span>
+
+ <div>
+ <span>
+ <u style="color: pink;"><span class="update data__product~~text--name text"></span></u><br>
+ <span style="color: pink" class="update data__product~~text--description text"></span>
+ </span>
+
+ <span class="styleProductNotes update data__product~~textarea--notes text"></span>
+
+ <span style="color: yellow" class="update data~~textarea--anchorNotes text"></span>
+ </div>
+ </body>
+</html>
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/info.css">
+ <script type="importmap">
+ {
+ "imports": {
+ "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
+ }
+ }
+ </script>
+ <script type="module" defer>
+
+import setupUpdate from "/script/update.js";
+setupUpdate();
+
+ </script>
+ </head>
+ <body class="infoB">
+ <span class="update style~~text--bFontSize style"></span>
+
+ <div>
+ <span>
+ <span style="background-color: green" class="update data__product__currentPriceString~~text--currentPriceString text"></span> <em>(was <span class="update data__product__originalPriceString~~text--originalPriceString text"></span>)</em>
+ </span>
+
+ <span>
+ <span class="update data__product~~number--quantityCurrent text"></span>
+ left of
+ <span class="update data__product~~number--quantityTotal text"></span>
+ total
+ </span>
+
+ <span>
+ <span style="background-color: purple;" class="update data__product~~number--discount discount"></span> off
+ </span>
+
+ <span>
+ <span style="background-color: purple;" class="update data__product~~number--discount reverseDiscount"></span> full price
+ </span>
+
+ <span style="grid-column: 1 / span 2;">
+ Target: <span style="background-color: orange;" class="update data__product~~number--targetDiscount discount"></span> (<span class="update data__product__remainingDiscount~~number--remainingDiscount discount"></span> left to drop)
+ </span>
+
+ <span style="grid-column: 1 / span 2;">
+ Max: <span style="background-color: red;" class="update data__product~~number--maximumDiscount discount"></span> (<span class="update data__product__remainingMaximumDiscount~~number--remainingMaximumDiscount discount"></span> left to drop)
+ </span>
+ </div>
+ </body>
+</html>
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/info.css">
+ <script type="importmap">
+ {
+ "imports": {
+ "socket.io-client": "https://cdn.socket.io/4.8.3/socket.io.esm.min.js"
+ }
+ }
+ </script>
+ <script type="module" defer>
+
+import setupUpdate from "/script/update.js";
+setupUpdate();
+
+ </script>
+ </head>
+ <body class="infoC">
+ <span class="update style~~text--cFontSize style"></span>
+
+ <div>
+ <span>Current product:<br><span style="background-color: blue;" class="update data__product~~text--name text"></span></span>
+ <span>→</span>
+ <span>Next product:<br><span style="background-color: blue;" class="update data__product__next~~text--name text"></span></span>
+
+ <span>Current state: <span class="update data__state~~text--state title"></span></span>
+ <span>→</span>
+ <span>Next state: <span class="update data__state~~text--nextState title"></span></span>
+
+ <span style="grid-column: 1 / span 3; font-size: 2em; font-size: 2em;">
+ <span class="update data~~trigger--startTrigger since"></span> <span style="background-color: red;">T-<span class="update data__endTime~~text--endTime until"></span></span>
+ </span>
+ </div>
+ </body>
+</html>
--- /dev/null
+{#
+
+ builds the admin page
+
+#}
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="stylesheet" href="/static/admin.css">
+
+ <script type="module" defer>
+
+import { setupTrigger } from "/script/admin.js";
+setupTrigger();
+
+ </script>
+ </head>
+ <body>
+ <nav>
+ <p><a href="/admin">.. back</a></p>
+ <p><b>{{ table_name }}</b></p>
+ <p>Generated in {{ (time_taken * 1000)|round(2) }}ms</p>
+ <ol>
+ {% for row in table %}
+ <li style="border: 1px solid gray; margin-top: -1px;"><ul>
+ {% set group = loop.index0 %}
+ {% for key in row %}
+ <li><a href="#{{ table_name ~ "~~" ~ group ~ "~~" ~ key }}">{{ key }}</a></li>
+ {% endfor %}
+ </ul></li>
+ {% endfor %}
+ </ol>
+ </nav>
+ <main>
+ <h1>Table: {{ table_name }}</h1>
+ <form class="optionsGrid" method="post">
+ <input type="submit" value="Submit table" style="grid-column: 1 / span 4;">
+ <span class="optionsGridSpacer" style="grid-column: 1 / span 4;"></span>
+ {% for row in table %}
+
+ {# CREATE RADIO BUTTON FOR ROW #}
+
+ <span style="
+ grid-row: {{ ((row|length + 1) * loop.index0) + 3 }} / span {{ row|length }};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ ">
+ {% if loop.index0 == indexes["int--" ~ table_name] %}
+ <input type="radio" name="{{ "indexes~~0~~int--" ~ table_name }}" value="{{ loop.index0 }}" checked="checked" class="currentValue">
+ {% else %}
+ <input type="radio" name="{{ "indexes~~0~~int--" ~ table_name }}" value="{{ loop.index0 }}" >
+ {% endif %}
+ </span>
+
+ {# CREATE FEILDS #}
+
+ {% set group = loop.index0 %}
+ {% for feild in row %}
+
+ {# SELECT ELEMENT FROM TYPE #}
+
+ {% set type, label = feild.split('--') %}
+ {% set value = row[feild] %}
+ {% set name = table_name ~ "~~" ~ group ~ "~~" ~ feild %}
+
+ {% if type == "textarea" %}
+ {% include "admin/textarea.html" %}
+ {% elif type == "text" %}
+ {% include "admin/text.html" %}
+ {% elif type == "number" %}
+ {% include "admin/number.html" %}
+ {% elif type == "float" %}
+ {% include "admin/float.html" %}
+ {% elif type == "trigger" %}
+ {% include "admin/trigger.html" %}
+ {% elif type == "color" %}
+ {% include "admin/color.html" %}
+ {% endif %}
+ {% endfor %}
+ <span class="optionsGridSpacer" style="grid-column: 1 / span 4;"></span>
+
+ {% endfor %}
+ <input type="submit" value="Submit table" style="grid-column: 1 / span 4;">
+ </form>
+ </main>
+ </body>
+</html>
--- /dev/null
+def deindex(data, indexes):
+ deindexed = {}
+ for key in data:
+ table, index, feild = key.split("~~")
+ if table != "indexes" and int(index) == indexes[f"int--{table}"]:
+ deindexed.update({
+ f"{table}~~{feild}": data[key]
+ })
+
+ return deindexed