Reorg skeleton

This commit is contained in:
Sergey Lyubka committed 2024-03-09 16:01:06 +00:00
1 parent 4027bcf7ea
commit d46e505a80
37 files changed
+1811

No files matched your search

@@ -0,0 +1,51 @@
# Environment setup: https://mongoose.ws/documentation/tutorials/tools/
PROG ?= ./example # Program we are building
PACK ?= ./pack # Packing executable
DELETE = rm -rf # Command to remove files
NPX ?= npx # For generating optimised tailwind CSS
OUT ?= -o $(PROG) # Compiler argument for output file
SOURCES = main.c mongoose.c net.c packed_fs.c
CFLAGS += -W -Wall -Wextra -Wundef -Wshadow -Wdouble-promotion -Wconversion -Wno-sign-conversion
CFLAGS += -fno-common -ffunction-sections -fdata-sections
CFLAGS += -g3 -Os -I.
# Mongoose build options. See https://mongoose.ws/documentation/#build-options
CFLAGS_MONGOOSE += -DMG_ENABLE_PACKED_FS=1
ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe
PROG = example.exe # Use .exe suffix for the binary
PACK = pack.exe # Packing executable
CC = gcc # Use MinGW gcc compiler
CFLAGS += -lws2_32 # Link against Winsock library
DELETE = cmd /C del /Q /F /S # Command prompt command to delete files
endif
# Default target. Build and run program
all: $(PROG)
$(RUN) $(PROG) $(ARGS)
# Build program from sources
$(PROG): $(SOURCES)
$(CC) $(SOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(OUT)
# Build using Microsoft Visual C compiler
vc: $(SOURCES) Makefile
cl $(SOURCES) /DMG_ENABLE_PACKED_FS=1 /MD /W3 /nologo /Fedemo.exe
# Bundle JS libraries (preact, preact-router, ...) into a single file
web_root/bundle.js:
curl -s https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-router -o $@
# Create optimised CSS. Prerequisite: npm -g i tailwindcss tailwindcss-font-inter
web_root/main.css: web_root/index.html $(wildcard web_root/*.js)
$(NPX) tailwindcss -o $@ --minify
# Generate packed filesystem for serving Web UI
packed_fs.c: $(wildcard web_root/*) $(wildcard certs/*) Makefile
node pack.js $(addsuffix ::gzip, $(wildcard web_root/*)) certs/* > $@
# Cleanup. Delete built program and all build artifacts
clean:
$(DELETE) $(PROG) $(PACK) *.o *.obj *.exe *.dSYM mbedtls
@@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE-----
MIIBCTCBsAIJAK9wbIDkHnAoMAoGCCqGSM49BAMCMA0xCzAJBgNVBAYTAklFMB4X
DTIzMDEyOTIxMjEzOFoXDTMzMDEyNjIxMjEzOFowDTELMAkGA1UEBhMCSUUwWTAT
BgcqhkjOPQIBBggqhkjOPQMBBwNCAARzSQS5OHd17lUeNI+6kp9WYu0cxuEIi/JT
jphbCmdJD1cUvhmzM9/phvJT9ka10Z9toZhgnBq0o0xfTQ4jC1vwMAoGCCqGSM49
BAMCA0gAMEUCIQCe0T2E0GOiVe9KwvIEPeX1J1J0T7TNacgR0Ya33HV9VgIgNvdn
aEWiBp1xshs4iz6WbpxrS1IHucrqkZuJLfNZGZI=
-----END CERTIFICATE-----
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEICBz3HOkQLPBDtdknqC7k1PNsWj6HfhyNB5MenfjmqiooAoGCCqGSM49
AwEHoUQDQgAEc0kEuTh3de5VHjSPupKfVmLtHMbhCIvyU46YWwpnSQ9XFL4ZszPf
6YbyU/ZGtdGfbaGYYJwatKNMX00OIwtb8A==
-----END EC PRIVATE KEY-----
@@ -0,0 +1,21 @@
// Copyright (c) 2024 Cesanta Software Limited
// All rights reserved
#pragma once
#define LED1 0
#define LED2 1
#define UART_DEBUG NULL
// No-op HAL API implementation for a device with GPIO and UART
#define hal_init()
#define hal_ram_free() 0
#define hal_ram_used() 0
#define gpio_output(pin)
#define gpio_toggle(pin)
#define gpio_read(pin) 0
#define gpio_write(pin, val)
#define uart_init(uart, baud)
#define uart_read_ready(uart) 0
#define uart_write_byte(uart, ch)
#define uart_write_buf(uart, buf, len)
@@ -0,0 +1,32 @@
// Copyright (c) 2024 Cesanta Software Limited
// All rights reserved
#include "hal.h"
#include "mongoose.h"
#include "net.h"
#define BLINK_PERIOD_MS 1000 // LED blinking period in millis
static void timer_fn(void *arg) {
gpio_toggle(LED1); // Blink LED
(void) arg; // Unused
}
// In RTOS environment, you can run this function in a separate task
static void run_mongoose(void) {
struct mg_mgr mgr; // Mongoose event manager
mg_mgr_init(&mgr); // Initialise event manager
mg_log_set(MG_LL_DEBUG); // Set log level to debug
mg_timer_add(&mgr, BLINK_PERIOD_MS, MG_TIMER_REPEAT, timer_fn, NULL);
net_init(&mgr); // Initialise application defined in net.c
for (;;) { // Infinite event loop
mg_mgr_poll(&mgr, 0); // Process network events
}
}
int main(void) {
hal_init(); // Cross-platform hardware init
run_mongoose(); // Initialise and run network application
return 0;
}
@@ -0,0 +1 @@
../../../mongoose.c
@@ -0,0 +1 @@
../../../mongoose.h
@@ -0,0 +1,324 @@
// Copyright (c) 2023 Cesanta Software Limited
// All rights reserved
#include "hal.h"
#include "net.h"
// Authenticated user.
// A user can be authenticated by:
// - a name:pass pair, passed in a header Authorization: Basic .....
// - an access_token, passed in a header Cookie: access_token=....
// When a user is shown a login screen, she enters a user:pass. If successful,
// a server responds with a http-only access_token cookie set.
struct user {
const char *name, *pass, *access_token;
};
// Settings
struct settings {
bool log_enabled;
int log_level;
long brightness;
char *device_name;
};
static struct settings s_settings = {true, 1, 57, NULL};
static const char *s_json_header =
"Content-Type: application/json\r\n"
"Cache-Control: no-cache\r\n";
static uint64_t s_boot_timestamp = 0; // Updated by SNTP
// This is for newlib and TLS (mbedTLS)
uint64_t mg_now(void) {
return mg_millis() + s_boot_timestamp;
}
int ui_event_next(int no, struct ui_event *e) {
if (no < 0 || no >= MAX_EVENTS_NO) return 0;
srand((unsigned) no);
e->type = (uint8_t) rand() % 4;
e->prio = (uint8_t) rand() % 3;
e->timestamp =
(unsigned long) ((int64_t) mg_now() - 86400 * 1000 /* one day back */ +
no * 300 * 1000 /* 5 mins between alerts */ +
1000 * (rand() % 300) /* randomize event time */) /
1000UL;
mg_snprintf(e->text, MAX_EVENT_TEXT_SIZE, "event#%d", no);
return no + 1;
}
// SNTP connection event handler. When we get a response from an SNTP server,
// adjust s_boot_timestamp. We'll get a valid time from that point on
static void sfn(struct mg_connection *c, int ev, void *ev_data) {
uint64_t *expiration_time = (uint64_t *) c->data;
if (ev == MG_EV_OPEN) {
*expiration_time = mg_millis() + 3000; // Store expiration time in 3s
} else if (ev == MG_EV_SNTP_TIME) {
uint64_t t = *(uint64_t *) ev_data;
s_boot_timestamp = t - mg_millis();
c->is_closing = 1;
} else if (ev == MG_EV_POLL) {
if (mg_millis() > *expiration_time) c->is_closing = 1;
}
}
static void timer_sntp_fn(void *param) { // SNTP timer function. Sync up time
mg_sntp_connect(param, "udp://time.google.com:123", sfn, NULL);
}
// Parse HTTP requests, return authenticated user or NULL
static struct user *authenticate(struct mg_http_message *hm) {
// In production, make passwords strong and tokens randomly generated
// In this example, user list is kept in RAM. In production, it can
// be backed by file, database, or some other method.
static struct user users[] = {
{"admin", "admin", "admin_token"},
{"user1", "user1", "user1_token"},
{"user2", "user2", "user2_token"},
{NULL, NULL, NULL},
};
char user[64], pass[64];
struct user *u, *result = NULL;
mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass));
MG_VERBOSE(("user [%s] pass [%s]", user, pass));
if (user[0] != '\0' && pass[0] != '\0') {
// Both user and password is set, search by user/password
for (u = users; result == NULL && u->name != NULL; u++)
if (strcmp(user, u->name) == 0 && strcmp(pass, u->pass) == 0) result = u;
} else if (user[0] == '\0') {
// Only password is set, search by token
for (u = users; result == NULL && u->name != NULL; u++)
if (strcmp(pass, u->access_token) == 0) result = u;
}
return result;
}
static void handle_login(struct mg_connection *c, struct user *u) {
char cookie[256];
mg_snprintf(cookie, sizeof(cookie),
"Set-Cookie: access_token=%s; Path=/; "
"%sHttpOnly; SameSite=Lax; Max-Age=%d\r\n",
u->access_token, c->is_tls ? "Secure; " : "", 3600 * 24);
mg_http_reply(c, 200, cookie, "{%m:%m}", MG_ESC("user"), MG_ESC(u->name));
}
static void handle_logout(struct mg_connection *c) {
char cookie[256];
mg_snprintf(cookie, sizeof(cookie),
"Set-Cookie: access_token=; Path=/; "
"Expires=Thu, 01 Jan 1970 00:00:00 UTC; "
"%sHttpOnly; Max-Age=0; \r\n",
c->is_tls ? "Secure; " : "");
mg_http_reply(c, 200, cookie, "true\n");
}
static void handle_debug(struct mg_connection *c, struct mg_http_message *hm) {
int level = (int) mg_json_get_long(hm->body, "$.level", MG_LL_DEBUG);
mg_log_set(level);
mg_http_reply(c, 200, "", "Debug level set to %d\n", level);
}
static size_t print_int_arr(void (*out)(char, void *), void *ptr, va_list *ap) {
size_t i, len = 0, num = va_arg(*ap, size_t); // Number of items in the array
int *arr = va_arg(*ap, int *); // Array ptr
for (i = 0; i < num; i++) {
len += mg_xprintf(out, ptr, "%s%d", i == 0 ? "" : ",", arr[i]);
}
return len;
}
static void handle_stats_get(struct mg_connection *c) {
int points[] = {21, 22, 22, 19, 18, 20, 23, 23, 22, 22, 22, 23, 22};
mg_http_reply(c, 200, s_json_header, "{%m:%d,%m:%d,%m:[%M]}\n",
MG_ESC("temperature"), 21, //
MG_ESC("humidity"), 67, //
MG_ESC("points"), print_int_arr,
sizeof(points) / sizeof(points[0]), points);
}
static size_t print_events(void (*out)(char, void *), void *ptr, va_list *ap) {
size_t len = 0;
struct ui_event ev;
int pageno = va_arg(*ap, int);
int no = (pageno - 1) * EVENTS_PER_PAGE;
int end = no + EVENTS_PER_PAGE;
while ((no = ui_event_next(no, &ev)) != 0 && no <= end) {
len += mg_xprintf(out, ptr, "%s{%m:%lu,%m:%d,%m:%d,%m:%m}\n", //
len == 0 ? "" : ",", //
MG_ESC("time"), ev.timestamp, //
MG_ESC("type"), ev.type, //
MG_ESC("prio"), ev.prio, //
MG_ESC("text"), MG_ESC(ev.text));
}
return len;
}
static void handle_events_get(struct mg_connection *c,
struct mg_http_message *hm) {
int pageno = (int) mg_json_get_long(hm->body, "$.page", 1);
mg_http_reply(c, 200, s_json_header, "{%m:[%M], %m:%d}\n", MG_ESC("arr"),
print_events, pageno, MG_ESC("totalCount"), MAX_EVENTS_NO);
}
static void handle_settings_set(struct mg_connection *c, struct mg_str body) {
struct settings settings;
char *s = mg_json_get_str(body, "$.device_name");
bool ok = true;
memset(&settings, 0, sizeof(settings));
mg_json_get_bool(body, "$.log_enabled", &settings.log_enabled);
settings.log_level = (int) mg_json_get_long(body, "$.log_level", 0);
settings.brightness = mg_json_get_long(body, "$.brightness", 0);
if (s && strlen(s) < MAX_DEVICE_NAME) {
free(settings.device_name);
settings.device_name = s;
} else {
free(s);
}
s_settings = settings; // Save to the device flash
mg_http_reply(c, 200, s_json_header,
"{%m:%s,%m:%m}", //
MG_ESC("status"), ok ? "true" : "false", //
MG_ESC("message"), MG_ESC(ok ? "Success" : "Failed"));
}
static void handle_settings_get(struct mg_connection *c) {
mg_http_reply(c, 200, s_json_header, "{%m:%s,%m:%hhu,%m:%hhu,%m:%m}\n", //
MG_ESC("log_enabled"),
s_settings.log_enabled ? "true" : "false", //
MG_ESC("log_level"), s_settings.log_level, //
MG_ESC("brightness"), s_settings.brightness, //
MG_ESC("device_name"), MG_ESC(s_settings.device_name));
}
static void handle_firmware_upload(struct mg_connection *c,
struct mg_http_message *hm) {
char name[64], offset[20], total[20];
struct mg_str data = hm->body;
long ofs = -1, tot = -1;
name[0] = offset[0] = '\0';
mg_http_get_var(&hm->query, "name", name, sizeof(name));
mg_http_get_var(&hm->query, "offset", offset, sizeof(offset));
mg_http_get_var(&hm->query, "total", total, sizeof(total));
MG_INFO(("File %s, offset %s, len %lu", name, offset, data.len));
if ((ofs = mg_json_get_long(mg_str(offset), "$", -1)) < 0 ||
(tot = mg_json_get_long(mg_str(total), "$", -1)) < 0) {
mg_http_reply(c, 500, "", "offset and total not set\n");
} else if (ofs == 0 && mg_ota_begin((size_t) tot) == false) {
mg_http_reply(c, 500, "", "mg_ota_begin(%ld) failed\n", tot);
} else if (data.len > 0 && mg_ota_write(data.ptr, data.len) == false) {
mg_http_reply(c, 500, "", "mg_ota_write(%lu) @%ld failed\n", data.len, ofs);
mg_ota_end();
} else if (data.len == 0 && mg_ota_end() == false) {
mg_http_reply(c, 500, "", "mg_ota_end() failed\n", tot);
} else {
mg_http_reply(c, 200, s_json_header, "true\n");
if (data.len == 0) {
// Successful mg_ota_end() called, schedule device reboot
mg_timer_add(c->mgr, 500, 0, (void (*)(void *)) mg_device_reset, NULL);
}
}
}
static void handle_firmware_commit(struct mg_connection *c) {
mg_http_reply(c, 200, s_json_header, "%s\n",
mg_ota_commit() ? "true" : "false");
}
static void handle_firmware_rollback(struct mg_connection *c) {
mg_http_reply(c, 200, s_json_header, "%s\n",
mg_ota_rollback() ? "true" : "false");
}
static size_t print_status(void (*out)(char, void *), void *ptr, va_list *ap) {
int fw = va_arg(*ap, int);
return mg_xprintf(out, ptr, "{%m:%d,%m:%c%lx%c,%m:%u,%m:%u}\n",
MG_ESC("status"), mg_ota_status(fw), MG_ESC("crc32"), '"',
mg_ota_crc32(fw), '"', MG_ESC("size"), mg_ota_size(fw),
MG_ESC("timestamp"), mg_ota_timestamp(fw));
}
static void handle_firmware_status(struct mg_connection *c) {
mg_http_reply(c, 200, s_json_header, "[%M,%M]\n", print_status,
MG_FIRMWARE_CURRENT, print_status, MG_FIRMWARE_PREVIOUS);
}
static void handle_device_reset(struct mg_connection *c) {
mg_http_reply(c, 200, s_json_header, "true\n");
mg_timer_add(c->mgr, 500, 0, (void (*)(void *)) mg_device_reset, NULL);
}
static void handle_device_eraselast(struct mg_connection *c) {
size_t ss = mg_flash_sector_size(), size = mg_flash_size();
char *base = (char *) mg_flash_start(), *last = base + size - ss;
if (mg_flash_bank() == 2) last -= size / 2;
mg_flash_erase(last);
mg_http_reply(c, 200, s_json_header, "true\n");
}
// HTTP request handler function
static void ev_handler(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_ACCEPT) {
if (c->fn_data != NULL) { // TLS listener!
struct mg_tls_opts opts = {0};
opts.cert = mg_unpacked("/certs/server_cert.pem");
opts.key = mg_unpacked("/certs/server_key.pem");
mg_tls_init(c, &opts);
}
} else if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
struct user *u = authenticate(hm);
if (mg_http_match_uri(hm, "/api/#") && u == NULL) {
mg_http_reply(c, 403, "", "Not Authorised\n");
} else if (mg_http_match_uri(hm, "/api/login")) {
handle_login(c, u);
} else if (mg_http_match_uri(hm, "/api/logout")) {
handle_logout(c);
} else if (mg_http_match_uri(hm, "/api/debug")) {
handle_debug(c, hm);
} else if (mg_http_match_uri(hm, "/api/stats/get")) {
handle_stats_get(c);
} else if (mg_http_match_uri(hm, "/api/events/get")) {
handle_events_get(c, hm);
} else if (mg_http_match_uri(hm, "/api/settings/get")) {
handle_settings_get(c);
} else if (mg_http_match_uri(hm, "/api/settings/set")) {
handle_settings_set(c, hm->body);
} else if (mg_http_match_uri(hm, "/api/firmware/upload")) {
handle_firmware_upload(c, hm);
} else if (mg_http_match_uri(hm, "/api/firmware/commit")) {
handle_firmware_commit(c);
} else if (mg_http_match_uri(hm, "/api/firmware/rollback")) {
handle_firmware_rollback(c);
} else if (mg_http_match_uri(hm, "/api/firmware/status")) {
handle_firmware_status(c);
} else if (mg_http_match_uri(hm, "/api/device/reset")) {
handle_device_reset(c);
} else if (mg_http_match_uri(hm, "/api/device/eraselast")) {
handle_device_eraselast(c);
} else {
struct mg_http_serve_opts opts;
memset(&opts, 0, sizeof(opts));
opts.fs = HAL_FS;
opts.root_dir = HAL_WEB_ROOT_DIR;
mg_http_serve_dir(c, ev_data, &opts);
}
MG_DEBUG(("%lu %.*s %.*s -> %.*s", c->id, (int) hm->method.len,
hm->method.ptr, (int) hm->uri.len, hm->uri.ptr, (int) 3,
&c->send.buf[9]));
}
}
void net_init(struct mg_mgr *mgr) {
s_settings.device_name = strdup("My Device");
mg_http_listen(mgr, HTTP_URL, ev_handler, NULL);
mg_http_listen(mgr, HTTPS_URL, ev_handler, (void *) 1);
mg_timer_add(mgr, 3600 * 1000, MG_TIMER_RUN_NOW | MG_TIMER_REPEAT,
timer_sntp_fn, mgr);
}
@@ -0,0 +1,50 @@
// Copyright (c) 2023-2024 Cesanta Software Limited
// All rights reserved
#pragma once
#include "mongoose.h"
#ifdef __cplusplus
extern "C" {
#endif
#if MG_ENABLE_FATFS
#define HAL_FS (&mg_fs_fat)
#define HAL_ROOT_DIR "/"
#elif MG_ENABLE_PACKED_FS
#define HAL_FS (&mg_fs_packed)
#define HAL_ROOT_DIR "/"
#else
#define HAL_FS (&mg_fs_posix)
#define HAL_ROOT_DIR "./"
#endif
#define HAL_WEB_ROOT_DIR HAL_ROOT_DIR "web_root"
#if defined(HTTP_URL) && defined(HTTPS_URL)
// Allow to override via the build flags
#elif MG_ENABLE_TCPIP
#define HTTP_URL "http://0.0.0.0:80" // Embedded build:
#define HTTPS_URL "https://0.0.0.0:443" // Use standard privileged ports
#else
#define HTTP_URL "http://0.0.0.0:8000" // Workstation build:
#define HTTPS_URL "https://0.0.0.0:8443" // Use non-privileged ports
#endif
#define MAX_DEVICE_NAME 40
#define MAX_EVENTS_NO 400
#define MAX_EVENT_TEXT_SIZE 10
#define EVENTS_PER_PAGE 20
// Event log entry
struct ui_event {
uint8_t type, prio;
unsigned long timestamp;
char text[10];
};
void net_init(struct mg_mgr *mgr);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,81 @@
// Copyright (c) 2024 Cesanta Software Limited
//
// Utility that generates packed filesystem C file compatible with the
// Mongoose Network Library, https://github.com/cesanta/mongoose
//
// Usage:
// node pack.js FILE[:DESTINATION[:gzip]] ...
const fs = require('fs');
const zlib = require('zlib');
const argv = process.argv.slice(2);
// Convert each command-line arguments into [ DATA_ARRAY, STRUCT_INITIALIZER ]
const entries = argv.map(function(filename, i) {
const parts = filename.split(':');
const stat = fs.statSync(parts[0]);
const data = fs.readFileSync(parts[0], null);
let bytes = Array.from(data);
let destination = (parts[1] || parts[0]).replace(/^\.+[\/\\]*/, '');
if (parts[2] == 'gzip') {
bytes = Array.from(zlib.gzipSync(data));
destination += '.gz';
}
// concat(0) appends trailing 0, in order to make any file an asciz string
return [
`static const unsigned char v${i}[] = {${bytes.concat(0).join(',')}};`,
` {"/${destination}", v${i}, sizeof(v${i}) - 1, ${parseInt(stat.mtimeMs / 1000)}}`,
];
});
process.stdout.write(`// DO NOT EDIT. This file is generated using this command:
// ${process.argv.join(' ')}
#include <stddef.h>
#include <string.h>
#include <time.h>
#if defined(__cplusplus)
extern "C" {
#endif
const char *mg_unlist(size_t no);
const char *mg_unpack(const char *, size_t *, time_t *);
#if defined(__cplusplus)
}
#endif
${entries.map(x => x[0]).join('\n\n')}
static const struct packed_file {
const char *name;
const unsigned char *data;
size_t size;
time_t mtime;
} packed_files[] = {
${entries.map(x => x[1]).join(',\n')},
{NULL, NULL, 0, 0}
};
static int scmp(const char *a, const char *b) {
while (*a && (*a == *b)) a++, b++;
return *(const unsigned char *) a - *(const unsigned char *) b;
}
const char *mg_unlist(size_t no) {
return packed_files[no].name;
}
const char *mg_unpack(const char *name, size_t *size, time_t *mtime) {
const struct packed_file *p;
for (p = packed_files; p->name != NULL; p++) {
if (scmp(p->name, name) != 0) continue;
if (size != NULL) *size = p->size;
if (mtime != NULL) *mtime = p->mtime;
return (const char *) p->data;
}
return NULL;
};
`);
@@ -0,0 +1,70 @@
// DO NOT EDIT. This file is generated using this command:
// /opt/homebrew/Cellar/node/21.6.0/bin/node /Users/cpq/src/mongoose/reference-projects/windows-macos-linux/web-ui-dashboard/pack.js web_root/bundle.js::gzip web_root/components.js::gzip web_root/history.min.js::gzip web_root/index.html::gzip web_root/main.css::gzip web_root/main.js::gzip certs/server_cert.pem certs/server_key.pem
#include <stddef.h>
#include <string.h>
#include <time.h>
#if defined(__cplusplus)
extern "C" {
#endif
const char *mg_unlist(size_t no);
const char *mg_unpack(const char *, size_t *, time_t *);
#if defined(__cplusplus)
}
#endif
static const unsigned char v0[] = {31,139,8,0,0,0,0,0,0,19,173,60,139,114,219,56,146,191,18,105,83,90,98,132,200,118,102,178,119,71,25,81,37,30,79,156,153,60,188,118,30,187,171,209,170,40,10,178,56,166,64,153,132,228,120,76,221,183,95,119,227,65,82,166,231,246,182,174,42,17,65,60,27,141,126,163,233,109,148,63,81,92,115,201,167,60,231,25,79,248,134,167,60,22,247,59,30,137,241,132,23,226,32,138,19,93,202,111,193,40,44,202,171,82,149,235,242,41,43,243,245,178,188,202,147,121,153,221,22,229,74,197,165,210,183,101,162,228,56,94,78,202,223,179,172,252,103,150,207,203,68,203,60,58,72,134,139,141,138,117,146,169,39,139,0,22,99,247,139,44,15,182,176,178,124,146,168,39,154,169,177,156,8,13,63,195,92,234,77,174,158,168,157,31,177,14,20,187,199,190,90,168,193,58,202,165,210,31,178,185,28,234,94,79,15,114,185,202,182,242,100,153,164,115,232,86,13,90,6,180,35,51,208,110,11,182,52,196,117,19,92,83,178,238,181,188,235,10,145,140,114,33,199,201,36,236,230,114,65,239,153,121,223,192,15,149,134,201,34,136,242,171,205,10,86,46,6,169,84,87,122,249,242,121,175,23,108,6,49,46,12,16,137,7,237,223,143,212,32,142,210,180,26,201,159,179,112,202,120,215,193,8,107,233,187,181,204,22,79,96,39,106,147,166,29,161,7,115,185,136,54,169,62,207,179,117,193,60,176,123,245,219,44,153,63,57,20,66,32,136,8,7,66,218,236,3,53,204,225,114,14,184,216,208,217,226,42,53,36,1,202,220,161,27,68,1,138,16,164,80,241,53,78,18,78,57,224,40,204,57,96,38,204,248,116,122,29,226,12,80,112,207,89,120,8,191,210,189,206,67,3,26,20,99,87,183,52,133,56,83,133,206,55,177,206,242,170,211,150,218,16,231,253,190,12,147,157,63,125,83,91,161,101,171,224,196,241,188,169,16,108,24,223,84,219,216,6,236,222,12,188,143,55,57,210,7,77,187,171,122,172,144,132,220,220,254,208,170,246,59,67,148,122,153,20,3,218,185,128,119,124,1,168,181,252,166,133,174,250,94,153,190,64,19,6,74,205,252,196,211,233,8,90,225,193,241,7,254,93,15,18,53,151,223,62,66,95,214,63,98,4,214,208,147,254,80,31,43,234,100,72,102,168,251,125,102,167,237,136,64,10,106,28,235,9,115,120,144,80,33,221,114,244,98,17,214,66,83,106,128,5,132,199,44,91,109,96,230,249,137,203,97,181,158,162,245,252,90,248,18,27,62,197,162,129,38,30,204,162,2,138,120,160,90,28,254,155,27,184,127,48,161,217,203,44,151,209,245,206,110,15,161,172,157,224,53,2,29,116,112,204,28,40,158,158,162,115,8,51,231,131,245,166,88,66,115,175,215,57,129,234,188,223,47,203,172,35,136,33,102,25,76,32,47,36,156,66,158,168,43,232,19,4,89,107,75,89,38,44,56,169,49,199,73,96,176,228,100,35,138,16,32,99,58,190,124,80,100,185,6,50,28,42,1,229,101,178,208,1,27,50,7,157,134,74,131,18,62,21,150,216,51,87,72,69,144,16,114,24,244,222,226,143,228,65,76,40,56,71,248,166,40,116,1,200,69,0,18,56,97,212,75,36,248,219,63,226,159,130,24,192,200,56,246,86,220,204,8,91,141,7,217,173,146,249,229,151,55,167,169,68,97,195,13,190,113,216,114,52,78,39,150,25,185,33,217,20,200,34,97,97,202,169,157,241,139,96,10,43,209,155,236,136,180,215,155,65,51,227,110,19,47,53,98,217,238,152,13,9,201,226,176,194,212,183,224,161,254,40,12,242,22,124,205,151,124,203,239,248,140,95,243,19,49,237,245,136,47,202,50,226,223,196,137,35,28,196,41,110,233,26,247,190,0,194,90,28,107,223,214,32,170,45,33,234,122,188,152,8,179,23,168,209,240,6,231,215,157,101,89,42,163,26,15,108,161,242,33,103,108,71,56,50,236,130,56,130,131,111,246,86,155,213,76,230,205,186,89,114,149,40,93,31,63,39,104,96,91,244,48,101,22,190,202,243,232,110,144,20,244,12,182,12,186,173,248,189,19,54,225,118,87,235,78,130,56,220,194,86,102,47,15,161,227,150,184,149,111,141,244,129,39,200,93,248,5,185,59,162,95,115,126,91,162,152,112,203,72,252,224,155,144,84,57,35,172,204,128,64,12,82,68,176,20,39,6,43,203,94,143,102,19,98,137,15,124,195,165,4,190,99,129,97,63,75,155,67,153,22,242,9,30,198,26,206,96,125,252,109,184,6,228,227,90,52,223,26,25,249,143,39,187,199,94,110,54,195,207,75,58,168,221,39,32,146,45,95,138,101,89,198,13,58,225,119,98,107,152,96,45,104,175,176,200,18,159,29,177,6,118,0,82,9,144,44,24,167,202,94,239,218,240,59,189,57,220,115,91,185,38,108,196,101,9,200,3,2,54,52,115,55,178,146,122,6,211,205,196,93,171,10,54,27,193,13,33,21,226,126,224,57,218,146,152,73,197,105,176,5,88,65,146,166,226,179,217,7,63,1,154,78,91,167,146,118,42,162,104,24,205,96,24,110,9,101,30,50,87,90,51,98,64,206,66,199,84,92,5,75,6,178,199,178,129,20,51,224,130,111,195,197,179,103,32,84,104,19,120,74,208,243,15,150,171,250,225,12,189,158,43,9,162,141,185,135,231,85,48,101,3,5,74,237,50,153,165,40,249,248,215,0,251,114,34,24,134,26,225,154,44,15,195,135,215,117,62,124,31,32,231,241,235,113,191,239,31,53,145,121,106,4,65,101,221,129,68,48,74,0,196,223,225,48,239,245,178,99,39,84,134,25,204,7,194,46,31,103,19,18,123,72,205,48,92,180,108,113,106,116,217,41,136,41,156,62,252,28,160,176,65,113,51,37,202,209,204,219,58,53,69,125,102,20,181,107,16,186,44,65,186,24,82,80,109,226,2,42,131,38,19,43,6,86,92,145,173,100,16,184,105,81,15,153,153,119,112,176,218,41,31,198,107,43,127,174,11,68,35,8,137,218,17,183,94,102,107,60,13,150,152,39,167,223,6,27,122,3,67,150,1Line truncated
static const unsigned char v1[] = {31,139,8,0,0,0,0,0,0,19,237,93,235,115,219,70,146,255,238,191,98,162,242,45,169,181,0,225,77,64,182,148,114,156,77,57,85,246,94,42,78,244,37,151,59,67,36,40,34,198,131,7,64,148,100,133,255,251,245,175,103,0,12,64,74,206,195,187,117,167,219,84,12,14,230,61,253,238,158,38,53,185,170,19,81,55,85,58,111,38,207,159,164,249,186,172,26,113,39,86,71,162,74,138,69,82,29,9,234,240,174,137,155,132,75,127,91,46,147,121,195,197,239,147,229,145,88,53,121,118,36,190,47,175,154,164,18,91,177,172,202,92,136,137,121,124,113,85,44,178,196,252,165,166,73,159,28,31,139,215,73,182,166,30,203,171,98,222,164,101,33,154,85,220,208,10,205,85,85,212,34,22,107,26,151,210,70,84,117,93,102,155,132,234,151,152,117,145,100,241,237,147,121,89,212,141,248,26,101,113,42,166,121,125,36,54,113,118,40,78,207,68,145,92,139,239,228,4,83,53,22,213,117,210,252,144,230,9,109,173,173,61,18,237,176,67,218,85,114,195,103,149,19,127,139,15,154,248,238,137,16,171,36,174,154,19,236,105,93,99,34,156,241,253,139,122,115,41,230,89,92,215,167,79,239,184,201,228,183,173,184,201,179,162,62,61,88,53,205,250,228,248,248,250,250,218,188,118,205,178,186,60,118,44,203,58,166,97,7,98,153,102,217,233,65,81,22,201,129,216,164,201,245,87,229,205,233,129,37,44,225,120,244,255,193,217,139,117,220,172,128,133,242,67,98,100,105,145,204,227,245,233,65,85,18,16,15,244,234,95,202,180,24,215,95,167,139,102,117,122,224,28,136,197,233,193,91,207,116,237,80,4,120,198,158,233,11,252,163,133,44,11,117,129,247,198,118,132,99,161,148,205,204,32,116,12,126,14,122,26,220,209,232,186,83,15,55,200,12,27,83,202,231,158,238,194,250,72,167,56,198,49,232,131,206,124,246,254,136,32,185,40,175,139,151,85,85,226,192,255,76,128,142,96,99,155,126,91,117,122,48,191,170,136,178,155,87,101,86,86,7,103,226,15,129,30,144,142,68,104,58,254,235,153,233,199,14,21,4,63,24,34,221,235,38,26,53,117,111,171,125,45,60,112,99,140,155,140,174,237,181,237,211,178,182,147,185,194,205,169,41,115,13,55,55,232,229,28,173,7,226,152,142,211,3,255,106,253,47,208,127,78,208,231,150,225,102,6,64,14,208,3,236,57,189,156,219,59,128,159,151,151,143,14,226,166,31,121,194,53,35,111,110,90,145,97,250,158,99,250,129,65,239,194,54,109,27,133,21,213,68,238,220,244,1,58,219,36,176,186,81,200,173,212,152,153,142,237,210,139,19,218,52,65,224,154,238,12,146,202,37,233,19,152,129,231,155,225,204,180,168,202,242,76,219,163,98,232,154,142,99,218,206,204,116,29,170,138,2,115,6,44,204,48,241,204,167,6,47,163,201,236,153,97,122,126,16,211,34,132,48,249,36,184,208,138,238,204,244,34,116,137,2,96,211,155,237,244,49,76,39,160,87,207,181,33,219,44,203,53,67,103,54,167,218,136,150,246,104,94,55,52,3,219,69,129,14,16,57,49,109,193,143,132,124,98,2,75,208,134,124,26,97,89,180,203,89,104,218,52,130,246,230,17,148,176,180,101,209,169,156,112,110,122,116,2,215,55,125,215,51,35,223,107,151,197,170,14,1,232,158,221,209,17,130,136,206,192,187,107,15,74,139,185,190,111,208,74,180,175,25,21,172,153,131,205,207,2,64,132,118,72,0,226,135,58,32,32,24,98,16,29,193,14,105,140,31,210,210,17,13,12,60,207,12,131,40,51,58,188,224,32,132,101,15,189,2,194,24,182,192,168,179,87,6,48,235,81,7,160,22,235,57,6,112,107,180,152,151,179,24,18,187,52,77,128,246,25,193,208,181,169,68,24,230,245,12,194,49,182,232,8,126,180,91,52,128,101,236,209,193,193,34,234,75,103,2,170,229,193,12,198,181,132,193,94,92,75,72,25,64,55,67,116,102,236,135,40,193,221,144,232,6,106,176,27,90,150,250,99,49,143,234,9,194,132,110,19,231,151,216,142,92,236,20,79,198,182,193,216,6,178,113,184,16,72,8,37,18,228,16,73,69,60,111,184,151,216,120,245,236,161,45,130,108,13,198,57,142,27,48,198,9,225,1,240,77,43,1,30,142,232,161,194,216,39,142,1,203,80,141,69,44,36,193,25,18,194,81,32,38,242,73,197,3,227,10,1,132,113,154,217,147,168,250,40,229,214,31,21,9,54,109,221,137,73,10,130,221,109,35,160,15,249,98,217,1,204,129,145,80,36,107,172,73,139,203,250,245,99,19,141,182,69,150,80,176,138,8,67,170,76,136,5,90,125,9,24,2,73,78,255,244,74,11,149,111,129,83,17,64,139,81,187,237,240,12,185,129,231,160,183,181,111,10,139,167,32,221,207,189,121,142,200,8,86,46,79,225,254,174,41,228,130,188,250,125,56,59,127,108,56,35,17,76,188,113,206,224,178,4,142,62,128,150,5,243,202,112,7,192,226,58,129,17,231,196,154,126,78,162,205,253,83,19,24,129,17,181,19,184,191,125,130,136,39,176,204,93,100,205,227,226,177,225,137,232,154,97,243,58,120,57,52,204,36,239,108,104,209,183,0,166,236,100,135,195,94,54,121,58,142,234,6,200,157,239,116,32,87,137,251,172,12,116,49,72,177,143,87,178,121,37,59,220,24,188,214,111,150,121,66,250,63,73,253,161,41,215,143,13,45,228,131,204,96,47,67,227,205,226,22,2,80,240,145,128,205,225,188,1,226,28,123,5,3,35,112,108,126,188,108,251,1,136,176,223,103,231,60,75,78,154,209,33,43,122,104,122,219,189,41,255,218,167,199,24,43,194,246,207,81,159,19,10,173,17,113,216,176,195,132,187,103,156,229,138,110,208,57,97,242,119,46,233,240,146,99,198,139,179,228,159,27,46,248,167,104,53,178,15,97,110,195,22,39,56,145,117,236,17,175,200,15,0,146,236,92,223,227,112,192,203,208,140,130,1Line truncated
static const unsigned char v2[] = {31,139,8,0,0,0,0,0,0,19,237,91,109,111,219,186,146,254,43,182,47,214,144,96,86,181,123,206,61,187,43,151,215,72,211,52,9,144,32,65,147,246,124,8,140,3,197,166,109,181,50,233,82,146,147,192,214,127,223,25,190,72,148,45,167,233,222,158,189,7,187,251,37,150,248,50,28,14,135,51,207,67,49,237,89,206,39,89,44,184,151,17,238,111,58,226,254,11,155,100,29,74,179,167,21,19,179,22,123,92,9,153,165,221,238,94,205,82,76,243,132,141,244,79,96,218,81,238,249,97,199,202,172,26,79,217,44,230,172,219,213,191,65,180,156,142,244,163,119,55,134,113,195,67,227,142,204,111,112,22,167,153,144,79,74,126,230,190,21,94,182,136,83,82,78,195,223,72,150,229,146,183,170,137,249,27,251,220,226,158,240,55,241,204,99,119,98,236,155,134,248,108,245,31,174,35,217,146,20,139,232,198,148,133,155,130,196,211,80,144,68,68,83,54,13,219,131,98,104,186,102,216,117,18,37,137,39,173,4,34,73,245,204,125,120,209,221,104,187,95,85,20,56,12,163,155,82,16,15,150,20,22,32,152,80,6,127,87,180,211,33,220,235,195,228,238,220,245,33,12,86,40,79,89,43,205,100,12,214,26,150,243,18,56,75,171,84,183,155,5,127,252,193,210,75,189,66,89,184,1,91,71,121,146,133,89,81,112,167,10,117,130,49,37,139,50,118,29,101,11,10,67,71,50,181,207,137,152,68,40,61,61,146,236,228,91,30,37,212,54,190,48,53,101,193,37,91,194,122,148,203,98,74,207,162,116,177,91,246,78,138,135,148,73,91,188,22,241,180,213,183,102,247,222,248,195,43,229,9,129,118,143,107,41,86,76,102,79,30,39,157,250,208,29,178,97,60,95,50,25,221,39,44,132,137,204,89,22,238,123,129,220,209,184,40,124,114,112,132,189,9,191,120,144,189,158,48,142,154,84,12,147,26,60,51,169,210,222,47,28,42,174,86,232,217,169,84,139,250,98,193,85,23,171,124,4,202,255,187,79,38,84,120,145,79,114,120,251,15,159,164,240,150,251,100,6,111,255,233,147,4,222,102,254,240,192,234,78,2,227,121,164,201,37,210,189,218,186,27,37,182,190,32,59,81,202,221,3,59,14,13,239,209,116,122,193,162,105,204,231,55,9,12,71,221,72,160,103,219,121,13,145,134,102,193,100,17,201,163,12,54,26,108,18,40,235,193,72,60,64,193,171,31,23,16,164,249,61,116,133,213,14,43,49,215,18,86,230,145,214,213,55,22,239,43,9,49,159,178,199,171,153,199,29,17,176,245,24,159,103,11,87,210,173,140,226,228,229,26,101,70,196,171,129,146,155,196,19,230,245,201,43,171,92,181,205,93,73,184,230,156,102,219,45,8,35,12,131,144,192,63,18,54,175,85,179,243,183,142,63,148,109,74,95,13,186,93,79,64,141,209,89,250,132,87,111,16,235,124,187,1,156,206,35,236,108,252,205,202,96,85,175,120,71,70,236,99,193,148,77,196,148,125,250,120,14,54,34,155,21,40,205,163,37,11,57,73,89,36,39,139,16,164,194,204,217,168,211,9,25,89,128,117,66,80,18,74,4,150,136,162,168,135,184,134,249,6,86,38,204,25,108,165,164,194,204,179,0,133,193,236,25,47,21,80,166,41,231,0,9,13,6,135,137,192,131,39,123,212,104,226,120,5,67,237,122,204,39,2,154,254,13,155,10,211,84,171,232,52,21,168,118,79,64,198,40,234,254,254,231,68,253,23,4,247,90,108,54,145,38,74,211,120,14,102,168,101,87,33,61,109,202,193,144,191,141,228,28,162,13,135,156,173,61,112,200,123,61,109,106,70,203,186,59,62,30,218,110,162,21,131,41,125,51,192,74,138,76,32,8,64,235,95,61,112,27,212,116,154,101,68,248,96,64,204,187,42,73,251,133,157,62,228,104,12,181,125,159,68,16,146,98,140,90,240,58,192,176,37,188,9,134,45,21,136,247,38,89,183,52,136,223,104,175,53,147,55,155,11,77,207,231,21,60,201,70,94,76,193,67,211,106,39,249,96,10,18,195,102,5,233,20,32,13,52,144,30,64,7,40,213,178,96,189,227,210,213,96,18,213,11,108,50,213,85,121,222,72,187,148,125,173,60,68,117,209,133,232,105,61,251,226,135,85,113,7,196,160,215,142,180,179,233,151,29,17,88,132,254,215,211,143,216,93,23,117,140,162,208,145,119,187,142,210,106,78,122,120,51,59,84,247,43,123,2,180,34,106,51,25,193,246,104,187,243,220,25,187,156,49,216,46,178,225,221,119,42,136,40,31,125,212,172,236,224,148,147,184,104,116,224,198,40,91,237,110,152,10,119,236,111,119,186,42,214,143,88,168,76,129,69,248,128,5,56,77,124,135,95,152,66,159,228,149,218,153,182,135,10,209,240,235,127,111,227,42,111,167,78,238,45,134,89,137,156,197,159,190,235,75,148,245,11,152,144,122,2,99,182,163,12,214,102,148,231,73,66,156,109,225,88,146,114,183,121,70,149,155,120,186,11,206,157,57,189,112,43,65,14,64,172,141,213,109,154,217,141,213,192,14,178,81,134,61,32,55,13,247,118,90,60,106,232,32,70,176,197,65,124,40,189,118,31,255,98,54,105,15,252,130,37,96,49,85,88,64,4,7,110,33,27,2,62,196,63,230,174,2,71,35,70,171,85,242,228,105,167,39,101,156,242,75,132,14,254,151,167,11,143,213,76,6,178,6,48,142,8,102,113,146,49,233,237,167,229,86,134,233,161,64,243,196,238,152,54,248,101,116,55,94,130,241,143,164,140,158,48,156,48,218,31,178,183,217,144,65,8,229,119,108,236,4,80,54,30,194,176,66,158,68,147,69,227,184,245,25,113,191,40,231,178,73,89,6,97,117,185,202,32,137,78,4,159,197,114,9,240,130,167,49,202,184,21,144,70,161,47,227,211,11,0,98,140,51,25,74,194,69,22,207,158,108,65,26,198,136,17,185,221,10,52,254,65,136,54,137,248,167,148,189,191,186,164,109,175,147,115,141,95,167,213,2,63,0,100,16,15,219,109,91,63,4,83,49,81,243,222,47Line truncated
static const unsigned char v3[] = {31,139,8,0,0,0,0,0,0,19,93,83,77,111,219,48,12,189,247,87,112,186,248,176,90,254,254,72,97,5,232,186,30,118,24,182,195,54,108,71,213,86,98,181,178,148,73,74,220,252,251,209,118,146,165,5,12,147,122,239,145,162,68,170,249,240,249,219,195,143,63,223,31,161,247,131,90,223,52,147,1,197,245,150,17,161,9,180,138,59,199,72,31,110,246,74,193,211,54,28,123,233,5,89,223,0,52,189,224,221,228,160,235,165,87,98,221,68,139,93,176,65,120,14,109,207,173,19,158,145,189,223,132,53,129,232,154,236,189,223,133,226,239,94,30,24,249,29,254,188,15,31,204,176,227,94,62,41,129,59,27,237,133,198,200,47,143,76,116,91,241,46,86,243,65,48,114,144,98,220,25,235,175,228,163,236,124,207,58,113,144,173,8,231,197,45,72,45,189,228,42,116,45,87,130,37,52,254,159,76,73,253,2,86,40,70,36,166,32,224,143,59,204,43,7,190,21,145,59,108,63,190,14,138,64,111,197,134,145,142,123,126,247,134,185,109,208,1,116,180,99,193,116,154,187,40,26,199,145,142,25,53,118,27,165,113,28,79,210,0,54,82,41,22,104,163,69,0,83,205,159,204,43,11,98,136,33,205,241,11,192,121,107,94,78,229,178,32,161,197,25,98,65,187,183,22,15,246,96,148,177,193,26,26,188,160,254,172,199,226,69,203,119,44,176,102,175,187,224,26,126,54,82,95,240,142,5,95,147,156,214,69,5,73,69,227,58,229,105,70,235,188,134,147,193,74,226,130,230,69,30,38,52,75,238,107,186,42,43,88,254,72,37,73,13,43,90,21,135,144,86,191,86,247,37,148,115,64,9,171,3,162,252,189,58,76,49,71,10,37,141,211,180,77,104,149,101,180,204,33,163,69,9,120,243,117,1,211,78,5,76,59,13,5,173,146,28,98,158,230,52,69,236,100,230,44,39,234,34,201,32,67,34,185,16,1,182,16,154,233,122,215,239,186,185,116,107,224,82,211,214,57,178,116,215,249,163,18,174,23,194,159,212,77,116,158,223,230,201,116,199,243,164,15,82,135,61,14,138,21,56,254,56,209,19,55,139,16,146,59,15,206,182,248,28,164,243,198,30,41,138,233,179,155,100,11,123,45,92,6,105,48,221,126,154,230,57,108,46,233,173,30,171,152,223,221,63,169,238,250,235,136,3,0,0,0};
static const unsigned char v4[] = {31,139,8,0,0,0,0,0,0,19,237,60,107,143,227,200,113,127,133,222,195,1,51,23,146,203,151,164,25,10,9,28,4,8,98,192,231,15,78,62,196,216,217,15,124,180,36,122,248,10,73,141,70,195,200,191,221,85,253,98,179,73,189,238,246,156,179,145,93,104,165,174,170,174,174,174,234,46,86,87,23,247,243,15,191,49,186,40,203,15,89,153,38,109,107,188,249,182,111,123,198,255,26,63,254,238,191,140,223,103,9,41,91,2,173,93,215,213,109,248,249,179,66,106,39,85,241,195,231,31,204,48,218,116,164,49,195,152,108,170,134,244,113,245,110,181,217,71,86,110,195,184,106,82,210,88,0,89,179,159,161,99,180,85,158,165,198,119,100,65,86,36,62,105,125,45,171,59,88,73,85,118,164,236,194,79,159,78,187,174,200,251,60,43,137,181,35,217,118,215,133,174,189,88,91,7,18,191,102,157,213,145,247,14,71,34,86,148,254,121,223,2,210,113,190,95,91,69,245,97,117,81,76,49,97,176,182,42,181,165,252,220,192,48,214,38,42,178,252,24,254,14,70,108,140,183,168,49,255,181,201,162,220,252,15,146,191,145,46,75,34,179,141,202,214,106,73,147,109,120,7,18,117,251,134,0,168,235,96,138,109,248,41,121,115,221,79,230,167,182,117,220,79,140,6,248,100,81,151,85,165,66,85,213,237,199,39,195,247,78,113,149,30,251,34,106,182,89,25,58,107,117,110,89,185,131,113,186,211,174,233,57,196,89,39,85,94,53,2,195,149,104,117,85,109,29,178,180,219,133,110,253,126,138,226,184,9,15,64,64,30,190,116,89,151,147,175,143,253,72,69,41,73,170,134,202,19,238,75,232,143,99,26,105,213,117,36,93,95,35,56,237,92,115,231,153,59,223,220,5,230,110,97,238,150,61,157,34,213,161,16,139,66,14,227,73,68,253,88,116,125,32,65,23,155,109,215,84,229,182,87,153,196,85,14,98,156,146,42,37,230,107,156,154,117,67,192,16,69,221,171,54,219,103,96,234,178,106,235,40,33,230,127,254,251,143,240,219,250,35,217,238,115,176,226,143,164,204,43,19,64,81,82,153,255,86,149,176,232,162,214,252,125,22,19,54,188,129,212,128,216,55,25,216,253,15,228,96,74,86,235,97,126,46,41,78,109,17,229,185,50,231,39,231,251,83,187,7,169,247,181,2,93,45,190,31,153,210,89,215,85,155,209,137,54,36,135,33,223,200,250,141,52,184,162,114,43,202,179,45,42,0,240,81,142,204,96,203,116,93,85,132,150,237,45,112,72,96,13,38,134,38,182,96,197,230,164,167,234,131,109,135,251,194,17,235,96,118,113,0,48,143,234,150,132,226,199,41,222,3,247,210,204,202,122,223,153,85,221,109,155,106,95,155,45,201,73,210,153,200,56,106,72,52,82,237,200,176,76,21,184,181,102,236,60,183,128,181,69,43,215,122,29,165,41,122,5,71,72,196,68,96,115,235,26,216,104,224,3,138,176,172,74,114,250,210,29,107,242,207,140,238,171,201,90,13,129,237,36,26,160,182,34,131,22,35,145,11,62,170,107,18,1,171,132,132,12,179,142,163,228,21,103,92,166,82,95,84,241,42,34,43,162,45,97,3,135,212,127,108,170,100,223,54,32,108,95,237,59,156,98,24,237,187,138,35,97,225,101,229,27,88,49,101,190,110,23,165,213,129,245,174,155,106,11,98,182,253,25,99,135,161,16,52,43,75,48,86,91,103,165,197,181,49,224,96,204,49,78,248,3,42,4,159,63,76,51,217,125,157,155,56,234,115,147,145,60,93,115,225,173,106,179,1,213,133,150,7,238,98,24,134,177,80,54,229,28,51,166,20,217,103,147,229,196,218,215,121,21,165,214,85,213,227,122,145,27,189,221,23,176,18,142,125,154,181,117,30,29,195,60,107,65,11,29,172,240,56,175,146,215,255,217,87,29,49,211,212,76,115,115,147,109,193,197,154,19,215,99,238,26,179,70,103,32,253,231,137,206,19,230,54,133,152,57,217,146,50,237,135,85,87,144,114,111,86,185,185,199,71,10,12,222,118,199,156,77,112,110,141,202,141,1,246,196,29,32,44,122,162,251,40,100,75,1,102,146,144,29,245,86,114,39,77,81,125,5,142,37,235,142,161,203,183,198,119,207,73,228,71,27,193,106,158,203,45,12,190,52,85,62,108,19,110,144,100,223,180,64,83,87,25,62,213,78,33,104,28,125,72,42,16,41,217,68,251,28,92,244,62,205,42,51,137,96,45,183,38,41,98,146,154,217,166,137,10,98,102,197,214,172,226,63,163,127,104,223,182,230,91,150,146,74,26,142,154,75,247,102,69,150,166,57,57,97,71,70,93,68,239,226,25,133,174,99,180,128,119,64,76,202,175,146,35,93,99,16,71,132,184,37,211,166,170,245,144,130,134,5,220,191,161,147,6,11,89,239,96,173,57,248,81,192,169,71,1,223,75,6,210,1,36,169,154,170,131,182,104,181,175,228,48,144,211,150,164,108,97,182,200,203,85,155,71,209,172,163,18,112,198,208,56,202,70,86,194,46,251,168,192,193,27,162,43,152,45,183,218,50,170,97,17,54,89,210,149,224,51,66,112,30,239,25,56,181,35,163,218,54,81,154,17,116,202,77,85,88,242,121,98,104,216,183,44,58,143,236,170,9,14,180,149,149,81,46,101,129,7,227,142,164,214,7,105,42,1,43,247,5,108,217,196,98,251,80,135,114,69,79,136,155,40,81,199,65,239,9,94,14,253,142,10,97,174,136,47,13,167,126,159,162,248,10,223,108,54,10,142,3,253,248,201,219,44,159,156,105,47,238,133,29,195,49,190,115,224,143,66,49,143,186,4,101,195,145,84,199,198,249,190,17,179,137,27,92,209,212,112,28,130,113,107,19,181,157,98,133,35,93,37,2,176,219,19,177,222,56,4,158,35,176,143,164,45,48,180,84,176,45,169,179,72,52,112,95,8,161,133,8,124,187,140,229,146,192,137,128,18,165,75,42,17,19,145,37,102,42,187,68,141,39,33,193,194,103,233,112,125,150,3,130,77,247,100,183,141,85,149,249,177,151,75,55,13Line truncated
static const unsigned char v5[] = {31,139,8,0,0,0,0,0,0,19,189,91,233,114,219,72,146,254,239,167,168,230,184,155,164,155,0,9,82,151,105,81,29,62,219,158,112,219,14,75,118,108,143,66,225,6,137,34,137,22,174,0,64,145,28,13,223,105,159,97,159,108,191,204,194,81,32,33,74,125,236,206,168,105,28,85,89,89,89,121,124,153,85,232,118,133,248,240,241,226,245,80,60,255,244,78,76,108,207,75,132,191,72,82,145,164,118,156,138,165,155,206,69,211,142,220,110,83,184,129,8,99,71,198,34,13,69,34,227,27,41,210,185,20,118,20,9,59,21,118,176,22,95,62,191,123,244,168,185,72,36,58,199,238,36,109,62,123,228,250,81,8,50,183,98,222,17,177,12,208,187,35,208,224,60,181,83,201,87,175,167,83,57,73,249,242,179,156,118,196,60,245,189,142,248,28,46,82,12,180,17,211,56,244,133,104,154,221,241,34,112,60,105,254,158,232,68,223,77,194,32,233,136,247,225,204,13,58,226,92,166,169,27,204,58,226,197,34,77,67,122,128,81,58,34,117,163,151,161,23,198,104,200,255,74,167,35,62,132,169,59,117,39,118,234,82,187,79,54,250,103,215,95,34,47,180,157,55,174,39,21,149,156,7,176,48,9,49,110,32,131,52,81,108,60,162,209,83,26,61,20,35,17,197,97,148,136,209,25,207,224,183,211,228,102,38,38,158,157,36,163,199,183,252,202,228,187,141,88,249,94,144,140,26,243,52,141,134,221,238,114,185,52,151,3,51,140,103,221,126,175,215,235,162,91,67,220,184,114,249,34,92,141,26,61,209,19,86,223,60,57,230,223,195,198,217,169,35,167,201,217,105,146,174,61,121,102,122,158,49,241,18,195,186,157,186,158,55,12,192,219,51,8,62,188,150,195,127,128,86,118,109,248,46,68,233,185,248,103,104,21,15,151,174,147,206,135,61,243,48,90,61,219,156,118,21,193,211,174,34,63,19,174,51,106,188,183,215,50,254,214,111,8,199,78,109,35,176,125,153,61,19,253,70,181,141,101,212,181,178,208,42,178,161,62,74,12,141,156,91,52,29,53,126,193,132,142,250,29,203,60,233,127,61,49,159,90,207,45,243,240,164,195,63,61,252,223,234,88,248,235,153,7,39,111,15,108,203,60,56,232,240,15,191,50,44,195,28,28,63,55,143,158,226,47,107,13,233,160,73,207,51,76,188,180,250,182,121,2,202,86,214,222,180,14,13,80,250,122,104,30,30,227,205,49,254,178,110,230,201,145,129,255,222,30,152,199,131,175,199,102,255,4,175,143,240,199,175,123,29,190,57,124,251,212,60,32,138,135,248,43,94,128,226,201,225,215,129,121,112,248,188,210,5,60,91,131,206,160,99,30,31,225,175,120,200,12,218,102,255,41,254,212,67,98,19,255,209,188,158,170,31,245,152,39,247,182,111,14,158,126,37,225,144,92,142,59,252,163,88,62,232,152,253,195,183,150,181,243,162,20,232,191,26,221,189,146,39,177,42,225,126,181,118,5,143,213,96,90,111,107,135,199,224,88,26,76,252,233,193,46,7,232,128,137,130,125,208,176,171,19,179,168,239,224,216,195,172,77,203,214,164,131,199,150,101,90,7,154,20,233,209,129,121,112,156,175,72,33,120,203,80,55,160,111,13,242,229,44,133,111,168,101,57,122,75,203,162,47,52,189,161,102,95,177,146,186,106,96,33,173,67,136,1,92,177,210,148,26,69,111,6,166,101,117,170,186,215,83,179,96,1,119,103,217,127,48,217,179,223,224,13,166,139,96,66,62,68,188,149,54,252,92,235,214,131,99,88,40,215,6,175,151,200,244,124,30,46,207,93,71,142,109,186,47,111,54,109,113,251,72,192,63,166,139,56,80,254,227,209,169,227,222,228,235,55,158,25,203,57,172,24,110,213,157,92,175,225,127,35,163,39,254,109,92,30,156,92,137,213,210,152,46,60,79,140,217,55,27,99,17,173,141,190,120,124,171,209,23,63,252,32,154,145,103,28,247,155,27,145,198,118,144,184,196,168,1,103,47,156,69,204,158,207,24,244,122,234,221,52,140,253,198,25,248,209,89,136,86,32,154,141,132,1,122,194,167,159,169,39,87,2,140,249,137,49,129,95,148,49,247,67,207,177,242,157,233,58,130,59,80,55,13,17,6,19,15,252,195,33,202,27,114,148,85,137,180,248,217,119,55,237,77,62,102,42,87,169,145,120,136,20,198,65,175,151,145,6,241,199,183,236,247,77,116,74,6,69,235,185,113,212,16,221,108,252,110,126,161,77,129,153,165,31,195,18,51,59,50,86,198,1,88,240,166,6,92,162,76,39,115,225,205,134,234,249,81,57,150,214,63,150,96,197,69,212,211,8,193,197,229,67,213,12,166,75,166,24,178,102,20,116,77,34,59,168,206,219,23,219,243,135,62,205,164,67,97,216,78,134,88,97,210,171,141,54,126,149,131,185,235,56,50,160,225,198,94,56,185,166,139,185,26,126,105,68,43,126,62,51,102,177,13,109,1,113,97,199,174,109,168,62,224,32,94,200,202,212,88,234,42,32,66,131,220,212,35,47,207,234,221,16,46,22,99,148,175,137,210,249,141,182,214,249,147,82,76,221,234,42,241,63,244,3,35,218,232,118,148,43,198,237,34,246,148,189,100,134,162,226,238,7,251,230,189,27,92,35,244,182,110,153,165,14,179,2,8,17,19,144,64,39,52,207,227,113,166,206,217,192,54,183,25,53,254,241,248,150,46,54,141,92,104,234,94,140,70,212,93,252,36,154,144,145,90,129,195,158,90,143,177,183,144,198,17,108,101,22,135,139,168,41,134,162,201,207,89,146,199,120,62,15,111,100,60,172,182,85,207,114,121,31,230,157,55,74,149,148,58,12,4,158,1,32,57,134,239,136,8,214,150,171,129,7,119,2,88,99,28,137,105,24,224,137,244,221,113,232,57,186,61,208,188,11,67,88,162,37,25,67,33,240,199,74,60,155,92,232,133,212,33,240,253,94,231,198,13,61,153,26,22,77,33,49,104,18,158,189,214,46,141,48,146,193,144,189,6,11,105,101,244,10,133,161,2Line truncated
static const unsigned char v6[] = {45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,66,67,84,67,66,115,65,73,74,65,75,57,119,98,73,68,107,72,110,65,111,77,65,111,71,67,67,113,71,83,77,52,57,66,65,77,67,77,65,48,120,67,122,65,74,66,103,78,86,66,65,89,84,65,107,108,70,77,66,52,88,10,68,84,73,122,77,68,69,121,79,84,73,120,77,106,69,122,79,70,111,88,68,84,77,122,77,68,69,121,78,106,73,120,77,106,69,122,79,70,111,119,68,84,69,76,77,65,107,71,65,49,85,69,66,104,77,67,83,85,85,119,87,84,65,84,10,66,103,99,113,104,107,106,79,80,81,73,66,66,103,103,113,104,107,106,79,80,81,77,66,66,119,78,67,65,65,82,122,83,81,83,53,79,72,100,49,55,108,85,101,78,73,43,54,107,112,57,87,89,117,48,99,120,117,69,73,105,47,74,84,10,106,112,104,98,67,109,100,74,68,49,99,85,118,104,109,122,77,57,47,112,104,118,74,84,57,107,97,49,48,90,57,116,111,90,104,103,110,66,113,48,111,48,120,102,84,81,52,106,67,49,118,119,77,65,111,71,67,67,113,71,83,77,52,57,10,66,65,77,67,65,48,103,65,77,69,85,67,73,81,67,101,48,84,50,69,48,71,79,105,86,101,57,75,119,118,73,69,80,101,88,49,74,49,74,48,84,55,84,78,97,99,103,82,48,89,97,51,51,72,86,57,86,103,73,103,78,118,100,110,10,97,69,87,105,66,112,49,120,115,104,115,52,105,122,54,87,98,112,120,114,83,49,73,72,117,99,114,113,107,90,117,74,76,102,78,90,71,90,73,61,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,0};
static const unsigned char v7[] = {45,45,45,45,45,66,69,71,73,78,32,69,67,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10,77,72,99,67,65,81,69,69,73,67,66,122,51,72,79,107,81,76,80,66,68,116,100,107,110,113,67,55,107,49,80,78,115,87,106,54,72,102,104,121,78,66,53,77,101,110,102,106,109,113,105,111,111,65,111,71,67,67,113,71,83,77,52,57,10,65,119,69,72,111,85,81,68,81,103,65,69,99,48,107,69,117,84,104,51,100,101,53,86,72,106,83,80,117,112,75,102,86,109,76,116,72,77,98,104,67,73,118,121,85,52,54,89,87,119,112,110,83,81,57,88,70,76,52,90,115,122,80,102,10,54,89,98,121,85,47,90,71,116,100,71,102,98,97,71,89,89,74,119,97,116,75,78,77,88,48,48,79,73,119,116,98,56,65,61,61,10,45,45,45,45,45,69,78,68,32,69,67,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10,0};
static const struct packed_file {
const char *name;
const unsigned char *data;
size_t size;
time_t mtime;
} packed_files[] = {
{"/web_root/bundle.js.gz", v0, sizeof(v0) - 1, 1709984472},
{"/web_root/components.js.gz", v1, sizeof(v1) - 1, 1709984472},
{"/web_root/history.min.js.gz", v2, sizeof(v2) - 1, 1709984472},
{"/web_root/index.html.gz", v3, sizeof(v3) - 1, 1709996122},
{"/web_root/main.css.gz", v4, sizeof(v4) - 1, 1709996124},
{"/web_root/main.js.gz", v5, sizeof(v5) - 1, 1709984472},
{"/certs/server_cert.pem", v6, sizeof(v6) - 1, 1709980241},
{"/certs/server_key.pem", v7, sizeof(v7) - 1, 1709980241},
{NULL, NULL, 0, 0}
};
static int scmp(const char *a, const char *b) {
while (*a && (*a == *b)) a++, b++;
return *(const unsigned char *) a - *(const unsigned char *) b;
}
const char *mg_unlist(size_t no) {
return packed_files[no].name;
}
const char *mg_unpack(const char *name, size_t *size, time_t *mtime) {
const struct packed_file *p;
for (p = packed_files; p->name != NULL; p++) {
if (scmp(p->name, name) != 0) continue;
if (size != NULL) *size = p->size;
if (mtime != NULL) *mtime = p->mtime;
return (const char *) p->data;
}
return NULL;
};
@@ -0,0 +1,17 @@
module.exports = {
content: ['./web_root/*.{html,js}'],
xplugins: [ 'tailwindcss', 'xautoprefixer' ],
corePlugins: {outline: false},
theme: {
extend: {},
fontFamily: {
sans:
[
"Inter var, Arial, Helvetica, sans-serif", {
fontFeatureSettings: '"cv11", "ss01"',
fontVariationSettings: '"opsz" 32',
}
]
}
}
}
@@ -0,0 +1 @@
var n,t,e,_,r,o,i,u,l,c={},a=[],s=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function f(n,t){for(var e in t)n[e]=t[e];return n}function p(n){var t=n.parentNode;t&&t.removeChild(n)}function h(t,e,_){var r,o,i,u={};for(i in e)"key"==i?r=e[i]:"ref"==i?o=e[i]:u[i]=e[i];if(arguments.length>2&&(u.children=arguments.length>3?n.call(arguments,2):_),"function"==typeof t&&null!=t.defaultProps)for(i in t.defaultProps)void 0===u[i]&&(u[i]=t.defaultProps[i]);return d(t,u,r,o,null)}function d(n,_,r,o,i){var u={type:n,props:_,key:r,ref:o,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==i?++e:i};return null==i&&null!=t.vnode&&t.vnode(u),u}function v(){return{current:null}}function m(n){return n.children}function y(n,t){this.props=n,this.context=t}function g(n,t){if(null==t)return n.__?g(n.__,n.__.__k.indexOf(n)+1):null;for(var e;t<n.__k.length;t++)if(null!=(e=n.__k[t])&&null!=e.__e)return e.__e;return"function"==typeof n.type?g(n):null}function b(n){var t,e;if(null!=(n=n.__)&&null!=n.__c){for(n.__e=n.__c.base=null,t=0;t<n.__k.length;t++)if(null!=(e=n.__k[t])&&null!=e.__e){n.__e=n.__c.base=e.__e;break}return b(n)}}function k(n){(!n.__d&&(n.__d=!0)&&r.push(n)&&!C.__r++||o!==t.debounceRendering)&&((o=t.debounceRendering)||i)(C)}function C(){var n,t,e,_,o,i,l,c;for(r.sort(u);n=r.shift();)n.__d&&(t=r.length,_=void 0,o=void 0,l=(i=(e=n).__v).__e,(c=e.__P)&&(_=[],(o=f({},i)).__v=i.__v+1,T(c,i,o,e.__n,void 0!==c.ownerSVGElement,null!=i.__h?[l]:null,_,null==l?g(i):l,i.__h),R(_,i),i.__e!=l&&b(i)),r.length>t&&r.sort(u));C.__r=0}function x(n,t,e,_,r,o,i,u,l,s){var f,p,h,v,y,b,k,C=_&&_.__k||a,x=C.length;for(e.__k=[],f=0;f<t.length;f++)if(null!=(v=e.__k[f]=null==(v=t[f])||"boolean"==typeof v||"function"==typeof v?null:"string"==typeof v||"number"==typeof v||"bigint"==typeof v?d(null,v,null,null,v):Array.isArray(v)?d(m,{children:v},null,null,null):v.__b>0?d(v.type,v.props,v.key,v.ref?v.ref:null,v.__v):v)){if(v.__=e,v.__b=e.__b+1,null===(h=C[f])||h&&v.key==h.key&&v.type===h.type)C[f]=void 0;else for(p=0;p<x;p++){if((h=C[p])&&v.key==h.key&&v.type===h.type){C[p]=void 0;break}h=null}T(n,v,h=h||c,r,o,i,u,l,s),y=v.__e,(p=v.ref)&&h.ref!=p&&(k||(k=[]),h.ref&&k.push(h.ref,null,v),k.push(p,v.__c||y,v)),null!=y?(null==b&&(b=y),"function"==typeof v.type&&v.__k===h.__k?v.__d=l=E(v,l,n):l=U(n,v,h,C,y,l),"function"==typeof e.type&&(e.__d=l)):l&&h.__e==l&&l.parentNode!=n&&(l=g(h))}for(e.__e=b,f=x;f--;)null!=C[f]&&("function"==typeof e.type&&null!=C[f].__e&&C[f].__e==e.__d&&(e.__d=A(_).nextSibling),W(C[f],C[f]));if(k)for(f=0;f<k.length;f++)M(k[f],k[++f],k[++f])}function E(n,t,e){for(var _,r=n.__k,o=0;r&&o<r.length;o++)(_=r[o])&&(_.__=n,t="function"==typeof _.type?E(_,t,e):U(e,_,_,r,_.__e,t));return t}function H(n,t){return t=t||[],null==n||"boolean"==typeof n||(Array.isArray(n)?n.some((function(n){H(n,t)})):t.push(n)),t}function U(n,t,e,_,r,o){var i,u,l;if(void 0!==t.__d)i=t.__d,t.__d=void 0;else if(null==e||r!=o||null==r.parentNode)n:if(null==o||o.parentNode!==n)n.appendChild(r),i=null;else{for(u=o,l=0;(u=u.nextSibling)&&l<_.length;l+=1)if(u==r)break n;n.insertBefore(r,o),i=o}return void 0!==i?i:r.nextSibling}function A(n){var t,e,_;if(null==n.type||"string"==typeof n.type)return n.__e;if(n.__k)for(t=n.__k.length-1;t>=0;t--)if((e=n.__k[t])&&(_=A(e)))return _;return null}function P(n,t,e,_,r){var o;for(o in e)"children"===o||"key"===o||o in t||N(n,o,null,e[o],_);for(o in t)r&&"function"!=typeof t[o]||"children"===o||"key"===o||"value"===o||"checked"===o||e[o]===t[o]||N(n,o,t[o],e[o],_)}function S(n,t,e){"-"===t[0]?n.setProperty(t,null==e?"":e):n[t]=null==e?"":"number"!=typeof e||s.test(t)?e:e+"px"}function N(n,t,e,_,r){var o;n:if("style"===t)if("string"==typeof e)n.style.cssText=e;else{if("string"==typeof _&&(n.style.cssText=_=""),_)for(t in _)e&&t in e||S(n.style,t,"");if(e)for(t in e)_&&e[t]===_[t]||S(n.style,t,e[t])}else if("o"===t[0]&&"n"===t[1])o=t!==(t=t.replace(/Capture$/,"")),t=t.toLowerCase()in n?t.toLowerCase().slice(2):t.slice(2),n.l||(n.l={}),n.l[t+o]=e,e?_||n.addEventListener(t,o?D:w,o):n.removeEventListener(t,o?D:w,o);else if("dangerouslySetInnerHTML"!==t){if(r)t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!==t&&"height"!==t&&"href"!==t&&"list"!==t&&"form"!==t&&"tabIndex"!==t&&"download"!==t&&t in n)try{n[t]=null==e?"":e;break n}catch(n){}"function"==typeof e||(null==e||!1===e&&"-"!==t[4]?n.removeAttribute(t):n.setAttribute(t,e))}}function w(n){return this.l[n.type+!1](t.event?t.event(n):n)}function D(n){return this.l[n.type+!0](t.event?t.event(n):n)}function T(n,e,_,r,o,i,u,l,c){var a,s,p,h,d,v,g,b,k,C,E,H,U,A,P,S=e.type;if(void 0!==e.constructor)return null;null!=_.__h&&(c=_.__h,l=e.__e=_.__e,e.__h=null,i=[l]),(a=t.__b)&&a(e);try{n:if("function"==typeof S){if(b=e.props,k=(a=S.contextType)&&r[a.__c],C=a?k?k.props.value:a.__:r,_.__c?g=(s=e.__c=_.__c).__=s.__E:("prototype"in S&&S.prototype.render?e.__c=s=new S(b,C):(e.__c=s=new y(b,C),s.constructor=S,s.rLine truncated
@@ -0,0 +1,356 @@
'use strict';
import { h, render, useState, useEffect, useRef, html, Router } from './bundle.js';
// Helper function that returns a promise that resolves after delay
const Delay = (ms, val) => new Promise(resolve => setTimeout(resolve, ms, val));
export const Icons = {
heart: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path></svg>`,
downArrowBox: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15M9 12l3 3m0 0l3-3m-3 3V2.25" /> </svg>`,
upArrowBox: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15m0-3l-3-3m0 0l-3 3m3-3V15" /> </svg>`,
cog: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>`,
settingsH: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" /> </svg>`,
settingsV: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 13.5V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m12-3V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m-6-9V3.75m0 3.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 9.75V10.5" /> </svg>`,
scan: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 3.75H6A2.25 2.25 0 003.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0120.25 6v1.5m0 9V18A2.25 2.25 0 0118 20.25h-1.5m-9 0H6A2.25 2.25 0 013.75 18v-1.5M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg> `,
desktop: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" /> </svg>`,
alert: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0M3.124 7.5A8.969 8.969 0 015.292 3m13.416 0a8.969 8.969 0 012.168 4.5" /> </svg>`,
bell: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0M3.124 7.5A8.969 8.969 0 015.292 3m13.416 0a8.969 8.969 0 012.168 4.5" /> </svg>`,
refresh: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /> </svg> `,
bars4: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 5.25h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5" /> </svg>`,
bars3: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> </svg>`,
logout: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12.75 15l3-3m0 0l-3-3m3 3h-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>`,
save: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" /> </svg>`,
email: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /> </svg>`,
expand: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" /> </svg>`,
shrink: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" /> </svg>`,
ok: props => html`<svg class=${props.class} fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>`,
fail: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>`,
upload: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /> </svg> `,
download: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /> </svg> `,
bolt: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>`,
home: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> </svg> `,
link: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /> </svg> `,
shield: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /> </svg> `,
barsdown: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 4.5h14.25M3 9h9.75M3 13.5h9.75m4.5-4.5v12m0 0l-3.75-3.75M17.25 21L21 17.25" /> </svg> `,
arrowdown: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75" /> </svg> `,
arrowup: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75" /> </svg>`,
warn: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /> </svg>`,
info: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /> </svg>`,
exclamationTriangle: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /> </svg>`,
thumbUp: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M6.633 10.5c.806 0 1.533-.446 2.031-1.08a9.041 9.041 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75A2.25 2.25 0 0116.5 4.5c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H13.48c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23H5.904M14.25 9h2.25M5.904 18.75c.083.205.173.405.27.602.197.4-.078.898-.523.898h-.908c-.889 0-1.713-.518-1.972-1.368a12 12 0 01-.521-3.507c0-1.553.295-3.036.831-4.398C3.387 10.203 4.167 9.75 5 9.75h1.053c.472 0 .745.556.5.96a8.958 8.958 0 00-1.302 4.665c0 1.194.232 2.333.654 3.375z" /> </svg>`,
backward: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M21 16.811c0 .864-.933 1.405-1.683.977l-7.108-4.062a1.125 1.125 0 010-1.953l7.108-4.062A1.125 1.125 0 0121 8.688v8.123zM11.25 16.811c0 .864-.933 1.405-1.683.977l-7.108-4.062a1.125 1.125 0 010-1.953L9.567 7.71a1.125 1.125 0 011.683.977v8.123z" /> </svg>`,
chip: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25zm.75-12h9v9h-9v-9z" /> </svg>`,
camera: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z" /> </svg>`,
arrows: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" /> </svg>`,
doc: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>`,
};
export const tipColors = {
green: 'bg-green-100 text-green-900 ring-green-300',
yellow: 'bg-yellow-100 text-yellow-900 ring-yellow-300',
red: 'bg-red-100 text-red-900 ring-red-300',
};
export function Button({title, onclick, disabled, cls, icon, ref, colors, hovercolor, disabledcolor}) {
const [spin, setSpin] = useState(false);
const cb = function(ev) {
const res = onclick ? onclick() : null;
if (res && typeof (res.catch) === 'function') {
setSpin(true);
res.catch(() => false).then(() => setSpin(false));
}
};
if (!colors) colors = 'bg-blue-600 hover:bg-blue-500 disabled:bg-blue-400';
return html`
<button type="button" class="inline-flex justify-center items-center gap-2 rounded px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm ${colors} ${cls}"
ref=${ref} onclick=${cb} disabled=${disabled || spin} >
${title}
<${spin ? Icons.refresh : icon} class="w-4 ${spin ? 'animate-spin' : ''}" />
<//>`
};
export function Notification({ok, text, close}) {
const closebtn = useRef(null);
const from = 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2';
const to = 'translate-y-0 opacity-100 sm:translate-x-0';
const [tr, setTr] = useState(from);
useEffect(function() {
setTr(to);
setTimeout(ev => closebtn && closebtn.current.click && closebtn.current.click(), 1500);
}, []);
const onclose = ev => { setTr(from); setTimeout(close, 300); };
return html`
<div aria-live="assertive" class="z-10 pointer-events-none absolute inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transform ease-out duration-300 transition ${tr}">
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<${ok ? Icons.ok : Icons.fail} class="h-6 w-6 ${ok ? 'text-green-400' : 'text-red-400'}" />
<//>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium text-gray-900">${text}</p>
<p class="hidden mt-1 text-sm text-gray-500">Anyone with a link can now view this file.</p>
<//>
<div class="ml-4 flex flex-shrink-0">
<button type="button" ref=${closebtn} onclick=${onclose} class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none">
<span class="sr-only">Close</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
<//>
<//>
<//>
<//>
<//>
<//>
<//>
<//>`;
};
export function Login({loginFn, logoIcon, title, tipText}) {
const [user, setUser] = useState('');
const [pass, setPass] = useState('');
const onsubmit = function(ev) {
const authhdr = 'Basic ' + btoa(user + ':' + pass);
const headers = {Authorization: authhdr};
return fetch('api/login', {headers}).then(loginFn).finally(r => setPass(''));
};
return html`
<div class="h-full flex items-center justify-center bg-slate-200">
<div class="border rounded bg-white w-96 p-5">
<div class="my-5 py-2 flex items-center justify-center gap-x-4">
<${logoIcon} class="h-12 stroke-cyan-600 stroke-1" />
<h1 class="font-bold text-xl">${title || 'Login'}<//>
<//>
<div class="my-3">
<label class="block text-sm mb-1 dark:text-white">Username</label>
<input type="text" autocomplete="current-user" required
class="font-normal bg-white rounded border border-gray-300 w-full
flex-1 py-0.5 px-2 text-gray-900 placeholder:text-gray-400
focus:outline-none sm:text-sm sm:leading-6 disabled:cursor-not-allowed
disabled:bg-gray-100 disabled:text-gray-500"
oninput=${ev => setUser(ev.target.value)} value=${user} />
<//>
<div class="my-3">
<label class="block text-sm mb-1 dark:text-white">Password</label>
<input type="password" autocomplete="current-password" required
class="font-normal bg-white rounded border border-gray-300 w-full flex-1 py-0.5 px-2 text-gray-900 placeholder:text-gray-400 focus:outline-none sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-500"
oninput=${ev => setPass(ev.target.value)}
value=${pass} onchange=${onsubmit} />
<//>
<div class="mt-7">
<${Button} title="Sign In" icon=${Icons.logout} onclick=${onsubmit} cls="flex w-full justify-center" />
<//>
<div class="mt-5 text-slate-400 text-xs">${tipText}<//>
<//>
<//>`;
};
export function Colored({icon, text, colors}) {
colors ||= 'bg-slate-100 text-slate-900';
return html`
<span class="inline-flex items-center gap-1.5 py-0.5">
${icon && html`<${icon} class="w-5 h-5" />`}
<span class="inline-block font-medium rounded-md px-2 py-1 text-xs ring-1 ring-inset ${colors}">${text}<//>
<//>`;
};
export function Stat({title, text, tipText, tipIcon, tipColors, colors}) {
return html`
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-slate-900 dark:border-gray-800">
<div class="overflow-auto rounded-lg bg-white px-4 py-2 ">
<div class="flex items-center gap-x-2">
<p class="text-sm truncate text-gray-500 font-medium"> ${title} </p>
<//>
<div class="mt-1 flex items-center gap-x-2">
<h3 class="text-xl truncate font-semibold tracking-tight ${colors || 'text-gray-800 dark:text-gray-200'}">
${text}
<//>
<span class="flex items-center ${tipText || 'hidden'}">
<${Colored} text=${tipText} icon=${tipIcon} colors=${tipColors} />
<//>
<//>
<//>
<//>`;
};
export function TextValue({value, setfn, disabled, placeholder, type, addonRight, addonLeft, attr, min, max, step, mult}) {
const [bg, setBg] = useState('bg-white');
useEffect(() => { if (type == 'number') checkval(+min, +max, +value); }, []);
step ||= '1', mult ||= 1;
const checkval = function(min, max, v) {
setBg('bg-white');
if (min && v < min) setBg('bg-red-100 border-red-200');
if (max && v > max) setBg('bg-red-100 border-red-200');
};
const m = step.match(/^.+\.(.+)/);
const digits = m ? m[1].length : 0;
const onchange = ev => {
let v = ev.target.value;
if (type == 'number') {
checkval(+min, +max, +v);
v = +(parseFloat(v) / mult).toFixed(digits);
}
setfn(v);
};
if (type == 'number') value = +(value * mult).toFixed(digits);
return html`
<div class="flex w-full items-center rounded border shadow-sm ${bg}">
${addonLeft && html`<span class="inline-flex font-normal truncate py-1 border-r bg-slate-100 items-center border-gray-300 px-2 text-gray-500 text-xs">${addonLeft}<//>` }
<input type=${type || 'text'} disabled=${disabled} value=${value}
step=${step} min=${min} max=${max}
onchange=${onchange} ...${attr}
class="${bg} font-normal text-sm rounded w-full flex-1 py-0.5 px-2 text-gray-700 placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-500" placeholder=${placeholder} />
${addonRight && html`<span class="inline-flex font-normal truncate py-1 border-l bg-slate-100 items-center border-gray-300 px-2 text-gray-500 text-xs overflow-scroll" style="min-width: 50%;">${addonRight}<//>` }
<//>`;
};
export function SelectValue({value, setfn, options, disabled}) {
const toInt = x => x == parseInt(x) ? parseInt(x) : x;
const onchange = ev => setfn(toInt(ev.target.value));
return html`
<select onchange=${onchange} class="w-full rounded font-normal border py-0.5 px-1 text-gray-600 focus:outline-none text-sm disabled:cursor-not-allowed" disabled=${disabled}>
${options.map(v => html`<option value=${v[0]} selected=${v[0] == value}>${v[1]}<//>`) }
<//>`;
};
export function SwitchValue({value, setfn}) {
const onclick = ev => setfn(!value);
const bg = !!value ? 'bg-blue-600' : 'bg-gray-200';
const tr = !!value ? 'translate-x-5' : 'translate-x-0';
return html`
<button type="button" onclick=${onclick} class="${bg} inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-0 ring-0" role="switch" aria-checked=${!!value}>
<span aria-hidden="true" class="${tr} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 focus:ring-0 transition duration-200 ease-in-out"></span>
</button>`;
};
export function Setting(props) {
let input = TextValue;
if (props.type == 'switch') input = SwitchValue;
if (props.type == 'select') input = SelectValue;
return html`
<div class=${props.cls || 'grid grid-cols-2 gap-2 my-1'}>
<label class="flex items-center text-sm text-gray-700 mr-2 font-medium ${props.title || 'hidden'}">${props.title}<//>
<div class="flex items-center">${h(input, props)}<//>
<//>`;
};
export function Pagination({ totalItems, itemsPerPage, currentPage, setPageFn, colors }) {
const totalPages = Math.ceil(totalItems / itemsPerPage);
const maxPageRange = 2;
const lessThanSymbol = "<";
const greaterThanSymbol = ">";
const whiteSpace = " ";
const itemcls = 'relative inline-flex items-center px-3 py-1 text-sm focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-blue-600';
colors ||= 'bg-blue-600';
const PageItem = ({ page, isActive }) => (
html`<a
onClick=${() => setPageFn(page)}
class="${itemcls} ${isActive ? `${colors} text-white` : 'cursor-pointer text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50'}"
>
${page}
</a>`
);
return html`
<div class="flex items-center justify-between bg-white px-3 py-2">
<div class="sm:flex sm:flex-1 sm:items-center sm:justify-between space-x-4 whitespace-nowrap select-none">
<p class="text-sm text-slate-500 font-medium">
showing <span class="font-bold text-slate-700">${(currentPage - 1) * itemsPerPage + 1}</span> - <span class="font-medium">${Math.min(currentPage * itemsPerPage, totalItems)}</span> of ${whiteSpace}
<span class="font-bold text-slate-700">${totalItems}</span> results
</p>
<div>
<nav class="isolate inline-flex -space-x-px rounded-md" aria-label="Pagination">
<a
onClick=${() => setPageFn(Math.max(currentPage - 1, 1))}
class="relative inline-flex px-3 items-center text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${currentPage != 1 ? 'cursor-pointer' : ''} focus:z-20 focus:outline-offset-0">
${lessThanSymbol}
</a>
<${PageItem} page=${1} isActive=${currentPage === 1} />
${currentPage > maxPageRange + 2 ? html`<span class="${itemcls} ring-1 ring-inset ring-gray-300 text-slate-300">...</span>` : ''}
${Array.from({length: Math.min(totalPages, maxPageRange * 2 + 1)}, (_, i) => Math.max(2, currentPage - maxPageRange) + i).map(page => page > 1 && page < totalPages && html`<${PageItem} page=${page} isActive=${currentPage === page} />`)}
${currentPage < totalPages - (maxPageRange + 1) ? html`<span class="${itemcls} ring-1 ring-inset ring-gray-300 text-slate-300">...</span>` : ''}
${totalPages > 1 ? html`<${PageItem} page=${totalPages} isActive=${currentPage === totalPages} />` : ''}
<a
onClick=${() => setPageFn(Math.min(currentPage + 1, totalPages))}
class="relative inline-flex px-3 items-center text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${currentPage != totalPages ? 'cursor-pointer' : ''} focus:z-20 focus:outline-offset-0">
${greaterThanSymbol}
</a>
</nav>
</div>
</div>
</div>`;
};
export function UploadFileButton(props) {
const [upload, setUpload] = useState(null); // Upload promise
const [status, setStatus] = useState(''); // Current upload status
const btn = useRef(null);
const input = useRef(null);
// Send a large file chunk by chunk
const sendFileData = function(url, fileName, fileData, chunkSize) {
return new Promise(function(resolve, reject) {
const finish = ok => {
setUpload(null);
const res = props.onupload ? props.onupload(ok, fileName, fileData.length) : null;
if (res && typeof (res.catch) === 'function') {
res.catch(() => false).then(() => ok ? resolve() : reject());
} else {
ok ? resolve() : reject();
}
};
const sendChunk = function(offset) {
var chunk = fileData.subarray(offset, offset + chunkSize) || '';
var opts = {method: 'POST', body: chunk};
var fullUrl = url + '?offset=' + offset +
'&total=' + fileData.length +
'&name=' + encodeURIComponent(fileName);
var ok;
setStatus('Uploading ' + fileName + ', bytes ' + offset + '..' +
(offset + chunk.length) + ' of ' + fileData.length);
fetch(fullUrl, opts)
.then(function(res) {
if (res.ok && chunk.length > 0) sendChunk(offset + chunk.length);
ok = res.ok;
return res.text();
})
.then(function(text) {
if (!ok) setStatus('Error: ' + text), finish(ok); // Fail
if (chunk.length > 0) return; // More chunks to send
setStatus(x => x + '. Done, resetting device...');
finish(ok); // All chunks sent
});
};
sendChunk(0);
});
};
const onchange = function(ev) {
if (!ev.target.files[0]) return;
let r = new FileReader(), f = ev.target.files[0];
r.readAsArrayBuffer(f);
r.onload = function() {
setUpload(sendFileData(props.url, f.name, new Uint8Array(r.result), 2048));
ev.target.value = '';
ev.preventDefault();
btn && btn.current.base.click();
};
};
const onclick = function(ev) {
let fn; setUpload(x => fn = x);
if (!fn) input.current.click(); // No upload in progress, show file dialog
return fn;
};
return html`
<div class="inline-flex flex-col ${props.class}">
<input class="hidden" type="file" ref=${input} onchange=${onchange} accept=${props.accept} />
<${Button} title=${props.title} icon=${Icons.download} onclick=${onclick} ref=${btn} colors=${props.colors} />
<div class="pt-2 text-sm text-slate-400 ${status || 'hidden'}">${status}<//>
<//>`;
};
@@ -0,0 +1 @@
!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.History=n():t.History=n()}(this,function(){return function(t){function n(o){if(e[o])return e[o].exports;var r=e[o]={exports:{},id:o,loaded:!1};return t[o].call(r.exports,r,r.exports,n),r.loaded=!0,r.exports}var e={};return n.m=t,n.c=e,n.p="",n(0)}([function(t,n,e){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0,n.createPath=n.parsePath=n.locationsAreEqual=n.createLocation=n.createMemoryHistory=n.createHashHistory=n.createBrowserHistory=void 0;var r=e(2);Object.defineProperty(n,"createLocation",{enumerable:!0,get:function(){return r.createLocation}}),Object.defineProperty(n,"locationsAreEqual",{enumerable:!0,get:function(){return r.locationsAreEqual}});var i=e(1);Object.defineProperty(n,"parsePath",{enumerable:!0,get:function(){return i.parsePath}}),Object.defineProperty(n,"createPath",{enumerable:!0,get:function(){return i.createPath}});var a=e(7),c=o(a),u=e(8),s=o(u),f=e(9),l=o(f);n.createBrowserHistory=c.default,n.createHashHistory=s.default,n.createMemoryHistory=l.default},function(t,n){"use strict";n.__esModule=!0;n.addLeadingSlash=function(t){return"/"===t.charAt(0)?t:"/"+t},n.stripLeadingSlash=function(t){return"/"===t.charAt(0)?t.substr(1):t},n.stripPrefix=function(t,n){return 0===t.indexOf(n)?t.substr(n.length):t},n.stripTrailingSlash=function(t){return"/"===t.charAt(t.length-1)?t.slice(0,-1):t},n.parsePath=function(t){var n=t||"/",e="",o="",r=n.indexOf("#");r!==-1&&(o=n.substr(r),n=n.substr(0,r));var i=n.indexOf("?");return i!==-1&&(e=n.substr(i),n=n.substr(0,i)),n=decodeURI(n),{pathname:n,search:"?"===e?"":e,hash:"#"===o?"":o}},n.createPath=function(t){var n=t.pathname,e=t.search,o=t.hash,r=encodeURI(n||"/");return e&&"?"!==e&&(r+="?"===e.charAt(0)?e:"?"+e),o&&"#"!==o&&(r+="#"===o.charAt(0)?o:"#"+o),r}},function(t,n,e){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0,n.locationsAreEqual=n.createLocation=void 0;var r=Object.assign||function(t){for(var n=1;n<arguments.length;n++){var e=arguments[n];for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&(t[o]=e[o])}return t},i=e(10),a=o(i),c=e(11),u=o(c),s=e(1);n.createLocation=function(t,n,e,o){var i=void 0;return"string"==typeof t?(i=(0,s.parsePath)(t),i.state=n):(i=r({},t),void 0===i.pathname&&(i.pathname=""),i.search?"?"!==i.search.charAt(0)&&(i.search="?"+i.search):i.search="",i.hash?"#"!==i.hash.charAt(0)&&(i.hash="#"+i.hash):i.hash="",void 0!==n&&void 0===i.state&&(i.state=n)),i.key=e,o&&(i.pathname?"/"!==i.pathname.charAt(0)&&(i.pathname=(0,a.default)(i.pathname,o.pathname)):i.pathname=o.pathname),i},n.locationsAreEqual=function(t,n){return t.pathname===n.pathname&&t.search===n.search&&t.hash===n.hash&&t.key===n.key&&(0,u.default)(t.state,n.state)}},function(t,n,e){"use strict";var o=function(){};t.exports=o},function(t,n,e){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0;var r=e(3),i=(o(r),function(){var t=null,n=function(n){return t=n,function(){t===n&&(t=null)}},e=function(n,e,o,r){if(null!=t){var i="function"==typeof t?t(n,e):t;"string"==typeof i?"function"==typeof o?o(i,r):r(!0):r(i!==!1)}else r(!0)},o=[],r=function(t){var n=!0,e=function(){n&&t.apply(void 0,arguments)};return o.push(e),function(){n=!1,o=o.filter(function(t){return t!==e})}},i=function(){for(var t=arguments.length,n=Array(t),e=0;e<t;e++)n[e]=arguments[e];o.forEach(function(t){return t.apply(void 0,n)})};return{setPrompt:n,confirmTransitionTo:e,appendListener:r,notifyListeners:i}});n.default=i},function(t,n){"use strict";n.__esModule=!0;n.canUseDOM=!("undefined"==typeof window||!window.document||!window.document.createElement),n.addEventListener=function(t,n,e){return t.addEventListener?t.addEventListener(n,e,!1):t.attachEvent("on"+n,e)},n.removeEventListener=function(t,n,e){return t.removeEventListener?t.removeEventListener(n,e,!1):t.detachEvent("on"+n,e)},n.getConfirmation=function(t,n){return n(window.confirm(t))},n.supportsHistory=function(){var t=window.navigator.userAgent;return(t.indexOf("Android 2.")===-1&&t.indexOf("Android 4.0")===-1||t.indexOf("Mobile Safari")===-1||t.indexOf("Chrome")!==-1||t.indexOf("Windows Phone")!==-1)&&(window.history&&"pushState"in window.history)},n.supportsPopStateOnHashChange=function(){return window.navigator.userAgent.indexOf("Trident")===-1},n.supportsGoWithoutReloadUsingHash=function(){return window.navigator.userAgent.indexOf("Firefox")===-1},n.isExtraneousPopstateEvent=function(t){return void 0===t.state&&navigator.userAgent.indexOf("CriOS")===-1}},function(t,n,e){"use strict";var o=function(t,n,e,o,r,i,a,c){if(!t){var u;if(void 0===n)u=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var s=[e,o,r,i,a,c],f=0;u=new Error(n.replace(/%s/g,function(){return s[f++]})),u.name="InLine truncated
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en" class="h-full bg-white">
<head>
<title></title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor'> <path stroke-linecap='round' stroke-linejoin='round' d='M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0' /> </svg>" />
<link href="main.css" rel="stylesheet" />
</head>
<body class="min-h-screen"></body>
<script src="history.min.js"></script>
<script type="module" src="main.js"></script>
</html>
@@ -0,0 +1 @@
/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter var,Arial,Helvetica,sans-serif;font-feature-settings:"cv11","ss01";font-variation-settings:"opsz" 32}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.right-auto{right:auto}.top-0{top:0}.isolate{isolation:isolate}.z-10{z-index:10}.z-\[48\]{z-index:48}.z-\[60\]{z-index:60}.col-span-2{grid-column:span 2/span 2}.m-4{margin:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mb-1{margin-bottom:.25rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-3{margin-top:.75rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-0{width:0}.w-11{width:2.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-96{width:24rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-\[30px\]{flex-basis:30px}.border-separate{border-collapse:initial}.border-spaLine truncated
@@ -0,0 +1,376 @@
// NOTE: API calls must start with 'api/' in order to serve the app at any URI
'use strict';
import { h, render, useState, useEffect, useRef, html, Router } from './bundle.js';
import { Icons, Login, Setting, Button, Stat, tipColors, Colored, Notification, Pagination, UploadFileButton } from './components.js';
const Logo = props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.87 12.85"><defs><style>.ll-cls-1{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:0.5px;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="ll-cls-1" d="M12.62,1.82V8.91A1.58,1.58,0,0,1,11,10.48H4a1.44,1.44,0,0,1-1-.37A.69.69,0,0,1,2.84,10l-.1-.12a.81.81,0,0,1-.15-.48V5.57a.87.87,0,0,1,.86-.86H4.73V7.28a.86.86,0,0,0,.86.85H9.42a.85.85,0,0,0,.85-.85V3.45A.86.86,0,0,0,10.13,3,.76.76,0,0,0,10,2.84a.29.29,0,0,0-.12-.1,1.49,1.49,0,0,0-1-.37H2.39V1.82A1.57,1.57,0,0,1,4,.25H11A1.57,1.57,0,0,1,12.62,1.82Z"/><path class="ll-cls-1" d="M10.48,10.48V11A1.58,1.58,0,0,1,8.9,12.6H1.82A1.57,1.57,0,0,1,.25,11V3.94A1.57,1.57,0,0,1,1.82,2.37H8.9a1.49,1.49,0,0,1,1,.37l.12.1a.76.76,0,0,1,.11.14.86.86,0,0,1,.14.47V7.28a.85.85,0,0,1-.85.85H8.13V5.57a.86.86,0,0,0-.85-.86H3.45a.87.87,0,0,0-.86.86V9.4a.81.81,0,0,0,.15.48l.1.12a.69.69,0,0,0,.13.11,1.44,1.44,0,0,0,1,.37Z"/></g></g></svg>`;
function Header({logout, user, setShowSidebar, showSidebar}) {
return html`
<div class="bg-white sticky top-0 z-[48] xw-full border-b py-2 ${showSidebar && 'pl-72'} transition-all duration-300 transform">
<div class="px-2 w-full py-0 my-0 flex items-center">
<button type="button" onclick=${ev => setShowSidebar(v => !v)} class="text-slate-400">
<${Icons.bars3} class="h-6" />
<//>
<div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
<div class="relative flex flex-1"><//>
<div class="flex items-center gap-x-4 lg:gap-x-6">
<span class="text-sm text-slate-400">logged in as: ${user}<//>
<div class="hidden lg:block lg:h-4 lg:w-px lg:bg-gray-200" aria-hidden="true"><//>
<${Button} title="Logout" icon=${Icons.logout} onclick=${logout} />
<//>
<//>
<//>
<//>`;
};
function Sidebar({url, show}) {
const NavLink = ({title, icon, href, url}) => html`
<div>
<a href="#${href}" class="${href == url ? 'bg-slate-50 text-blue-600 group' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50 group'} flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
<${icon} class="w-6 h-6"/>
${title}
<///>
<//>`;
return html`
<div class="bg-violet-100 hs-overlay hs-overlay-open:translate-x-0
-translate-x-full transition-all duration-300 transform
fixed top-0 left-0 bottom-0 z-[60] w-72 bg-white border-r
border-gray-200 overflow-y-auto scrollbar-y
${show && 'translate-x-0'} right-auto bottom-0">
<div class="flex flex-col m-4 gap-y-6">
<div class="flex h-10 shrink-0 items-center gap-x-4 font-bold text-xl text-slate-500">
<${Logo} class="h-full"/> Your Brand
<//>
<div class="flex flex-1 flex-col">
<${NavLink} title="Dashboard" icon=${Icons.home} href="/" url=${url} />
<${NavLink} title="Settings" icon=${Icons.cog} href="/settings" url=${url} />
<${NavLink} title="Firmware Update" icon=${Icons.download} href="/update" url=${url} />
<${NavLink} title="Events" icon=${Icons.alert} href="/events" url=${url} />
<//>
<//>
<//>`;
};
function Events({}) {
const [events, setEvents] = useState([]);
const [page, setPage] = useState(1);
const refresh = () =>
fetch('api/events/get', {
method: 'POST', body: JSON.stringify({page: page}),
}).then(r => r.json())
.then(r => setEvents(r));
useEffect(refresh, [page]);
useEffect(() => {
setPage(JSON.parse(localStorage.getItem('page')));
}, []);
useEffect(() => {
localStorage.setItem('page', page.toString());
}, [page]);
const Th = props => html`<th scope="col" class="sticky top-0 z-10 border-b border-slate-300 bg-white bg-opacity-75 py-1.5 px-4 text-left text-sm font-semibold text-slate-900 backdrop-blur backdrop-filter">${props.title}</th>`;
const Td = props => html`<td class="whitespace-nowrap border-b border-slate-200 py-2 px-4 pr-3 text-sm text-slate-900">${props.text}</td>`;
const Prio = ({prio}) => {
const text = ['high', 'medium', 'low'][prio];
const colors = [tipColors.red, tipColors.yellow, tipColors.green][prio];
return html`<${Colored} colors=${colors} text=${text} />`;
};
const Event = ({e}) => html`
<tr>
<${Td} text=${['power', 'hardware', 'tier3', 'tier4'][e.type]} />
<${Td} text=${html`<${Prio} prio=${e.prio}/>`} />
<${Td} text=${e.time ? (new Date(e.time * 1000)).toLocaleString() : '1970-01-01'} />
<${Td} text=${e.text} />
<//>`;
return html`
<div class="m-4 divide-y divide-gray-200 overflow-auto rounded bg-white">
<div class="font-semibold flex items-center text-gray-600 px-3 justify-between whitespace-nowrap">
<div class="font-semibold flex items-center text-gray-600">
<div class="mr-4">EVENT LOG</div>
</div>
<${Pagination} currentPage=${page} setPageFn=${setPage} totalItems=400 itemsPerPage=20 />
<//>
<div class="inline-block min-w-full align-middle" style="max-height: 82vh; overflow: auto;">
<table class="min-w-full border-separate border-spacing-0">
<thead>
<tr>
<${Th} title="Type" />
<${Th} title="Prio" />
<${Th} title="Time" />
<${Th} title="Description" />
</tr>
</thead>
<tbody>
${(events.arr ? events.arr : []).map(e => h(Event, {e}))}
</tbody>
</table>
<//>
<//>`;
};
function Chart({data}) {
const n = data.length /* entries */, w = 20 /* entry width */, ls = 15/* left space */;
const h = 100 /* graph height */, yticks = 5 /* Y axis ticks */, bs = 10 /* bottom space */;
const ymax = 25;
const yt = i => (h - bs) / yticks * (i + 1);
const bh = p => (h - bs) * p / 100; // Bar height
const by = p => (h - bs) - bh(p);
const range = (start, size, step) => Array.from({length: size}, (_, i) => i * (step || 1) + start);
// console.log(ds);
return html`
<div class="my-4 divide-y divide-gray-200 overflow-auto rounded bg-white">
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
Temperature, last 24h
<//>
<div class="relative">
<svg class="bg-yellow-x50 w-full p-4" viewBox="0 0 ${n*w+ls} ${h}">
${range(0, yticks).map(i => html`
<line x1=0 y1=${yt(i)} x2=${ls+n*w} y2=${yt(i)} stroke-width=0.3 class="stroke-slate-300" stroke-dasharray="1,1" />
<text x=0 y=${yt(i)-2} class="text-[6px] fill-slate-400">${ymax-ymax/yticks*(i+1)}<//>
`)}
${range(0, n).map(x => html`
<rect x=${ls+x*w} y=${by(data[x]*100/ymax)} width=12 height=${bh(data[x]*100/ymax)} rx=2 class="fill-cyan-500" />
<text x=${ls+x*w} y=100 class="text-[6px] fill-slate-400">${x*2}:00<//>
`)}
<//>
<//>
<//>`;
};
function DeveloperNote({text, children}) {
return html`
<div class="flex p-4 gap-2">
<div class="text-sm text-slate-500">
<div class="flex items-center">
<${Icons.info} class="self-start basis-[30px] grow-0 shrink-0 text-green-600 mr-2" />
<div class="font-semibold">Developer Note<//>
<//>
${(text || '').split('.').map(v => html` <p class="my-2 ">${v}<//>`)}
${children}
<//>
<//>`;
};
function Main({}) {
const [stats, setStats] = useState(null);
const refresh = () => fetch('api/stats/get').then(r => r.json()).then(r => setStats(r));
useEffect(refresh, []);
if (!stats) return '';
return html`
<div class="p-2">
<div class="p-4 sm:p-2 mx-auto grid grid-cols-2 lg:grid-cols-4 gap-4">
<${Stat} title="Temperature" text="${stats.temperature} °C" tipText="good" tipIcon=${Icons.ok} tipColors=${tipColors.green} />
<${Stat} title="Humidity" text="${stats.humidity} %" tipText="warn" tipIcon=${Icons.warn} tipColors=${tipColors.yellow} />
<div class="bg-white col-span-2 border rounded-md shadow-lg" role="alert">
<${DeveloperNote} text="Stats data is received from the Mongoose backend" />
<//>
<//>
<div class="p-4 sm:p-2 mx-auto grid grid-cols-1 lg:grid-cols-2 gap-4">
<${Chart} data=${stats.points} />
<div class="my-4 hx-24 bg-white border rounded-md shadow-lg" role="alert">
<${DeveloperNote}
text="This chart is an SVG image, generated on the fly from the
data returned by the api/stats/get API call" />
<//>
<//>
<//>`;
};
function FirmwareStatus({title, info, children}) {
const state = ['UNAVAILABLE', 'FIRST_BOOT', 'NOT_COMMITTED', 'COMMITTED'][(info.status || 0) % 4];
const valid = info.status > 0;
return html`
<div class="bg-white py-1 divide-y border rounded">
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
${title}
<//>
<div class="px-4 py-2 relative">
<div class="my-1">Status: ${state}<//>
<div class="my-1">CRC32: ${valid ? info.crc32.toString(16) : 'n/a'}<//>
<div class="my-1">Size: ${valid ? info.size : 'n/a'}<//>
<div class="my-1">Flashed at: ${valid ? new Date(info.timestamp * 1000).toLocaleString() : 'n/a'}<//>
${children}
<//>
<//>`;
};
function FirmwareUpdate({}) {
const [info, setInfo] = useState([{}, {}]);
const refresh = () => fetch('api/firmware/status').then(r => r.json()).then(r => setInfo(r));
useEffect(refresh, []);
const oncommit = ev => fetch('api/firmware/commit')
.then(r => r.json())
.then(refresh);
const onreboot = ev => fetch('api/device/reset')
.then(r => r.json())
.then(r => new Promise(r => setTimeout(ev => { refresh(); r(); }, 3000)));
const onrollback = ev => fetch('api/firmware/rollback')
.then(onreboot);
const onerase = ev => fetch('api/device/eraselast').then(refresh);
const onupload = function(ok, name, size) {
if (!ok) return false;
return new Promise(r => setTimeout(ev => { refresh(); r(); }, 3000));
};
return html`
<div class="m-4 gap-4 grid grid-cols-1 lg:grid-cols-3">
<${FirmwareStatus} title="Current firmware image" info=${info[0]}>
<div class="flex flex-wrap gap-2">
<${Button} title="Commit this firmware" onclick=${oncommit}
icon=${Icons.thumbUp} disabled=${info[0].status == 3} cls="w-full" />
<//>
<//>
<${FirmwareStatus} title="Previous firmware image" info=${info[1]}>
<${Button} title="Rollback to this firmware" onclick=${onrollback}
icon=${Icons.backward} disabled=${info[1].status == 0} cls="w-full" />
<//>
<div class="bg-white xm-4 divide-y border rounded flex flex-col">
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
Device control
<//>
<div class="px-4 py-3 flex flex-col gap-2 grow">
<${UploadFileButton}
title="Upload new firmware .bin file" onupload=${onupload}
url="api/firmware/upload" accept=".bin,.uf2" />
<div class="grow"><//>
<${Button} title="Reboot device" onclick=${onreboot} icon=${Icons.refresh} cls="w-full" />
<${Button} title="Erase last sector" onclick=${onerase} icon=${Icons.doc} cls="w-full hidden" />
<//>
<//>
<//>
<div class="m-4 gap-4 grid grid-cols-1 lg:grid-cols-2">
<div class="bg-white border shadow-lg">
<${DeveloperNote}>
<div class="my-2">
Firmware status and other information is stored in the last sector
of flash
<//>
<div class="my-2">
Firmware status can be FIRST_BOOT, UNCOMMITTED or COMMITTED. If no
information is available, it is UNAVAILABLE.
<//>
<div class="my-2">
This GUI loads a firmware file and sends it chunk by chunk to the
device, passing current chunk offset, total firmware size and a file name:
api/firmware/upload?offset=X&total=Y&name=Z
<//>
<//>
<//>
<div class="bg-white border shadow-lg">
<${DeveloperNote}>
<div>
Firmware update mechanism defines 3 API functions that the target
device must implement: mg_ota_begin(), mg_ota_write() and mg_ota_end()
<//>
<div class="my-2">
RESTful API handlers use ota_xxx() API to save firmware to flash.
The last 0-length chunk triggers ota_end() which performs firmware
update using saved firmware image
<//>
<div class="my-2">
<a class="link text-blue-600 underline"
href="https://mongoose.ws/webinars/">Join our free webinar</a> to
get detailed explanations about possible firmware updates strategies
and implementation demo
<//>
<//>
<//>
<//>`;
};
function Settings({}) {
const [settings, setSettings] = useState(null);
const [saveResult, setSaveResult] = useState(null);
const refresh = () => fetch('api/settings/get')
.then(r => r.json())
.then(r => setSettings(r));
useEffect(refresh, []);
const mksetfn = k => (v => setSettings(x => Object.assign({}, x, {[k]: v})));
const onsave = ev => fetch('api/settings/set', {
method: 'post', body: JSON.stringify(settings)
}).then(r => r.json())
.then(r => setSaveResult(r))
.then(refresh);
if (!settings) return '';
const logOptions = [[0, 'Disable'], [1, 'Error'], [2, 'Info'], [3, 'Debug']];
return html`
<div class="m-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="py-1 divide-y border rounded bg-white flex flex-col">
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
Device Settings
<//>
<div class="py-2 px-5 flex-1 flex flex-col relative">
${saveResult && html`<${Notification} ok=${saveResult.status}
text=${saveResult.message} close=${() => setSaveResult(null)} />`}
<${Setting} title="Enable Logs" value=${settings.log_enabled} setfn=${mksetfn('log_enabled')} type="switch" />
<${Setting} title="Log Level" value=${settings.log_level} setfn=${mksetfn('log_level')} type="select" addonLeft="0-3" disabled=${!settings.log_enabled} options=${logOptions}/>
<${Setting} title="Brightness" value=${settings.brightness} setfn=${mksetfn('brightness')} type="number" addonRight="%" />
<${Setting} title="Device Name" value=${settings.device_name} setfn=${mksetfn('device_name')} type="" />
<div class="mb-1 mt-3 flex place-content-end"><${Button} icon=${Icons.save} onclick=${onsave} title="Save Settings" /><//>
<//>
<//>
<div class="bg-white border rounded-md text-ellipsis overflow-auto" role="alert">
<${DeveloperNote}
text="A variety of controls are pre-defined to ease the development:
toggle button, dropdown select, input field with left and right
addons. Device settings are received by calling
api/settings/get API call, which returns settings JSON object.
Clicking on the save button calls api/settings/set
API call" />
<//>
<//>`;
};
const App = function({}) {
const [loading, setLoading] = useState(true);
const [url, setUrl] = useState('/');
const [user, setUser] = useState('');
const [showSidebar, setShowSidebar] = useState(true);
const logout = () => fetch('api/logout').then(r => setUser(''));
const login = r => !r.ok ? setLoading(false) && setUser(null) : r.json()
.then(r => setUser(r.user))
.finally(r => setLoading(false));
useEffect(() => fetch('api/login').then(login), []);
if (loading) return ''; // Show blank page on initial load
if (!user) return html`<${Login} loginFn=${login} logoIcon=${Logo}
title="Device Dashboard Login"
tipText="To login, use: admin/admin, user1/user1, user2/user2" />`; // If not logged in, show login screen
return html`
<div class="min-h-screen bg-slate-100">
<${Sidebar} url=${url} show=${showSidebar} />
<${Header} logout=${logout} user=${user} showSidebar=${showSidebar} setShowSidebar=${setShowSidebar} />
<div class="${showSidebar && 'pl-72'} transition-all duration-300 transform">
<${Router} onChange=${ev => setUrl(ev.url)} history=${History.createHashHistory()} >
<${Main} default=${true} />
<${Settings} path="settings" />
<${FirmwareUpdate} path="update" />
<${Events} path="events" />
<//>
<//>
<//>`;
};
window.onload = () => render(h(App), document.body);