mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2026-04-30 18:49:09 +00:00
Compare commits
49 Commits
4a9ff47dcc
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cab2533fab | ||
|
|
5f65cc7a73 | ||
|
|
03e6a9036f | ||
| 16ea1db55f | |||
| a0ed04a560 | |||
|
|
4e988e8f01 | ||
| 009f6cbb2e | |||
|
|
967da9fc30 | ||
| 00cb7efedb | |||
| ebbec7885e | |||
| 7ecd2052d8 | |||
| 96512c8a12 | |||
| c3eaff03d9 | |||
| 4bf89626b9 | |||
| 7c0c0699b5 | |||
| 1ea70e4993 | |||
| 770dca5b0f | |||
| 2e75ba2908 | |||
| 141c1aa9cb | |||
|
|
4abbd844d2 | ||
| 7346b47816 | |||
| cd63dd3ee4 | |||
| f5d4ae1e05 | |||
| bd3f6731fd | |||
| 6fdcf7679f | |||
|
|
c4d6ed45f1 | ||
|
|
41adf7353d | ||
| 6421074931 | |||
| a34dc18381 | |||
| 252e63c607 | |||
| 99848f0e6d | |||
| f46cdc4d29 | |||
| a8d64f6af5 | |||
| 8fb6bac651 | |||
| 7eb18376e1 | |||
| b8bba28bda | |||
| 5c0ad18b94 | |||
| 75130e2d20 | |||
| 6b2c56f3e5 | |||
| 2980d34394 | |||
| 9b926f7a34 | |||
| f1b471c6d8 | |||
| 030a372949 | |||
| 211961a770 | |||
| dfe5197ab8 | |||
| 0f5ca88ae4 | |||
| 9dd2f88cbc | |||
| aa91d69f0b | |||
| b13ae76bc5 |
@@ -12,3 +12,7 @@ target = "riscv32imac-unknown-none-elf"
|
|||||||
|
|
||||||
[unstable]
|
[unstable]
|
||||||
build-std = ["alloc", "core"]
|
build-std = ["alloc", "core"]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
WIFI_PASSWD = "hunter22"
|
||||||
|
WIFI_SSID = "fwa"
|
||||||
|
|||||||
941
Cargo.lock
generated
941
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
76
Cargo.toml
76
Cargo.toml
@@ -12,64 +12,44 @@ bench = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
esp-bootloader-esp-idf = "0.1.0"
|
esp-bootloader-esp-idf = "0.1.0"
|
||||||
embassy-net = { version = "0.7.0", features = [
|
esp-hal = { version = "1.0.0-rc.1", features = ["esp32c6", "unstable"] }
|
||||||
"dhcpv4",
|
esp-alloc = "0.9.0"
|
||||||
"medium-ethernet",
|
esp-println = { version = "0.16.0", features = ["esp32c6", "log-04"] }
|
||||||
"tcp",
|
esp-radio = { version = "0.16.0", features = ["esp32c6","esp-alloc", "wifi", "log-04", "smoltcp","unstable"]}
|
||||||
"udp",
|
esp-rtos = { version = "0.1.1", features = ["esp32c6", "embassy", "esp-radio", "esp-alloc"] }
|
||||||
] }
|
|
||||||
embedded-hal = "=1.0.0"
|
|
||||||
embedded-io = "0.6.1"
|
|
||||||
embedded-io-async = "0.6.1"
|
|
||||||
esp-alloc = "0.8.0"
|
|
||||||
esp-hal = { version = "1.0.0-beta.1", features = ["esp32c6", "unstable"] }
|
|
||||||
smoltcp = { version = "0.12.0", default-features = false, features = [
|
|
||||||
"medium-ethernet",
|
|
||||||
"multicast",
|
|
||||||
"proto-dhcpv4",
|
|
||||||
"proto-dns",
|
|
||||||
"proto-ipv4",
|
|
||||||
"socket-dns",
|
|
||||||
"socket-icmp",
|
|
||||||
"socket-raw",
|
|
||||||
"socket-tcp",
|
|
||||||
"socket-udp",
|
|
||||||
] }
|
|
||||||
# for more networking protocol support see https://crates.io/crates/edge-net
|
|
||||||
bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [
|
|
||||||
"async",
|
|
||||||
"macros",
|
|
||||||
] }
|
|
||||||
critical-section = "1.2.0"
|
critical-section = "1.2.0"
|
||||||
embassy-executor = { version = "0.7.0", features = ["nightly"] }
|
|
||||||
embassy-time = { version = "0.4.0", features = ["generic-queue-8"] }
|
|
||||||
esp-hal-embassy = { version = "0.9.0", features = ["esp32c6"] }
|
|
||||||
esp-wifi = { version = "0.15.0", features = [
|
|
||||||
"wifi",
|
|
||||||
"builtin-scheduler",
|
|
||||||
"esp-alloc",
|
|
||||||
"esp32c6",
|
|
||||||
"log-04",
|
|
||||||
] }
|
|
||||||
heapless = { version = "0.8.0", default-features = false }
|
|
||||||
static_cell = { version = "2.1.0", features = ["nightly"] }
|
|
||||||
esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] }
|
|
||||||
log = { version = "0.4" }
|
log = { version = "0.4" }
|
||||||
|
static_cell = { version = "2.1.1", features = ["nightly"] }
|
||||||
|
heapless = { version = "0.8.0", default-features = false }
|
||||||
|
chrono = { version = "0.4.41", default-features = false }
|
||||||
|
|
||||||
|
embedded-hal = "1.0.0"
|
||||||
|
embedded-io = "0.7.1"
|
||||||
|
embedded-io-async = "0.7.0"
|
||||||
|
embassy-executor = { version = "0.9.0", features = [] }
|
||||||
|
embassy-time = { version = "0.5.0", features = [] }
|
||||||
|
embassy-futures = { version = "0.1.2", features = ["log"] }
|
||||||
|
embassy-sync = { version = "0.7.2", features = ["log"] }
|
||||||
|
|
||||||
|
embassy-net = { version = "0.7.0", features = [ "dhcpv4", "medium-ethernet", "tcp", "udp" ] }
|
||||||
|
smoltcp = { version = "0.12.0", default-features = false, features = [ "medium-ethernet", "multicast", "proto-dhcpv4", "proto-dns", "proto-ipv4", "socket-dns", "socket-icmp", "socket-raw", "socket-tcp", "socket-udp" ] }
|
||||||
|
bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [ "async", "macros" ] }
|
||||||
edge-dhcp = { version = "0.6.0", features = ["log"] }
|
edge-dhcp = { version = "0.6.0", features = ["log"] }
|
||||||
edge-nal = "0.5.0"
|
edge-nal = "0.5.0"
|
||||||
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
|
edge-nal-embassy = { version = "0.6.0", features = ["log"] }
|
||||||
|
|
||||||
picoserve = { git = "https://github.com/sammhicks/picoserve.git", rev = "400df53f61137e1bb2883ec610fc191bfe551a3a", features = ["embassy", "log", "json"] }
|
picoserve = { git = "https://github.com/sammhicks/picoserve.git", rev = "400df53f61137e1bb2883ec610fc191bfe551a3a", features = ["embassy", "log", "json"] }
|
||||||
embassy-sync = { version = "0.7.0", features = ["log"] }
|
|
||||||
ds3231 = { version = "0.3.0", features = ["async", "temperature_f32"] }
|
|
||||||
chrono = { version = "0.4.41", default-features = false }
|
|
||||||
dir-embed = "0.3.0"
|
dir-embed = "0.3.0"
|
||||||
esp-hal-smartled = { git = "https://github.com/esp-rs/esp-hal-community.git", package = "esp-hal-smartled", branch = "main", features = ["esp32c6"]}
|
|
||||||
smart-leds = "0.4.0"
|
|
||||||
serde = { version = "1.0.219", default-features = false, features = ["derive", "alloc"] }
|
serde = { version = "1.0.219", default-features = false, features = ["derive", "alloc"] }
|
||||||
|
serde_json = { version = "1.0.143", default-features = false, features = ["alloc"]}
|
||||||
|
|
||||||
|
ds3231 = { version = "0.3.0", features = ["async", "temperature_f32"] }
|
||||||
|
esp-hal-smartled = { git = "https://github.com/esp-rs/esp-hal-community.git", rev = "ab4316534d90e3a12785907f043f6899faee0f20", package = "esp-hal-smartled", features = ["esp32c6"]}
|
||||||
|
smart-leds = "0.4.0"
|
||||||
|
|
||||||
embedded-sdmmc = "0.8.0"
|
embedded-sdmmc = "0.8.0"
|
||||||
embedded-hal-bus = "0.3.0"
|
embedded-hal-bus = "0.3.0"
|
||||||
serde_json = { version = "1.0.143", default-features = false, features = ["alloc"]}
|
|
||||||
embassy-futures = { version = "0.1.2", features = ["log"] }
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
# Rust debug is too slow.
|
# Rust debug is too slow.
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
const DEVICE_TYPE_CODE: u8 = 0b10100000;
|
|
||||||
|
|
||||||
const DEVICE_ADDRESS_CODE: u8 = 0b000000; // 3 bits for device address | default A0 = 0 A1 = 0 A2 = 0
|
|
||||||
|
|
||||||
const WRITE_CODE: u8 = 0b00000000; // 0 for write
|
|
||||||
const READ_CODE: u8 = 0b00000001; // 1 for read
|
|
||||||
|
|
||||||
const DEVICE_ADDRESS_WRITE: u8 = DEVICE_TYPE_CODE | DEVICE_ADDRESS_CODE | WRITE_CODE; // I2C address write for FRAM
|
|
||||||
const DEVICE_ADDRESS_READ: u8 = DEVICE_TYPE_CODE | DEVICE_ADDRESS_CODE | READ_CODE; // I2C address read for FRAM
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_hal::{Async, uart::Uart};
|
use esp_hal::{Async, uart::Uart};
|
||||||
use log::{debug, info};
|
use log::{debug, info, warn};
|
||||||
|
|
||||||
use crate::{TallyPublisher, store::TallyID};
|
use crate::TallyPublisher;
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn rfid_reader_task(mut uart_device: Uart<'static, Async>, chan: TallyPublisher) {
|
pub async fn rfid_reader_task(mut uart_device: Uart<'static, Async>, chan: TallyPublisher) {
|
||||||
@@ -17,7 +17,15 @@ pub async fn rfid_reader_task(mut uart_device: Uart<'static, Async>, chan: Tally
|
|||||||
core::fmt::Write::write_fmt(&mut hex_str, format_args!("{:02X} ", byte)).ok();
|
core::fmt::Write::write_fmt(&mut hex_str, format_args!("{:02X} ", byte)).ok();
|
||||||
}
|
}
|
||||||
info!("Read {n} bytes from UART: {hex_str}");
|
info!("Read {n} bytes from UART: {hex_str}");
|
||||||
chan.publish(uart_buffer[..8].try_into().unwrap()).await;
|
|
||||||
|
match extract_id(&uart_buffer) {
|
||||||
|
Some(read) => {
|
||||||
|
chan.publish(read.try_into().unwrap()).await;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("Invalid read from the RFID reader");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error reading from UART: {e}");
|
log::error!("Error reading from UART: {e}");
|
||||||
@@ -26,3 +34,35 @@ pub async fn rfid_reader_task(mut uart_device: Uart<'static, Async>, chan: Tally
|
|||||||
Timer::after(Duration::from_millis(200)).await;
|
Timer::after(Duration::from_millis(200)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scans the UART output and retuns the first propper read ID
|
||||||
|
/// This ensures that only valid ID are parsed
|
||||||
|
///
|
||||||
|
/// A valid read looks like this:
|
||||||
|
/// The first byte is always 0x02 (Start of text)
|
||||||
|
/// Followed by 12 Bytes of chars
|
||||||
|
/// Ended by 0x03 (End of text)
|
||||||
|
pub fn extract_id(buffer: &[u8]) -> Option<[u8; 12]> {
|
||||||
|
const STX: u8 = 0x02; // Start of Text ASCII char
|
||||||
|
const ETX: u8 = 0x03; // End of Text ASCII char
|
||||||
|
const ID_LENGTH: usize = 12;
|
||||||
|
const MINIMUM_SEQUENCE: usize = ID_LENGTH + 2; // STX + 12 bytes + ETX
|
||||||
|
|
||||||
|
if buffer.len() < MINIMUM_SEQUENCE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for window_start in 0..=buffer.len() - MINIMUM_SEQUENCE {
|
||||||
|
if buffer[window_start] == STX {
|
||||||
|
let id_end = window_start + ID_LENGTH + 1;
|
||||||
|
|
||||||
|
if buffer[id_end] == ETX {
|
||||||
|
let mut id = [0u8; ID_LENGTH];
|
||||||
|
id.copy_from_slice(&buffer[window_start + 1..id_end]);
|
||||||
|
return Some(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use chrono::{TimeZone, Utc};
|
||||||
use ds3231::{
|
use ds3231::{
|
||||||
Config, DS3231, DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency,
|
Config, DS3231, DS3231Error, InterruptControl, Oscillator, SquareWaveFrequency,
|
||||||
TimeRepresentation,
|
TimeRepresentation,
|
||||||
@@ -8,8 +9,7 @@ use esp_hal::{
|
|||||||
};
|
};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
|
|
||||||
use crate::{FEEDBACK_STATE, drivers, feedback, store::Date};
|
use crate::{FEEDBACK_STATE, drivers, feedback};
|
||||||
use chrono::{TimeZone, Utc};
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/build_time.rs"));
|
include!(concat!(env!("OUT_DIR"), "/build_time.rs"));
|
||||||
|
|
||||||
@@ -45,32 +45,6 @@ impl RTCClock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_date(&mut self) -> Date {
|
|
||||||
let (year, month, day) = unix_to_ymd_string(self.get_time().await);
|
|
||||||
|
|
||||||
let mut buffer: Date = [0; 10] ;
|
|
||||||
|
|
||||||
// Write YYYY
|
|
||||||
buffer[0] = b'0' + ((year / 1000) % 10) as u8;
|
|
||||||
buffer[1] = b'0' + ((year / 100) % 10) as u8;
|
|
||||||
buffer[2] = b'0' + ((year / 10) % 10) as u8;
|
|
||||||
buffer[3] = b'0' + (year % 10) as u8;
|
|
||||||
buffer[4] = b'.';
|
|
||||||
|
|
||||||
// Write MM
|
|
||||||
buffer[5] = b'0' + (month / 10) as u8;
|
|
||||||
buffer[6] = b'0' + (month % 10) as u8;
|
|
||||||
|
|
||||||
buffer[7] = b'.';
|
|
||||||
|
|
||||||
// Write DD
|
|
||||||
buffer[8] = b'0' + (day / 10) as u8;
|
|
||||||
buffer[9] = b'0' + (day % 10) as u8;
|
|
||||||
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unix_to_ymd_string(timestamp: u64) -> (u16, u8, u8) {
|
fn unix_to_ymd_string(timestamp: u64) -> (u16, u8, u8) {
|
||||||
@@ -82,11 +56,11 @@ fn unix_to_ymd_string(timestamp: u64) -> (u16, u8, u8) {
|
|||||||
|
|
||||||
// Convert to proleptic Gregorian date
|
// Convert to proleptic Gregorian date
|
||||||
civil_from_days(days_since_epoch as i64 + UNIX_OFFSET_DAYS as i64)
|
civil_from_days(days_since_epoch as i64 + UNIX_OFFSET_DAYS as i64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function returns (year, month, day).
|
// This function returns (year, month, day).
|
||||||
// Based on the algorithm by Howard Hinnant.
|
// Based on the algorithm by Howard Hinnant.
|
||||||
fn civil_from_days(z: i64) -> (u16, u8, u8) {
|
fn civil_from_days(z: i64) -> (u16, u8, u8) {
|
||||||
let mut z = z;
|
let mut z = z;
|
||||||
z -= 60; // shift epoch for algorithm
|
z -= 60; // shift epoch for algorithm
|
||||||
let era = (z >= 0).then_some(z).unwrap_or(z - 146096) / 146097;
|
let era = (z >= 0).then_some(z).unwrap_or(z - 146096) / 146097;
|
||||||
@@ -98,7 +72,7 @@ fn unix_to_ymd_string(timestamp: u64) -> (u16, u8, u8) {
|
|||||||
let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
|
let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
|
||||||
let m = mp + (if mp < 10 { 3 } else { -9 }); // [1, 12]
|
let m = mp + (if mp < 10 { 3 } else { -9 }); // [1, 12]
|
||||||
((y + (m <= 2) as i64) as u16, m as u8, d as u8)
|
((y + (m <= 2) as i64) as u16, m as u8, d as u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn rtc_config(i2c: I2c<'static, Async>) -> DS3231<I2c<'static, Async>> {
|
pub async fn rtc_config(i2c: I2c<'static, Async>) -> DS3231<I2c<'static, Async>> {
|
||||||
let mut rtc: DS3231<I2c<'static, Async>> = DS3231::new(i2c, RTC_ADDRESS);
|
let mut rtc: DS3231<I2c<'static, Async>> = DS3231::new(i2c, RTC_ADDRESS);
|
||||||
|
|||||||
138
src/feedback.rs
138
src/feedback.rs
@@ -1,12 +1,13 @@
|
|||||||
use embassy_time::{Delay, Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_hal::{delay, gpio::Output, peripherals, rmt::ConstChannelAccess};
|
use esp_hal::rmt::Rmt;
|
||||||
use esp_hal_smartled::SmartLedsAdapterAsync;
|
use esp_hal::peripherals;
|
||||||
use init::hardware;
|
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
|
||||||
use log::{debug, error, info};
|
use log::debug;
|
||||||
use smart_leds::SmartLedsWriteAsync;
|
use smart_leds::SmartLedsWriteAsync;
|
||||||
use smart_leds::colors::{BLACK, GREEN, RED, YELLOW};
|
use smart_leds::colors::{BLACK, GREEN, RED, YELLOW};
|
||||||
use smart_leds::{brightness, colors::BLUE};
|
use smart_leds::{brightness, colors::BLUE};
|
||||||
|
|
||||||
|
use crate::init::hardware;
|
||||||
use crate::{FEEDBACK_STATE, init};
|
use crate::{FEEDBACK_STATE, init};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
@@ -25,14 +26,18 @@ const LED_LEVEL: u8 = 255;
|
|||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn feedback_task(
|
pub async fn feedback_task(
|
||||||
mut led: SmartLedsAdapterAsync<
|
rmt: Rmt<'static, esp_hal::Async>,
|
||||||
ConstChannelAccess<esp_hal::rmt::Tx, 0>,
|
led_gpio: peripherals::GPIO1<'static>,
|
||||||
{ init::hardware::LED_BUFFER_SIZE },
|
buzzer_gpio: peripherals::GPIO21<'static>,
|
||||||
>,
|
|
||||||
buzzer: peripherals::GPIO21<'static>,
|
|
||||||
) {
|
) {
|
||||||
debug!("Starting feedback task");
|
debug!("Starting feedback task");
|
||||||
let mut buzzer = init::hardware::setup_buzzer(buzzer);
|
|
||||||
|
let rmt_channel = rmt.channel0;
|
||||||
|
let rmt_buffer = [esp_hal::rmt::PulseCode::default(); buffer_size_async(hardware::NUM_LEDS)];
|
||||||
|
|
||||||
|
let mut led = SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer);
|
||||||
|
|
||||||
|
let mut buzzer = init::hardware::setup_buzzer(buzzer_gpio);
|
||||||
loop {
|
loop {
|
||||||
let feedback_state = FEEDBACK_STATE.wait().await;
|
let feedback_state = FEEDBACK_STATE.wait().await;
|
||||||
match feedback_state {
|
match feedback_state {
|
||||||
@@ -47,6 +52,12 @@ pub async fn feedback_task(
|
|||||||
Timer::after(Duration::from_millis(100)).await;
|
Timer::after(Duration::from_millis(100)).await;
|
||||||
buzzer.set_low();
|
buzzer.set_low();
|
||||||
Timer::after(Duration::from_millis(50)).await;
|
Timer::after(Duration::from_millis(50)).await;
|
||||||
|
led.write(brightness(
|
||||||
|
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||||
|
LED_LEVEL,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
FeedbackState::Nack => {
|
FeedbackState::Nack => {
|
||||||
led.write(brightness(
|
led.write(brightness(
|
||||||
@@ -119,7 +130,7 @@ pub async fn feedback_task(
|
|||||||
}
|
}
|
||||||
FeedbackState::Idle => {
|
FeedbackState::Idle => {
|
||||||
led.write(brightness(
|
led.write(brightness(
|
||||||
[GREEN; init::hardware::NUM_LEDS].into_iter(),
|
[BLACK; init::hardware::NUM_LEDS].into_iter(),
|
||||||
LED_LEVEL,
|
LED_LEVEL,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
@@ -129,106 +140,3 @@ pub async fn feedback_task(
|
|||||||
debug!("Feedback state: {:?}", feedback_state);
|
debug!("Feedback state: {:?}", feedback_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// async fn beep_ack() {
|
|
||||||
// buzzer.set_high();
|
|
||||||
// buzzer.set_low();
|
|
||||||
// //Timer::after(Duration::from_millis(100)).await;
|
|
||||||
// }
|
|
||||||
|
|
||||||
/* pub async fn failure(&mut self) {
|
|
||||||
let buzzer_handle = Self::beep_nak(&mut self.buzzer);
|
|
||||||
let led_handle = Self::flash_led_for_duration(&mut self.led, RED, LED_BLINK_DURATION);
|
|
||||||
|
|
||||||
let (buzzer_result, _) = join!(buzzer_handle, led_handle);
|
|
||||||
|
|
||||||
buzzer_result.unwrap_or_else(|err| { error!("Failed to buzz: {err}");
|
|
||||||
});
|
|
||||||
|
|
||||||
let _ = self.led_to_status();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn activate_error_state(&mut self) -> Result<()> {
|
|
||||||
self.led.turn_on(RED)?;
|
|
||||||
Self::beep_nak(&mut self.buzzer).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn startup(&mut self){
|
|
||||||
self.device_status = DeviceStatus::Ready;
|
|
||||||
|
|
||||||
let led_handle = Self::flash_led_for_duration(&mut self.led, GREEN, Duration::from_secs(1));
|
|
||||||
let buzzer_handle = Self::beep_startup(&mut self.buzzer);
|
|
||||||
|
|
||||||
let (buzzer_result, led_result) = join!(buzzer_handle, led_handle);
|
|
||||||
|
|
||||||
buzzer_result.unwrap_or_else(|err| {
|
|
||||||
error!("Failed to buzz: {err}");
|
|
||||||
});
|
|
||||||
|
|
||||||
led_result.unwrap_or_else(|err| {
|
|
||||||
error!("Failed to blink led: {err}");
|
|
||||||
});
|
|
||||||
|
|
||||||
let _ = self.led_to_status();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async fn flash_led_for_duration(led: &mut L, color: RGB8, duration: Duration) -> Result<()> {
|
|
||||||
led.turn_on(color)?;
|
|
||||||
|
|
||||||
sleep(duration).await;
|
|
||||||
|
|
||||||
led.turn_off()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn beep_ack(buzzer: &mut B) -> Result<()> {
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(1200.0, Duration::from_millis(100))
|
|
||||||
.await?;
|
|
||||||
sleep(Duration::from_millis(10)).await;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(2000.0, Duration::from_millis(50))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn beep_nak(buzzer: &mut B) -> Result<()> {
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(600.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(600.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn beep_startup(buzzer: &mut B) -> Result<()> {
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(523.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(659.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(784.0, Duration::from_millis(150))
|
|
||||||
.await?;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(1046.0, Duration::from_millis(200))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
|
||||||
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(784.0, Duration::from_millis(100))
|
|
||||||
.await?;
|
|
||||||
buzzer
|
|
||||||
.modulated_tone(880.0, Duration::from_millis(200))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
use core::cell::RefCell;
|
use core::cell::RefCell;
|
||||||
|
|
||||||
use bleps::att::Att;
|
|
||||||
use critical_section::Mutex;
|
use critical_section::Mutex;
|
||||||
use ds3231::InterruptControl;
|
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_net::Stack;
|
use embassy_net::Stack;
|
||||||
|
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_hal::gpio::{Input, InputConfig};
|
use esp_hal::Blocking;
|
||||||
|
use esp_hal::delay::Delay;
|
||||||
|
use esp_hal::gpio::Input;
|
||||||
use esp_hal::i2c::master::Config;
|
use esp_hal::i2c::master::Config;
|
||||||
use esp_hal::peripherals::{
|
use esp_hal::peripherals::{
|
||||||
GPIO0, GPIO1, GPIO16, GPIO17, GPIO18, GPIO19, GPIO20, GPIO21, GPIO22, GPIO23, I2C0, RMT, SPI2,
|
GPIO0, GPIO1, GPIO16, GPIO17, GPIO18, GPIO19, GPIO20, GPIO21, GPIO22, GPIO23, I2C0, RMT, SPI2,
|
||||||
UART1,
|
UART1,
|
||||||
};
|
};
|
||||||
use esp_hal::rmt::{ConstChannelAccess, Rmt};
|
use esp_hal::rmt::Rmt;
|
||||||
use esp_hal::spi::master::{Config as Spi_config, Spi};
|
use esp_hal::spi::master::{Config as Spi_config, Spi};
|
||||||
|
use esp_hal::system::software_reset;
|
||||||
use esp_hal::Blocking;
|
|
||||||
use esp_hal::time::Rate;
|
use esp_hal::time::Rate;
|
||||||
use esp_hal::timer::timg::TimerGroup;
|
use esp_hal::timer::timg::TimerGroup;
|
||||||
use esp_hal::{
|
use esp_hal::{
|
||||||
@@ -28,16 +25,12 @@ use esp_hal::{
|
|||||||
uart::Uart,
|
uart::Uart,
|
||||||
};
|
};
|
||||||
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
|
use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async};
|
||||||
use esp_println::dbg;
|
|
||||||
use esp_println::logger::init_logger;
|
use esp_println::logger::init_logger;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error};
|
||||||
|
|
||||||
use crate::FEEDBACK_STATE;
|
|
||||||
use crate::init::network;
|
use crate::init::network;
|
||||||
use crate::init::sd_card::{setup_sdcard, SDCardPersistence};
|
use crate::init::sd_card::{SDCardPersistence, setup_sdcard};
|
||||||
use crate::init::wifi;
|
use crate::init::wifi;
|
||||||
use crate::store::AttendanceDay;
|
|
||||||
use crate::store::persistence::Persistence;
|
|
||||||
|
|
||||||
/*************************************************
|
/*************************************************
|
||||||
* GPIO Pinout Xiao Esp32c6
|
* GPIO Pinout Xiao Esp32c6
|
||||||
@@ -56,47 +49,50 @@ use crate::store::persistence::Persistence;
|
|||||||
*
|
*
|
||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
pub const NUM_LEDS: usize = 66;
|
pub const NUM_LEDS: usize = 1;
|
||||||
pub const LED_BUFFER_SIZE: usize = NUM_LEDS * 25;
|
|
||||||
|
|
||||||
static SD_DET: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));
|
static SD_DET: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));
|
||||||
|
|
||||||
#[panic_handler]
|
#[panic_handler]
|
||||||
fn panic(info: &core::panic::PanicInfo) -> ! {
|
fn panic(info: &core::panic::PanicInfo) -> ! {
|
||||||
loop {
|
let delay = Delay::new();
|
||||||
error!("PANIC: {info}");
|
error!("PANIC: {info}");
|
||||||
}
|
delay.delay(esp_hal::time::Duration::from_secs(30));
|
||||||
|
software_reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_bootloader_esp_idf::esp_app_desc!();
|
esp_bootloader_esp_idf::esp_app_desc!();
|
||||||
|
|
||||||
pub async fn hardware_init(
|
pub async fn hardware_init(
|
||||||
spawner: &mut Spawner,
|
spawner: Spawner,
|
||||||
) -> (
|
) -> (
|
||||||
Uart<'static, Async>,
|
Uart<'static, Async>,
|
||||||
Stack<'static>,
|
Stack<'static>,
|
||||||
I2c<'static, Async>,
|
I2c<'static, Async>,
|
||||||
SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE>,
|
Rmt<'static, esp_hal::Async>,
|
||||||
|
GPIO1<'static>,
|
||||||
GPIO21<'static>,
|
GPIO21<'static>,
|
||||||
GPIO0<'static>,
|
GPIO0<'static>,
|
||||||
|
SmartLedsAdapterAsync<'static, LED_BUFFER_SIZE>,
|
||||||
SDCardPersistence,
|
SDCardPersistence,
|
||||||
) {
|
) {
|
||||||
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
|
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
|
||||||
let peripherals = esp_hal::init(config);
|
let peripherals = esp_hal::init(config);
|
||||||
|
|
||||||
esp_alloc::heap_allocator!(size: 72 * 1024);
|
esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 65536);
|
||||||
|
|
||||||
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
|
let timg0 = TimerGroup::new(peripherals.TIMG0);
|
||||||
esp_hal_embassy::init(timer0.alarm0);
|
let sw_interrupt =
|
||||||
|
esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
|
||||||
|
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
|
||||||
|
|
||||||
init_logger(log::LevelFilter::Debug);
|
init_logger(log::LevelFilter::Debug);
|
||||||
|
|
||||||
let timer1 = TimerGroup::new(peripherals.TIMG0);
|
let rng = esp_hal::rng::Rng::new();
|
||||||
let mut rng = esp_hal::rng::Rng::new(peripherals.RNG);
|
|
||||||
let network_seed = (rng.random() as u64) << 32 | rng.random() as u64;
|
let network_seed = (rng.random() as u64) << 32 | rng.random() as u64;
|
||||||
|
|
||||||
wifi::set_antenna_mode(peripherals.GPIO3, peripherals.GPIO14).await;
|
wifi::set_antenna_mode(peripherals.GPIO3, peripherals.GPIO14).await;
|
||||||
let interfaces = wifi::setup_wifi(timer1.timer0, rng, peripherals.WIFI, spawner);
|
let interfaces = wifi::setup_wifi(peripherals.WIFI, spawner);
|
||||||
let stack = network::setup_network(network_seed, interfaces.ap, spawner);
|
let stack = network::setup_network(network_seed, interfaces.ap, spawner);
|
||||||
|
|
||||||
Timer::after(Duration::from_millis(1)).await;
|
Timer::after(Duration::from_millis(1)).await;
|
||||||
@@ -107,6 +103,10 @@ pub async fn hardware_init(
|
|||||||
|
|
||||||
let sd_det_gpio = peripherals.GPIO0;
|
let sd_det_gpio = peripherals.GPIO0;
|
||||||
|
|
||||||
|
let rmt: Rmt<'_, esp_hal::Async> = {let frequency: Rate = Rate::from_mhz(80);
|
||||||
|
Rmt::new(peripherals.RMT, frequency)} .expect("Failed to initialize RMT")
|
||||||
|
.into_async();
|
||||||
|
|
||||||
let spi_bus = setup_spi(
|
let spi_bus = setup_spi(
|
||||||
peripherals.SPI2,
|
peripherals.SPI2,
|
||||||
peripherals.GPIO19,
|
peripherals.GPIO19,
|
||||||
@@ -122,21 +122,24 @@ pub async fn hardware_init(
|
|||||||
|
|
||||||
let vol_mgr = setup_sdcard(spi_bus, sd_cs_pin);
|
let vol_mgr = setup_sdcard(spi_bus, sd_cs_pin);
|
||||||
|
|
||||||
|
let led_gpio = peripherals.GPIO1;
|
||||||
let buzzer_gpio = peripherals.GPIO21;
|
let buzzer_gpio = peripherals.GPIO21;
|
||||||
|
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
|
||||||
|
|
||||||
let led = setup_led(peripherals.RMT, peripherals.GPIO1);
|
let led = setup_led(peripherals.RMT, peripherals.GPIO1);
|
||||||
|
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
debug!("hardware init done");
|
debug!("hardware init done");
|
||||||
|
|
||||||
(
|
(
|
||||||
uart_device,
|
uart_device,
|
||||||
stack,
|
stack,
|
||||||
i2c_device,
|
i2c_device,
|
||||||
led,
|
rmt,
|
||||||
|
led_gpio,
|
||||||
buzzer_gpio,
|
buzzer_gpio,
|
||||||
sd_det_gpio,
|
sd_det_gpio,
|
||||||
|
led,
|
||||||
vol_mgr,
|
vol_mgr,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -195,11 +198,10 @@ pub fn setup_buzzer(buzzer_gpio: GPIO21<'static>) -> Output<'static> {
|
|||||||
buzzer
|
buzzer
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_led(
|
fn setup_led<'a>(
|
||||||
rmt: RMT<'static>,
|
rmt: RMT<'a>,
|
||||||
led_gpio: GPIO1<'static>,
|
led_gpio: GPIO1<'a>,
|
||||||
) -> SmartLedsAdapterAsync<ConstChannelAccess<esp_hal::rmt::Tx, 0>, LED_BUFFER_SIZE> {
|
) -> esp_hal_smartled::SmartLedsAdapterAsync<'a, LED_BUFFER_SIZE> {
|
||||||
debug!("setup led");
|
|
||||||
let rmt: Rmt<'_, esp_hal::Async> = {
|
let rmt: Rmt<'_, esp_hal::Async> = {
|
||||||
let frequency: Rate = Rate::from_mhz(80);
|
let frequency: Rate = Rate::from_mhz(80);
|
||||||
Rmt::new(rmt, frequency)
|
Rmt::new(rmt, frequency)
|
||||||
@@ -208,10 +210,7 @@ fn setup_led(
|
|||||||
.into_async();
|
.into_async();
|
||||||
|
|
||||||
let rmt_channel = rmt.channel0;
|
let rmt_channel = rmt.channel0;
|
||||||
let rmt_buffer = [0_u32; buffer_size_async(NUM_LEDS)];
|
let rmt_buffer = [esp_hal::rmt::PulseCode::default(); LED_BUFFER_SIZE];
|
||||||
|
|
||||||
let led: SmartLedsAdapterAsync<_, LED_BUFFER_SIZE> =
|
SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer)
|
||||||
SmartLedsAdapterAsync::new(rmt_channel, led_gpio, rmt_buffer);
|
|
||||||
|
|
||||||
led
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
use core::{net::Ipv4Addr, str::FromStr};
|
use core::{net::Ipv4Addr, str::FromStr};
|
||||||
|
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
use embassy_net::{Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_wifi::wifi::WifiDevice;
|
use esp_radio::wifi::WifiDevice;
|
||||||
use static_cell::make_static;
|
use static_cell::make_static;
|
||||||
|
|
||||||
use crate::webserver::WEB_TAKS_SIZE;
|
use crate::webserver::WEB_TAKS_SIZE;
|
||||||
|
|
||||||
pub const NETWORK_STACK_SIZE: usize = WEB_TAKS_SIZE + 2; // + 2 for other network taks. Breaks
|
pub const NETWORK_STACK_SIZE: usize = WEB_TAKS_SIZE + 2; // + 2 for other network taks. Breaks
|
||||||
// without
|
// without
|
||||||
|
|
||||||
pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: &mut Spawner) -> Stack<'a> {
|
pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: Spawner) -> Stack<'a> {
|
||||||
let gw_ip_addr_str = "192.168.2.1";
|
let gw_ip_addr_str = "192.168.2.1";
|
||||||
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
|
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
|
||||||
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
|
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
|
||||||
@@ -20,12 +19,10 @@ pub fn setup_network<'a>(seed: u64, wifi: WifiDevice<'static>, spawner: &mut Spa
|
|||||||
dns_servers: Default::default(),
|
dns_servers: Default::default(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let (stack, runner) = embassy_net::new(
|
let nw_stack: &'static mut StackResources<NETWORK_STACK_SIZE> =
|
||||||
wifi,
|
make_static!(StackResources::<NETWORK_STACK_SIZE>::new());
|
||||||
config,
|
|
||||||
make_static!(StackResources::<NETWORK_STACK_SIZE>::new()),
|
let (stack, runner) = embassy_net::new(wifi, config, nw_stack, seed);
|
||||||
seed,
|
|
||||||
);
|
|
||||||
|
|
||||||
spawner.must_spawn(net_task(runner));
|
spawner.must_spawn(net_task(runner));
|
||||||
spawner.must_spawn(run_dhcp(stack, gw_ip_addr_str));
|
spawner.must_spawn(run_dhcp(stack, gw_ip_addr_str));
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use embassy_time::Delay;
|
use embassy_time::Delay;
|
||||||
use embedded_hal_bus::spi::ExclusiveDevice;
|
use embedded_hal_bus::spi::ExclusiveDevice;
|
||||||
use embedded_sdmmc::{SdCard, TimeSource, Timestamp, VolumeIdx, VolumeManager};
|
use embedded_sdmmc::{SdCard, ShortFileName, TimeSource, Timestamp, VolumeIdx, VolumeManager};
|
||||||
use esp_hal::{Blocking, gpio::Output, spi::master::Spi};
|
use esp_hal::{Blocking, gpio::Output, spi::master::Spi};
|
||||||
|
|
||||||
use crate::store::{AttendanceDay, Date, persistence::Persistence};
|
use crate::store::{AttendanceDay, IDMapping, day::Day, persistence::Persistence};
|
||||||
|
|
||||||
pub struct DummyTimesource;
|
pub struct DummyTimesource;
|
||||||
|
|
||||||
@@ -38,15 +38,31 @@ pub struct SDCardPersistence {
|
|||||||
vol_mgr: VolMgr,
|
vol_mgr: VolMgr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SDCardPersistence {
|
||||||
|
const MAPPING_FILENAME: &'static str = "MAPPING.JS";
|
||||||
|
|
||||||
|
fn generate_filename(day: Day) -> ShortFileName {
|
||||||
|
let basename = day.to_string();
|
||||||
|
let mut filename: heapless::String<11> = heapless::String::new();
|
||||||
|
filename.push_str(&basename).unwrap();
|
||||||
|
filename.push_str(".js").unwrap();
|
||||||
|
|
||||||
|
ShortFileName::create_from_str(&filename).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Persistence for SDCardPersistence {
|
impl Persistence for SDCardPersistence {
|
||||||
async fn load_day(&mut self, day: crate::store::Date) -> Option<AttendanceDay> {
|
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
|
||||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
let mut root_dir = vol_0.open_root_dir().unwrap();
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
let mut file = root_dir.open_file_in_dir("day.jsn", embedded_sdmmc::Mode::ReadOnly);
|
|
||||||
|
|
||||||
if let Err(e) = file {
|
let filename = Self::generate_filename(day);
|
||||||
|
let file = root_dir.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadOnly);
|
||||||
|
|
||||||
|
if file.is_err() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut open_file = file.unwrap();
|
let mut open_file = file.unwrap();
|
||||||
|
|
||||||
let mut read_buffer: [u8; 1024] = [0; 1024];
|
let mut read_buffer: [u8; 1024] = [0; 1024];
|
||||||
@@ -58,35 +74,75 @@ impl Persistence for SDCardPersistence {
|
|||||||
Some(day)
|
Some(day)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_day(&mut self, day: Date, data: &AttendanceDay) {
|
async fn save_day(&mut self, day: Day, data: &AttendanceDay) {
|
||||||
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
|
|
||||||
|
let filename = Self::generate_filename(day);
|
||||||
|
|
||||||
|
let mut file = root_dir
|
||||||
|
.open_file_in_dir(filename, embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
|
||||||
|
|
||||||
|
file.flush().unwrap();
|
||||||
|
file.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_mapping(&mut self) -> Option<crate::store::IDMapping> {
|
||||||
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
|
|
||||||
|
let file =
|
||||||
|
root_dir.open_file_in_dir(Self::MAPPING_FILENAME, embedded_sdmmc::Mode::ReadOnly);
|
||||||
|
|
||||||
|
if file.is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut open_file = file.unwrap();
|
||||||
|
|
||||||
|
let mut read_buffer: [u8; 1024] = [0; 1024];
|
||||||
|
let read = open_file.read(&mut read_buffer).unwrap();
|
||||||
|
open_file.close().unwrap();
|
||||||
|
|
||||||
|
let mapping: IDMapping = serde_json::from_slice(&read_buffer[..read]).unwrap();
|
||||||
|
|
||||||
|
Some(mapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_mapping(&mut self, data: &crate::store::IDMapping) {
|
||||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
let mut root_dir = vol_0.open_root_dir().unwrap();
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
|
|
||||||
let mut file = root_dir
|
let mut file = root_dir
|
||||||
.open_file_in_dir("day.jsn", embedded_sdmmc::Mode::ReadWriteCreateOrTruncate)
|
.open_file_in_dir(
|
||||||
|
Self::MAPPING_FILENAME,
|
||||||
|
embedded_sdmmc::Mode::ReadWriteCreateOrTruncate,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
|
file.write(&serde_json::to_vec(data).unwrap()).unwrap();
|
||||||
file.flush();
|
|
||||||
file.close();
|
file.flush().unwrap();
|
||||||
|
file.close().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_mapping(&mut self) -> Option<crate::store::IDMapping> {
|
async fn list_days(&mut self) -> Vec<Day> {
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn save_mapping(&mut self, data: &crate::store::IDMapping) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_days(&mut self) -> Vec<Date> {
|
|
||||||
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
let mut vol_0 = self.vol_mgr.open_volume(VolumeIdx(0)).unwrap();
|
||||||
let mut root_dir = vol_0.open_root_dir().unwrap();
|
let mut root_dir = vol_0.open_root_dir().unwrap();
|
||||||
let mut days_dir = root_dir.open_dir("days").unwrap();
|
|
||||||
|
|
||||||
let mut days: Vec<[u8; 10]> = Vec::new();
|
let mut days_dir = root_dir.open_dir(".").unwrap();
|
||||||
|
|
||||||
|
let mut days: Vec<Day> = Vec::new();
|
||||||
days_dir
|
days_dir
|
||||||
.iterate_dir(|e| {
|
.iterate_dir(|e| {
|
||||||
days.push([0; 10]);
|
let filename = e.name.clone();
|
||||||
|
|
||||||
|
if let Ok(day) = filename.try_into() {
|
||||||
|
days.push(day);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ use embassy_executor::Spawner;
|
|||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use esp_hal::gpio::{Output, OutputConfig};
|
use esp_hal::gpio::{Output, OutputConfig};
|
||||||
use esp_hal::peripherals::{GPIO3, GPIO14, WIFI};
|
use esp_hal::peripherals::{GPIO3, GPIO14, WIFI};
|
||||||
use esp_wifi::wifi::{AccessPointConfiguration, Configuration, WifiController, WifiEvent, WifiState};
|
use esp_radio::Controller;
|
||||||
use esp_wifi::{EspWifiRngSource, EspWifiTimerSource, wifi::Interfaces};
|
use esp_radio::wifi::{
|
||||||
use static_cell::make_static;
|
AccessPointConfig, Interfaces, ModeConfig, WifiApState, WifiController, WifiEvent,
|
||||||
|
};
|
||||||
|
use log::debug;
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
static ESP_WIFI_CTRL: StaticCell<Controller<'static>> = StaticCell::new();
|
||||||
|
|
||||||
pub async fn set_antenna_mode(gpio3: GPIO3<'static>, gpio14: GPIO14<'static>) {
|
pub async fn set_antenna_mode(gpio3: GPIO3<'static>, gpio14: GPIO14<'static>) {
|
||||||
let mut rf_switch = Output::new(gpio3, esp_hal::gpio::Level::Low, OutputConfig::default());
|
let mut rf_switch = Output::new(gpio3, esp_hal::gpio::Level::Low, OutputConfig::default());
|
||||||
@@ -18,26 +23,23 @@ pub async fn set_antenna_mode(gpio3: GPIO3<'static>, gpio14: GPIO14<'static>) {
|
|||||||
antenna_mode.set_low();
|
antenna_mode.set_low();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_wifi<'d: 'static>(
|
pub fn setup_wifi<'d: 'static>(wifi: WIFI<'static>, spawner: Spawner) -> Interfaces<'d> {
|
||||||
timer: impl EspWifiTimerSource + 'd,
|
let esp_wifi_ctrl = ESP_WIFI_CTRL.init(esp_radio::init().unwrap());
|
||||||
rng: impl EspWifiRngSource + 'd,
|
|
||||||
wifi: WIFI<'static>,
|
|
||||||
spawner: &mut Spawner,
|
|
||||||
) -> Interfaces<'d> {
|
|
||||||
let esp_wifi_ctrl = make_static!(esp_wifi::init(timer, rng).unwrap());
|
|
||||||
|
|
||||||
let (controller, interfaces) = esp_wifi::wifi::new(esp_wifi_ctrl, wifi).unwrap();
|
let config = esp_radio::wifi::Config::default();
|
||||||
|
let (controller, interfaces) = esp_radio::wifi::new(esp_wifi_ctrl, wifi, config).unwrap();
|
||||||
|
|
||||||
spawner.must_spawn(connection(controller));
|
spawner.must_spawn(connection(controller));
|
||||||
|
|
||||||
interfaces
|
interfaces
|
||||||
}
|
}
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
async fn connection(mut controller: WifiController<'static>) {
|
async fn connection(mut controller: WifiController<'static>) {
|
||||||
|
debug!("start connection task");
|
||||||
|
debug!("Device capabilities: {:?}", controller.capabilities());
|
||||||
loop {
|
loop {
|
||||||
match esp_wifi::wifi::wifi_state() {
|
match esp_radio::wifi::ap_state() {
|
||||||
WifiState::ApStarted => {
|
WifiApState::Started => {
|
||||||
// wait until we're no longer connected
|
// wait until we're no longer connected
|
||||||
controller.wait_for_event(WifiEvent::ApStop).await;
|
controller.wait_for_event(WifiEvent::ApStop).await;
|
||||||
Timer::after(Duration::from_millis(5000)).await
|
Timer::after(Duration::from_millis(5000)).await
|
||||||
@@ -45,12 +47,16 @@ async fn connection(mut controller: WifiController<'static>) {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
if !matches!(controller.is_started(), Ok(true)) {
|
if !matches!(controller.is_started(), Ok(true)) {
|
||||||
let client_config = Configuration::AccessPoint(AccessPointConfiguration {
|
let client_config = ModeConfig::AccessPoint(
|
||||||
ssid: "esp-wifi".try_into().unwrap(),
|
AccessPointConfig::default()
|
||||||
..Default::default()
|
.with_ssid(env!("WIFI_SSID").try_into().unwrap())
|
||||||
});
|
.with_password(env!("WIFI_PASSWD").try_into().unwrap())
|
||||||
controller.set_configuration(&client_config).unwrap();
|
.with_auth_method(esp_radio::wifi::AuthMethod::Wpa2Personal),
|
||||||
|
);
|
||||||
|
controller.set_config(&client_config).unwrap();
|
||||||
|
debug!("Starting wifi");
|
||||||
controller.start_async().await.unwrap();
|
controller.start_async().await.unwrap();
|
||||||
|
debug!("Wifi started!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/main.rs
36
src/main.rs
@@ -2,6 +2,7 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
#![feature(type_alias_impl_trait)]
|
#![feature(type_alias_impl_trait)]
|
||||||
#![feature(impl_trait_in_assoc_type)]
|
#![feature(impl_trait_in_assoc_type)]
|
||||||
|
#![warn(clippy::unwrap_used)]
|
||||||
|
|
||||||
use alloc::rc::Rc;
|
use alloc::rc::Rc;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
@@ -10,7 +11,7 @@ use embassy_sync::{
|
|||||||
blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex},
|
blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex},
|
||||||
mutex::Mutex,
|
mutex::Mutex,
|
||||||
pubsub::{
|
pubsub::{
|
||||||
PubSubChannel, Publisher,
|
PubSubChannel, Publisher, Subscriber,
|
||||||
WaitResult::{Lagged, Message},
|
WaitResult::{Lagged, Message},
|
||||||
},
|
},
|
||||||
signal::Signal,
|
signal::Signal,
|
||||||
@@ -19,16 +20,16 @@ use embassy_time::{Duration, Timer};
|
|||||||
use esp_hal::gpio::Input;
|
use esp_hal::gpio::Input;
|
||||||
use esp_hal::{gpio::InputConfig, peripherals};
|
use esp_hal::{gpio::InputConfig, peripherals};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use static_cell::make_static;
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
init::sd_card::SDCardPersistence,
|
init::sd_card::SDCardPersistence,
|
||||||
store::{Date, IDStore, TallyID},
|
store::{IDStore, day::Day, tally_id::TallyID},
|
||||||
webserver::start_webserver,
|
webserver::start_webserver,
|
||||||
};
|
};
|
||||||
|
|
||||||
extern crate alloc;
|
|
||||||
|
|
||||||
mod drivers;
|
mod drivers;
|
||||||
mod feedback;
|
mod feedback;
|
||||||
mod init;
|
mod init;
|
||||||
@@ -39,27 +40,30 @@ static FEEDBACK_STATE: Signal<CriticalSectionRawMutex, feedback::FeedbackState>
|
|||||||
|
|
||||||
type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
|
type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
|
||||||
type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>;
|
type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>;
|
||||||
|
type TallySubscriber = Subscriber<'static, NoopRawMutex, TallyID, 8, 2, 1>;
|
||||||
type UsedStore = IDStore<SDCardPersistence>;
|
type UsedStore = IDStore<SDCardPersistence>;
|
||||||
|
|
||||||
#[esp_hal_embassy::main]
|
static CHAN: StaticCell<TallyChannel> = StaticCell::new();
|
||||||
async fn main(mut spawner: Spawner) {
|
|
||||||
let (uart_device, stack, _i2c, _led, buzzer_gpio, sd_det_gpio, persistence_layer) =
|
#[esp_rtos::main]
|
||||||
init::hardware::hardware_init(&mut spawner).await;
|
async fn main(spawner: Spawner) -> ! {
|
||||||
|
let (uart_device, stack, i2c, rmt, led_gpio, buzzer_gpio, sd_det_gpio, persistence_layer) =
|
||||||
|
init::hardware::hardware_init(spawner).await;
|
||||||
|
|
||||||
info!("Starting up...");
|
info!("Starting up...");
|
||||||
|
|
||||||
let mut rtc = drivers::rtc::RTCClock::new(_i2c).await;
|
let mut rtc = drivers::rtc::RTCClock::new(i2c).await;
|
||||||
|
|
||||||
let store: UsedStore = IDStore::new_from_storage(persistence_layer).await;
|
let store: UsedStore = IDStore::new_from_storage(persistence_layer).await;
|
||||||
let shared_store = Rc::new(Mutex::new(store));
|
let shared_store = Rc::new(Mutex::new(store));
|
||||||
|
|
||||||
let chan: &'static mut TallyChannel = make_static!(PubSubChannel::new());
|
let chan: &'static mut TallyChannel = CHAN.init(PubSubChannel::new());
|
||||||
let publisher = chan.publisher().unwrap();
|
let publisher: TallyPublisher = chan.publisher().unwrap();
|
||||||
let mut sub = chan.subscriber().unwrap();
|
let mut sub: TallySubscriber = chan.subscriber().unwrap();
|
||||||
|
|
||||||
wait_for_stack_up(stack).await;
|
wait_for_stack_up(stack).await;
|
||||||
|
|
||||||
start_webserver(&mut spawner, stack, shared_store.clone());
|
start_webserver(spawner, stack, shared_store.clone(), chan);
|
||||||
|
|
||||||
/****************************** Spawning tasks ***********************************/
|
/****************************** Spawning tasks ***********************************/
|
||||||
debug!("spawing NFC reader task...");
|
debug!("spawing NFC reader task...");
|
||||||
@@ -69,7 +73,7 @@ async fn main(mut spawner: Spawner) {
|
|||||||
));
|
));
|
||||||
|
|
||||||
debug!("spawing feedback task..");
|
debug!("spawing feedback task..");
|
||||||
spawner.must_spawn(feedback::feedback_task(_led, buzzer_gpio));
|
spawner.must_spawn(feedback::feedback_task(rmt, led_gpio, buzzer_gpio));
|
||||||
|
|
||||||
debug!("spawn sd detect task");
|
debug!("spawn sd detect task");
|
||||||
spawner.must_spawn(sd_detect_task(sd_det_gpio));
|
spawner.must_spawn(sd_detect_task(sd_det_gpio));
|
||||||
@@ -85,7 +89,7 @@ async fn main(mut spawner: Spawner) {
|
|||||||
Message(msg) => {
|
Message(msg) => {
|
||||||
debug!("Got message: {msg:?}");
|
debug!("Got message: {msg:?}");
|
||||||
|
|
||||||
let day: Date = rtc.get_date().await;
|
let day: Day = rtc.get_time().await.into();
|
||||||
let added = shared_store.lock().await.add_id(msg, day).await;
|
let added = shared_store.lock().await.add_id(msg, day).await;
|
||||||
|
|
||||||
if added {
|
if added {
|
||||||
|
|||||||
63
src/store/day.rs
Normal file
63
src/store/day.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
use embedded_sdmmc::ShortFileName;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Day(u32);
|
||||||
|
|
||||||
|
impl Day {
|
||||||
|
const SECONDS_PER_DAY: u64 = 86_400;
|
||||||
|
|
||||||
|
pub fn new(daystamp: u32) -> Self {
|
||||||
|
Day(daystamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_from_timestamp(time: u64) -> Self {
|
||||||
|
let day = time / Self::SECONDS_PER_DAY;
|
||||||
|
|
||||||
|
if day > u32::MAX as u64 {
|
||||||
|
// TBH this would only happen if about 11 million years have passed
|
||||||
|
// I sure hope i don't have to work on this project any more then
|
||||||
|
// So we just cap it at this
|
||||||
|
Day(u32::MAX)
|
||||||
|
} else {
|
||||||
|
Day(day as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_timestamp(self) -> u64 {
|
||||||
|
(self.0 as u64) * Self::SECONDS_PER_DAY
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(self) -> heapless::String<8> {
|
||||||
|
let mut s: heapless::String<8> = heapless::String::new();
|
||||||
|
write!(s, "{:08X}", self.0).unwrap();
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_hex_str(s: &str) -> Result<Self, &'static str> {
|
||||||
|
if s.len() > 8 {
|
||||||
|
return Err("hex string too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
u32::from_str_radix(s, 16)
|
||||||
|
.map_err(|_| "invalid hex string")
|
||||||
|
.map(Day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for Day {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self::new_from_timestamp(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<ShortFileName> for Day {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: ShortFileName) -> Result<Self, Self::Error> {
|
||||||
|
let name = core::str::from_utf8(value.base_name()).map_err(|_| ())?;
|
||||||
|
Self::from_hex_str(name).map_err(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
use super::TallyID;
|
|
||||||
use alloc::collections::BTreeMap;
|
use alloc::collections::BTreeMap;
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::store::tally_id::TallyID;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Name {
|
pub struct Name {
|
||||||
pub first: String,
|
pub first: String,
|
||||||
pub last: String,
|
pub last: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct IDMapping {
|
pub struct IDMapping {
|
||||||
|
#[serde(flatten)]
|
||||||
id_map: BTreeMap<TallyID, Name>,
|
id_map: BTreeMap<TallyID, Name>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
use crate::drivers::rtc;
|
|
||||||
use crate::drivers::rtc::RTCClock;
|
|
||||||
use crate::store::persistence::Persistence;
|
|
||||||
|
|
||||||
use super::Date;
|
|
||||||
use super::IDMapping;
|
|
||||||
use super::TallyID;
|
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use super::IDMapping;
|
||||||
|
use crate::store::day::Day;
|
||||||
|
use crate::store::persistence::Persistence;
|
||||||
|
use crate::store::tally_id::TallyID;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct AttendanceDay {
|
pub struct AttendanceDay {
|
||||||
date: Date,
|
date: Day,
|
||||||
ids: Vec<TallyID>,
|
ids: Vec<TallyID>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttendanceDay {
|
impl AttendanceDay {
|
||||||
pub fn new(date: Date) -> Self {
|
pub fn new(date: Day) -> Self {
|
||||||
Self {
|
Self {
|
||||||
date,
|
date,
|
||||||
ids: Vec::new(),
|
ids: Vec::new(),
|
||||||
@@ -43,12 +41,12 @@ pub struct IDStore<T: Persistence> {
|
|||||||
|
|
||||||
impl<T: Persistence> IDStore<T> {
|
impl<T: Persistence> IDStore<T> {
|
||||||
pub async fn new_from_storage(mut persistence_layer: T) -> Self {
|
pub async fn new_from_storage(mut persistence_layer: T) -> Self {
|
||||||
// let mapping = match persistence_layer.load_mapping().await {
|
let mapping = match persistence_layer.load_mapping().await {
|
||||||
// Some(map) => map,
|
Some(map) => map,
|
||||||
// None => IDMapping::new(),
|
None => IDMapping::new(),
|
||||||
// };
|
};
|
||||||
|
|
||||||
let current_date: Date = [0; 10];
|
let current_date: Day = Day::new(1);
|
||||||
|
|
||||||
let day = persistence_layer
|
let day = persistence_layer
|
||||||
.load_day(current_date)
|
.load_day(current_date)
|
||||||
@@ -57,7 +55,7 @@ impl<T: Persistence> IDStore<T> {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
current_day: day,
|
current_day: day,
|
||||||
mapping: IDMapping::new(),
|
mapping,
|
||||||
persistence_layer,
|
persistence_layer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,14 +66,13 @@ impl<T: Persistence> IDStore<T> {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn persist_mapping(&mut self) {
|
pub async fn persist_mapping(&mut self) {
|
||||||
self.persistence_layer.save_mapping(&self.mapping).await
|
self.persistence_layer.save_mapping(&self.mapping).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new id for the current day
|
/// Add a new id for the current day
|
||||||
/// Returns false if ID is already present at the current day.
|
/// Returns false if ID is already present at the current day.
|
||||||
pub async fn add_id(&mut self, id: TallyID, current_date: Date) -> bool {
|
pub async fn add_id(&mut self, id: TallyID, current_date: Day) -> bool {
|
||||||
|
|
||||||
if self.current_day.date == current_date {
|
if self.current_day.date == current_date {
|
||||||
let changed = self.current_day.add_id(id);
|
let changed = self.current_day.add_id(id);
|
||||||
if changed {
|
if changed {
|
||||||
@@ -93,4 +90,23 @@ impl<T: Persistence> IDStore<T> {
|
|||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load and return a AttendanceDay. Nothing more. Nothing less.
|
||||||
|
pub async fn load_day(&mut self, day: Day) -> Option<AttendanceDay> {
|
||||||
|
if day == self.current_day.date {
|
||||||
|
return Some(self.current_day.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.persistence_layer.load_day(day).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_days_in_timespan(&mut self, from: Day, to: Day) -> Vec<Day> {
|
||||||
|
let all_days = self.persistence_layer.list_days().await;
|
||||||
|
|
||||||
|
all_days
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| *e >= from)
|
||||||
|
.filter(|e| *e <= to)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
mod id_mapping;
|
|
||||||
pub mod persistence;
|
|
||||||
mod id_store;
|
|
||||||
|
|
||||||
pub use id_mapping::{IDMapping, Name};
|
pub use id_mapping::{IDMapping, Name};
|
||||||
pub use id_store::{IDStore,AttendanceDay};
|
pub use id_store::{IDStore,AttendanceDay};
|
||||||
|
|
||||||
pub type TallyID = [u8; 6];
|
mod id_mapping;
|
||||||
pub type Date = [u8; 10];
|
pub mod persistence;
|
||||||
|
mod id_store;
|
||||||
|
pub mod tally_id;
|
||||||
|
pub mod day;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
use crate::store::{Date, IDMapping, id_store::AttendanceDay};
|
use crate::store::{IDMapping, day::Day, id_store::AttendanceDay};
|
||||||
|
|
||||||
pub trait Persistence {
|
pub trait Persistence {
|
||||||
async fn load_day(&mut self, day: Date) -> Option<AttendanceDay>;
|
async fn load_day(&mut self, day: Day) -> Option<AttendanceDay>;
|
||||||
async fn save_day(&mut self, day: Date, data: &AttendanceDay);
|
async fn save_day(&mut self, day: Day, data: &AttendanceDay);
|
||||||
async fn list_days(&mut self) -> Vec<Date>;
|
async fn list_days(&mut self) -> Vec<Day>;
|
||||||
|
|
||||||
async fn load_mapping(&mut self) -> Option<IDMapping>;
|
async fn load_mapping(&mut self) -> Option<IDMapping>;
|
||||||
async fn save_mapping(&mut self, data: &IDMapping);
|
async fn save_mapping(&mut self, data: &IDMapping);
|
||||||
|
|||||||
108
src/store/tally_id.rs
Normal file
108
src/store/tally_id.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use core::{fmt::Display, str::FromStr};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct TallyID([u8; 6]);
|
||||||
|
|
||||||
|
impl FromStr for TallyID {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
s.as_bytes().try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<heapless::String<12>> for TallyID {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: heapless::String<12>) -> Result<Self, Self::Error> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
|
||||||
|
let mut out: [u8; 6] = [0; 6];
|
||||||
|
for i in 0..6 {
|
||||||
|
let hi = hex_val(bytes[2 * i])?;
|
||||||
|
let lo = hex_val(bytes[2 * i + 1])?;
|
||||||
|
out[i] = (hi << 4) | lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TallyID(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_val(b: u8) -> Result<u8, ()> {
|
||||||
|
match b {
|
||||||
|
b'0'..=b'9' => Ok(b - b'0'),
|
||||||
|
b'a'..=b'f' => Ok(b - b'a' + 10),
|
||||||
|
b'A'..=b'F' => Ok(b - b'A' + 10),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TallyID> for heapless::String<12> {
|
||||||
|
fn from(value: TallyID) -> Self {
|
||||||
|
const HEX_CHARS: &[u8; 16] = b"0123456789ABCDEF";
|
||||||
|
let mut s: Self = Self::new();
|
||||||
|
|
||||||
|
for &b in &value.0 {
|
||||||
|
// Should be safe to unwrap since the string is already long enough
|
||||||
|
s.push(HEX_CHARS[(b >> 4) as usize] as char).unwrap();
|
||||||
|
s.push(HEX_CHARS[(b & 0x0F) as usize] as char).unwrap();
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From a array of hex chars
|
||||||
|
impl TryFrom<&[u8]> for TallyID {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
if value.len() != 12 {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out: [u8; 6] = [0; 6];
|
||||||
|
for i in 0..6 {
|
||||||
|
let hi = hex_val(value[2 * i])?;
|
||||||
|
let lo = hex_val(value[2 * i + 1])?;
|
||||||
|
out[i] = (hi << 4) | lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TallyID(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<[u8; 12]> for TallyID {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: [u8; 12]) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(&value as &[u8])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TallyID {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
let s: heapless::String<12> = (*self).into();
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for TallyID {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let s: heapless::String<12> = (*self).into();
|
||||||
|
serializer.serialize_str(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for TallyID {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = <&str>::deserialize(deserializer)?;
|
||||||
|
TallyID::from_str(s).map_err(|_| de::Error::custom("Failed to parse Tally ID"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,66 +1,94 @@
|
|||||||
use alloc::string::String;
|
use log::error;
|
||||||
use picoserve::{
|
use picoserve::{
|
||||||
extract::{Json, State},
|
extract::{Json, Query, State},
|
||||||
response::{self, IntoResponse},
|
response::{self, IntoResponse},
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
store::{Name, TallyID},
|
store::{Name, day::Day, tally_id::TallyID},
|
||||||
webserver::app::AppState,
|
webserver::{app::AppState, sse::IDEvents},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct NewMapping {
|
pub struct NewMapping {
|
||||||
id: String,
|
id: TallyID,
|
||||||
name: Name,
|
name: Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hex_string_to_tally_id(s: &str) -> Option<TallyID> {
|
#[derive(Deserialize)]
|
||||||
let bytes = s.as_bytes();
|
pub struct QueryTimespan {
|
||||||
if bytes.len() != 24 {
|
from: u64,
|
||||||
return None;
|
to: u64,
|
||||||
}
|
|
||||||
|
|
||||||
let mut out = [0u8; 12];
|
|
||||||
for i in 0..12 {
|
|
||||||
let hi = hex_val(bytes[2 * i])?;
|
|
||||||
let lo = hex_val(bytes[2 * i + 1])?;
|
|
||||||
out[i] = (hi << 4) | lo;
|
|
||||||
}
|
|
||||||
Some(out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hex_val(b: u8) -> Option<u8> {
|
#[derive(Deserialize)]
|
||||||
match b {
|
pub struct QueryDay {
|
||||||
b'0'..=b'9' => Some(b - b'0'),
|
timestamp: Option<u64>,
|
||||||
b'a'..=b'f' => Some(b - b'a' + 10),
|
day: Option<u32>,
|
||||||
b'A'..=b'F' => Some(b - b'A' + 10),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// GET /api/mapping
|
||||||
* #[get("/api/idevent")]
|
|
||||||
* #[get("/api/csv")]
|
|
||||||
* #[get("/api/mapping")]
|
|
||||||
* #[post("/api/mapping", format = "json", data = "<new_mapping>")]
|
|
||||||
* struct NewMapping {
|
|
||||||
* id: String,
|
|
||||||
* name: Name,
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub async fn get_mapping(State(state): State<AppState>) -> impl IntoResponse {
|
pub async fn get_mapping(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
let store = state.store.lock().await;
|
let store = state.store.lock().await;
|
||||||
response::Json(store.mapping.clone())
|
response::Json(store.mapping.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /api/mapping
|
||||||
pub async fn add_mapping(
|
pub async fn add_mapping(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(data): Json<NewMapping>,
|
Json(data): Json<NewMapping>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut store = state.store.lock().await;
|
let mut store = state.store.lock().await;
|
||||||
let tally_id = hex_string_to_tally_id(&data.id).unwrap();
|
store.mapping.add_mapping(data.id, data.name);
|
||||||
store.mapping.add_mapping(tally_id, data.name);
|
store.persist_mapping().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE /api/idevent
|
||||||
|
pub async fn get_idevent(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||||
|
match state.chan.subscriber() {
|
||||||
|
Ok(chan) => Ok(response::EventStream(IDEvents(chan))),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to create SSE: {:?}", e);
|
||||||
|
Err((
|
||||||
|
response::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Internal server error",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/days
|
||||||
|
pub async fn get_days(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(QueryTimespan { from, to }): Query<QueryTimespan>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let from_day = Day::new_from_timestamp(from);
|
||||||
|
let to_day = Day::new_from_timestamp(to);
|
||||||
|
|
||||||
|
let mut store = state.store.lock().await;
|
||||||
|
|
||||||
|
let days = store.list_days_in_timespan(from_day, to_day).await;
|
||||||
|
|
||||||
|
response::Json(days)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/day
|
||||||
|
pub async fn get_day(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(QueryDay { timestamp, day }): Query<QueryDay>,
|
||||||
|
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||||
|
let parsed_day = timestamp
|
||||||
|
.map(Day::new_from_timestamp)
|
||||||
|
.or_else(|| day.map(Day::new))
|
||||||
|
.ok_or((response::StatusCode::NOT_FOUND, "Not found"))?;
|
||||||
|
|
||||||
|
let mut store = state.store.lock().await;
|
||||||
|
|
||||||
|
match store.load_day(parsed_day).await {
|
||||||
|
Some(att_day) => Ok(response::Json(att_day)),
|
||||||
|
None => Err((response::StatusCode::NOT_FOUND, "Not found")),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,17 @@ use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
|
|||||||
use picoserve::{AppWithStateBuilder, routing::get};
|
use picoserve::{AppWithStateBuilder, routing::get};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
TallyChannel, UsedStore,
|
||||||
webserver::{
|
webserver::{
|
||||||
api::{add_mapping, get_mapping},
|
api::{add_mapping, get_day, get_days, get_idevent, get_mapping},
|
||||||
assets::Assets,
|
assets::Assets,
|
||||||
}, UsedStore,
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
pub store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
||||||
|
pub chan: &'static TallyChannel,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppProps;
|
pub struct AppProps;
|
||||||
@@ -23,9 +25,8 @@ impl AppWithStateBuilder for AppProps {
|
|||||||
fn build_app(self) -> picoserve::Router<Self::PathRouter, AppState> {
|
fn build_app(self) -> picoserve::Router<Self::PathRouter, AppState> {
|
||||||
picoserve::Router::from_service(Assets)
|
picoserve::Router::from_service(Assets)
|
||||||
.route("/api/mapping", get(get_mapping).post(add_mapping))
|
.route("/api/mapping", get(get_mapping).post(add_mapping))
|
||||||
// .route(
|
.route("/api/idevent", get(get_idevent))
|
||||||
// "/api/idevent",
|
.route("/api/days", get(get_days))
|
||||||
// get(move || response::EventStream(Events(self.chan))),
|
.route("/api/day", get(get_day))
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ impl<State, CurrentPathParameters>
|
|||||||
);
|
);
|
||||||
|
|
||||||
response_writer
|
response_writer
|
||||||
.write_response(request.body_connection.finalize().await.unwrap(), response)
|
.write_response(request.body_connection.finalize().await?, response)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -68,10 +68,7 @@ impl Content for StaticAsset {
|
|||||||
self.0.len()
|
self.0.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_content<W: embedded_io_async::Write>(
|
async fn write_content<W: edge_nal::io::Write>(self, mut writer: W) -> Result<(), W::Error> {
|
||||||
self,
|
|
||||||
mut writer: W,
|
|
||||||
) -> Result<(), W::Error> {
|
|
||||||
writer.write_all(self.0).await
|
writer.write_all(self.0).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,25 +7,26 @@ use picoserve::{AppRouter, AppWithStateBuilder};
|
|||||||
use static_cell::make_static;
|
use static_cell::make_static;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
UsedStore,
|
TallyChannel, UsedStore,
|
||||||
webserver::app::{AppProps, AppState},
|
webserver::app::{AppProps, AppState},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod assets;
|
|
||||||
// mod sse;
|
|
||||||
mod api;
|
mod api;
|
||||||
mod app;
|
mod app;
|
||||||
|
mod assets;
|
||||||
|
mod sse;
|
||||||
|
|
||||||
pub const WEB_TAKS_SIZE: usize = 3; // Up this number if request start fail with Timeouts.
|
pub const WEB_TAKS_SIZE: usize = 5; // Up this number if request start fail with Timeouts.
|
||||||
|
|
||||||
pub fn start_webserver(
|
pub fn start_webserver(
|
||||||
spawner: &mut Spawner,
|
spawner: Spawner,
|
||||||
stack: Stack<'static>,
|
stack: Stack<'static>,
|
||||||
store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
store: Rc<Mutex<CriticalSectionRawMutex, UsedStore>>,
|
||||||
|
chan: &'static TallyChannel,
|
||||||
) {
|
) {
|
||||||
let app = make_static!(AppProps.build_app());
|
let app = make_static!(AppProps.build_app());
|
||||||
|
|
||||||
let state = make_static!(AppState { store });
|
let state = make_static!(AppState { store, chan });
|
||||||
|
|
||||||
let config = make_static!(picoserve::Config::new(picoserve::Timeouts {
|
let config = make_static!(picoserve::Config::new(picoserve::Timeouts {
|
||||||
start_read_request: Some(Duration::from_secs(5)),
|
start_read_request: Some(Duration::from_secs(5)),
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ use embassy_time::{Duration, Timer};
|
|||||||
use log::warn;
|
use log::warn;
|
||||||
use picoserve::response;
|
use picoserve::response;
|
||||||
|
|
||||||
pub struct Events(pub TallySubscriber);
|
use crate::TallySubscriber;
|
||||||
|
|
||||||
impl response::sse::EventSource for Events {
|
pub struct IDEvents(pub TallySubscriber);
|
||||||
|
|
||||||
|
impl response::sse::EventSource for IDEvents {
|
||||||
async fn write_events<W: picoserve::io::Write>(
|
async fn write_events<W: picoserve::io::Write>(
|
||||||
mut self,
|
mut self,
|
||||||
mut writer: response::sse::EventWriter<W>,
|
mut writer: response::sse::EventWriter<W>,
|
||||||
@@ -16,7 +18,8 @@ impl response::sse::EventSource for Events {
|
|||||||
match sel.await {
|
match sel.await {
|
||||||
embassy_futures::select::Either::First(msg) => match msg {
|
embassy_futures::select::Either::First(msg) => match msg {
|
||||||
embassy_sync::pubsub::WaitResult::Message(id) => {
|
embassy_sync::pubsub::WaitResult::Message(id) => {
|
||||||
writer.write_event("msg", id.to_string().as_str()).await?
|
let id_str: heapless::String<12> = id.into();
|
||||||
|
writer.write_event("msg", id_str.as_str()).await?
|
||||||
}
|
}
|
||||||
embassy_sync::pubsub::WaitResult::Lagged(_) => {
|
embassy_sync::pubsub::WaitResult::Lagged(_) => {
|
||||||
warn!("SSE subscriber got lagged");
|
warn!("SSE subscriber got lagged");
|
||||||
|
|||||||
108
web/mock/data.json
Normal file
108
web/mock/data.json
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"mapping": [
|
||||||
|
[
|
||||||
|
"123456789ABC",
|
||||||
|
{
|
||||||
|
"first": "Feuerwehrman",
|
||||||
|
"last": "Sam"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"A1B2C3D4E5F6",
|
||||||
|
{
|
||||||
|
"first": "Luna",
|
||||||
|
"last": "Starforge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"0F1E2D3C4B5A",
|
||||||
|
{
|
||||||
|
"first": "Gareth",
|
||||||
|
"last": "Ironwill"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ABCDEF123456",
|
||||||
|
{
|
||||||
|
"first": "Nina",
|
||||||
|
"last": "Skylark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"654321FEDCBA",
|
||||||
|
{
|
||||||
|
"first": "Tobias",
|
||||||
|
"last": "Marrow"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"DEADBEEFCAFE",
|
||||||
|
{
|
||||||
|
"first": "Astra",
|
||||||
|
"last": "Vale"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"BADA55C0FFEE",
|
||||||
|
{
|
||||||
|
"first": "Rowan",
|
||||||
|
"last": "Tempest"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"C001D00D1337",
|
||||||
|
{
|
||||||
|
"first": "Juniper",
|
||||||
|
"last": "Voss"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"A0B1DB0D133B",
|
||||||
|
{
|
||||||
|
"first": "Öäü",
|
||||||
|
"last": "ßẞ"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"days": [
|
||||||
|
{
|
||||||
|
"date": 20372,
|
||||||
|
"ids": [
|
||||||
|
"123456789ABC",
|
||||||
|
"A1B2C3D4E5F6",
|
||||||
|
"A0B1DB0D133B"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 20373,
|
||||||
|
"ids": [
|
||||||
|
"0F1E2D3C4B5A",
|
||||||
|
"ABCDEF123456"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 20374,
|
||||||
|
"ids": [
|
||||||
|
"654321FEDCBA",
|
||||||
|
"DEADBEEFCAFE",
|
||||||
|
"BADA55C0FFEE",
|
||||||
|
"A0B1DB0D133B"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 20375,
|
||||||
|
"ids": [
|
||||||
|
"C001D00D1337",
|
||||||
|
"A1B2C3D4E5F6",
|
||||||
|
"123456789ABC"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": 20376,
|
||||||
|
"ids": [
|
||||||
|
"N0T3X1ST1D0",
|
||||||
|
"654321FEDCBA"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
111
web/mock/server.js
Normal file
111
web/mock/server.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import express from "express";
|
||||||
|
import bodyParser from "body-parser";
|
||||||
|
|
||||||
|
import mockData from "./data.json" with {type: "json"};
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
const SECS_IN_DAY = 86_400;
|
||||||
|
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
function generateRandomId() {
|
||||||
|
const chars = "ABCDEF0123456789";
|
||||||
|
let id = "";
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/mapping
|
||||||
|
app.get("/api/mapping", (req, res) => {
|
||||||
|
res.json(mockData.mapping);
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/mapping
|
||||||
|
app.post("/api/mapping", (req, res) => {
|
||||||
|
const { id, name } = req.body;
|
||||||
|
|
||||||
|
if (!id || !name || !name.first || !name.last) {
|
||||||
|
return res.status(400).json({ error: "Invalid request body" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ID already exists
|
||||||
|
const existing = mappings.find((entry) => entry[0] === id);
|
||||||
|
if (existing) {
|
||||||
|
return res.status(409).json({ error: "ID already exists" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new mapping
|
||||||
|
mockData.mappings.push([id, name]);
|
||||||
|
|
||||||
|
res.status(201).send("");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/day", (req, res) => {
|
||||||
|
let day;
|
||||||
|
|
||||||
|
if (req.query.day) {
|
||||||
|
day = parseInt(req.query.day, 10);
|
||||||
|
}else if (req.query.timestamp) {
|
||||||
|
let ts = parseInt(req.query.timestamp, 10);
|
||||||
|
day = ts / SECS_IN_DAY;
|
||||||
|
}else {
|
||||||
|
return res.status(400).json({ error: "Missing or invalid 'day' parameter" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(day)) {
|
||||||
|
return res.status(400).json({ error: "Missing or invalid 'day' parameter" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundDay = mockData.days.find(e => e.date == day);
|
||||||
|
|
||||||
|
if (!foundDay) {
|
||||||
|
return res.status(404).send("Not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(foundDay);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/days", (req,res) => {
|
||||||
|
|
||||||
|
let qFrom = parseInt(req.query.from) / SECS_IN_DAY;
|
||||||
|
let qTo = parseInt(req.query.to) / SECS_IN_DAY;
|
||||||
|
|
||||||
|
let days = mockData.days.filter(e => e.date >= qFrom && e.date <= qTo).map(e => e.date);
|
||||||
|
|
||||||
|
res.status(200).json(days);
|
||||||
|
});
|
||||||
|
|
||||||
|
// SSE route: /api/idevent
|
||||||
|
app.get("/api/idevent", (req, res) => {
|
||||||
|
// Set headers for SSE
|
||||||
|
res.setHeader("Content-Type", "text/event-stream");
|
||||||
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
|
res.setHeader("Connection", "keep-alive");
|
||||||
|
|
||||||
|
res.flushHeaders(); // flush the headers to establish SSE connection
|
||||||
|
|
||||||
|
// Send initial event
|
||||||
|
const sendEvent = () => {
|
||||||
|
const id = generateRandomId();
|
||||||
|
res.write(`data: ${id}\n\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send immediately and then every 10 seconds
|
||||||
|
sendEvent();
|
||||||
|
const interval = setInterval(sendEvent, 10000);
|
||||||
|
|
||||||
|
// When client closes connection, stop interval
|
||||||
|
req.on("close", () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Mock API server running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
871
web/package-lock.json
generated
871
web/package-lock.json
generated
@@ -14,6 +14,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
"svelte": "^5.28.1",
|
"svelte": "^5.28.1",
|
||||||
"svelte-check": "^4.1.6",
|
"svelte-check": "^4.1.6",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
@@ -1078,6 +1080,20 @@
|
|||||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"negotiator": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
@@ -1111,6 +1127,68 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "^3.1.2",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"raw-body": "^3.0.0",
|
||||||
|
"type-is": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@@ -1146,6 +1224,49 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -1174,6 +1295,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
@@ -1183,6 +1314,38 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.1",
|
"version": "5.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||||
@@ -1196,6 +1359,39 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||||
@@ -1236,6 +1432,13 @@
|
|||||||
"@esbuild/win32-x64": "0.25.4"
|
"@esbuild/win32-x64": "0.25.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esm-env": {
|
"node_modules/esm-env": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||||
@@ -1253,6 +1456,59 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "^2.0.0",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"content-disposition": "^1.0.0",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cookie": "^0.7.1",
|
||||||
|
"cookie-signature": "^1.2.1",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"finalhandler": "^2.1.0",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"merge-descriptors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"proxy-addr": "^2.0.7",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"router": "^2.2.0",
|
||||||
|
"send": "^1.1.0",
|
||||||
|
"serve-static": "^2.2.0",
|
||||||
|
"statuses": "^2.0.1",
|
||||||
|
"type-is": "^2.0.1",
|
||||||
|
"vary": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.4.4",
|
"version": "6.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||||
@@ -1267,6 +1523,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/finalhandler": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1281,12 +1575,164 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"inherits": "2.0.4",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "2.0.1",
|
||||||
|
"toidentifier": "1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors/node_modules/statuses": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-reference": {
|
"node_modules/is-reference": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
@@ -1560,6 +2006,62 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@@ -1631,6 +2133,73 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1677,6 +2246,79 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
|
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"iconv-lite": "0.7.0",
|
||||||
|
"unpipe": "1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body/node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -1730,6 +2372,23 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/router": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"is-promise": "^4.0.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"path-to-regexp": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||||
@@ -1743,6 +2402,156 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.5",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.1",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"send": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -1752,6 +2561,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.30.1",
|
"version": "5.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz",
|
||||||
@@ -1850,6 +2669,31 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"media-typer": "^1.1.0",
|
||||||
|
"mime-types": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
@@ -1864,6 +2708,26 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "6.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||||
@@ -1957,6 +2821,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
"svelte": "^5.28.1",
|
"svelte": "^5.28.1",
|
||||||
"svelte-check": "^4.1.6",
|
"svelte-check": "^4.1.6",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"express": "^5.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
|
|||||||
@@ -3,15 +3,21 @@
|
|||||||
import IDTable from "./lib/IDTable.svelte";
|
import IDTable from "./lib/IDTable.svelte";
|
||||||
import LastId from "./lib/LastID.svelte";
|
import LastId from "./lib/LastID.svelte";
|
||||||
import AddIDModal from "./lib/AddIDModal.svelte";
|
import AddIDModal from "./lib/AddIDModal.svelte";
|
||||||
|
import ExportModal from "./lib/ExportModal.svelte";
|
||||||
|
import { generateCSVFile } from "./lib/exporting";
|
||||||
|
import { fetchMapping, type IDMap } from "./lib/IDMapping";
|
||||||
|
import { downloadBlob } from "./lib/downloadBlob";
|
||||||
|
|
||||||
let lastID: string = $state("");
|
let lastID: string = $state("");
|
||||||
|
let mapping: IDMap | null = $state(null);
|
||||||
|
|
||||||
let addModal: AddIDModal;
|
let addModal: AddIDModal;
|
||||||
let idTable: IDTable;
|
let exportModal: ExportModal;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
mapping = await fetchMapping();
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
let sse = new EventSource("/api/idevent");
|
let sse = new EventSource("/api/idevent");
|
||||||
|
|
||||||
sse.onmessage = (e) => {
|
sse.onmessage = (e) => {
|
||||||
lastID = e.data;
|
lastID = e.data;
|
||||||
};
|
};
|
||||||
@@ -25,13 +31,14 @@
|
|||||||
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
|
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Anwesenheit</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<button
|
||||||
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
class="px-6 py-3 text-lg font-semibold text-white bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
||||||
href="/api/csv"
|
onclick={() => {
|
||||||
download="anwesenheit.csv"
|
exportModal.open();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Download CSV
|
Export CSV
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<div class="pt-3 pb-2">
|
<div class="pt-3 pb-2">
|
||||||
<LastId
|
<LastId
|
||||||
@@ -42,15 +49,32 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IDTable bind:this={idTable} onEdit={(id,firstName,lastName)=>{
|
{#if mapping}
|
||||||
addModal.open(id,firstName,lastName);
|
<IDTable
|
||||||
}}/>
|
data={mapping}
|
||||||
|
onEdit={(id, firstName, lastName) => {
|
||||||
|
addModal.open(id, firstName, lastName);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddIDModal
|
<AddIDModal
|
||||||
bind:this={addModal}
|
bind:this={addModal}
|
||||||
onSubmitted={() => {
|
onSubmitted={async () => {
|
||||||
idTable.reloadData();
|
mapping = await fetchMapping();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExportModal
|
||||||
|
bind:this={exportModal}
|
||||||
|
onSubmitted={async (from, to) => {
|
||||||
|
if (!mapping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let csvFile = await generateCSVFile(from, to, mapping);
|
||||||
|
|
||||||
|
downloadBlob("export.csv",csvFile,"text/csv");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
31
web/src/lib/Day.ts
Normal file
31
web/src/lib/Day.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export type Day = number;
|
||||||
|
|
||||||
|
export interface AttendanceDay {
|
||||||
|
date: Day,
|
||||||
|
ids: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dayToDate(day: Day): Date {
|
||||||
|
const SEC_PER_DAY = 86_400;
|
||||||
|
|
||||||
|
return new Date(day * SEC_PER_DAY * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDay(day: Day): Promise<AttendanceDay> {
|
||||||
|
let res = await fetch("/api/day?" + (new URLSearchParams({ day: day.toString() }).toString()));
|
||||||
|
|
||||||
|
let json = await res.json();
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDays(from: Date, to: Date): Promise<Day[]> {
|
||||||
|
let q = new URLSearchParams({ from: (from.getTime() / 1000).toString(), to: (to.getTime() / 1000).toString() });
|
||||||
|
|
||||||
|
let res = await fetch("/api/days?" + q);
|
||||||
|
|
||||||
|
let json = await res.json();
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
142
web/src/lib/ExportModal.svelte
Normal file
142
web/src/lib/ExportModal.svelte
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "./Modal.svelte";
|
||||||
|
|
||||||
|
let { onSubmitted }: { onSubmitted?: (from: Date, to: Date) => void } =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
let modal: Modal;
|
||||||
|
|
||||||
|
let fromDate: string | undefined = $state();
|
||||||
|
let toDate: string | undefined = $state();
|
||||||
|
|
||||||
|
let selectedYear: number = $state(new Date().getFullYear());
|
||||||
|
|
||||||
|
let selectedTab = $state(0);
|
||||||
|
|
||||||
|
export function open() {
|
||||||
|
modal.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateYears() {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const startingYear = 2020;
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
new Array(currentYear + 1 - startingYear),
|
||||||
|
(_, i) => i + startingYear,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onsubmit(e: SubmitEvent) {
|
||||||
|
let from: Date;
|
||||||
|
let to: Date;
|
||||||
|
|
||||||
|
switch (selectedTab) {
|
||||||
|
case 0:
|
||||||
|
if (!fromDate || !toDate) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
from = new Date(fromDate);
|
||||||
|
to = new Date(toDate);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
from = new Date(selectedYear, 0);
|
||||||
|
to = new Date(selectedYear + 1, 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Invalid tab");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitted?.(from, to);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<div class="flex">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
selectedTab = 0;
|
||||||
|
}}
|
||||||
|
class="tab {selectedTab === 0 ? 'tab-active' : ''}"
|
||||||
|
>
|
||||||
|
Datum
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
selectedTab = 1;
|
||||||
|
}}
|
||||||
|
class="tab {selectedTab === 1 ? 'tab-active' : ''}"
|
||||||
|
>
|
||||||
|
Jahr
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" {onsubmit} class="flex flex-col">
|
||||||
|
{#if selectedTab === 0}
|
||||||
|
<div>
|
||||||
|
<label class="form-row">
|
||||||
|
<span>Von:</span>
|
||||||
|
<input type="date" class="form-input" bind:value={fromDate} />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-row">
|
||||||
|
<span>Bis:</span>
|
||||||
|
<input type="date" class="form-input" bind:value={toDate} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedTab === 1}
|
||||||
|
<div>
|
||||||
|
<label class="form-row">
|
||||||
|
<span>Kalendar Jahr:</span>
|
||||||
|
<select class="form-input" bind:value={selectedYear}>
|
||||||
|
{#each generateYears() as year}
|
||||||
|
<option value={year}>{year}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-3">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
class="mr-5 px-2 py-1 bg-red-500 rounded-2xl shadow-md"
|
||||||
|
onclick={() => {
|
||||||
|
modal.close();
|
||||||
|
|
||||||
|
fromDate = undefined;
|
||||||
|
toDate = undefined;
|
||||||
|
}}>Abbrechen</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-2 py-1 bg-indigo-600 rounded-2xl shadow-md hover:bg-indigo-700 transition"
|
||||||
|
>Export CSV</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "../app.css";
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
@apply px-4 py-2 rounded-t-lg bg-indigo-600 hover:bg-indigo-700 font-medium border-b-2 border-transparent cursor-pointer transition-colors duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-active {
|
||||||
|
@apply px-4 py-2 bg-indigo-500 font-semibold border-b-2 border-blue-600 shadow-sm cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
@apply flex justify-between my-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
@apply ml-20;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
export interface IDMapping {
|
|
||||||
id_map: IDMap
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDMap {
|
export interface IDMap {
|
||||||
[name: string]: Name
|
[name: string]: Name
|
||||||
}
|
}
|
||||||
@@ -11,6 +7,23 @@ export interface Name {
|
|||||||
last: string,
|
last: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stupidSerdeFix(pairs: [string, Name][]): IDMap {
|
||||||
|
const map: IDMap = {};
|
||||||
|
for (const [key, value] of pairs) {
|
||||||
|
map[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMapping(): Promise<IDMap> {
|
||||||
|
let res = await fetch("/api/mapping");
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
return stupidSerdeFix(data);
|
||||||
|
}
|
||||||
|
|
||||||
export async function addMapping(id: string, firstName: string, lastName: string) {
|
export async function addMapping(id: string, firstName: string, lastName: string) {
|
||||||
let req = await fetch("/api/mapping", {
|
let req = await fetch("/api/mapping", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { type IDMap } from "./IDMapping";
|
||||||
import type { IDMapping } from "./IDMapping";
|
|
||||||
let data: IDMapping | undefined = $state();
|
|
||||||
|
|
||||||
let { onEdit }: { onEdit?: (string,string,string) => void } = $props();
|
let {
|
||||||
|
onEdit,
|
||||||
export async function reloadData() {
|
data,
|
||||||
let res = await fetch("/api/mapping");
|
}: {
|
||||||
|
onEdit?: (id: string, firstName: string, lastName: string) => void;
|
||||||
data = await res.json();
|
data: IDMap;
|
||||||
}
|
} = $props();
|
||||||
|
|
||||||
let rows = $derived(
|
let rows = $derived(
|
||||||
data
|
data
|
||||||
? Object.entries(data.id_map).map(([id, value]) => ({
|
? Object.entries(data).map(([id, value]) => ({
|
||||||
id,
|
id,
|
||||||
...value,
|
...value,
|
||||||
}))
|
}))
|
||||||
@@ -43,17 +41,9 @@
|
|||||||
if (sortKey !== key) return "";
|
if (sortKey !== key) return "";
|
||||||
return sortDirection === "asc" ? "▲" : "▼";
|
return sortDirection === "asc" ? "▲" : "▼";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await reloadData();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data == null}
|
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
||||||
Loading...
|
|
||||||
{:else}
|
|
||||||
<div class="bg-indigo-500 py-2 rounded-2xl overflow-x-auto">
|
|
||||||
<table class="px-10">
|
<table class="px-10">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -84,8 +74,7 @@
|
|||||||
|
|
||||||
<span class="indicator">{indicator("first")}</span>
|
<span class="indicator">{indicator("first")}</span>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th> </th>
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -94,19 +83,22 @@
|
|||||||
<td class="whitespace-nowrap pr-5 pl-2 py-1">{row.id}</td>
|
<td class="whitespace-nowrap pr-5 pl-2 py-1">{row.id}</td>
|
||||||
<td class="whitespace-nowrap pr-5">{row.last}</td>
|
<td class="whitespace-nowrap pr-5">{row.last}</td>
|
||||||
<td class="whitespace-nowrap pr-5">{row.first}</td>
|
<td class="whitespace-nowrap pr-5">{row.first}</td>
|
||||||
<td class="pr-5" ><button onclick={()=>{
|
<td class="pr-5"
|
||||||
onEdit && onEdit(row.id,row.first,row.last);
|
><button
|
||||||
}} class="cursor-pointer">🔧</button></td>
|
onclick={() => {
|
||||||
|
onEdit && onEdit(row.id, row.first, row.last);
|
||||||
|
}}
|
||||||
|
class="cursor-pointer">🔧</button
|
||||||
|
></td
|
||||||
|
>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
@reference "../app.css";
|
@reference "../app.css";
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
@apply ml-1 w-4 inline-block;
|
@apply ml-1 w-4 inline-block;
|
||||||
}
|
}
|
||||||
|
|||||||
103
web/src/lib/csv.ts
Normal file
103
web/src/lib/csv.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
interface CSVOptions {
|
||||||
|
delimiter?: string;
|
||||||
|
headerOrder?: string[];
|
||||||
|
eol?: string;
|
||||||
|
includeBOM?: boolean;
|
||||||
|
nullString?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowObject = Record<string, any>;
|
||||||
|
type InputRows = RowObject[];
|
||||||
|
|
||||||
|
export function generateCSVString(input: InputRows, opts: CSVOptions = {}): string {
|
||||||
|
const {
|
||||||
|
delimiter = ";",
|
||||||
|
headerOrder,
|
||||||
|
eol = "\r\n",
|
||||||
|
includeBOM = true,
|
||||||
|
nullString = "",
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
// Check if the value need quoting
|
||||||
|
// Usually not needed in our use case but still in case
|
||||||
|
const needsQuoting = (s: string) =>
|
||||||
|
s.includes(delimiter) || s.includes('"') || s.includes("\n") || s.includes("\r") || /^\s|\s$/.test(s);
|
||||||
|
|
||||||
|
// Transform the value of a cell in a SAFE string
|
||||||
|
const escapeCell = (raw: any): string => {
|
||||||
|
if (raw === null || raw === undefined) return nullString;
|
||||||
|
|
||||||
|
let s = stringify(raw);
|
||||||
|
|
||||||
|
// Replace quotes
|
||||||
|
if (s.includes('"')) s = s.replace(/"/g, '""');
|
||||||
|
|
||||||
|
return needsQuoting(s) ? `"${s}"` : s;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transform the value of a cell into a string
|
||||||
|
function stringify(v: any): string {
|
||||||
|
if (v === null || v === undefined) return nullString;
|
||||||
|
if (v instanceof Date) return v.toLocaleDateString();
|
||||||
|
if (typeof v === "boolean") return v ? "X" : "";
|
||||||
|
|
||||||
|
if (typeof v === "object") {
|
||||||
|
// CHeck if array and join with "|"
|
||||||
|
if (Array.isArray(v)) return v.map(item => (item === null || item === undefined ? nullString : String(item))).join("|");
|
||||||
|
|
||||||
|
// If all fails parse it via json
|
||||||
|
// Should also not happen in our use case
|
||||||
|
try { return JSON.stringify(v); } catch { return String(v); }
|
||||||
|
}
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objs = (input as RowObject[]) || [];
|
||||||
|
|
||||||
|
// Derive headers in the order keys are first encountered (fixed: no reduce)
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const derivedOrder: string[] = [];
|
||||||
|
for (const obj of objs) {
|
||||||
|
if (!obj || typeof obj !== "object") continue;
|
||||||
|
for (const k of Object.keys(obj)) {
|
||||||
|
if (!seen.has(k)) {
|
||||||
|
seen.add(k);
|
||||||
|
derivedOrder.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply headerOrder if provided: put listed columns first (in the order provided),
|
||||||
|
// then append the remaining derived headers in their derived order.
|
||||||
|
let finalHeaders = derivedOrder.slice();
|
||||||
|
if (Array.isArray(headerOrder) && headerOrder.length > 0) {
|
||||||
|
const headSet = new Set(headerOrder);
|
||||||
|
const first = headerOrder.filter(h => seen.has(h)); // keep only headers that actually exist
|
||||||
|
const rest = derivedOrder.filter(h => !headSet.has(h));
|
||||||
|
finalHeaders = first.concat(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsOut: string[] = [];
|
||||||
|
|
||||||
|
if (finalHeaders.length > 0) {
|
||||||
|
rowsOut.push(finalHeaders.map(h => escapeCell(h)).join(delimiter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse every row
|
||||||
|
for (const obj of objs) {
|
||||||
|
|
||||||
|
// Check if obj is null or somthing else we can't convert
|
||||||
|
if (!obj || typeof obj !== "object") {
|
||||||
|
|
||||||
|
// produce empty row with same number of columns
|
||||||
|
rowsOut.push(finalHeaders.map(() => escapeCell(null)).join(delimiter));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For every header check if a value on our row exist and print it
|
||||||
|
const row = finalHeaders.map(col => escapeCell(col in obj ? (obj as any)[col] : null)).join(delimiter);
|
||||||
|
rowsOut.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (includeBOM ? "\uFEFF" : "") + rowsOut.join(eol);
|
||||||
|
}
|
||||||
8
web/src/lib/downloadBlob.ts
Normal file
8
web/src/lib/downloadBlob.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function downloadBlob(filename: string, content: string, mimeType = "text/plain") {
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
}
|
||||||
53
web/src/lib/exporting.ts
Normal file
53
web/src/lib/exporting.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { generateCSVString } from "./csv";
|
||||||
|
import { dayToDate, fetchDay, fetchDays, type AttendanceDay, type Day } from "./Day";
|
||||||
|
import type { IDMap } from "./IDMapping";
|
||||||
|
|
||||||
|
interface CSVRow {
|
||||||
|
ID: string
|
||||||
|
Vorname: string
|
||||||
|
Nachname: string
|
||||||
|
[key: string]: string | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareRows(mapping: IDMap, days: AttendanceDay[]): CSVRow[] {
|
||||||
|
let csvData: CSVRow[] = [];
|
||||||
|
|
||||||
|
const allIDs = Object.keys(mapping);
|
||||||
|
|
||||||
|
for (const id of allIDs) {
|
||||||
|
const name = mapping[id];
|
||||||
|
const row: CSVRow = {
|
||||||
|
ID: id,
|
||||||
|
Vorname: name.first,
|
||||||
|
Nachname: name.last,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const day of days) {
|
||||||
|
const dayKey = dayToDate(day.date).toLocaleDateString();
|
||||||
|
row[dayKey] = day.ids.includes(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
csvData.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return csvData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDays(from: Date, to: Date): Promise<AttendanceDay[]> {
|
||||||
|
const recordedDays: Day[] = await fetchDays(from, to);
|
||||||
|
let days: AttendanceDay[] = [];
|
||||||
|
|
||||||
|
for (const day of recordedDays) {
|
||||||
|
days.push(await fetchDay(day))
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateCSVFile(from: Date, to: Date, mapping: IDMap): Promise<string> {
|
||||||
|
const days = await getDays(from, to);
|
||||||
|
const rows = prepareRows(mapping, days);
|
||||||
|
const csvString = generateCSVString(rows);
|
||||||
|
|
||||||
|
return csvString;
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8080",
|
target: "http://localhost:3000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user