Compare commits

..

16 Commits

Author SHA1 Message Date
7434165ab0 Pocketbase: Update workflow to v0.33.0
Some checks failed
Build Formula11 Docker Image / pocketbase-docker (push) Has been cancelled
Build Pocketbase Docker Image / pocketbase-docker (push) Successful in 10s
2025-11-23 12:57:26 +01:00
cae1457081 Pocketbase: Update schema (fix incorrect DNF points calculation with multiple DNFs)
Some checks failed
Build Formula11 Docker Image / pocketbase-docker (push) Has been cancelled
Build Pocketbase Docker Image / pocketbase-docker (push) Successful in 3m28s
2025-11-23 12:48:47 +01:00
0afc4c2e80 Pocketbase: Update image to v0.33.0 2025-11-23 12:48:30 +01:00
b58ed8e0f0 Env: Update browser-list-db 2025-11-23 12:48:13 +01:00
9ff333f90e Env: Update flake 2025-11-23 12:47:41 +01:00
d31c6b6735 Lib: Fix f1.com scraping after the keks changed their site
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 55s
2025-06-21 22:35:06 +02:00
cb48ee68e5 Data/Official: Add warning toast to official data fetch button 2025-06-21 22:34:42 +02:00
f3c75fc921 Lib: Update toast.ts to allow action toasts 2025-06-21 22:33:47 +02:00
eb0de24754 Statistics: Fix nullpointer issue 2025-06-07 22:46:56 +02:00
5adc05e1bb Statistics: Implement statistics page with team/driver standings + driver cumsum chart
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 31s
2025-06-07 22:44:41 +02:00
f049805124 Leaderboard: Use chart options generator function 2025-06-07 22:44:24 +02:00
e4be7c4830 Data/Official: Update scraped data "depends" name 2025-06-07 22:44:01 +02:00
f0950d3241 Lib: Add chart options generator function 2025-06-07 22:43:30 +02:00
454b77e778 Lib: Update fetcher + schema after database update 2025-06-07 22:43:21 +02:00
35c0003159 Data/Official: Disable scrape button for official data clientside
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 30s
2025-06-07 20:40:59 +02:00
5a6156adb7 Skeleton: Subscribe to scraped_startinggrids collection 2025-06-07 20:40:19 +02:00
21 changed files with 1072 additions and 445 deletions

2
.envrc
View File

@ -1,2 +0,0 @@
use flake
layout node

View File

@ -20,6 +20,6 @@ jobs:
username: ${{ secrets.CONTAINER_REGISTRY_USER }}
password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }}
- name: Build Pocketbase Docker Image
run: docker build --build-arg PB_VERSION=0.25.0 --file pocketbase.dockerfile --tag gitea.vps.chriphost.de/christoph/pocketbase:0.25.0 .
run: docker build --build-arg PB_VERSION=0.33.0 --file pocketbase.dockerfile --tag gitea.vps.chriphost.de/christoph/pocketbase:0.33.0 .
- name: Push Pocketbase Docker Image
run: docker push gitea.vps.chriphost.de/christoph/pocketbase:0.25.0
run: docker push gitea.vps.chriphost.de/christoph/pocketbase:0.33.0

120
flake.lock generated
View File

@ -1,8 +1,33 @@
{
"nodes": {
"clj-nix": {
"inputs": {
"devshell": "devshell",
"nix-fetcher-data": "nix-fetcher-data",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1763549559,
"narHash": "sha256-w7qhicMuDyfm9/dJKs5+47XqhZmGXRfkZjyn8XjO+c0=",
"owner": "jlesquembre",
"repo": "clj-nix",
"rev": "a55b9fbce3da4aa35c94221f76d40c79e6de4d81",
"type": "github"
},
"original": {
"owner": "jlesquembre",
"repo": "clj-nix",
"type": "github"
}
},
"devshell": {
"inputs": {
"nixpkgs": "nixpkgs"
"nixpkgs": [
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1741473158,
@ -18,6 +43,24 @@
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1719745305,
"narHash": "sha256-xwgjVUpqSviudEkpQnioeez1Uo2wzrsMaJKJClh+Bls=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c3c5ecc05edc7dafba779c6c1a61cd08ac6583e9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
@ -36,43 +79,80 @@
"type": "github"
}
},
"nixpkgs": {
"nix-fetcher-data": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": [
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1722073938,
"narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae",
"lastModified": 1728229178,
"narHash": "sha256-p5Fx880uBYstIsbaDYN7sECJT11oHxZQKtHgMAVblWA=",
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"rev": "f3a73c34d28db49ef90fd7872a142bfe93120e55",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"type": "github"
}
},
"nixpkgs_2": {
"nixpkgs": {
"locked": {
"lastModified": 1741865919,
"narHash": "sha256-4thdbnP6dlbdq+qZWTsm4ffAwoS8Tiq1YResB+RP6WE=",
"lastModified": 1763618868,
"narHash": "sha256-v5afmLjn/uyD9EQuPBn7nZuaZVV9r+JerayK/4wvdWA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "573c650e8a14b2faa0041645ab18aed7e60f0c9a",
"rev": "a8d610af3f1a5fb71e23e08434d8d61a466fc942",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1717284937,
"narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
}
},
"root": {
"inputs": {
"devshell": "devshell",
"clj-nix": "clj-nix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1763865987,
"narHash": "sha256-DJpzM8Jz3B0azJcAoF+YFHr8rEbxYLJ0wy1kWZ29HOw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "042d905c01a6eec3bcae8530dacb19cda9758a63",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {

385
flake.nix
View File

@ -1,113 +1,328 @@
{
description = "Svelte F1 Guessgame";
rec {
description = "Formula11";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.devshell.url = "github:numtide/devshell";
inputs = {
nixpkgs.url = "nixpkgs"; # Use nixpkgs from system registry
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
clj-nix.url = "github:jlesquembre/clj-nix";
clj-nix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = {
self,
nixpkgs,
flake-utils,
devshell,
rust-overlay,
clj-nix,
}:
# Create a shell (and possibly package) for each possible system, not only x86_64-linux
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [devshell.overlays.default];
};
timple = pkgs.python312Packages.buildPythonPackage rec {
pname = "timple";
version = "0.1.8";
src = pkgs.python312Packages.fetchPypi {
inherit pname version;
hash = "sha256-u8EgMA8BA6OpPlSg0ASRxLcIcv5psRIEcBpIicagXw8=";
};
doCheck = false;
pyproject = true;
# Build time deps
nativeBuildInputs = with pkgs.python312Packages; [
setuptools
];
# Run time deps
dependencies = with pkgs.python312Packages; [
matplotlib
numpy
overlays = [
rust-overlay.overlays.default
];
};
inherit (pkgs) lib stdenv;
fastf1 = pkgs.python312Packages.buildPythonPackage rec {
pname = "fastf1";
version = "3.4.4";
# ===========================================================================================
# Define custom dependencies
# ===========================================================================================
src = pkgs.python312Packages.fetchPypi {
inherit pname version;
hash = "sha256-nELQtvzlLsUYyVaPe1KqvMmzHy5l5W7u1I6m8r8md/4=";
};
# Python package example
# typed-ffmpeg = pkgs.python313Packages.buildPythonPackage rec {
# pname = "typed_ffmpeg";
# version = "3.6";
#
# src = pkgs.python313Packages.fetchPypi {
# inherit pname version;
# hash = "sha256-YPspq/lqI/jx/9FCQntmQPw4lrPIsdxtHTUg0F0QbrM=";
# };
#
# pyproject = true;
# build-system = [
# pkgs.python313Packages.setuptools
# pkgs.python313Packages.setuptools-scm
# ];
# };
doCheck = false;
pyproject = true;
# python = pkgs.python313.withPackages (p:
# with p; [
# # numpy
# # matplotlib
# # typed-ffmpeg
# # pyside6
# ]);
# Build time deps
nativeBuildInputs = with pkgs.python312Packages; [
hatchling
hatch-vcs
];
# rust = pkgs.rust-bin.stable.latest.default.override {
# extensions = ["rust-src"]; # Include the Rust stdlib source (for IntelliJ)
# };
# Run time deps
dependencies = with pkgs.python312Packages; [
matplotlib
numpy
pandas
python-dateutil
requests
requests-cache
scipy
rapidfuzz
websockets
timple
];
};
in {
devShell = pkgs.devshell.mkShell {
name = "Formula11";
# 64 bit C/C++ compilers that don't collide (use the same libc)
# bintools = pkgs.wrapBintoolsWith {
# bintools = pkgs.bintools.bintools; # Unwrapped bintools
# libc = pkgs.glibc;
# };
# gcc = pkgs.hiPrio (pkgs.wrapCCWith {
# cc = pkgs.gcc.cc; # Unwrapped gcc
# libc = pkgs.glibc;
# bintools = bintools;
# });
# clang = pkgs.wrapCCWith {
# cc = pkgs.clang.cc; # Unwrapped clang
# libc = pkgs.glibc;
# bintools = bintools;
# };
packages = with pkgs; [
nodejs_23
# Multilib C/C++ compilers that don't collide (use the same libc)
# bintools_multilib = pkgs.wrapBintoolsWith {
# bintools = pkgs.bintools.bintools; # Unwrapped bintools
# libc = pkgs.glibc_multi;
# };
# gcc_multilib = pkgs.hiPrio (pkgs.wrapCCWith {
# cc = pkgs.gcc.cc; # Unwrapped gcc
# libc = pkgs.glibc_multi;
# bintools = bintools_multilib;
# });
# clang_multilib = pkgs.wrapCCWith {
# cc = pkgs.clang.cc; # Unwrapped clang
# libc = pkgs.glibc_multi;
# bintools = bintools_multilib;
# };
# ===========================================================================================
# Specify dependencies
# https://nixos.org/manual/nixpkgs/stable/#ssec-stdenv-dependencies-overview
# Just for a "nix develop" shell, buildInputs can be used for everything.
# ===========================================================================================
# Add dependencies to nativeBuildInputs if they are executed during the build:
# - Those which are needed on $PATH during the build, for example cmake and pkg-config
# - Setup hooks, for example makeWrapper
# - Interpreters needed by patchShebangs for build scripts (with the --build flag), which can be the case for e.g. perl
nativeBuildInputs = with pkgs; [
nodejs_24
pocketbase
sqlite # For sqlite console
sqlitebrowser # To check low-level pocketbase data
# Languages:
# python
# rust
# bintools
# gcc
# clang
# bintools_multilib
# gcc_multilib
# clang_multilib
# clojure
# jdk
# C/C++:
# gdb
# valgrind
# gnumake
# cmake
# pkg-config
# Clojure:
# leiningen
# clj-nix.packages.${system}.deps-lock
# Java:
# gradle
# Python:
# hatch
# py-spy
# Qt:
# qt6.wrapQtAppsHook # For the shellHook
];
# Use $1 for positional args
commands = [
{
name = "pb";
help = "Serve PocketBase";
command = "pocketbase serve --http 192.168.86.50:8090 --dev";
}
{
name = "dev";
help = "Serve Formula 11 (Dev)";
command = "npm run dev -- --host --port 5173";
}
{
name = "prod";
help = "Serve Formula 11 (Prod)";
command = "npm run build && npm run preview -- --host --port 5173";
}
{
name = "check";
help = "Continuously monitor for SvelteKit issues";
command = "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch";
}
# Add dependencies to buildInputs if they will end up copied or linked into the final output or otherwise used at runtime:
# - Libraries used by compilers, for example zlib
# - Interpreters needed by patchShebangs for scripts which are installed, which can be the case for e.g. perl
buildInputs = with pkgs; [
# C/C++:
# boost
# sfml
# Qt:
# qt6.qtbase
# qt6.full
];
# ===========================================================================================
# Define buildable + installable packages
# ===========================================================================================
# package = stdenv.mkDerivation {
# inherit nativeBuildInputs buildInputs;
# pname = "";
# version = "1.0.0";
# src = ./.;
#
# installPhase = ''
# mkdir -p $out/bin
# mv ./BINARY $out/bin
# '';
# };
# package = clj-nix.lib.mkCljApp {
# inherit pkgs;
# modules = [
# # Option list: https://jlesquembre.github.io/clj-nix/options/
# {
# name = "";
# version = "1.0.0";
# main-ns = "";
# projectSrc = ./.;
# withLeiningen = true;
# buildCommand = "lein uberjar"; # Requires "withLeiningen = true;"
# jdk = pkgs.jdk; # Default is pkgs.jdk_headless
# # customJdk.enable = true;
# # nativeImage.enable = true;
# }
# ];
# };
in rec {
# Provide package for "nix build"
# defaultPackage = package;
# defaultApp = flake-utils.lib.mkApp {
# drv = defaultPackage;
# };
# Provide environment for "nix develop"
devShell = pkgs.mkShell {
inherit nativeBuildInputs buildInputs;
name = description;
# =========================================================================================
# Define environment variables
# =========================================================================================
# Rust stdlib source:
# RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
# Custom dynamic libraries:
# LD_LIBRARY_PATH = builtins.concatStringsSep ":" [
# # Rust Bevy GUI app:
# # "${pkgs.xorg.libX11}/lib"
# # "${pkgs.xorg.libXcursor}/lib"
# # "${pkgs.xorg.libXrandr}/lib"
# # "${pkgs.xorg.libXi}/lib"
# # "${pkgs.libGL}/lib"
#
# # JavaFX app:
# # "${pkgs.libGL}/lib"
# # "${pkgs.gtk3}/lib"
# # "${pkgs.glib.out}/lib"
# # "${pkgs.xorg.libXtst}/lib"
# ];
# Dynamic libraries from buildinputs:
# LD_LIBRARY_PATH = nixpkgs.lib.makeLibraryPath buildInputs;
# QT imports to use with "qmlls -E"
# QML_IMPORT_PATH = "${pkgs.qt6.full}/lib/qt-6/qml";
# Set PYTHONPATH
# PYTHONPATH = ".";
# Set matplotlib backend
# MPLBACKEND = "TkAgg";
# =========================================================================================
# Define shell environment
# =========================================================================================
# Setup the shell when entering the "nix develop" environment (bash script).
shellHook = let
mkCmakeScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-${typeLower}.fish" ''
cd $FLAKE_PROJECT_ROOT
echo "Removing build directory ./cmake-build-${typeLower}/"
rm -rf ./cmake-build-${typeLower}
echo "Creating build directory"
mkdir cmake-build-${typeLower}
cd cmake-build-${typeLower}
echo "Running cmake"
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE="${type}" -DCMAKE_EXPORT_COMPILE_COMMANDS="On" ..
echo "Linking compile_commands.json"
cd ..
ln -sf ./cmake-build-${typeLower}/compile_commands.json ./compile_commands.json
'';
cmakeDebug = mkCmakeScript "Debug";
cmakeRelease = mkCmakeScript "Release";
mkBuildScript = type: let
typeLower = lib.toLower type;
in
pkgs.writers.writeFish "cmake-build.fish" ''
cd $FLAKE_PROJECT_ROOT/cmake-build-${typeLower}
echo "Running cmake"
cmake --build .
'';
buildDebug = mkBuildScript "Debug";
buildRelease = mkBuildScript "Release";
# Use this to specify commands that should be ran after entering fish shell
initProjectShell = pkgs.writers.writeFish "init-shell.fish" ''
echo "Entering \"${description}\" environment..."
# Determine the project root, used e.g. in cmake scripts
set -g -x FLAKE_PROJECT_ROOT (git rev-parse --show-toplevel)
# Rust Bevy:
# abbr -a build-release-windows "CARGO_FEATURE_PURE=1 cargo xwin build --release --target x86_64-pc-windows-msvc"
# C/C++:
# abbr -a cmake-debug "${cmakeDebug}"
# abbr -a cmake-release "${cmakeRelease}"
# abbr -a build-debug "${buildDebug}"
# abbr -a build-release "${buildRelease}"
# Clojure:
# abbr -a clojure-deps "deps-lock --lein"
# Python:
# abbr -a run "python ./app/main.py"
# abbr -a profile "py-spy record -o profile.svg -- python ./app/main.py && firefox profile.svg"
# abbr -a ptop "py-spy top -- python ./app/main.py"
# Formula11:
abbr -a pb "pocketbase serve --http 192.168.86.50:8090 --dev"
abbr -a dev "npm run dev -- --host --port 5173"
abbr -a prod "npm run build && npm run preview -- --host --port 5173"
abbr -a check "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
'';
in
builtins.concatStringsSep "\n" [
# Launch into pure fish shell
''
exec "$(type -p fish)" -C "source ${initProjectShell} && abbr -a menu '${pkgs.bat}/bin/bat "${initProjectShell}"'"
''
# Qt: Launch into wrapped fish shell
# https://nixos.org/manual/nixpkgs/stable/#sec-language-qt
# ''
# fishdir=$(mktemp -d)
# makeWrapper "$(type -p fish)" "$fishdir/fish" "''${qtWrapperArgs[@]}"
# exec "$fishdir/fish" -C "source ${initProjectShell} && abbr -a menu '${pkgs.bat}/bin/bat "${initProjectShell}"'"
# ''
];
};
});

20
package-lock.json generated
View File

@ -1576,6 +1576,7 @@
"integrity": "sha512-1u8FGWtT+V0eBSQpDtsjINhQd6RTjadvEgVApdNVxbUeOXmQTDENTeRiZw2i1lNUx1/5Wa65CHzxtMh3P3yviw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^0.6.0",
@ -1614,6 +1615,7 @@
"integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
"debug": "^4.4.0",
@ -1972,6 +1974,7 @@
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.20.0"
}
@ -2116,6 +2119,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -2318,6 +2322,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@ -2342,9 +2347,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001704",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz",
"integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==",
"version": "1.0.30001756",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz",
"integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==",
"dev": true,
"funding": [
{
@ -2982,6 +2987,7 @@
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"dev": true,
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@ -4114,6 +4120,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@ -4250,6 +4257,7 @@
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@ -4266,6 +4274,7 @@
"integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"prettier": "^3.0.0",
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
@ -4440,6 +4449,7 @@
"integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.6"
},
@ -4821,6 +4831,7 @@
"integrity": "sha512-v0lL3NuKontiCxholEiAXCB+BYbndlKbwlDMK0DS86WgGELMJSpyqCSbJeMEMBDwOglnS7Ar2Rq0wwa/z2L8Vg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
@ -4888,6 +4899,7 @@
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@ -5099,6 +5111,7 @@
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -5182,6 +5195,7 @@
"integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"postcss": "^8.5.3",

View File

@ -1,176 +1,4 @@
[
{
"id": "pbc_3142635823",
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"name": "_superusers",
"type": "auth",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cost": 0,
"hidden": true,
"id": "password901924565",
"max": 0,
"min": 8,
"name": "password",
"pattern": "",
"presentable": false,
"required": true,
"system": true,
"type": "password"
},
{
"autogeneratePattern": "[a-zA-Z0-9]{50}",
"hidden": true,
"id": "text2504183744",
"max": 60,
"min": 30,
"name": "tokenKey",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": true,
"type": "text"
},
{
"exceptDomains": null,
"hidden": false,
"id": "email3885137012",
"name": "email",
"onlyDomains": null,
"presentable": false,
"required": true,
"system": true,
"type": "email"
},
{
"hidden": false,
"id": "bool1547992806",
"name": "emailVisibility",
"presentable": false,
"required": false,
"system": true,
"type": "bool"
},
{
"hidden": false,
"id": "bool256245529",
"name": "verified",
"presentable": false,
"required": false,
"system": true,
"type": "bool"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": true,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": true,
"type": "autodate"
}
],
"indexes": [
"CREATE UNIQUE INDEX `idx_tokenKey_pbc_3142635823` ON `_superusers` (`tokenKey`)",
"CREATE UNIQUE INDEX `idx_email_pbc_3142635823` ON `_superusers` (`email`) WHERE `email` != ''"
],
"system": true,
"authRule": "",
"manageRule": null,
"authAlert": {
"enabled": true,
"emailTemplate": {
"subject": "Login from a new location",
"body": "<p>Hello,</p>\n<p>We noticed a login to your {APP_NAME} account from a new location.</p>\n<p>If this was you, you may disregard this email.</p>\n<p><strong>If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.</strong></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
}
},
"oauth2": {
"mappedFields": {
"id": "",
"name": "",
"username": "",
"avatarURL": ""
},
"enabled": false
},
"passwordAuth": {
"enabled": true,
"identityFields": [
"email"
]
},
"mfa": {
"enabled": false,
"duration": 1800,
"rule": ""
},
"otp": {
"enabled": false,
"duration": 180,
"length": 8,
"emailTemplate": {
"subject": "OTP for {APP_NAME}",
"body": "<p>Hello,</p>\n<p>Your one-time password is: <strong>{OTP}</strong></p>\n<p><i>If you didn't ask for the one-time password, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
}
},
"authToken": {
"duration": 86400
},
"passwordResetToken": {
"duration": 1800
},
"emailChangeToken": {
"duration": 1800
},
"verificationToken": {
"duration": 259200
},
"fileToken": {
"duration": 180
},
"verificationTemplate": {
"subject": "Verify your {APP_NAME} email",
"body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Click on the button below to verify your email address.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Verify</a>\n</p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
},
"resetPasswordTemplate": {
"subject": "Reset your {APP_NAME} password",
"body": "<p>Hello,</p>\n<p>Click on the button below to reset your password.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Reset password</a>\n</p>\n<p><i>If you didn't ask to reset your password, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
},
"confirmEmailChangeTemplate": {
"subject": "Confirm your {APP_NAME} new email address",
"body": "<p>Hello,</p>\n<p>Click on the button below to confirm your new email address.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Confirm new email</a>\n</p>\n<p><i>If you didn't ask to change your email address, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
}
},
{
"id": "pbc_1736455494",
"listRule": "",
@ -337,7 +165,7 @@
"enabled": false,
"emailTemplate": {
"subject": "Login from a new location",
"body": "<p>Hello,</p>\n<p>We noticed a login to your {APP_NAME} account from a new location.</p>\n<p>If this was you, you may disregard this email.</p>\n<p><strong>If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.</strong></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
"body": "<p>Hello,</p>\n<p>We noticed a login to your {APP_NAME} account from a new location:</p>\n<p><em>{ALERT_INFO}</em></p>\n<p><strong>If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.</strong></p>\n<p>If this was you, you may disregard this email.</p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>"
}
},
"oauth2": {
@ -1553,6 +1381,109 @@
],
"system": false
},
{
"id": "pbc_2435909571",
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",
"name": "scraped_startinggrids",
"type": "base",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"hidden": false,
"id": "number516222579",
"max": 24,
"min": 1,
"name": "race_step",
"onlyInt": true,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2428491277",
"max": 3,
"min": 3,
"name": "driver_code",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "number1177347317",
"max": 20,
"min": 1,
"name": "position",
"onlyInt": true,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1872009285",
"max": 0,
"min": 0,
"name": "time",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": true,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": true,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"indexes": [
"CREATE UNIQUE INDEX `idx_QPaZOui4P1` ON `scraped_startinggrids` (\n `race_step`,\n `driver_code`\n)",
"CREATE UNIQUE INDEX `idx_1bfQwZkKBX` ON `scraped_startinggrids` (\n `race_step`,\n `position`\n)"
],
"system": false
},
{
"id": "pbc_286850423",
"listRule": "",
@ -2033,7 +1964,7 @@
{
"autogeneratePattern": "",
"hidden": false,
"id": "_clone_fPMW",
"id": "_clone_8Abk",
"max": 10,
"min": 3,
"name": "username",
@ -2047,7 +1978,7 @@
{
"autogeneratePattern": "",
"hidden": false,
"id": "_clone_Wusg",
"id": "_clone_s6Vb",
"max": 0,
"min": 0,
"name": "firstname",
@ -2060,7 +1991,7 @@
},
{
"hidden": false,
"id": "_clone_NeO9",
"id": "_clone_3shc",
"maxSelect": 1,
"maxSize": 0,
"mimeTypes": [
@ -2076,7 +2007,7 @@
},
{
"hidden": false,
"id": "_clone_hY91",
"id": "_clone_Phc6",
"name": "admin",
"presentable": false,
"required": false,
@ -2125,7 +2056,7 @@
{
"autogeneratePattern": "",
"hidden": false,
"id": "_clone_DK1r",
"id": "_clone_Lqek",
"max": 0,
"min": 0,
"name": "name",
@ -2138,7 +2069,7 @@
},
{
"hidden": false,
"id": "_clone_vhUL",
"id": "_clone_74gl",
"max": null,
"min": null,
"name": "step",
@ -2150,7 +2081,7 @@
},
{
"hidden": false,
"id": "_clone_3jG4",
"id": "_clone_XSiN",
"maxSelect": 1,
"maxSize": 0,
"mimeTypes": [
@ -2166,7 +2097,7 @@
},
{
"hidden": false,
"id": "_clone_kazm",
"id": "_clone_Dt5c",
"max": null,
"min": null,
"name": "pxx",
@ -2178,7 +2109,7 @@
},
{
"hidden": false,
"id": "_clone_KPr7",
"id": "_clone_DjBN",
"max": "",
"min": "",
"name": "sprintqualidate",
@ -2189,7 +2120,7 @@
},
{
"hidden": false,
"id": "_clone_a1zU",
"id": "_clone_9JoH",
"max": "",
"min": "",
"name": "sprintdate",
@ -2200,7 +2131,7 @@
},
{
"hidden": false,
"id": "_clone_TUqn",
"id": "_clone_3g96",
"max": "",
"min": "",
"name": "qualidate",
@ -2211,7 +2142,7 @@
},
{
"hidden": false,
"id": "_clone_YboV",
"id": "_clone_ikdE",
"max": "",
"min": "",
"name": "racedate",
@ -2253,7 +2184,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1736455494",
"hidden": false,
"id": "_clone_pIDk",
"id": "_clone_m714",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
@ -2265,7 +2196,7 @@
{
"autogeneratePattern": "",
"hidden": false,
"id": "_clone_ysK2",
"id": "_clone_kDMs",
"max": 0,
"min": 0,
"name": "hottake",
@ -2309,7 +2240,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1736455494",
"hidden": false,
"id": "_clone_4RjP",
"id": "_clone_jTU8",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
@ -2320,7 +2251,7 @@
},
{
"hidden": false,
"id": "_clone_OwkZ",
"id": "_clone_VwLb",
"max": null,
"min": null,
"name": "step",
@ -2353,10 +2284,10 @@
],
"indexes": [],
"system": false,
"viewQuery": "SELECT\n rp.id,\n rp.user,\n -- rp.race,\n r.step,\n \n -- rp.pxx,\n -- rr.pxxs,\n (CASE\n -- Correct pxx pick\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[3]') THEN 10\n \n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[2]') THEN 6\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[4]') THEN 6\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[1]') THEN 3\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[5]') THEN 3\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[0]') THEN 1\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[6]') THEN 1\n \n ELSE 0 \n END) AS pxx_points,\n \n -- rp.dnf,\n -- rr.dnfs,\n (CASE\n -- Correct dnf pick with a single DNF\n WHEN rr.dnfs LIKE '[\"' || rp.dnf || '\"]' THEN 10\n \n -- Correct dnf pick with multiple DNFs\n WHEN rr.dnfs LIKE '[%,\"' || rp.dnf || '\",%]' THEN 10\n \n ELSE 0 \n END) AS dnf_points\nFROM racepicks rp\nJOIN races r ON rp.race = r.id\nJOIN raceresults rr ON rp.race = rr.race\nWHERE pxx_points > 0 OR dnf_points > 0\nORDER BY r.step ASC;"
"viewQuery": "SELECT\n rp.id,\n rp.user,\n -- rp.race,\n r.step,\n \n -- rp.pxx,\n -- rr.pxxs,\n (CASE\n -- Correct pxx pick\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[3]') THEN 10\n \n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[2]') THEN 6\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[4]') THEN 6\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[1]') THEN 3\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[5]') THEN 3\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[0]') THEN 1\n WHEN rp.pxx = JSON_EXTRACT(rr.pxxs, '$[6]') THEN 1\n \n ELSE 0 \n END) AS pxx_points,\n \n -- rp.dnf,\n -- rr.dnfs,\n (CASE\n -- Correct dnf pick with a single DNF\n -- WHEN rr.dnfs LIKE '[\"' || rp.dnf || '\"]' THEN 10\n \n -- Correct dnf pick with single or multiple DNFs\n WHEN rr.dnfs LIKE '[%\"' || rp.dnf || '\"%]' THEN 10\n \n ELSE 0 \n END) AS dnf_points\nFROM racepicks rp\nJOIN races r ON rp.race = r.id\nJOIN raceresults rr ON rp.race = rr.race\n-- WHERE pxx_points > 0 OR dnf_points > 0\nORDER BY r.step ASC;"
},
{
"id": "pbc_3585110310",
"id": "pbc_4223880558",
"listRule": "",
"viewRule": "",
"createRule": null,
@ -2383,7 +2314,91 @@
"cascadeDelete": false,
"collectionId": "pbc_1736455494",
"hidden": false,
"id": "_clone_oTTu",
"id": "_clone_fDe7",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "_clone_dFLq",
"max": null,
"min": null,
"name": "step",
"onlyInt": true,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "json1186524854",
"maxSize": 1,
"name": "acc_pxx_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json3726038982",
"maxSize": 1,
"name": "acc_dnf_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2780867541",
"maxSize": 1,
"name": "acc_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"indexes": [],
"system": false,
"viewQuery": "SELECT\n p.id,\n p.user,\n p.step,\n (SUM(p.pxx_points) OVER(PARTITION BY p.user ORDER BY p.step ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) AS acc_pxx_points,\n (SUM(p.dnf_points) OVER(PARTITION BY p.user ORDER BY p.step ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) AS acc_dnf_points,\n (SUM(p.pxx_points + p.dnf_points) OVER(PARTITION BY p.user ORDER BY p.step ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) AS acc_points\nFROM racepickpoints p\nORDER BY step ASC;"
},
{
"id": "pbc_3585110310",
"listRule": "",
"viewRule": "",
"createRule": null,
"updateRule": null,
"deleteRule": null,
"name": "racepickpointstotal",
"type": "view",
"fields": [
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3208210256",
"max": 0,
"min": 0,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1736455494",
"hidden": false,
"id": "_clone_M37B",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
@ -2421,11 +2436,21 @@
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json1247457462",
"maxSize": 1,
"name": "total_points_per_pick",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"indexes": [],
"system": false,
"viewQuery": "SELECT\n p.id,\n p.user,\n SUM(p.pxx_points) AS total_pxx_points,\n SUM(p.dnf_points) AS total_dnf_points,\n SUM(p.pxx_points + p.dnf_points) AS total_points\nFROM racepickpoints p\nGROUP BY user\nORDER BY total_points DESC;"
"viewQuery": "SELECT\n p.id,\n p.user,\n SUM(p.pxx_points) AS total_pxx_points,\n SUM(p.dnf_points) AS total_dnf_points,\n SUM(p.pxx_points + p.dnf_points) AS total_points,\n (CAST(SUM(p.pxx_points + p.dnf_points) AS REAL) / COUNT(*)) AS total_points_per_pick\nFROM racepickpoints p\nGROUP BY user\nORDER BY total_points DESC;"
},
{
"id": "pbc_2823555101",
@ -2455,7 +2480,7 @@
"cascadeDelete": false,
"collectionId": "pbc_2533072930",
"hidden": false,
"id": "_clone_XoxW",
"id": "_clone_Lw6s",
"maxSelect": 1,
"minSelect": 0,
"name": "race",
@ -2468,7 +2493,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "_clone_lN2E",
"id": "_clone_dlrx",
"maxSelect": 7,
"minSelect": 0,
"name": "pxxs",
@ -2481,7 +2506,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "_clone_aCgW",
"id": "_clone_YWtq",
"maxSelect": 20,
"minSelect": 0,
"name": "dnfs",
@ -2495,6 +2520,71 @@
"system": false,
"viewQuery": "-- This query simply returns the raceresults sorted by their race steps (descending).\nSELECT result.id, result.race, result.pxxs, result.dnfs\nFROM raceresults result\nJOIN races race ON result.race = race.id\nORDER BY race.step DESC;"
},
{
"id": "pbc_3189024959",
"listRule": "",
"viewRule": "",
"createRule": null,
"updateRule": null,
"deleteRule": null,
"name": "scraped_raceresultsacc",
"type": "view",
"fields": [
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3208210256",
"max": 0,
"min": 0,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"hidden": false,
"id": "_clone_YEQY",
"max": 24,
"min": 1,
"name": "race_step",
"onlyInt": true,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "_clone_lePz",
"max": 3,
"min": 3,
"name": "driver_code",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "json2780867541",
"maxSize": 1,
"name": "acc_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"indexes": [],
"system": false,
"viewQuery": "SELECT\n r.id,\n r.race_step,\n r.driver_code,\n (SUM(r.points) OVER(PARTITION BY r.driver_code ORDER BY r.race_step ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) AS acc_points\nFROM scraped_raceresults r\nORDER BY race_step ASC;"
},
{
"id": "pbc_1743016826",
"listRule": "",
@ -2522,7 +2612,7 @@
{
"autogeneratePattern": "",
"hidden": false,
"id": "_clone_ASwf",
"id": "_clone_u2Xp",
"max": 10,
"min": 3,
"name": "username",
@ -2536,7 +2626,7 @@
{
"autogeneratePattern": "",
"hidden": false,
"id": "_clone_feSn",
"id": "_clone_IGYL",
"max": 0,
"min": 0,
"name": "firstname",
@ -2549,7 +2639,7 @@
},
{
"hidden": false,
"id": "_clone_AuI7",
"id": "_clone_sEZH",
"maxSelect": 1,
"maxSize": 0,
"mimeTypes": [
@ -2565,7 +2655,7 @@
},
{
"hidden": false,
"id": "_clone_kxwg",
"id": "_clone_Aev1",
"name": "admin",
"presentable": false,
"required": false,
@ -2615,7 +2705,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1736455494",
"hidden": false,
"id": "_clone_gNum",
"id": "_clone_a4Gn",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
@ -2628,7 +2718,7 @@
"cascadeDelete": false,
"collectionId": "pbc_2533072930",
"hidden": false,
"id": "_clone_JngA",
"id": "_clone_scFa",
"maxSelect": 1,
"minSelect": 0,
"name": "race",
@ -2641,7 +2731,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "_clone_lSLH",
"id": "_clone_Z5l5",
"maxSelect": 1,
"minSelect": 0,
"name": "pxx",
@ -2654,7 +2744,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "_clone_fr0S",
"id": "_clone_0ry6",
"maxSelect": 1,
"minSelect": 0,
"name": "dnf",
@ -2696,7 +2786,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1736455494",
"hidden": false,
"id": "_clone_G7K2",
"id": "_clone_Z44W",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
@ -2708,7 +2798,7 @@
{
"autogeneratePattern": "",
"hidden": false,
"id": "_clone_oLff",
"id": "_clone_ivB8",
"max": 0,
"min": 0,
"name": "hottake",
@ -2723,7 +2813,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "_clone_1le8",
"id": "_clone_RgzD",
"maxSelect": 1,
"minSelect": 0,
"name": "wdcwinner",
@ -2736,7 +2826,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1568971955",
"hidden": false,
"id": "_clone_HQds",
"id": "_clone_nnZX",
"maxSelect": 1,
"minSelect": 0,
"name": "wccwinner",
@ -2749,7 +2839,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "_clone_k21B",
"id": "_clone_wNY4",
"maxSelect": 1,
"minSelect": 0,
"name": "mostovertakes",
@ -2762,7 +2852,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "_clone_pgZL",
"id": "_clone_okPh",
"maxSelect": 1,
"minSelect": 0,
"name": "mostdnfs",
@ -2773,7 +2863,7 @@
},
{
"hidden": false,
"id": "_clone_u3OO",
"id": "_clone_9rOH",
"max": null,
"min": null,
"name": "doohanstarts",
@ -2787,7 +2877,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "_clone_8Z5h",
"id": "_clone_WZjB",
"maxSelect": 10,
"minSelect": 10,
"name": "teamwinners",
@ -2800,7 +2890,7 @@
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "_clone_ahZg",
"id": "_clone_870o",
"maxSelect": 20,
"minSelect": 3,
"name": "podiums",

View File

@ -2,7 +2,7 @@
# because fucking Docker thinks "toomanyrequests". Fuckers
FROM docker.io/library/alpine:3.21.2
ARG PB_VERSION=0.25.0
ARG PB_VERSION=0.33.0
RUN apk add --no-cache \
unzip \

60
src/lib/chart.ts Normal file
View File

@ -0,0 +1,60 @@
import { type LineChartOptions, ScaleTypes } from "@carbon/charts-svelte";
export const make_chart_options = (
title: string,
bottom: string,
left: string,
group: string = "group",
width: string = "100%",
height: string = "400px",
): LineChartOptions => {
return {
title: title,
axes: {
bottom: {
mapsTo: bottom,
scaleType: ScaleTypes.LABELS,
},
left: {
mapsTo: left,
scaleType: ScaleTypes.LINEAR,
},
},
data: {
groupMapsTo: group,
},
curve: "curveMonotoneX",
// toolbar: {
// enabled: false,
// },
animations: true,
// canvasZoom: {
// enabled: false,
// },
grid: {
x: {
enabled: true,
alignWithAxisTicks: true,
},
y: {
enabled: true,
alignWithAxisTicks: true,
},
},
legend: {
enabled: true,
clickable: true,
position: "top",
},
points: {
enabled: true,
radius: 5,
},
tooltip: {
showTotal: false,
},
resizable: true,
width: width,
height: height,
};
};

View File

@ -13,6 +13,7 @@ import type {
RaceResult,
ScrapedDriverStanding,
ScrapedRaceResult,
ScrapedRaceResultAcc,
ScrapedStartingGrid,
ScrapedTeamStanding,
SeasonPick,
@ -359,3 +360,16 @@ export const fetch_scraped_raceresults = async (
return scraped_raceresults;
};
/**
* Fetch all [ScrapedRaceResultsAcc] from the database, ordered ascendingly by race step.
*/
export const fetch_scraped_raceresultsacc = async (
fetch: (_: any) => Promise<Response>,
): Promise<ScrapedRaceResultAcc[]> => {
const scraped_raceresultsacc: ScrapedRaceResultAcc[] = await pb
.collection("scraped_raceresultsacc")
.getFullList({ fetch: fetch, sort: "+race_step" });
return scraped_raceresultsacc;
};

View File

@ -176,6 +176,13 @@ export interface ScrapedRaceResult {
points: number;
}
export interface ScrapedRaceResultAcc {
id: string;
race_step: number; // This maps to races
driver_code: string; // This maps to drivers
acc_points: number;
}
export interface ScrapedDriverStanding {
id: string;
driver_code: string; // This maps to drivers

View File

@ -21,12 +21,16 @@ export const scrape_race_links = async (): Promise<string[]> => {
const $ = cheerio.load(races_text);
const race_links: string[] = [];
$("tbody > tr > td:first-child > p > a[href]", "div.f1-inner-wrapper table.f1-table").each(
(_, element) => {
race_links.push(element.attribs["href"]);
},
);
$("tbody > tr > td:first-child > p > a[href]", "table.f1-table").each((_, element) => {
const href: string = element.attribs["href"];
// Keks changed the link format, cut off the start
const substring: string = href.replace("/../../en/results/2025/", "");
race_links.push(substring);
});
console.log(`Found ${race_links.length} races...`);
console.log(race_links);
return race_links;
};
@ -53,13 +57,15 @@ export const scrape_starting_grids = async (
const $ = cheerio.load(starting_grids_text);
// Obtain the positions for this starting grid for each driver
$("tbody > tr", "div.f1-inner-wrapper table.f1-table").each((driver_index, element) => {
$("tbody > tr", "table.f1-table").each((driver_index, element) => {
const $$ = cheerio.load(element);
let result: ScrapedStartingGrid = {
id: "",
race_step: index + 1,
driver_code: $$("td:nth-child(3) > p > span:last-child").text(),
driver_code: $$(
"td:nth-child(3) > p > span:first-child > span:last-child > span:last-child",
).text(),
position: driver_index + 1, // parseInt($$("td:nth-child(1) > p").text()),
time: $$("td:nth-child(5) > p").text(),
};
@ -69,6 +75,7 @@ export const scrape_starting_grids = async (
}),
);
console.log(`Scraped ${starting_grids.length} starting grids...`);
// console.log(starting_grids);
return starting_grids;
};
@ -88,13 +95,15 @@ export const scrape_race_results = async (race_links: string[]): Promise<Scraped
const $ = cheerio.load(race_text);
// Obtain the results for this race for each driver
$("tbody > tr", "div.f1-inner-wrapper table.f1-table").each((driver_index, element) => {
$("tbody > tr", "table.f1-table").each((driver_index, element) => {
const $$ = cheerio.load(element);
let result: ScrapedRaceResult = {
id: "",
race_step: index + 1,
driver_code: $$("td:nth-child(3) > p > span:last-child").text(),
driver_code: $$(
"td:nth-child(3) > p > span:first-child > span:last-child > span:last-child",
).text(),
position: driver_index + 1, // parseInt($$("td:nth-child(1) > p").text()),
status: $$("td:nth-child(6) > p").text(),
points: parseInt($$("td:nth-child(7) > p").text()),
@ -110,6 +119,7 @@ export const scrape_race_results = async (race_links: string[]): Promise<Scraped
}),
);
console.log(`Scraped ${race_results.length} race results...`);
// console.log(race_results);
return race_results;
};
@ -124,12 +134,12 @@ export const scrape_driver_standings = async (): Promise<ScrapedDriverStanding[]
const $ = cheerio.load(standings_text);
const driver_standings: ScrapedDriverStanding[] = [];
$("tbody > tr", "div.f1-inner-wrapper table.f1-table").each((driver_index, element) => {
$("tbody > tr", "table.f1-table").each((driver_index, element) => {
const $$ = cheerio.load(element);
let standing: ScrapedDriverStanding = {
id: "",
driver_code: $$("td:nth-child(2) > p > a > span:last-child").text(),
driver_code: $$("td:nth-child(2) > p > a > span:last-child > span:last-child").text(),
position: driver_index + 1,
points: parseInt($$("td:nth-child(5) > p").text()),
};
@ -137,6 +147,7 @@ export const scrape_driver_standings = async (): Promise<ScrapedDriverStanding[]
driver_standings.push(standing);
});
console.log(`Scraped ${driver_standings.length} driver standings...`);
// console.log(driver_standings);
return driver_standings;
};
@ -151,7 +162,7 @@ export const scrape_team_standings = async (): Promise<ScrapedTeamStanding[]> =>
const $ = cheerio.load(standings_text);
const team_standings: ScrapedTeamStanding[] = [];
$("tbody > tr", "div.f1-inner-wrapper table.f1-table").each((team_index, element) => {
$("tbody > tr", "table.f1-table").each((team_index, element) => {
const $$ = cheerio.load(element);
let standing: ScrapedTeamStanding = {
@ -164,6 +175,7 @@ export const scrape_team_standings = async (): Promise<ScrapedTeamStanding[]> =>
team_standings.push(standing);
});
console.log(`Scraped ${team_standings.length} team standings...`);
// console.log(team_standings);
return team_standings;
};

View File

@ -1,28 +1,63 @@
import type { ToastSettings } from "@skeletonlabs/skeleton";
export const get_info_toast = (message: string, timeout: number = 2000): ToastSettings => {
return {
message,
hideDismiss: true,
timeout,
background: "variant-filled-tertiary",
};
};
export const get_info_toast = (
message: string,
timeout: number | null = 2000,
action_label: string | undefined = undefined,
action_response: (() => void) | undefined = undefined,
): ToastSettings =>
get_toast(message, "variant-filled-tertiary", timeout, action_label, action_response);
export const get_warning_toast = (message: string, timeout: number = 2000): ToastSettings => {
return {
message,
hideDismiss: true,
timeout,
background: "variant-filled-secondary",
};
};
export const get_warning_toast = (
message: string,
timeout: number | null = 2000,
action_label: string | undefined = undefined,
action_response: (() => void) | undefined = undefined,
): ToastSettings =>
get_toast(message, "variant-filled-secondary", timeout, action_label, action_response);
export const get_error_toast = (message: string, timeout: number = 2000): ToastSettings => {
export const get_error_toast = (
message: string,
timeout: number | null = 2000,
action_label: string | undefined = undefined,
action_response: (() => void) | undefined = undefined,
): ToastSettings =>
get_toast(message, "variant-filled-primary", timeout, action_label, action_response);
/**
* Utility function to create [ToastSettings].
* If [timeout] is defined, the toast will disappear automatically and the dismiss-button will be hidden.
* If [timeout] is undefined, the toast will stay until dismissed from the dismiss-button.
* If [action_label] and [action_response] are defined, a callback function will be executed after accepting.
* In this case, [timeout] behaves as if undefined.
*/
const get_toast = (
message: string,
background: string,
timeout: number | null = 2000,
action_label: string | undefined = undefined,
action_response: (() => void) | undefined = undefined,
): ToastSettings => {
return {
message,
hideDismiss: true,
timeout,
background: "variant-filled-primary",
background,
// If we have a timeout and no action, dismiss is hidden
hideDismiss: !!timeout && (!action_label || !action_response),
// If we have a timeout and no action, the timeout is used
timeout: !!timeout && (!action_label || !action_response) ? timeout : undefined,
// If we have a timeout and no action, autohide is true
autohide: !!timeout && (!action_label || !action_response),
// If we have a label and a response, use the action
action:
!!action_label && !!action_response
? {
label: action_label,
response: action_response,
}
: undefined,
};
};

View File

@ -329,6 +329,7 @@
"seasonpicks",
"substitutions",
"teams",
"scraped_startinggrids",
"scraped_raceresults",
"scraped_driverstandings",
"scraped_teamstandings",
@ -347,6 +348,7 @@
"seasonpicks",
"substitutions",
"teams",
"scraped_startinggrids",
"scraped_raceresults",
"scraped_driverstandings",
"scraped_teamstandings",

View File

@ -1,12 +1,32 @@
<script lang="ts">
import { invalidate } from "$app/navigation";
import { Button } from "$lib/components";
import { pbUser } from "$lib/pocketbase";
import { get_error_toast, get_warning_toast } from "$lib/toast";
import { getToastStore, type ToastStore } from "@skeletonlabs/skeleton";
import type { Snippet } from "svelte";
let { children }: { children: Snippet } = $props();
const scrape_official_data = async () => {
// TODO: Success/error toast
const toastStore: ToastStore = getToastStore();
const scrape_message: string =
"This will clear and redownload all data from f1.com. Please don't refresh the page during the process.";
// This callback will be executed once the admin presses the "Proceed"-button in the warning toast
const scrape_callback = async () => {
const response: Response = await fetch("/api/scrape", { method: "POST" });
invalidate("data:official");
};
const scrape_official_data = async () => {
if (!$pbUser || !$pbUser.admin) {
toastStore.trigger(get_error_toast("Only admins may perform this action!"));
return;
}
// No timeout + action toast
toastStore.trigger(get_warning_toast(scrape_message, null, "Proceed", scrape_callback));
};
</script>
@ -24,7 +44,13 @@
<!-- Each child's contents will be inserted here -->
<div style="margin-top: 56px;">
<div class="pb-2">
<Button width="w-full" color="tertiary" onclick={scrape_official_data} shadow>
<Button
width="w-full"
color="tertiary"
onclick={scrape_official_data}
shadow
disabled={!$pbUser?.admin}
>
<span class="font-bold">Refresh All Data</span>
</Button>
</div>

View File

@ -2,7 +2,7 @@ import { fetch_drivers, fetch_scraped_driverstandings } from "$lib/fetch";
import type { PageLoad } from "../../../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:scraped_driverstandings", "data:drivers");
depends("data:official", "data:drivers");
return {
scraped_driverstandings: fetch_scraped_driverstandings(fetch),

View File

@ -2,7 +2,7 @@ import { fetch_drivers, fetch_races, fetch_scraped_raceresults } from "$lib/fetc
import type { PageLoad } from "../../../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:scraped_raceresults", "data:races", "data:drivers");
depends("data:official", "data:races", "data:drivers");
return {
scraped_raceresults: fetch_scraped_raceresults(fetch),

View File

@ -2,7 +2,7 @@ import { fetch_drivers, fetch_races, fetch_scraped_startinggrids } from "$lib/fe
import type { PageLoad } from "../../../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:scraped_startinggrids", "data:races", "data:drivers");
depends("data:official", "data:races", "data:drivers");
return {
scraped_startinggrids: fetch_scraped_startinggrids(fetch),

View File

@ -2,7 +2,7 @@ import { fetch_scraped_teamstandings, fetch_teams } from "$lib/fetch";
import type { PageLoad } from "../../../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:scraped_teamstandings", "data:teams");
depends("data:official", "data:teams");
return {
scraped_teamstandings: fetch_scraped_teamstandings(fetch),

View File

@ -1,4 +1,5 @@
<script lang="ts">
import { make_chart_options } from "$lib/chart";
import { Table, type TableColumn } from "$lib/components";
import { get_by_value } from "$lib/database";
import type { RacePickPoints, RacePickPointsAcc, User } from "$lib/schema";
@ -28,11 +29,13 @@
data_value_name: "user",
label: "User",
valuefun: async (value: string): Promise<string> =>
get_by_value(await data.users, "id", value)?.firstname ?? "Invalid",
`<span class='badge variant-filled-surface'>${get_by_value(await data.users, "id", value)?.firstname ?? "Invalid"}</span>`,
},
{
data_value_name: "total_points",
label: "Total",
valuefun: async (value: string): Promise<string> =>
`<span class='badge variant-filled-surface'>${value}</span>`,
},
{
data_value_name: "total_pxx_points",
@ -71,85 +74,23 @@
);
});
const points_chart_options: LineChartOptions = {
title: "I ❤️ CumSum",
axes: {
bottom: {
mapsTo: "step",
scaleType: ScaleTypes.LABELS,
},
left: {
mapsTo: "points",
scaleType: ScaleTypes.LINEAR,
},
},
curve: "curveMonotoneX",
// toolbar: {
// enabled: false,
// },
animations: true,
// canvasZoom: {
// enabled: false,
// },
grid: {
x: {
enabled: true,
alignWithAxisTicks: true,
},
y: {
enabled: true,
alignWithAxisTicks: true,
},
},
legend: {
enabled: true,
clickable: true,
position: "top",
},
points: {
enabled: true,
radius: 5,
},
tooltip: {
showTotal: false,
},
resizable: true,
width: "100%",
height: "400px",
};
const points_chart_options: LineChartOptions = make_chart_options(
"I ❤️ CumSum",
"step",
"points",
);
</script>
<svelte:head>
<title>Formula 11 - Leaderboard</title>
</svelte:head>
{#await Promise.all( [data.users, data.racepickpoints, data.racepickpointsacc, data.racepickpointstotal], ) then [users, racepickpoints, racepickpointsacc, racepickpointstotal]}
<div class="flex gap-2">
<!-- Podium -->
<!-- <div class="card w-60 bg-surface-100 p-2 shadow"> -->
<!-- <div class="flex h-20 w-full gap-1"></div> -->
<!-- <div class="flex h-20 w-full gap-1"> -->
<!-- <div class="w-20"> -->
<!-- <div class="h-[30%] w-full"></div> -->
<!-- <div class="h-[70%] w-full bg-surface-500"></div> -->
<!-- </div> -->
<!-- <div class="w-20"> -->
<!-- <div class="h-[100%] w-full bg-surface-500"></div> -->
<!-- </div> -->
<!-- <div class="w-20"> -->
<!-- <div class="h-[60%] w-full"></div> -->
<!-- <div class="h-[40%] w-full bg-surface-600"></div> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
<!-- Points chart -->
<div class="card w-full bg-surface-100 p-2 shadow">
<div class="card w-full bg-surface-100 p-2 shadow">
<LineChart data={points_chart_data} options={points_chart_options} />
</div>
</div>
</div>
<div class="mt-2">
<div class="mt-2">
{#await data.racepickpointstotal then racepickpointstotal}
<Table data={racepickpointstotal} columns={leaderboard_columns} />
</div>
{/await}
{/await}
</div>

View File

@ -1,3 +1,113 @@
<script lang="ts">
import { Table, type TableColumn } from "$lib/components";
import type { Driver, ScrapedRaceResultAcc } from "$lib/schema";
import {
LineChart,
ScaleTypes,
type ChartTabularData,
type LineChartOptions,
} from "@carbon/charts-svelte";
import "@carbon/charts-svelte/styles.css";
import type { PageData } from "./$types";
import { get_by_value } from "$lib/database";
import { make_chart_options } from "$lib/chart";
let { data }: { data: PageData } = $props();
// Await promises
let drivers: Driver[] | undefined = $state(undefined);
data.drivers.then((d: Driver[]) => (drivers = d));
let scraped_raceresultsacc: ScrapedRaceResultAcc[] | undefined = $state(undefined);
data.scraped_raceresultsacc.then((s: ScrapedRaceResultAcc[]) => (scraped_raceresultsacc = s));
const drivers_columns: TableColumn[] = $derived([
{
data_value_name: "position",
label: "Position",
valuefun: async (value: string): Promise<string> =>
`<span class='badge variant-filled-surface'>${value}</span>`,
},
{
data_value_name: "driver_code",
label: "Driver",
},
{
data_value_name: "points",
label: "Points",
valuefun: async (value: string): Promise<string> =>
`<span class='badge variant-filled-surface'>${value}</span>`,
},
]);
const teams_columns: TableColumn[] = $derived([
{
data_value_name: "position",
label: "Position",
valuefun: async (value: string): Promise<string> =>
`<span class='badge variant-filled-surface'>${value}</span>`,
},
{
data_value_name: "team_fullname",
label: "team",
},
{
data_value_name: "points",
label: "Points",
valuefun: async (value: string): Promise<string> =>
`<span class='badge variant-filled-surface'>${value}</span>`,
},
]);
const drivers_chart_data: ChartTabularData = $derived.by(() => {
if (!drivers || !scraped_raceresultsacc) return [];
const active_drivers: Driver[] = drivers.filter((driver: Driver) => driver.active);
const active_results: ScrapedRaceResultAcc[] = scraped_raceresultsacc.filter(
(result: ScrapedRaceResultAcc) =>
get_by_value(drivers ?? [], "code", result.driver_code)?.active,
);
return active_drivers
.map((driver: Driver) => {
return {
group: driver.code,
step: "0",
points: 0,
};
})
.concat(
active_results.map((result: ScrapedRaceResultAcc) => {
return {
group: result.driver_code,
step: result.race_step.toString(),
points: result.acc_points,
};
}),
);
});
const drivers_chart_options: LineChartOptions = make_chart_options("Drivers", "step", "points");
</script>
<svelte:head>
<title>Formula 11 - Statistics</title>
</svelte:head>
<div class="card w-full bg-surface-100 p-2 shadow">
<LineChart data={drivers_chart_data} options={drivers_chart_options} />
</div>
<div class="mt-2 grid w-full grid-cols-1 gap-2 lg:grid-cols-2">
<div class="w-full">
{#await data.scraped_driverstandings then driverstandings}
<Table data={driverstandings} columns={drivers_columns} />
{/await}
</div>
<div class="w-full">
{#await data.scraped_teamstandings then teamstandings}
<Table data={teamstandings} columns={teams_columns} />
{/await}
</div>
</div>

View File

@ -0,0 +1,23 @@
import {
fetch_drivers,
fetch_scraped_driverstandings,
fetch_scraped_raceresults,
fetch_scraped_raceresultsacc,
fetch_scraped_startinggrids,
fetch_scraped_teamstandings,
} from "$lib/fetch";
import type { PageLoad } from "../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:drivers", "data:official");
return {
drivers: fetch_drivers(fetch),
scraped_driverstandings: fetch_scraped_driverstandings(fetch),
scraped_teamstandings: fetch_scraped_teamstandings(fetch),
scraped_startinggrids: fetch_scraped_startinggrids(fetch),
scraped_raceresults: fetch_scraped_raceresults(fetch),
scraped_raceresultsacc: fetch_scraped_raceresultsacc(fetch),
};
};