From bb2d84cc377c6773558bb28600ea4acd3a3b1e7a Mon Sep 17 00:00:00 2001 From: Alexkill536ITA Date: Thu, 8 May 2025 11:05:13 +0200 Subject: [PATCH 01/23] feat: add TeamLogo component to page standings, add Teams logo img --- dash/public/team-logos/alpine.png | Bin 0 -> 3772 bytes dash/public/team-logos/alpine.svg | 1 + dash/public/team-logos/aston martin.png | Bin 0 -> 2353 bytes dash/public/team-logos/aston martin.svg | 1 + dash/public/team-logos/ferrari.png | Bin 0 -> 8074 bytes dash/public/team-logos/ferrari.svg | 132 + dash/public/team-logos/haas f1 team.png | Bin 0 -> 10860 bytes dash/public/team-logos/haas f1 team.svg | 9 + dash/public/team-logos/kick sauber.png | Bin 0 -> 3421 bytes dash/public/team-logos/kick sauber.svg | 1 + dash/public/team-logos/mclaren.png | Bin 0 -> 1698 bytes dash/public/team-logos/mclaren.svg | 5 + dash/public/team-logos/mercedes.png | Bin 0 -> 5269 bytes dash/public/team-logos/mercedes.svg | 21428 +++++++++++++++++++ dash/public/team-logos/racing bulls.png | Bin 0 -> 10637 bytes dash/public/team-logos/racing bulls.svg | 1 + dash/public/team-logos/red bull racing.png | Bin 0 -> 5225 bytes dash/public/team-logos/red bull racing.svg | 9 + dash/public/team-logos/williams.png | Bin 0 -> 4888 bytes dash/public/team-logos/williams.svg | 27 + dash/src/app/dashboard/standings/page.tsx | 5 +- dash/src/components/TeamLogo.tsx | 25 + 22 files changed, 21643 insertions(+), 1 deletion(-) create mode 100644 dash/public/team-logos/alpine.png create mode 100644 dash/public/team-logos/alpine.svg create mode 100644 dash/public/team-logos/aston martin.png create mode 100644 dash/public/team-logos/aston martin.svg create mode 100644 dash/public/team-logos/ferrari.png create mode 100644 dash/public/team-logos/ferrari.svg create mode 100644 dash/public/team-logos/haas f1 team.png create mode 100644 dash/public/team-logos/haas f1 team.svg create mode 100644 dash/public/team-logos/kick sauber.png create mode 100644 dash/public/team-logos/kick sauber.svg create mode 100644 dash/public/team-logos/mclaren.png create mode 100644 dash/public/team-logos/mclaren.svg create mode 100644 dash/public/team-logos/mercedes.png create mode 100644 dash/public/team-logos/mercedes.svg create mode 100644 dash/public/team-logos/racing bulls.png create mode 100644 dash/public/team-logos/racing bulls.svg create mode 100644 dash/public/team-logos/red bull racing.png create mode 100644 dash/public/team-logos/red bull racing.svg create mode 100644 dash/public/team-logos/williams.png create mode 100644 dash/public/team-logos/williams.svg create mode 100644 dash/src/components/TeamLogo.tsx diff --git a/dash/public/team-logos/alpine.png b/dash/public/team-logos/alpine.png new file mode 100644 index 0000000000000000000000000000000000000000..b5d447b520bd7a2ccd2b4ef9381e7230c26a4101 GIT binary patch literal 3772 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 logomark logo - Brandlogos.net \ No newline at end of file diff --git a/dash/public/team-logos/aston martin.png b/dash/public/team-logos/aston martin.png new file mode 100644 index 0000000000000000000000000000000000000000..424f33ea3dd4992ee0bf790a8800eb6bec4aec3b GIT binary patch literal 2353 zcmb`Jc{J4T7so#{jcpJ$7-=jOS(@yI82eysV@pMdRFfiGeeGKqWDg;0C}eB;s>l*y zRAS61B0G_745lm%^PTVCzkhz`cYc4|=brmK=RD`$b6@Z0K1sGXGk&NT6aWBzb1cT5 z?cM)1Zcg?(n-`?TcA!9eGb5mW0I|dlAb3M-LjWMA^D;d+*fEbE)+rDGc#r;TAnHUU z69BkL<`~2C!ES2>9ElfQVWeIoqNn5PogRz{aRhP*(}Ux)J%Us=5Hd9E?CJjI*xg~^ z{h|j$Y-v#@{%PGpPqwT-uW4zRqNUM`Bwf~7g{;)Th5URq{Dig4NnHA+!awy(=2z{X z-v7vwxd8a?WXlMh)Hv86*@PGsH~{J5)#rKE4+uNl$|PR*6nKZIQnZR2;Moe0fN5L}i%)l|HkxOh?yRhkrPaV3XunrS9YTLC3pdWn zDkxao_?&mbBw2(%)O)loEQr1YH;A6HYYq0_{h38edJzFv+QWH54}NSdlF^Zc4#8jQE^9Vzj!27wC=|*|?@`sIu~)YTv(%Xk zuD+yi853{sSzf4jk+&{D<+wBlaI{SY?=h;q$D1;b*jHdPOEq_Qb9U=pTTaVZWM@pa zg>mU_WK|XUSy`o66{DR(89z_NLxrL3yK2;@ma;1&4Y??vF)ZU2$z5Ha zY*Y23N{q;`0m53F#2@NwT}XEd+oBe6$TSG{~E_#)LtSQk~~zL=bI! zCi-H7n>9IVe@{a;_V!CW8TVxKM_(^bxd4F;cXlULMNlF&{$9 z_SQ&!KLe~p=si_NM_>f(trkA#hbWPX**dsH6m(*f>{($1R1`$~`Moj9ir3%slH&sl zK)8F^iBMN&K#9d# zNY^&r8z&rAIhyfThNWeWf=rHrW7)PrMRVYy#j9IietEr*N|aJx$kUIC6oI|_Uce-SrA6YHY=!6Zc20N3=99ZLPBA5593DL} z>Tx~7)pc%dp)M7f5*dW&=c`Yer6Fd(4;G8g1rqEIDV6Akp&YndVe))SKxSN3EPO7k z)h*ELml`Ayn!N!NLozDH+^#7JIMfu6Cq zHJC+W(Gdpx11lO6{(p0-m+17b6W6i=wI#K-I+~=G+wb!>4$rB#hx+>UzFFpx=T1fs3(T|5UViOom(bZ2uXzW$cX{Uw zWJi1koZOlo-E&UQxUAtu|KnCpJhHLr;?PCEo+H&sJ+Cr3>VK+#T<1GM+_X!bV;FgL+r>W$p*{{uz% BVYmPQ literal 0 HcmV?d00001 diff --git a/dash/public/team-logos/aston martin.svg b/dash/public/team-logos/aston martin.svg new file mode 100644 index 00000000..3817220a --- /dev/null +++ b/dash/public/team-logos/aston martin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dash/public/team-logos/ferrari.png b/dash/public/team-logos/ferrari.png new file mode 100644 index 0000000000000000000000000000000000000000..df115d54660e406aaca3075b38e7421a3bf57b64 GIT binary patch literal 8074 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 Y2iS}tr-%xO;Q#;t07*qoM6N<$f^+g`5C8xG literal 0 HcmV?d00001 diff --git a/dash/public/team-logos/ferrari.svg b/dash/public/team-logos/ferrari.svg new file mode 100644 index 00000000..2b1c54dc --- /dev/null +++ b/dash/public/team-logos/ferrari.svg @@ -0,0 +1,132 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dash/public/team-logos/haas f1 team.png b/dash/public/team-logos/haas f1 team.png new file mode 100644 index 0000000000000000000000000000000000000000..1307f15fc450e2569cc980d8dd5038ac23f7eb1c GIT binary patch literal 10860 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 + + + + + + + + diff --git a/dash/public/team-logos/kick sauber.png b/dash/public/team-logos/kick sauber.png new file mode 100644 index 0000000000000000000000000000000000000000..bf02137ea82c3c3ff8f1e6b8348ab9be4090dc3e GIT binary patch literal 3421 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 literal 0 HcmV?d00001 diff --git a/dash/public/team-logos/kick sauber.svg b/dash/public/team-logos/kick sauber.svg new file mode 100644 index 00000000..8b276a4a --- /dev/null +++ b/dash/public/team-logos/kick sauber.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dash/public/team-logos/mclaren.png b/dash/public/team-logos/mclaren.png new file mode 100644 index 0000000000000000000000000000000000000000..2e01f6e6f5a6a1abfad43e38f39b07f3ddf1aa52 GIT binary patch literal 1698 zcma)-`#;kQ1IND`+cSo24$f)LQo@s&)XI)%=GN5Qb3}SvYI2)Cq-LxwIv#-q%W519G+M+m&;@(;EbWUGhUsz-9c9^N}iF)0G=+uvf1iQ71uC&*c_u z*Mmqe`{cb$fpeJIf0ZUqCcrmnrj`czs=Bc=w0JM1w-}(g0f+bh{d{jNORp{C<4;Ah zuZ(s>dPRs(mmUhaD@mg@D}8TzxOZ!m$?@6#q3zGEYqQ`ltdsrZ)Xxf=0b)bdW5Mzt$Vyb3SZ) z+%%|!LqNWFCok4$8TyPW%I>0OzJu5Nm_P@CMLcRaRW3C*Vxa)Hh+=d~3#@WS{l zm=6Xzwx(B|Gp{L85SRWTepC@YaEqm{D~o}Zef!$`A^rE8j}g{Rh7EP9Pj|wnZp|7N zuYDTfH*h4|TaIm@R>DCsT{GV+miQQjjv(X*GpEHG3!p9Hb{e&<(bovte}Dlo+de+b z@ye~0t>gzXl?FDjCC$F7LqC)QY39eqV5cbyGYjt_FR&q}c;#Yq>b#@~u(@*ow$gYq z+=y2mR$t@*?T>p3ypm%>{}4q765t-p?(~c|vl^ldQZc@}^ga+jMbM2@c#rlU-~m|G zC1tRPA}n}BA>4sA&>3$lD&lJK{t))YnMV2Nnw%-MRPfc9YD+vcK$yW<+F`jb!1!aK z2Qmvkql#yyZhmmC+uF2^Z0O{gW>fhU1=ef@MKIa2ubIwgRT3exoT51w2nW;hvE}Zn z6}ihOSM=&&qc*9Mr%uRMn$=hsixj4*XO3Rye!pU7JRzAYZ3EWZ|Ak6ikGC&gE+CrF zks=BgIoh4(`n338d8on%5WKX-C(ixeI|5;Dk{A*ImT!U`d2IgZX^+`BSKHfd5 zq|opgNod^}h+bJPVv$L1l>GNd9%SXdGPf!geIiV$CQv)-t!H2wrdPne+? zYDn?CXCI6CZcRPN9}AC7LQLXpm>Yv|Z{dF#A9`t;?Mhohq`0={$Q)&N0rY~hFDqB|4 zK^s84hZ=BR7G7SD?zsn469Sg!e!AjYo|N5FVy3c}q(XFgI+OZGxh%|EAzW&jS-1+b zF8yD-hD0m_!Lo_bzH-%X#_EmNgv-ghyat@T*A<4VyGcFE#4u7`$dv&cZWNpnYw_{& z-m~y`W;NN%sK8#Wkc-NPST2@U?UnjZ@kxP%W3U@OjUMpbDLUZ|(6h-HJUVt*s`{-_On@wlcxW&)g%1eBwXF(}VYF zq%@>Xp}rua%LeRhzWcPPM&Q}TZwWjY1ST{C)5?^8i`7*@Vzuq{_~$z9UnjgFF*mki z?8agO&yA@daRY8H_f1XJpuWjkR znF_IR4^SES!9!4p! z6!;Kl+%_B&tztHZ&MI7)7fz6QOQwl~$5N|u==)k~y&R&X=>#{X$71mDDK!I|yAIG( zU#Rzgk@7EGwQ_Z2ScXEL{nmtH-q0mC)Pc_l(yY^1YJHR6e8tG14(M<*;7%65jdyP8c- literal 0 HcmV?d00001 diff --git a/dash/public/team-logos/mclaren.svg b/dash/public/team-logos/mclaren.svg new file mode 100644 index 00000000..9ea80189 --- /dev/null +++ b/dash/public/team-logos/mclaren.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/dash/public/team-logos/mercedes.png b/dash/public/team-logos/mercedes.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c0fd1b104b565e763c394f8ad4abc143461b09 GIT binary patch literal 5269 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 b?8f{HqWA;Ac8m|I00000NkvXXu0mjffg8(v literal 0 HcmV?d00001 diff --git a/dash/public/team-logos/mercedes.svg b/dash/public/team-logos/mercedes.svg new file mode 100644 index 00000000..4638659b --- /dev/null +++ b/dash/public/team-logos/mercedes.svg @@ -0,0 +1,21428 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/dash/public/team-logos/racing bulls.png b/dash/public/team-logos/racing bulls.png new file mode 100644 index 0000000000000000000000000000000000000000..15c2bc38b08417072e6153cf21f278b18738d970 GIT binary patch literal 10637 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 literal 0 HcmV?d00001 diff --git a/dash/public/team-logos/racing bulls.svg b/dash/public/team-logos/racing bulls.svg new file mode 100644 index 00000000..4be70d05 --- /dev/null +++ b/dash/public/team-logos/racing bulls.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dash/public/team-logos/red bull racing.png b/dash/public/team-logos/red bull racing.png new file mode 100644 index 0000000000000000000000000000000000000000..4ec5610659cae5503794a1d4af4e21dbe3203105 GIT binary patch literal 5225 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 literal 0 HcmV?d00001 diff --git a/dash/public/team-logos/red bull racing.svg b/dash/public/team-logos/red bull racing.svg new file mode 100644 index 00000000..3e0b10c3 --- /dev/null +++ b/dash/public/team-logos/red bull racing.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/dash/public/team-logos/williams.png b/dash/public/team-logos/williams.png new file mode 100644 index 0000000000000000000000000000000000000000..7a9b3d02cac48601f24cb1f0dae8349025a0f538 GIT binary patch literal 4888 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 KMNUMnLSTX@9+Hm$ literal 0 HcmV?d00001 diff --git a/dash/public/team-logos/williams.svg b/dash/public/team-logos/williams.svg new file mode 100644 index 00000000..e7ca4e7e --- /dev/null +++ b/dash/public/team-logos/williams.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dash/src/app/dashboard/standings/page.tsx b/dash/src/app/dashboard/standings/page.tsx index d3506c59..c496dcd5 100644 --- a/dash/src/app/dashboard/standings/page.tsx +++ b/dash/src/app/dashboard/standings/page.tsx @@ -3,6 +3,7 @@ import { useDataStore } from "@/stores/useDataStore"; import NumberDiff from "@/components/NumberDiff"; +import TeamLogo from "@/components/TeamLogo"; export default function Standings() { const driverStandings = useDataStore((state) => state?.championshipPrediction?.drivers); @@ -78,13 +79,15 @@ export default function Standings() {

{team.predictedPosition}

+ +

{team.teamName}

{team.predictedPoints}

diff --git a/dash/src/components/TeamLogo.tsx b/dash/src/components/TeamLogo.tsx new file mode 100644 index 00000000..c0938e93 --- /dev/null +++ b/dash/src/components/TeamLogo.tsx @@ -0,0 +1,25 @@ +import Image from "next/image"; + +type Props = { + teamName: string | undefined; + width: number | undefined; + height: number | undefined; +}; + +export default function TeamLogo({ teamName, width, height }: Props) { + return ( +
+ {teamName ? ( + {teamName} + ) : ( +
+ )} +
+ ); +} From 59c080ca5996806dfc2a61a12f42c0ebd3230e28 Mon Sep 17 00:00:00 2001 From: Alexkill536ITA Date: Thu, 8 May 2025 11:42:28 +0200 Subject: [PATCH 02/23] fix: Change size teams logo to 400x400pxel --- dash/public/team-logos/alpine.svg | 2 +- dash/public/team-logos/aston martin.svg | 2 +- dash/public/team-logos/ferrari.svg | 133 +- dash/public/team-logos/haas f1 team.svg | 10 +- dash/public/team-logos/kick sauber.svg | 2 +- dash/public/team-logos/mclaren.svg | 6 +- dash/public/team-logos/mercedes.svg | 21429 +------------------ dash/public/team-logos/racing bulls.svg | 2 +- dash/public/team-logos/red bull racing.svg | 10 +- dash/public/team-logos/williams.svg | 28 +- 10 files changed, 10 insertions(+), 21614 deletions(-) diff --git a/dash/public/team-logos/alpine.svg b/dash/public/team-logos/alpine.svg index 0c48583e..e5ce0280 100644 --- a/dash/public/team-logos/alpine.svg +++ b/dash/public/team-logos/alpine.svg @@ -1 +1 @@ -Alpine logomark logo - Brandlogos.net \ No newline at end of file +Alpine logomark logo - Brandlogos.net \ No newline at end of file diff --git a/dash/public/team-logos/aston martin.svg b/dash/public/team-logos/aston martin.svg index 3817220a..4c428a01 100644 --- a/dash/public/team-logos/aston martin.svg +++ b/dash/public/team-logos/aston martin.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/dash/public/team-logos/ferrari.svg b/dash/public/team-logos/ferrari.svg index 2b1c54dc..75535f28 100644 --- a/dash/public/team-logos/ferrari.svg +++ b/dash/public/team-logos/ferrari.svg @@ -1,132 +1 @@ - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +image/svg+xml \ No newline at end of file diff --git a/dash/public/team-logos/haas f1 team.svg b/dash/public/team-logos/haas f1 team.svg index 7633c0f4..f6414f98 100644 --- a/dash/public/team-logos/haas f1 team.svg +++ b/dash/public/team-logos/haas f1 team.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/dash/public/team-logos/kick sauber.svg b/dash/public/team-logos/kick sauber.svg index 8b276a4a..472a6318 100644 --- a/dash/public/team-logos/kick sauber.svg +++ b/dash/public/team-logos/kick sauber.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/dash/public/team-logos/mclaren.svg b/dash/public/team-logos/mclaren.svg index 9ea80189..b844e0e9 100644 --- a/dash/public/team-logos/mclaren.svg +++ b/dash/public/team-logos/mclaren.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/dash/public/team-logos/mercedes.svg b/dash/public/team-logos/mercedes.svg index 4638659b..46bef0f7 100644 --- a/dash/public/team-logos/mercedes.svg +++ b/dash/public/team-logos/mercedes.svg @@ -1,21428 +1 @@ - - - -image/svg+xml \ No newline at end of file +image/svg+xml \ No newline at end of file diff --git a/dash/public/team-logos/racing bulls.svg b/dash/public/team-logos/racing bulls.svg index 4be70d05..41c77dbc 100644 --- a/dash/public/team-logos/racing bulls.svg +++ b/dash/public/team-logos/racing bulls.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/dash/public/team-logos/red bull racing.svg b/dash/public/team-logos/red bull racing.svg index 3e0b10c3..8bf36eea 100644 --- a/dash/public/team-logos/red bull racing.svg +++ b/dash/public/team-logos/red bull racing.svg @@ -1,9 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/dash/public/team-logos/williams.svg b/dash/public/team-logos/williams.svg index e7ca4e7e..c145f997 100644 --- a/dash/public/team-logos/williams.svg +++ b/dash/public/team-logos/williams.svg @@ -1,27 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file From eb6ebe80ac548418f0d8002f53c63d2b0a771e23 Mon Sep 17 00:00:00 2001 From: Alexkill536ITA Date: Thu, 8 May 2025 11:45:30 +0200 Subject: [PATCH 03/23] feat: Change TeamLogo use PNG file to SVG file Delete: all Teams Logo to png --- dash/public/team-logos/alpine.png | Bin 3772 -> 0 bytes dash/public/team-logos/aston martin.png | Bin 2353 -> 0 bytes dash/public/team-logos/ferrari.png | Bin 8074 -> 0 bytes dash/public/team-logos/haas f1 team.png | Bin 10860 -> 0 bytes dash/public/team-logos/kick sauber.png | Bin 3421 -> 0 bytes dash/public/team-logos/mclaren.png | Bin 1698 -> 0 bytes dash/public/team-logos/mercedes.png | Bin 5269 -> 0 bytes dash/public/team-logos/racing bulls.png | Bin 10637 -> 0 bytes dash/public/team-logos/red bull racing.png | Bin 5225 -> 0 bytes dash/public/team-logos/williams.png | Bin 4888 -> 0 bytes dash/src/components/TeamLogo.tsx | 2 +- 11 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 dash/public/team-logos/alpine.png delete mode 100644 dash/public/team-logos/aston martin.png delete mode 100644 dash/public/team-logos/ferrari.png delete mode 100644 dash/public/team-logos/haas f1 team.png delete mode 100644 dash/public/team-logos/kick sauber.png delete mode 100644 dash/public/team-logos/mclaren.png delete mode 100644 dash/public/team-logos/mercedes.png delete mode 100644 dash/public/team-logos/racing bulls.png delete mode 100644 dash/public/team-logos/red bull racing.png delete mode 100644 dash/public/team-logos/williams.png diff --git a/dash/public/team-logos/alpine.png b/dash/public/team-logos/alpine.png deleted file mode 100644 index b5d447b520bd7a2ccd2b4ef9381e7230c26a4101..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3772 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 BVYmPQ diff --git a/dash/public/team-logos/ferrari.png b/dash/public/team-logos/ferrari.png deleted file mode 100644 index df115d54660e406aaca3075b38e7421a3bf57b64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8074 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 Y2iS}tr-%xO;Q#;t07*qoM6N<$f^+g`5C8xG diff --git a/dash/public/team-logos/haas f1 team.png b/dash/public/team-logos/haas f1 team.png deleted file mode 100644 index 1307f15fc450e2569cc980d8dd5038ac23f7eb1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10860 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 diff --git a/dash/public/team-logos/mclaren.png b/dash/public/team-logos/mclaren.png deleted file mode 100644 index 2e01f6e6f5a6a1abfad43e38f39b07f3ddf1aa52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1698 zcma)-`#;kQ1IND`+cSo24$f)LQo@s&)XI)%=GN5Qb3}SvYI2)Cq-LxwIv#-q%W519G+M+m&;@(;EbWUGhUsz-9c9^N}iF)0G=+uvf1iQ71uC&*c_u z*Mmqe`{cb$fpeJIf0ZUqCcrmnrj`czs=Bc=w0JM1w-}(g0f+bh{d{jNORp{C<4;Ah zuZ(s>dPRs(mmUhaD@mg@D}8TzxOZ!m$?@6#q3zGEYqQ`ltdsrZ)Xxf=0b)bdW5Mzt$Vyb3SZ) z+%%|!LqNWFCok4$8TyPW%I>0OzJu5Nm_P@CMLcRaRW3C*Vxa)Hh+=d~3#@WS{l zm=6Xzwx(B|Gp{L85SRWTepC@YaEqm{D~o}Zef!$`A^rE8j}g{Rh7EP9Pj|wnZp|7N zuYDTfH*h4|TaIm@R>DCsT{GV+miQQjjv(X*GpEHG3!p9Hb{e&<(bovte}Dlo+de+b z@ye~0t>gzXl?FDjCC$F7LqC)QY39eqV5cbyGYjt_FR&q}c;#Yq>b#@~u(@*ow$gYq z+=y2mR$t@*?T>p3ypm%>{}4q765t-p?(~c|vl^ldQZc@}^ga+jMbM2@c#rlU-~m|G zC1tRPA}n}BA>4sA&>3$lD&lJK{t))YnMV2Nnw%-MRPfc9YD+vcK$yW<+F`jb!1!aK z2Qmvkql#yyZhmmC+uF2^Z0O{gW>fhU1=ef@MKIa2ubIwgRT3exoT51w2nW;hvE}Zn z6}ihOSM=&&qc*9Mr%uRMn$=hsixj4*XO3Rye!pU7JRzAYZ3EWZ|Ak6ikGC&gE+CrF zks=BgIoh4(`n338d8on%5WKX-C(ixeI|5;Dk{A*ImT!U`d2IgZX^+`BSKHfd5 zq|opgNod^}h+bJPVv$L1l>GNd9%SXdGPf!geIiV$CQv)-t!H2wrdPne+? zYDn?CXCI6CZcRPN9}AC7LQLXpm>Yv|Z{dF#A9`t;?Mhohq`0={$Q)&N0rY~hFDqB|4 zK^s84hZ=BR7G7SD?zsn469Sg!e!AjYo|N5FVy3c}q(XFgI+OZGxh%|EAzW&jS-1+b zF8yD-hD0m_!Lo_bzH-%X#_EmNgv-ghyat@T*A<4VyGcFE#4u7`$dv&cZWNpnYw_{& z-m~y`W;NN%sK8#Wkc-NPST2@U?UnjZ@kxP%W3U@OjUMpbDLUZ|(6h-HJUVt*s`{-_On@wlcxW&)g%1eBwXF(}VYF zq%@>Xp}rua%LeRhzWcPPM&Q}TZwWjY1ST{C)5?^8i`7*@Vzuq{_~$z9UnjgFF*mki z?8agO&yA@daRY8H_f1XJpuWjkR znF_IR4^SES!9!4p! z6!;Kl+%_B&tztHZ&MI7)7fz6QOQwl~$5N|u==)k~y&R&X=>#{X$71mDDK!I|yAIG( zU#Rzgk@7EGwQ_Z2ScXEL{nmtH-q0mC)Pc_l(yY^1YJHR6e8tG14(M<*;7%65jdyP8c- diff --git a/dash/public/team-logos/mercedes.png b/dash/public/team-logos/mercedes.png deleted file mode 100644 index b7c0fd1b104b565e763c394f8ad4abc143461b09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5269 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 b?8f{HqWA;Ac8m|I00000NkvXXu0mjffg8(v diff --git a/dash/public/team-logos/racing bulls.png b/dash/public/team-logos/racing bulls.png deleted file mode 100644 index 15c2bc38b08417072e6153cf21f278b18738d970..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10637 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 diff --git a/dash/public/team-logos/red bull racing.png b/dash/public/team-logos/red bull racing.png deleted file mode 100644 index 4ec5610659cae5503794a1d4af4e21dbe3203105..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5225 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 diff --git a/dash/public/team-logos/williams.png b/dash/public/team-logos/williams.png deleted file mode 100644 index 7a9b3d02cac48601f24cb1f0dae8349025a0f538..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4888 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 KMNUMnLSTX@9+Hm$ diff --git a/dash/src/components/TeamLogo.tsx b/dash/src/components/TeamLogo.tsx index c0938e93..039b8896 100644 --- a/dash/src/components/TeamLogo.tsx +++ b/dash/src/components/TeamLogo.tsx @@ -11,7 +11,7 @@ export default function TeamLogo({ teamName, width, height }: Props) {
{teamName ? ( {teamName} Date: Thu, 8 May 2025 11:51:37 +0200 Subject: [PATCH 04/23] Fix: Change color racing bulls use offical colored logos --- dash/public/team-logos/racing bulls.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/public/team-logos/racing bulls.svg b/dash/public/team-logos/racing bulls.svg index 41c77dbc..62bdb73e 100644 --- a/dash/public/team-logos/racing bulls.svg +++ b/dash/public/team-logos/racing bulls.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 39b340bbbdbc3bec1f99acc89a1d5c405d34e324 Mon Sep 17 00:00:00 2001 From: Alexkill536ITA Date: Fri, 9 May 2025 10:39:20 +0200 Subject: [PATCH 05/23] feat: adjustment space logo --- dash/src/app/dashboard/standings/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/src/app/dashboard/standings/page.tsx b/dash/src/app/dashboard/standings/page.tsx index c496dcd5..36786c16 100644 --- a/dash/src/app/dashboard/standings/page.tsx +++ b/dash/src/app/dashboard/standings/page.tsx @@ -79,7 +79,7 @@ export default function Standings() {
From 67607925a79a628d5644c359f60c606c4cdbdb62 Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Mon, 26 May 2025 22:27:59 +0200 Subject: [PATCH 06/23] fix: map avoid maptiler --- dash/src/app/dashboard/weather/map.tsx | 2 +- dash/src/app/dashboard/weather/page.tsx | 11 +---------- dash/src/env.ts | 2 -- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/dash/src/app/dashboard/weather/map.tsx b/dash/src/app/dashboard/weather/map.tsx index f1e5da52..9acc1564 100644 --- a/dash/src/app/dashboard/weather/map.tsx +++ b/dash/src/app/dashboard/weather/map.tsx @@ -74,7 +74,7 @@ export function WeatherMap() { const libMap = new maplibregl.Map({ container: mapContainerRef.current, - style: `https://api.maptiler.com/maps/dataviz-dark/style.json?key=${env.NEXT_PUBLIC_MAP_KEY}`, + style: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json", center: coords ? [coords.lon, coords.lat] : undefined, zoom: 10, canvasContextAttributes: { diff --git a/dash/src/app/dashboard/weather/page.tsx b/dash/src/app/dashboard/weather/page.tsx index 555d8885..b65a89ff 100644 --- a/dash/src/app/dashboard/weather/page.tsx +++ b/dash/src/app/dashboard/weather/page.tsx @@ -1,19 +1,10 @@ import { WeatherMap } from "@/app/dashboard/weather/map"; -import { env } from "@/env"; - export default function WeatherPage() { // calc height is a workaround, maybe think about refactoring sometime return (
- {!!env.NEXT_PUBLIC_MAP_KEY ? ( - - ) : ( -
-

weather map unavailable

-

setup the map key to the use the weather map

-
- )} +
); } diff --git a/dash/src/env.ts b/dash/src/env.ts index 8326d4f9..f004fea3 100644 --- a/dash/src/env.ts +++ b/dash/src/env.ts @@ -13,7 +13,6 @@ const server = z.object({ const client = z.object({ NEXT_PUBLIC_LIVE_URL: z.string().min(1).includes("http"), - NEXT_PUBLIC_MAP_KEY: z.string().optional(), }); const processEnv = { @@ -27,7 +26,6 @@ const processEnv = { DISABLE_IFRAME: process.env.DISABLE_IFRAME, NEXT_PUBLIC_LIVE_URL: process.env.NEXT_PUBLIC_LIVE_URL, - NEXT_PUBLIC_MAP_KEY: process.env.NEXT_PUBLIC_MAP_KEY, }; // Don't touch the part below From 42aa72249aa42e1d3f0aebeeee0b33964cc40562 Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Mon, 26 May 2025 22:31:00 +0200 Subject: [PATCH 07/23] fix: flags CLS --- dash/src/components/Flag.tsx | 12 ++++++------ dash/src/components/SessionInfo.tsx | 2 +- dash/src/components/schedule/Round.tsx | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dash/src/components/Flag.tsx b/dash/src/components/Flag.tsx index f8f1e7b6..f26ede95 100644 --- a/dash/src/components/Flag.tsx +++ b/dash/src/components/Flag.tsx @@ -1,20 +1,20 @@ +import { clsx } from "clsx"; import Image from "next/image"; type Props = { countryCode: string | undefined; - width: number | undefined; - height: number | undefined; + className?: string; }; -export default function Flag({ countryCode, width, height }: Props) { +export default function Flag({ countryCode, className }: Props) { return ( -
+
{countryCode ? ( {countryCode} ) : ( diff --git a/dash/src/components/SessionInfo.tsx b/dash/src/components/SessionInfo.tsx index c66b0ad6..8f69349c 100644 --- a/dash/src/components/SessionInfo.tsx +++ b/dash/src/components/SessionInfo.tsx @@ -38,7 +38,7 @@ export default function SessionInfo() { return (
- +
{session ? ( diff --git a/dash/src/components/schedule/Round.tsx b/dash/src/components/schedule/Round.tsx index 752b15f4..65a963f6 100644 --- a/dash/src/components/schedule/Round.tsx +++ b/dash/src/components/schedule/Round.tsx @@ -50,7 +50,7 @@ export default function Round({ round, nextName }: Props) {
- +

{round.countryName}

{round.name === nextName && ( From 146a97acfaf7d0a85820fe70d59caf76c0b02f37 Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Mon, 26 May 2025 23:02:35 +0200 Subject: [PATCH 08/23] fix(map): remove unused import --- dash/src/app/dashboard/weather/map.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/dash/src/app/dashboard/weather/map.tsx b/dash/src/app/dashboard/weather/map.tsx index 9acc1564..70626df3 100644 --- a/dash/src/app/dashboard/weather/map.tsx +++ b/dash/src/app/dashboard/weather/map.tsx @@ -5,8 +5,6 @@ import { useEffect, useRef, useState } from "react"; import maplibregl, { Map, Marker } from "maplibre-gl"; import "maplibre-gl/dist/maplibre-gl.css"; -import { env } from "@/env"; - import { fetchCoords } from "@/lib/geocode"; import { getRainviewer } from "@/lib/rainviewer"; From 3530f88b83aa2a7cc1640b98fe984ad13a24df09 Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Mon, 26 May 2025 23:30:10 +0200 Subject: [PATCH 09/23] fix: driver fastest lap not purple anymore --- dash/src/components/driver/DriverLapTime.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/src/components/driver/DriverLapTime.tsx b/dash/src/components/driver/DriverLapTime.tsx index 9c8574a8..b86a6be0 100644 --- a/dash/src/components/driver/DriverLapTime.tsx +++ b/dash/src/components/driver/DriverLapTime.tsx @@ -23,7 +23,7 @@ export default function DriverLapTime({ last, best, hasFastest }: Props) {

Date: Tue, 27 May 2025 17:52:45 +0200 Subject: [PATCH 10/23] fix: styling and imporve imports --- dash/next.config.ts | 2 +- dash/src/app/dashboard/track-map/page.tsx | 2 +- .../app/dashboard/weather/map-timeline.tsx | 2 +- dash/src/components/QualifyingDriver.tsx | 26 ++++---- .../components/dashboard/DriverViolations.tsx | 2 +- dash/src/components/dashboard/Map.tsx | 2 +- .../components/driver/DriverCarMetrics.tsx | 2 +- .../components/driver/DriverHistoryTires.tsx | 2 +- dash/src/components/driver/DriverInfo.tsx | 2 +- dash/src/components/driver/DriverLapTime.tsx | 22 +++---- .../components/driver/DriverMiniSectors.tsx | 15 ++--- dash/src/components/driver/DriverTire.tsx | 2 +- dash/src/components/schedule/Countdown.tsx | 2 +- dash/src/components/schedule/NextRound.tsx | 2 +- dash/src/components/schedule/Schedule.tsx | 2 +- .../components/schedule/WeekendSchedule.tsx | 2 +- dash/src/hooks/useStatefulBuffer.ts | 2 +- dash/src/lib/getTimeColor.ts | 17 ----- dash/src/lib/groupSessionByDay.ts | 2 +- dash/src/lib/rainviewer.ts | 2 +- dash/src/metadata.ts | 2 +- dash/src/types/message.type.ts | 2 +- dash/tsconfig.json | 65 +++++++++---------- 23 files changed, 77 insertions(+), 104 deletions(-) delete mode 100644 dash/src/lib/getTimeColor.ts diff --git a/dash/next.config.ts b/dash/next.config.ts index 7f84d27b..dd5da0d6 100644 --- a/dash/next.config.ts +++ b/dash/next.config.ts @@ -1,4 +1,4 @@ -import { NextConfig } from "next"; +import type { NextConfig } from "next"; import pack from "./package.json" with { type: "json" }; diff --git a/dash/src/app/dashboard/track-map/page.tsx b/dash/src/app/dashboard/track-map/page.tsx index 52fd2162..dd3f2382 100644 --- a/dash/src/app/dashboard/track-map/page.tsx +++ b/dash/src/app/dashboard/track-map/page.tsx @@ -13,7 +13,7 @@ import DriverLapTime from "@/components/driver/DriverLapTime"; import { sortPos } from "@/lib/sorting"; import { useCarDataStore, useDataStore } from "@/stores/useDataStore"; -import { Driver, TimingDataDriver } from "@/types/state.type"; +import type { Driver, TimingDataDriver } from "@/types/state.type"; import { useSettingsStore } from "@/stores/useSettingsStore"; export default function TrackMap() { diff --git a/dash/src/app/dashboard/weather/map-timeline.tsx b/dash/src/app/dashboard/weather/map-timeline.tsx index ef35a5e3..6c1fb5d3 100644 --- a/dash/src/app/dashboard/weather/map-timeline.tsx +++ b/dash/src/app/dashboard/weather/map-timeline.tsx @@ -3,7 +3,7 @@ import { unix } from "moment"; import { motion, useMotionValue, useDragControls, AnimatePresence } from "motion/react"; -import { useState, useRef, useEffect, RefObject } from "react"; +import { useState, useRef, useEffect, type RefObject } from "react"; function getProgressFromX({ x, diff --git a/dash/src/components/QualifyingDriver.tsx b/dash/src/components/QualifyingDriver.tsx index 779a29f6..1d1b79b0 100644 --- a/dash/src/components/QualifyingDriver.tsx +++ b/dash/src/components/QualifyingDriver.tsx @@ -6,9 +6,7 @@ import clsx from "clsx"; import DriverTag from "./driver/DriverTag"; -import { getSectorColorBG, getSectorColorText } from "@/lib/getTimeColor"; - -import { Driver as DriverType, TimingAppDataDriver, TimingDataDriver } from "@/types/state.type"; +import type { Driver as DriverType, TimingAppDataDriver, TimingDataDriver } from "@/types/state.type"; type Props = { driver: DriverType; @@ -85,18 +83,20 @@ export default function DriverQuali({ {timingDriver.sectors.map((sector, i) => (

{!!sector.value ? sector.value : "-- ---"}

diff --git a/dash/src/components/dashboard/DriverViolations.tsx b/dash/src/components/dashboard/DriverViolations.tsx index fa4326fd..fe7425da 100644 --- a/dash/src/components/dashboard/DriverViolations.tsx +++ b/dash/src/components/dashboard/DriverViolations.tsx @@ -1,4 +1,4 @@ -import { Driver, TimingData } from "@/types/state.type"; +import type { Driver, TimingData } from "@/types/state.type"; import { calculatePosition } from "@/lib/calculatePosition"; diff --git a/dash/src/components/dashboard/Map.tsx b/dash/src/components/dashboard/Map.tsx index a3fc0eb1..d882b738 100644 --- a/dash/src/components/dashboard/Map.tsx +++ b/dash/src/components/dashboard/Map.tsx @@ -13,7 +13,7 @@ import { createSectors, findYellowSectors, getSectorColor, - MapSector, + type MapSector, prioritizeColoredSectors, rad, rotate, diff --git a/dash/src/components/driver/DriverCarMetrics.tsx b/dash/src/components/driver/DriverCarMetrics.tsx index aaee01cb..9d0b8a0e 100644 --- a/dash/src/components/driver/DriverCarMetrics.tsx +++ b/dash/src/components/driver/DriverCarMetrics.tsx @@ -1,6 +1,6 @@ import { useSettingsStore } from "@/stores/useSettingsStore"; -import { CarDataChannels } from "@/types/state.type"; +import type { CarDataChannels } from "@/types/state.type"; import DriverPedals from "./DriverPedals"; diff --git a/dash/src/components/driver/DriverHistoryTires.tsx b/dash/src/components/driver/DriverHistoryTires.tsx index ef39f77d..13329f72 100644 --- a/dash/src/components/driver/DriverHistoryTires.tsx +++ b/dash/src/components/driver/DriverHistoryTires.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; -import { Stint } from "@/types/state.type"; +import type { Stint } from "@/types/state.type"; type Props = { stints: Stint[] | undefined; diff --git a/dash/src/components/driver/DriverInfo.tsx b/dash/src/components/driver/DriverInfo.tsx index 3c125e59..b5c0b26c 100644 --- a/dash/src/components/driver/DriverInfo.tsx +++ b/dash/src/components/driver/DriverInfo.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; -import { TimingDataDriver } from "@/types/state.type"; +import type { TimingDataDriver } from "@/types/state.type"; type Props = { timingDriver: TimingDataDriver; diff --git a/dash/src/components/driver/DriverLapTime.tsx b/dash/src/components/driver/DriverLapTime.tsx index b86a6be0..48e4045e 100644 --- a/dash/src/components/driver/DriverLapTime.tsx +++ b/dash/src/components/driver/DriverLapTime.tsx @@ -1,7 +1,6 @@ import clsx from "clsx"; -import { getTimeColor } from "@/lib/getTimeColor"; -import { TimingDataDriver } from "@/types/state.type"; +import type { TimingDataDriver } from "@/types/state.type"; type Props = { last: TimingDataDriver["lastLapTime"]; @@ -13,20 +12,19 @@ export default function DriverLapTime({ last, best, hasFastest }: Props) { return (

{!!last.value ? last.value : "-- -- ---"}

{!!best.value ? best.value : "-- -- ---"}

diff --git a/dash/src/components/driver/DriverMiniSectors.tsx b/dash/src/components/driver/DriverMiniSectors.tsx index 93e3e60c..25a5d82a 100644 --- a/dash/src/components/driver/DriverMiniSectors.tsx +++ b/dash/src/components/driver/DriverMiniSectors.tsx @@ -1,7 +1,6 @@ import clsx from "clsx"; -import { getTimeColor } from "@/lib/getTimeColor"; -import { TimingDataDriver, TimingStatsDriver } from "@/types/state.type"; +import type { TimingDataDriver, TimingStatsDriver } from "@/types/state.type"; import { useSettingsStore } from "@/stores/useSettingsStore"; type Props = { @@ -28,17 +27,17 @@ export default function DriverMiniSectors({ sectors = [], bestSectors, tla }: Pr

{!!sector.value ? sector.value : !!sector.previousValue ? sector.previousValue : "-- ---"}

{showBestSectors && ( -

+

{bestSectors && bestSectors[i].value ? bestSectors[i].value : "-- ---"}

)} diff --git a/dash/src/components/driver/DriverTire.tsx b/dash/src/components/driver/DriverTire.tsx index a215e063..ea96d11a 100644 --- a/dash/src/components/driver/DriverTire.tsx +++ b/dash/src/components/driver/DriverTire.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; -import { Stint } from "@/types/state.type"; +import type { Stint } from "@/types/state.type"; type Props = { stints: Stint[] | undefined; diff --git a/dash/src/components/schedule/Countdown.tsx b/dash/src/components/schedule/Countdown.tsx index e56dd447..276f789c 100644 --- a/dash/src/components/schedule/Countdown.tsx +++ b/dash/src/components/schedule/Countdown.tsx @@ -4,7 +4,7 @@ import { AnimatePresence, motion } from "motion/react"; import { useEffect, useRef, useState } from "react"; import { duration, now, utc } from "moment"; -import { Session } from "@/types/schedule.type"; +import type { Session } from "@/types/schedule.type"; type Props = { next: Session; diff --git a/dash/src/components/schedule/NextRound.tsx b/dash/src/components/schedule/NextRound.tsx index 1768c7a9..3a63f39c 100644 --- a/dash/src/components/schedule/NextRound.tsx +++ b/dash/src/components/schedule/NextRound.tsx @@ -5,7 +5,7 @@ import Countdown from "@/components/schedule/Countdown"; import Round from "@/components/schedule/Round"; import { env } from "@/env"; -import { Round as RoundType } from "@/types/schedule.type"; +import type { Round as RoundType } from "@/types/schedule.type"; export const getNext = async () => { await connection(); diff --git a/dash/src/components/schedule/Schedule.tsx b/dash/src/components/schedule/Schedule.tsx index de078e43..f18bf0e9 100644 --- a/dash/src/components/schedule/Schedule.tsx +++ b/dash/src/components/schedule/Schedule.tsx @@ -2,7 +2,7 @@ import { connection } from "next/server"; import Round from "@/components/schedule/Round"; -import { Round as RoundType } from "@/types/schedule.type"; +import type { Round as RoundType } from "@/types/schedule.type"; import { env } from "@/env"; diff --git a/dash/src/components/schedule/WeekendSchedule.tsx b/dash/src/components/schedule/WeekendSchedule.tsx index af43bc25..72a986dc 100644 --- a/dash/src/components/schedule/WeekendSchedule.tsx +++ b/dash/src/components/schedule/WeekendSchedule.tsx @@ -3,7 +3,7 @@ import clsx from "clsx"; import { groupSessionByDay } from "@/lib/groupSessionByDay"; -import { Session } from "@/types/schedule.type"; +import type { Session } from "@/types/schedule.type"; type Props = { sessions: Session[]; diff --git a/dash/src/hooks/useStatefulBuffer.ts b/dash/src/hooks/useStatefulBuffer.ts index 32da2860..f06ec9f5 100644 --- a/dash/src/hooks/useStatefulBuffer.ts +++ b/dash/src/hooks/useStatefulBuffer.ts @@ -4,7 +4,7 @@ import { merge } from "@/lib/merge"; import { useBuffer } from "@/hooks/useBuffer"; -import { RecursivePartial } from "@/types/message.type"; +import type { RecursivePartial } from "@/types/message.type"; export const useStatefulBuffer = () => { const currentRef = useRef(null); diff --git a/dash/src/lib/getTimeColor.ts b/dash/src/lib/getTimeColor.ts deleted file mode 100644 index 0cbb1b6c..00000000 --- a/dash/src/lib/getTimeColor.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const getTimeColor = (fastest: boolean, pb: boolean) => { - if (fastest) return "text-violet-600"; - else if (pb) return "text-emerald-500"; - return ""; -}; - -export const getSectorColorBG = (fastest: boolean, pb: boolean) => { - if (fastest) return "bg-violet-500"; - else if (pb) return "bg-emerald-500"; - return "bg-amber-400"; -}; - -export const getSectorColorText = (fastest: boolean, pb: boolean) => { - if (fastest) return "text-violet-500"; - else if (pb) return "text-emerald-500"; - return "text-yellow-500"; -}; diff --git a/dash/src/lib/groupSessionByDay.ts b/dash/src/lib/groupSessionByDay.ts index 5ae623a4..8788d071 100644 --- a/dash/src/lib/groupSessionByDay.ts +++ b/dash/src/lib/groupSessionByDay.ts @@ -1,6 +1,6 @@ import { utc } from "moment"; -import { Session } from "@/types/schedule.type"; +import type { Session } from "@/types/schedule.type"; type SessionDayGroup = { date: string; sessions: Session[] }; diff --git a/dash/src/lib/rainviewer.ts b/dash/src/lib/rainviewer.ts index 90cc891b..c9c26ade 100644 --- a/dash/src/lib/rainviewer.ts +++ b/dash/src/lib/rainviewer.ts @@ -1,4 +1,4 @@ -import { Rainviewer } from "@/types/rainviewer.type"; +import type { Rainviewer } from "@/types/rainviewer.type"; const rainviewerUrl = "https://api.rainviewer.com/public/weather-maps.json"; diff --git a/dash/src/metadata.ts b/dash/src/metadata.ts index ad7960a5..09a94b07 100644 --- a/dash/src/metadata.ts +++ b/dash/src/metadata.ts @@ -1,4 +1,4 @@ -import { Metadata } from "next"; +import type { Metadata } from "next"; const title = "f1-dash | Formula 1 live timing"; const description = diff --git a/dash/src/types/message.type.ts b/dash/src/types/message.type.ts index 70a31ca5..f979bc24 100644 --- a/dash/src/types/message.type.ts +++ b/dash/src/types/message.type.ts @@ -1,4 +1,4 @@ -import { type State } from "./state.type"; +import type { State } from "./state.type"; export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] diff --git a/dash/tsconfig.json b/dash/tsconfig.json index 569267ee..a6f72f73 100644 --- a/dash/tsconfig.json +++ b/dash/tsconfig.json @@ -1,38 +1,31 @@ { - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"], - "public/*": ["./public/*"] - } - }, - "include": [ - ".eslintrc.cjs", - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.cjs", - "**/*.mjs", - ".next/types/**/*.ts" - ], - "exclude": ["node_modules"] + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "public/*": ["./public/*"] + } + }, + "include": [".eslintrc.cjs", "next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs", ".next/types/**/*.ts"], + "exclude": ["node_modules"] } From 93dc3c18df069f7c1218884077c461a84a4e04c8 Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Tue, 27 May 2025 17:56:42 +0200 Subject: [PATCH 11/23] refactor: live, api and client services restructure --- Cargo.lock | 17 +-- Cargo.toml | 33 +++-- crates/api/src/main.rs | 45 ------- crates/client/src/client.rs | 29 ++--- crates/client/src/consumers.rs | 58 +++++++++ crates/client/src/manager.rs | 52 ++++++++ crates/client/src/message.rs | 33 +++-- crates/data/src/transformer.rs | 2 +- crates/env/Cargo.toml | 12 -- crates/env/src/env.rs | 8 -- crates/live/src/main.rs | 58 --------- crates/live/src/server.rs | 53 -------- crates/live/src/server/cors.rs | 10 -- crates/live/src/server/live.rs | 45 ------- crates/live/src/state.rs | 120 ------------------ {crates => services}/api/Cargo.toml | 8 +- {crates => services}/api/readme.md | 0 .../api/src/endpoints/health.rs | 0 .../api/src/endpoints/schedule.rs | 0 services/api/src/main.rs | 56 ++++++++ {crates => services}/live/Cargo.toml | 11 +- {crates => services}/live/readme.md | 0 services/live/src/main.rs | 80 ++++++++++++ .../live/src/server/drivers.rs | 7 +- .../live/src/server/health.rs | 0 services/live/src/server/live.rs | 72 +++++++++++ 26 files changed, 390 insertions(+), 419 deletions(-) delete mode 100644 crates/api/src/main.rs create mode 100644 crates/client/src/consumers.rs create mode 100644 crates/client/src/manager.rs delete mode 100644 crates/env/Cargo.toml delete mode 100644 crates/env/src/env.rs delete mode 100644 crates/live/src/main.rs delete mode 100644 crates/live/src/server.rs delete mode 100644 crates/live/src/server/cors.rs delete mode 100644 crates/live/src/server/live.rs delete mode 100644 crates/live/src/state.rs rename {crates => services}/api/Cargo.toml (90%) rename {crates => services}/api/readme.md (100%) rename {crates => services}/api/src/endpoints/health.rs (100%) rename {crates => services}/api/src/endpoints/schedule.rs (100%) create mode 100644 services/api/src/main.rs rename {crates => services}/live/Cargo.toml (80%) rename {crates => services}/live/readme.md (100%) create mode 100644 services/live/src/main.rs rename {crates => services}/live/src/server/drivers.rs (82%) rename {crates => services}/live/src/server/health.rs (100%) create mode 100644 services/live/src/server/live.rs diff --git a/Cargo.lock b/Cargo.lock index 5722275b..d42b584b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,13 +73,14 @@ dependencies = [ "axum", "cached", "chrono", - "env", + "dotenvy", "ical", "regex", "reqwest", "serde", "serde_json", "tokio", + "tower-http", "tracing", "tracing-subscriber", ] @@ -476,13 +477,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env" -version = "0.1.0" -dependencies = [ - "dotenvy", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1112,21 +1106,15 @@ version = "0.3.0" dependencies = [ "anyhow", "axum", - "base64", "client", "data", "dotenvy", - "env", - "flate2", "futures", - "heck", - "regex", "reqwest", "serde", "serde_json", "tokio", "tokio-stream", - "tokio-tungstenite", "tower-http", "tracing", "tracing-subscriber", @@ -2013,6 +2001,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.52.0", ] diff --git a/Cargo.toml b/Cargo.toml index 7ae43aef..dbbbffa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,30 +1,43 @@ [workspace] members = [ - "crates/data", # lib - "crates/client", # lib - "crates/env", # lib - "crates/api", # bin - "crates/live", # bin - "crates/saver", # bin - "crates/simulator", # bin + "crates/data", # lib + "crates/client", # lib + # "crates/timescale", # lib + "services/api", # bin - service + "services/live", # bin - service + # "services/analytics", # bin - service + # "services/importer", # bin - service + "crates/saver", # bin - util + "crates/simulator", # bin - util +] +default-members = [ + "services/live", + "services/api", + # "services/analytics", + # "services/importer", ] -default-members = ["crates/live", "crates/api"] resolver = "2" [workspace.dependencies] data = { path = "crates/data" } client = { path = "crates/client" } -env = { path = "crates/env" } +timescale = { path = "crates/timescale" } + -tokio = { version = "1.44.2", features = ["full"] } +tokio = { version = "1.44.2", features = ["full", "tracing"] } tokio-stream = { version = "0.1.15", features = ["full"] } + tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "chrono"] } + axum = { version = "0.8.3", features = ["http2"] } tower-http = { version = "0.6.2", features = ["cors"] } + tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] } reqwest = { version = "0.12.15", features = ["native-tls"] } + serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"] } diff --git a/crates/api/src/main.rs b/crates/api/src/main.rs deleted file mode 100644 index e723ec39..00000000 --- a/crates/api/src/main.rs +++ /dev/null @@ -1,45 +0,0 @@ -use axum::{routing::get, Router}; -use tokio::net::TcpListener; -use tracing::info; -use tracing::level_filters::LevelFilter; - -use env; - -mod endpoints { - pub(crate) mod health; - pub(crate) mod schedule; -} - -#[tokio::main] -async fn main() { - env::init(); - init_logs(); - - let app = Router::new() - .route("/api/schedule", get(endpoints::schedule::get)) - .route("/api/schedule/next", get(endpoints::schedule::get_next)) - .route("/api/health", get(endpoints::health::check)); - - let addr = addr(); - - info!("running on {}", addr); - - let listener = TcpListener::bind(addr).await.expect("failed to bind"); - - axum::serve(listener, app) - .await - .expect("failed to setup server"); -} - -fn addr() -> String { - std::env::var("API_BACKEND_ADDRESS").unwrap_or("0.0.0.0:5000".to_string()) -} - -fn init_logs() { - let env_filter = tracing_subscriber::EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .with_env_var("RUST_LOG") - .from_env_lossy(); - - tracing_subscriber::fmt().with_env_filter(env_filter).init(); -} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 0561930a..746af5e7 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -5,9 +5,8 @@ use axum::http::HeaderValue; use futures::SinkExt; use reqwest::{header, Url}; use serde_json::Value; -use tokio_stream::Stream; -use tokio_stream::StreamExt; +use tokio_stream::{Stream, StreamExt}; use tokio_tungstenite::tungstenite::client::IntoClientRequest; use tokio_tungstenite::{tungstenite::http::Request, MaybeTlsStream, WebSocketStream}; use tracing::{debug, trace}; @@ -15,8 +14,14 @@ use tracing::{debug, trace}; pub use tokio_tungstenite::tungstenite; mod consts; +pub mod consumers; +pub mod manager; pub mod message; +pub use consumers::broadcast; +pub use consumers::keep_state; +pub use manager::manage; + type WsStream = WebSocketStream>; pub async fn parse_stream(stream: WsStream) -> impl Stream { @@ -32,9 +37,7 @@ pub async fn parse_stream(stream: WsStream) -> impl Stream Result> { let req = create_request().await?; - debug!("created request"); - - trace!("request='{:?}'", req); + debug!(?req, "created request"); let (mut socket, _) = tokio_tungstenite::connect_async(req).await?; @@ -59,11 +62,7 @@ async fn create_request() -> Result, Box> { let negotiation = negotiate().await?; - trace!( - "token='{}' cookie='{}'", - negotiation.token, - negotiation.cookie - ); + trace!(negotiation.token, negotiation.cookie); let url = Url::parse_with_params( &format!("wss://{}/connect", consts::F1_BASE_URL), @@ -71,11 +70,11 @@ async fn create_request() -> Result, Box> { ("clientProtocol", "1.5"), ("transport", "webSockets"), ("connectionToken", &negotiation.token), - ("connectionData", &consts::SIGNALR_HUB), + ("connectionData", consts::SIGNALR_HUB), ], )?; - trace!("url='{}'", url); + trace!(?url); let mut req: Request<()> = url.into_client_request()?; @@ -89,7 +88,7 @@ async fn create_request() -> Result, Box> { HeaderValue::from_static("gzip,identity"), ); headers.insert( - header::COOKIE, //asd + header::COOKIE, // asd negotiation.cookie.parse().unwrap(), ); @@ -110,7 +109,7 @@ async fn negotiate() -> Result> { &format!("https://{}/negotiate", consts::F1_BASE_URL), &[ ("clientProtocol", "1.5"), - ("connectionData", &consts::SIGNALR_HUB), + ("connectionData", consts::SIGNALR_HUB), ], )?; @@ -121,7 +120,7 @@ async fn negotiate() -> Result> { let body = res.text().await?; let json = serde_json::from_str::(&body)?; - trace!("negotiation response='{}'", json); + trace!(?json, "negotiation response"); Ok(Negotiaion { token: json["ConnectionToken"] diff --git a/crates/client/src/consumers.rs b/crates/client/src/consumers.rs new file mode 100644 index 00000000..dbe9a809 --- /dev/null +++ b/crates/client/src/consumers.rs @@ -0,0 +1,58 @@ +use std::sync::{Arc, Mutex}; + +use data::merge::merge; +use serde_json::{json, Map, Value}; +use tokio::sync::broadcast::{self, Receiver, Sender}; +use tokio_stream::{wrappers::ReceiverStream, StreamExt}; +use tracing::error; + +use crate::message::Message; + +pub fn broadcast(mut stream: ReceiverStream) -> (Sender, Receiver) { + let (tx, rx) = broadcast::channel::(32); + + let manage_tx = tx.clone(); + + tokio::spawn(async move { + while let Some(message) = stream.next().await { + let _ = manage_tx.send(message); + } + }); + + (tx, rx) +} + +pub fn keep_state(mut reciver: Receiver) -> Arc> { + let state = Arc::new(Mutex::new(json!({}))); + + let manage_state = state.clone(); + + tokio::spawn(async move { + while let Ok(message) = reciver.recv().await { + match message { + Message::Updates(updates) => { + let Ok(mut state) = manage_state.lock() else { + error!("failed to lock state"); + continue; + }; + + for (topic, update) in updates { + let mut map = Map::new(); + map.insert(topic, update); + merge(&mut state, Value::Object(map)); + } + } + Message::Initial(initial) => { + let Ok(mut state) = manage_state.lock() else { + error!("failed to lock state"); + continue; + }; + + *state = initial; + } + } + } + }); + + state +} diff --git a/crates/client/src/manager.rs b/crates/client/src/manager.rs new file mode 100644 index 00000000..8c90f1f9 --- /dev/null +++ b/crates/client/src/manager.rs @@ -0,0 +1,52 @@ +use std::time::Duration; + +use tokio::{sync::mpsc, time::sleep}; +use tokio_stream::{wrappers::ReceiverStream, StreamExt}; +use tracing::error; + +use crate::{init, message::Message, parse_stream}; + +pub fn manage() -> ReceiverStream { + let (tx, rx) = mpsc::channel::(32); + + tokio::spawn(async move { + 'manage: loop { + sleep(Duration::from_secs(3)).await; + + let stream = match init().await { + Ok(stream) => stream, + Err(err) => { + error!(?err, "error occored starting the client, restarting"); + continue 'manage; + } + }; + + let mut parsed_stream = parse_stream(stream).await; + + while let Some(message) = parsed_stream.next().await { + if check_restart(&message) { + continue 'manage; + } + + let _ = tx.send(message).await; + } + } + }); + + ReceiverStream::new(rx) +} + +fn check_restart(message: &Message) -> bool { + match message { + Message::Updates(updates) => { + for (cat, update) in updates { + if cat == "sessionInfo" && update.pointer("/name").is_some() { + return true; + } + } + + false + } + Message::Initial(_) => false, + } +} diff --git a/crates/client/src/message.rs b/crates/client/src/message.rs index 13570dc2..00e628c8 100644 --- a/crates/client/src/message.rs +++ b/crates/client/src/message.rs @@ -1,21 +1,23 @@ -use std::mem; - -use serde_json::{Map, Value}; +use data::transformer::{to_camel_case, transform}; +use serde_json::Value; use tokio_tungstenite::tungstenite::Utf8Bytes; use tracing::trace; +#[derive(Clone)] pub enum Message { - Updates(Vec>), + Updates(Vec<(String, Value)>), Initial(Value), } pub fn parse(data: Utf8Bytes) -> Option { - trace!("parsing message '{}'", data); + trace!(?data, "parsing message"); let msg = serde_json::from_str::(&data).ok()?; if let Some(initial) = msg.pointer("/R") { - return Some(Message::Initial(initial.clone())); + let mut data = initial.clone(); + transform(&mut data); + return Some(Message::Initial(data)); }; if let Some(Value::Array(updates)) = msg.pointer("/M") { @@ -26,18 +28,23 @@ pub fn parse(data: Utf8Bytes) -> Option { let mut ups = Vec::new(); for update in updates { - let cat = update.pointer("/A/0")?.as_str()?; - let data = update.pointer("/A/1")?; + let Some(cat) = update.pointer("/A/0").and_then(|v| v.as_str()) else { + continue; + }; + + let Some(data) = update.pointer("/A/1") else { + continue; + }; + + let mut update_value = data.clone(); - let mut up = Map::new(); - up.insert(cat.to_owned(), data.clone()); - ups.push(up); + transform(&mut update_value); + + ups.push((to_camel_case(cat), update_value)); } return Some(Message::Updates(ups)); } - mem::drop(msg); // idk why I put this here - None } diff --git a/crates/data/src/transformer.rs b/crates/data/src/transformer.rs index 81c922d3..be5a62ac 100644 --- a/crates/data/src/transformer.rs +++ b/crates/data/src/transformer.rs @@ -2,7 +2,7 @@ use std::mem; use serde_json::{Map, Value}; -fn to_camel_case(string: &str) -> String { +pub fn to_camel_case(string: &str) -> String { heck::AsLowerCamelCase(string).to_string() } diff --git a/crates/env/Cargo.toml b/crates/env/Cargo.toml deleted file mode 100644 index 3990a652..00000000 --- a/crates/env/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "env" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/env.rs" - -[dependencies] -dotenvy.workspace = true - diff --git a/crates/env/src/env.rs b/crates/env/src/env.rs deleted file mode 100644 index 4de25987..00000000 --- a/crates/env/src/env.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub fn init() { - // we cant use info and warn here yet - // because we load envs before log levels because log levels are set by envs - match dotenvy::dotenv() { - Ok(_) => println!("successfully found env file and loaded vars"), - Err(_) => println!("no env file found"), - } -} diff --git a/crates/live/src/main.rs b/crates/live/src/main.rs deleted file mode 100644 index a9d9d0df..00000000 --- a/crates/live/src/main.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use serde_json::{json, Value}; -use tokio::sync::broadcast; - -mod server; -mod state; - -use env; -use tracing::level_filters::LevelFilter; - -type LiveState = Arc>; - -#[derive(Clone)] -pub enum LiveEvent { - Initial(String), - Update(String), -} - -impl LiveEvent { - pub fn name(&self) -> &str { - match self { - LiveEvent::Initial(_) => "initial", - LiveEvent::Update(_) => "update", - } - } - - pub fn inner(self) -> String { - match self { - LiveEvent::Initial(v) => v, - LiveEvent::Update(v) => v, - } - } -} - -#[tokio::main] -async fn main() { - env::init(); - init_logs(); - - let (tx, _rx) = broadcast::channel::(10); - let state = Arc::new(Mutex::new(json!({}))); - - state::manage(tx.clone(), state.clone()); - - server::init(tx, state) - .await - .expect("http server setup failed"); -} - -fn init_logs() { - let env_filter = tracing_subscriber::EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .with_env_var("RUST_LOG") - .from_env_lossy(); - - tracing_subscriber::fmt().with_env_filter(env_filter).init(); -} diff --git a/crates/live/src/server.rs b/crates/live/src/server.rs deleted file mode 100644 index 5d7f2b4e..00000000 --- a/crates/live/src/server.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::{error::Error, net::SocketAddr, sync::Arc}; - -use axum::{routing::get, Router}; -use tokio::sync::broadcast; - -use tracing::info; - -use crate::{LiveEvent, LiveState}; - -mod cors; -mod drivers; -mod health; -pub mod live; - -pub struct AppState { - tx: broadcast::Sender, - state: LiveState, -} - -fn addr() -> String { - std::env::var("LIVE_BACKEND_ADDRESS").unwrap_or("0.0.0.0:4000".to_string()) -} - -pub async fn init( - tx: broadcast::Sender, - state: LiveState, -) -> Result<(), Box> { - let cors = cors::init(); - - let app_state = Arc::new(AppState { tx, state }); - - let app = Router::new() - .route("/api/sse", get(live::sse_handler)) - .route("/api/health", get(health::check)) - .route("/api/drivers", get(drivers::get_drivers)) - .layer(cors) - .with_state(app_state) - .into_make_service_with_connect_info::(); - - let addr = addr(); - - info!("running on {}", addr); - - let listener = tokio::net::TcpListener::bind(addr) - .await - .expect("failed to bind to port"); - - axum::serve(listener, app) - .await - .expect("failed to serve http server"); - - Ok(()) -} diff --git a/crates/live/src/server/cors.rs b/crates/live/src/server/cors.rs deleted file mode 100644 index e735766b..00000000 --- a/crates/live/src/server/cors.rs +++ /dev/null @@ -1,10 +0,0 @@ -use axum::http::{HeaderValue, Method}; -use tower_http::cors::CorsLayer; - -pub fn init() -> CorsLayer { - let origin = std::env::var("ORIGIN").expect("no origin env found"); - - CorsLayer::new() - .allow_origin(origin.parse::().unwrap()) - .allow_methods([Method::GET, Method::CONNECT]) -} diff --git a/crates/live/src/server/live.rs b/crates/live/src/server/live.rs deleted file mode 100644 index b42af806..00000000 --- a/crates/live/src/server/live.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::{convert::Infallible, mem, sync::Arc, time::Duration}; - -use axum::{ - extract::State, - response::{sse, Sse}, -}; -use futures::Stream; -use tokio_stream::{wrappers::BroadcastStream, StreamExt}; -use tracing::{debug, info}; - -use data::compression; - -use super::AppState; - -pub async fn sse_handler( - State(state): State>, -) -> Sse>> { - let rx = state.tx.subscribe(); - - debug!("new sse connection"); - info!("connections: {}", state.tx.receiver_count()); - - let initial_stream = futures::stream::once(async { - let initial_state = state.state.lock().unwrap().to_string(); - mem::drop(state); - let initial = compression::deflate(initial_state).unwrap(); - - debug!("streaming current initial"); - - Ok(sse::Event::default().event("initial").data(initial)) - }); - - let updates_stream = BroadcastStream::new(rx) - .filter_map(|msg| msg.ok()) - .map(|msg| sse::Event::default().event(msg.name()).data(msg.inner())) - .map(Ok); - - let stream = initial_stream.chain(updates_stream); - - let keep_alive = sse::KeepAlive::new() - .interval(Duration::from_secs(10)) - .text("keep-alive-text"); - - Sse::new(stream).keep_alive(keep_alive) -} diff --git a/crates/live/src/state.rs b/crates/live/src/state.rs deleted file mode 100644 index 7fa820e9..00000000 --- a/crates/live/src/state.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::{mem, thread, time::Duration}; - -use futures::{pin_mut, Stream}; -use tokio::{sync::broadcast::Sender, time::sleep}; -use tokio_stream::StreamExt; -use tracing::{debug, error, info, trace}; - -use crate::{LiveEvent, LiveState}; - -use client; -use data::{compression, merge::merge, transformer}; - -pub fn manage(tx: Sender, state: LiveState) { - // TODO start and stop on connect and disconnect - - thread::spawn(|| { - let rt = tokio::runtime::Runtime::new().unwrap(); - - rt.block_on(async { - keep_client_alive(tx, state).await; - }) - }); -} - -async fn keep_client_alive(tx: Sender, state: LiveState) { - loop { - if tx.receiver_count() < 2 { - debug!("no connections yet"); - sleep(Duration::from_secs(5)).await; - continue; - } - - info!("starting client..."); - - let stream = client::init().await; - - let stream = match stream { - Ok(stream) => stream, - Err(e) => { - error!("client setup failed, restarting in 5 seconds {}", e); - sleep(Duration::from_secs(5)).await; - continue; - } - }; - - let parsed_stream = client::parse_stream(stream).await; - - handle_stream(parsed_stream, tx.clone(), state.clone()).await; - } -} - -async fn handle_stream( - stream: impl Stream, - tx: Sender, - state: LiveState, -) { - pin_mut!(stream); - - while let Some(message) = stream.next().await { - match message { - client::message::Message::Updates(mut updates) => { - trace!("recived update"); - - let mut state = state.lock().unwrap(); - - for update in updates.iter_mut() { - let update = transformer::transform_map(update); - - if let Some(new_session_name) = update.pointer("/sessionInfo/name") { - let current_session_name = state - .pointer("/sessionInfo/name") - .expect("we always should have a session name"); - - if new_session_name != current_session_name { - info!("session name changed, restarting client"); - return; - } - } - - let Some(update_compressed) = compression::deflate(update.to_string()) else { - error!("failed compressing update"); - continue; - }; - - trace!("update compressed='{}'", update_compressed); - - match tx.send(LiveEvent::Update(update_compressed)) { - Ok(_) => trace!("update sent"), - Err(e) => error!("failed sending update: {}", e), - }; - - merge(&mut state, update) - } - - mem::drop(state); - } - client::message::Message::Initial(mut initial) => { - trace!("recived initial"); - - transformer::transform(&mut initial); - - let mut state = state.lock().unwrap(); - *state = initial.clone(); - mem::drop(state); - - let Some(initial) = compression::deflate(initial.to_string()) else { - error!("failed compressing update"); - continue; - }; - - trace!("initial compressed='{}'", initial); - - match tx.send(LiveEvent::Initial(initial)) { - Ok(_) => trace!("initial sent"), - Err(e) => error!("failed sending initial: {}", e), - }; - } - } - } -} diff --git a/crates/api/Cargo.toml b/services/api/Cargo.toml similarity index 90% rename from crates/api/Cargo.toml rename to services/api/Cargo.toml index 76acf714..d5e2d21f 100644 --- a/crates/api/Cargo.toml +++ b/services/api/Cargo.toml @@ -10,19 +10,21 @@ name = "api" path = "src/main.rs" [dependencies] -env.workspace = true - axum.workspace = true +tower-http.workspace = true + tokio.workspace = true + tracing.workspace = true tracing-subscriber.workspace = true serde.workspace = true serde_json.workspace = true + regex.workspace = true reqwest.workspace = true - anyhow.workspace = true +dotenvy.workspace = true ical = "0.11" chrono = { version = "0.4", features = ["serde"] } diff --git a/crates/api/readme.md b/services/api/readme.md similarity index 100% rename from crates/api/readme.md rename to services/api/readme.md diff --git a/crates/api/src/endpoints/health.rs b/services/api/src/endpoints/health.rs similarity index 100% rename from crates/api/src/endpoints/health.rs rename to services/api/src/endpoints/health.rs diff --git a/crates/api/src/endpoints/schedule.rs b/services/api/src/endpoints/schedule.rs similarity index 100% rename from crates/api/src/endpoints/schedule.rs rename to services/api/src/endpoints/schedule.rs diff --git a/services/api/src/main.rs b/services/api/src/main.rs new file mode 100644 index 00000000..e72048ce --- /dev/null +++ b/services/api/src/main.rs @@ -0,0 +1,56 @@ +use std::env; + +use axum::{ + http::{HeaderValue, Method}, + routing::get, + Router, +}; +use dotenvy::dotenv; +use tokio::net::TcpListener; +use tower_http::cors::CorsLayer; +use tracing::info; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +mod endpoints { + pub(crate) mod health; + pub(crate) mod schedule; +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let _ = dotenv(); + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + let default_addr = "0.0.0.0:4001".to_string(); + let addr = env::var("API_ADDRESS").unwrap_or(default_addr); + + info!(addr, "starting api service"); + + let app = Router::new() + .route("/api/schedule", get(endpoints::schedule::get)) + .route("/api/schedule/next", get(endpoints::schedule::get_next)) + .route("/api/health", get(endpoints::health::check)); + + let listener = TcpListener::bind(addr).await?; + + axum::serve(listener, app).await?; + + Ok(()) +} + +pub fn cors_layer() -> Result { + let origin = env::var("ORIGIN")?; // origins string split by semicolumn + + let origins = origin + .split(';') + .filter_map(|o| HeaderValue::from_str(o).ok()) + .collect::>(); + + Ok(CorsLayer::new() + .allow_origin(origins) + .allow_methods([Method::GET, Method::CONNECT])) +} diff --git a/crates/live/Cargo.toml b/services/live/Cargo.toml similarity index 80% rename from crates/live/Cargo.toml rename to services/live/Cargo.toml index 75824b0d..97946d18 100644 --- a/crates/live/Cargo.toml +++ b/services/live/Cargo.toml @@ -12,29 +12,22 @@ path = "src/main.rs" [dependencies] data.workspace = true client.workspace = true -env.workspace = true anyhow.workspace = true tokio.workspace = true tokio-stream.workspace = true + tracing.workspace = true tracing-subscriber.workspace = true axum.workspace = true tower-http.workspace = true -tokio-tungstenite.workspace = true + reqwest = { version = "0.12.4", features = ["native-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"] } -heck.workspace = true -regex.workspace = true - -base64.workspace = true -flate2.workspace = true - futures.workspace = true dotenvy.workspace = true - diff --git a/crates/live/readme.md b/services/live/readme.md similarity index 100% rename from crates/live/readme.md rename to services/live/readme.md diff --git a/services/live/src/main.rs b/services/live/src/main.rs new file mode 100644 index 00000000..93830012 --- /dev/null +++ b/services/live/src/main.rs @@ -0,0 +1,80 @@ +use std::{ + env, + net::SocketAddr, + sync::{Arc, Mutex}, +}; + +use axum::{ + http::{HeaderValue, Method}, + routing::get, + Router, +}; +use dotenvy::dotenv; +use serde_json::Value; +use tokio::{net::TcpListener, sync::broadcast}; +use tower_http::cors::CorsLayer; +use tracing::info; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +use client::message::Message; + +mod server { + pub mod drivers; + pub mod health; + pub mod live; +} + +pub struct AppState { + tx: broadcast::Sender, + state: Arc>, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let _ = dotenv(); + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + let default_addr = "0.0.0.0:4000".to_string(); + let addr = env::var("LIVE_BACKEND_ADDRESS").unwrap_or(default_addr); + + info!(?addr, "starting live service"); + + let stream = client::manage(); + let (tx, rx) = client::broadcast(stream); + let state = client::keep_state(rx); + + let cors = cors_layer()?; + + let app_state = Arc::new(AppState { tx, state }); + + let app = Router::new() + .route("/api/health", get(server::health::check)) + .route("/api/sse", get(server::live::sse_handler)) + .route("/api/drivers", get(server::drivers::get_drivers)) + .layer(cors) + .with_state(app_state) + .into_make_service_with_connect_info::(); + + let listener = TcpListener::bind(addr).await?; + + axum::serve(listener, app).await?; + + Ok(()) +} + +pub fn cors_layer() -> Result { + let origin = env::var("ORIGIN")?; // origins string split by semicolumn + + let origins = origin + .split(';') + .filter_map(|o| HeaderValue::from_str(o).ok()) + .collect::>(); + + Ok(CorsLayer::new() + .allow_origin(origins) + .allow_methods([Method::GET, Method::CONNECT])) +} diff --git a/crates/live/src/server/drivers.rs b/services/live/src/server/drivers.rs similarity index 82% rename from crates/live/src/server/drivers.rs rename to services/live/src/server/drivers.rs index 0669dcaa..faf70f9b 100644 --- a/crates/live/src/server/drivers.rs +++ b/services/live/src/server/drivers.rs @@ -4,7 +4,7 @@ use axum::extract::State; use serde_json::Value; use tracing::error; -use super::AppState; +use crate::AppState; fn map_to_vec(value: Value) -> Vec { match value { @@ -16,8 +16,9 @@ fn map_to_vec(value: Value) -> Vec { pub async fn get_drivers( State(state): State>, ) -> Result>, axum::http::StatusCode> { - let live_state = state.state.lock().unwrap().clone(); - mem::drop(state); + let state_lock = state.state.lock().unwrap(); + let live_state = state_lock.clone(); + mem::drop(state_lock); match live_state.pointer("/driverList") { Some(drivers) => Ok(axum::Json(map_to_vec(drivers.clone()))), diff --git a/crates/live/src/server/health.rs b/services/live/src/server/health.rs similarity index 100% rename from crates/live/src/server/health.rs rename to services/live/src/server/health.rs diff --git a/services/live/src/server/live.rs b/services/live/src/server/live.rs new file mode 100644 index 00000000..8406eed8 --- /dev/null +++ b/services/live/src/server/live.rs @@ -0,0 +1,72 @@ +use std::{convert::Infallible, mem, sync::Arc, time::Duration}; + +use axum::{ + extract::State, + response::{sse, Sse}, +}; +use client::message::Message; +use futures::Stream; +use serde_json::{json, Map, Value}; +use tokio_stream::{wrappers::BroadcastStream, StreamExt}; +use tracing::{debug, info}; + +use data::merge::merge; + +use crate::AppState; + +// TODO clean this up a bit maybe +fn sse_event(message: Message) -> sse::Event { + let (event, data): (&str, Value) = match message { + Message::Updates(updates) => { + let mut batched_update = json!({}); + + for (topic, update) in updates { + let mut map = Map::new(); + map.insert(topic, update); + merge(&mut batched_update, Value::Object(map)); + } + + // TODO maybe send the updates in array instead of object + + ("update", batched_update) + } + Message::Initial(value) => ("initial", value), + }; + + sse::Event::default().event(event).json_data(data).unwrap() +} + +pub async fn sse_handler( + State(state): State>, +) -> Sse>> { + let rx = state.tx.subscribe(); + let connections = state.tx.receiver_count(); + + info!(connections, "new sse connection"); + + let initial_state_lock = state.state.lock().unwrap(); + let initial_state = initial_state_lock.clone(); + mem::drop(initial_state_lock); + + let initial_stream = futures::stream::once(async { + debug!("streaming current initial"); + + Ok(sse::Event::default() + .event("initial") + .json_data(initial_state) + .unwrap()) + }); + + let updates_stream = BroadcastStream::new(rx) + .filter_map(|msg| msg.ok()) + .map(|message| sse_event(message)) + .map(Ok); + + let stream = initial_stream.chain(updates_stream); + + let keep_alive = sse::KeepAlive::new() + .interval(Duration::from_secs(10)) + .text("keep-alive-text"); + + Sse::new(stream).keep_alive(keep_alive) +} From eea6cd8f5e6297143e04ee70e1879ea7efbf106d Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Tue, 27 May 2025 19:14:31 +0200 Subject: [PATCH 12/23] feat: implement first version of importer and analytics service --- .env.example | 9 +- .github/workflows/release.yaml | 9 +- Cargo.lock | 748 +++++++++++++++++- Cargo.toml | 22 +- DOCKER.md | 4 + compose.yaml | 32 +- crates/client/src/client.rs | 6 +- crates/timescale/Cargo.toml | 23 + .../migrations/20250520180813_initial.sql | 32 + crates/timescale/src/app_timing.rs | 25 + crates/timescale/src/lib.rs | 23 + crates/timescale/src/timing.rs | 107 +++ dash/src/hooks/useSocket.ts | 8 +- docker-bake.hcl | 18 +- dockerfile | 14 + services/analytics/Cargo.toml | 28 + services/analytics/readme.md | 19 + services/analytics/src/main.rs | 74 ++ services/analytics/src/server/gap.rs | 33 + services/analytics/src/server/health.rs | 6 + services/analytics/src/server/laptime.rs | 33 + services/importer/Cargo.toml | 28 + services/importer/src/main.rs | 167 ++++ services/importer/src/models.rs | 331 ++++++++ services/importer/src/parsers.rs | 137 ++++ 25 files changed, 1899 insertions(+), 37 deletions(-) create mode 100644 crates/timescale/Cargo.toml create mode 100644 crates/timescale/migrations/20250520180813_initial.sql create mode 100644 crates/timescale/src/app_timing.rs create mode 100644 crates/timescale/src/lib.rs create mode 100644 crates/timescale/src/timing.rs create mode 100644 services/analytics/Cargo.toml create mode 100644 services/analytics/readme.md create mode 100644 services/analytics/src/main.rs create mode 100644 services/analytics/src/server/gap.rs create mode 100644 services/analytics/src/server/health.rs create mode 100644 services/analytics/src/server/laptime.rs create mode 100644 services/importer/Cargo.toml create mode 100644 services/importer/src/main.rs create mode 100644 services/importer/src/models.rs create mode 100644 services/importer/src/parsers.rs diff --git a/.env.example b/.env.example index 7b91545b..5983cd8e 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,15 @@ # the address of the simulator backend, found in crates/simulator SIMULATOR_BACKEND_ADDRESS=localhost:8000 -# the address of the api backend, found in crates/api -API_BACKEND_ADDRESS=localhost:4001 -# the address of the live backend, found in crates/live -LIVE_BACKEND_ADDRESS=localhost:4000 +LIVE_ADDRESS=localhost:4000 +API_ADDRESS=localhost:4001 +ANALYTICS_ADDRESS=localhost:4002 # the origin of the frontend, found in dash/ ORIGIN=http://localhost:3000 -# by setting WS_URL you are telling the live backend to connect to this address +# by setting WS_URL you are telling the live backend to connect to this address # (preferably from the simualtor) and not to the f1 address # WS_URL=ws://localhost:8000/ws diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2bd8efef..3ec50294 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -19,7 +19,14 @@ jobs: strategy: matrix: - image: [f1-dash, f1-dash-api, f1-dash-live] + image: + [ + f1-dash, + f1-dash-api, + f1-dash-live, + f1-dash-importer, + f1-dash-analytics, + ] permissions: contents: read diff --git a/Cargo.lock b/Cargo.lock index d42b584b..46fe447c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,23 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "analytics" +version = "0.3.0" +dependencies = [ + "anyhow", + "axum", + "dotenvy", + "serde", + "serde_json", + "sqlx", + "timescale", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -96,6 +113,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -186,6 +212,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + [[package]] name = "bitflags" version = "1.3.2" @@ -197,6 +229,9 @@ name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -310,6 +345,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -335,6 +385,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -353,6 +418,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -420,6 +494,17 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -427,7 +512,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -468,6 +555,15 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -493,6 +589,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -509,12 +627,29 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -591,6 +726,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.12.3", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -720,6 +866,20 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", +] [[package]] name = "heck" @@ -727,6 +887,39 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.3.1" @@ -1025,6 +1218,23 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "importer" +version = "0.3.0" +dependencies = [ + "anyhow", + "client", + "dotenvy", + "serde", + "serde_json", + "sqlx", + "timescale", + "tokio", + "tokio-stream", + "tracing", + "tracing-subscriber", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -1071,6 +1281,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1078,6 +1291,12 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.3" @@ -1088,6 +1307,16 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1151,6 +1380,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1210,6 +1449,43 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1217,6 +1493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1290,6 +1567,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.11.2" @@ -1344,6 +1627,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1362,6 +1654,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1401,14 +1714,35 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -1418,7 +1752,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", ] [[package]] @@ -1583,6 +1926,26 @@ dependencies = [ "serde", ] +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1767,6 +2130,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1791,6 +2165,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simulator" version = "0.2.1" @@ -1832,6 +2216,9 @@ name = "smallvec" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1843,12 +2230,234 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.2", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags 2.9.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.12", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags 2.9.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.12", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.12", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1976,6 +2585,19 @@ dependencies = [ "once_cell", ] +[[package]] +name = "timescale" +version = "0.3.0" +dependencies = [ + "anyhow", + "chrono", + "serde", + "serde_json", + "sqlx", + "tokio", + "tracing", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1986,6 +2608,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.44.2" @@ -2197,7 +2834,7 @@ dependencies = [ "httparse", "log", "native-tls", - "rand", + "rand 0.9.1", "sha1", "thiserror 2.0.12", "url", @@ -2210,12 +2847,33 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "untrusted" version = "0.9.0" @@ -2293,6 +2951,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2384,6 +3048,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall 0.5.11", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2485,6 +3159,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2503,6 +3186,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2535,6 +3233,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2547,6 +3251,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2559,6 +3269,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2583,6 +3299,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2595,6 +3317,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2607,6 +3335,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2619,6 +3353,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index dbbbffa3..f1eabd85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,20 @@ [workspace] members = [ - "crates/data", # lib - "crates/client", # lib - # "crates/timescale", # lib - "services/api", # bin - service - "services/live", # bin - service - # "services/analytics", # bin - service - # "services/importer", # bin - service - "crates/saver", # bin - util - "crates/simulator", # bin - util + "crates/data", # lib + "crates/client", # lib + "crates/timescale", # lib + "services/api", # bin - service + "services/live", # bin - service + "services/analytics", # bin - service + "services/importer", # bin - service + "crates/saver", # bin - util + "crates/simulator", # bin - util ] default-members = [ "services/live", "services/api", - # "services/analytics", - # "services/importer", + "services/analytics", + "services/importer", ] resolver = "2" diff --git a/DOCKER.md b/DOCKER.md index f1f6c43a..98ed516f 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -1,5 +1,9 @@ # Docker and Docker Compose +> [!NOTE] +> The docker compose is not meant as a porduction deployment rather a tool to setup f1-dash localy for testing or development. +> If you want to deploy f1-dash, it can be used as a base but + To substitute the environment variables in compose.yaml, use `--env-file` flag as follows: ```bash diff --git a/compose.yaml b/compose.yaml index af615314..803b7c78 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,7 +9,7 @@ services: - 4000:4000 environment: - ORIGIN=http://localhost:3000 - - LIVE_BACKEND_ADDRESS=0.0.0.0:4000 + - LIVE_ADDRESS=0.0.0.0:4000 - RUST_LOG="live=debug" api: @@ -22,7 +22,7 @@ services: - 4001:4001 environment: - ORIGIN=http://localhost:3000 - - API_BACKEND_ADDRESS=0.0.0.0:4001 + - API_ADDRESS=0.0.0.0:4001 - RUST_LOG="api=debug" frontend: @@ -32,7 +32,6 @@ services: restart: unless-stopped ports: - 3000:3000 - depends_on: - api - live @@ -42,7 +41,28 @@ services: - DISABLE_IFRAME=1 - - TRACKING_ID=G-3ZKX0JY1QW - - TRACKING_URL=https://base.slowly.dev/rep.js + timescaledb: + image: timescale/timescaledb:latest-pg16 + ports: + - 5432:5432 + restart: unless-stopped + environment: + - POSTGRES_PASSWORD=password + + importer: + image: ghcr.io/slowlydev/f1-dash-importer:latest + build: + context: . + target: importer + environment: + - DATABASE_URL=postgres://postgres:password@timescaledb:5432/postgres - - NEXT_PUBLIC_MAP_KEY=lcgTreisBzMYLrFFSTK2 + analytics: + image: ghcr.io/slowlydev/f1-dash-analytics:latest + build: + context: . + target: analytics + environment: + - ORIGIN=http://localhost:3000 + - ANALYTICS_ADDRESS=0.0.0.0:4002 + - RUST_LOG="analytics=debug" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 746af5e7..58608c30 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -9,7 +9,7 @@ use serde_json::Value; use tokio_stream::{Stream, StreamExt}; use tokio_tungstenite::tungstenite::client::IntoClientRequest; use tokio_tungstenite::{tungstenite::http::Request, MaybeTlsStream, WebSocketStream}; -use tracing::{debug, trace}; +use tracing::{debug, info, trace}; pub use tokio_tungstenite::tungstenite; @@ -41,13 +41,13 @@ pub async fn init() -> Result> { let (mut socket, _) = tokio_tungstenite::connect_async(req).await?; - debug!("connected"); + info!("connected"); socket .send(tungstenite::Message::text(consts::SIGNALR_SUBSCRIBE)) .await?; - debug!("subscribed"); + info!("subscribed"); Ok(socket) } diff --git a/crates/timescale/Cargo.toml b/crates/timescale/Cargo.toml new file mode 100644 index 00000000..92d40707 --- /dev/null +++ b/crates/timescale/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "timescale" +version = "0.3.0" +edition = "2021" +publish = false +authors = ["slowlydev"] + +[lib] +name = "timescale" +path = "src/lib.rs" + +[dependencies] +tokio.workspace = true + +tracing.workspace = true + +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true + +sqlx.workspace = true + +chrono = { version = "0.4", features = ["serde"] } diff --git a/crates/timescale/migrations/20250520180813_initial.sql b/crates/timescale/migrations/20250520180813_initial.sql new file mode 100644 index 00000000..a1bf03dc --- /dev/null +++ b/crates/timescale/migrations/20250520180813_initial.sql @@ -0,0 +1,32 @@ +create table timing_driver ( + time timestamptz not null default now(), + nr text not null, + lap integer, + + -- timing data + gap bigint, + leader_gap bigint, + + laptime bigint, + sector_1 bigint, + sector_2 bigint, + sector_3 bigint +); + +create index on timing_driver (nr, time desc); +select create_hypertable('timing_driver', 'time'); +select add_retention_policy('timing_driver', interval '4 hours'); + +create table tire_driver ( + time timestamptz not null default now(), + nr text not null, + lap integer, + + -- timing app data + compound text, + laps integer +); + +create index on tire_driver (nr, time desc); +select create_hypertable('tire_driver', 'time'); +select add_retention_policy('tire_driver', interval '4 hours'); diff --git a/crates/timescale/src/app_timing.rs b/crates/timescale/src/app_timing.rs new file mode 100644 index 00000000..19bfd3b1 --- /dev/null +++ b/crates/timescale/src/app_timing.rs @@ -0,0 +1,25 @@ +use sqlx::PgPool; + +pub struct TireDriver { + pub nr: String, + pub lap: Option, + pub compound: String, + pub laps: i32, +} + +pub async fn insert_tire_driver(pool: &PgPool, driver: TireDriver) -> Result<(), anyhow::Error> { + sqlx::query( + r#" + insert into tire_driver (nr, lap, compound, laps) + values ($1, $2, $3, $4) + "#, + ) + .bind(driver.nr) + .bind(driver.lap) + .bind(driver.compound) + .bind(driver.laps) + .execute(pool) + .await?; + + Ok(()) +} diff --git a/crates/timescale/src/lib.rs b/crates/timescale/src/lib.rs new file mode 100644 index 00000000..bd39d606 --- /dev/null +++ b/crates/timescale/src/lib.rs @@ -0,0 +1,23 @@ +use sqlx::{postgres::PgPoolOptions, PgPool}; +use std::env; + +pub mod app_timing; +pub mod timing; + +// pub use app_timing::insert_tire_driver; +// pub use timing::{get_laptimes, insert_timing_driver}; + +pub async fn init_timescaledb(migrate: bool) -> Result { + let database_url = env::var("DATABASE_URL")?; + + let pool = PgPoolOptions::new() + .max_connections(10) + .connect(&database_url) + .await?; + + if migrate { + sqlx::migrate!().run(&pool).await?; + } + + Ok(pool) +} diff --git a/crates/timescale/src/timing.rs b/crates/timescale/src/timing.rs new file mode 100644 index 00000000..03ecd485 --- /dev/null +++ b/crates/timescale/src/timing.rs @@ -0,0 +1,107 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; + +pub struct TimingDriver { + pub nr: String, + pub lap: Option, + pub gap: i64, + pub leader_gap: i64, + pub laptime: i64, + pub sector_1: i64, + pub sector_2: i64, + pub sector_3: i64, +} + +pub async fn insert_timing_driver( + pool: &PgPool, + driver: TimingDriver, +) -> Result<(), anyhow::Error> { + sqlx::query!( + r#" + insert into timing_driver (nr, lap, gap, leader_gap, laptime, sector_1, sector_2, sector_3) + values ($1, $2, $3, $4, $5, $6, $7, $8) + "#, + driver.nr, + driver.lap, + driver.gap, + driver.leader_gap, + driver.laptime, + driver.sector_1, + driver.sector_2, + driver.sector_3 + ) + .execute(pool) + .await?; + + Ok(()) +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Laptime { + pub time: DateTime, + pub lap: Option, + pub laptime: i64, +} + +pub async fn get_laptimes(pool: &PgPool, nr: &str) -> Result, anyhow::Error> { + let laptimes = sqlx::query!( + r#" + select + lap, + min(laptime) AS "laptime!", + min(time) AS "time!" + from + timing_driver + where + nr = $1 + and laptime != 0 + group by + lap + order by + lap; + "#, + nr + ) + .map(|row| Laptime { + time: row.time, + lap: row.lap, + laptime: row.laptime, + }) + .fetch_all(pool) + .await?; + + Ok(laptimes) +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Gap { + pub time: DateTime, + pub gap: i64, +} + +pub async fn get_gaps(pool: &PgPool, nr: &str) -> Result, anyhow::Error> { + let gaps = sqlx::query!( + r#" + select + gap as "gap!", + time as "time!" + from + timing_driver + where + nr = $1 + and gap != 0 + "#, + nr + ) + .map(|row| Gap { + time: row.time, + gap: row.gap, + }) + .fetch_all(pool) + .await?; + + Ok(gaps) +} diff --git a/dash/src/hooks/useSocket.ts b/dash/src/hooks/useSocket.ts index 344cbab1..20b67f01 100644 --- a/dash/src/hooks/useSocket.ts +++ b/dash/src/hooks/useSocket.ts @@ -2,8 +2,6 @@ import { useEffect, useState } from "react"; import type { MessageInitial, MessageUpdate } from "@/types/message.type"; -import { inflate } from "@/lib/inflate"; - import { env } from "@/env"; type Props = { @@ -21,13 +19,11 @@ export const useSocket = ({ handleInitial, handleUpdate }: Props) => { sse.onopen = () => setConnected(true); sse.addEventListener("initial", (message) => { - const decompressed = inflate(message.data); - handleInitial(decompressed); + handleInitial(JSON.parse(message.data)); }); sse.addEventListener("update", (message) => { - const decompressed = inflate(message.data); - handleUpdate(decompressed); + handleUpdate(JSON.parse(message.data)); }); return () => sse.close(); diff --git a/docker-bake.hcl b/docker-bake.hcl index bb406577..674d7686 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -1,5 +1,5 @@ group "default" { - targets = ["f1-dash", "f1-dash-live", "f1-dash-api"] + targets = ["f1-dash", "f1-dash-live", "f1-dash-api", "f1-dash-importer", "f1-dash-analytics"] } target "docker-metadata-action" {} @@ -28,3 +28,19 @@ target "f1-dash-live" { dockerfile = "dockerfile" target = "live" } + +target "f1-dash-importer" { + inherits = ["docker-metadata-action"] + + context = "." + dockerfile = "dockerfile" + target = "importer" +} + +target "f1-dash-analytics" { + inherits = ["docker-metadata-action"] + + context = "." + dockerfile = "dockerfile" + target = "analytics" +} diff --git a/dockerfile b/dockerfile index 844801b6..7d955c02 100644 --- a/dockerfile +++ b/dockerfile @@ -12,6 +12,12 @@ RUN cargo b -r -p api FROM builder-base AS live-builder RUN cargo b -r -p live +FROM builder-base AS importer-builder +RUN cargo b -r -p importer + +FROM builder-base AS analytics-builder +RUN cargo b -r -p analytics + FROM alpine:3 AS api COPY --from=api-builder /usr/src/app/target/release/api /api CMD [ "/api" ] @@ -19,3 +25,11 @@ CMD [ "/api" ] FROM alpine:3 AS live COPY --from=live-builder /usr/src/app/target/release/live /live CMD [ "/live" ] + +FROM alpine:3 AS importer +COPY --from=importer-builder /usr/src/app/target/release/importer /importer +CMD [ "/importer" ] + +FROM alpine:3 AS analytics +COPY --from=analytics-builder /usr/src/app/target/release/analytics /analytics +CMD [ "/analytics" ] diff --git a/services/analytics/Cargo.toml b/services/analytics/Cargo.toml new file mode 100644 index 00000000..daa3698e --- /dev/null +++ b/services/analytics/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "analytics" +version = "0.3.0" +edition = "2021" +publish = false +authors = ["slowlydev"] + +[[bin]] +name = "analytics" +path = "src/main.rs" + +[dependencies] +axum.workspace = true +tower-http.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true + +serde.workspace = true +serde_json.workspace = true + +anyhow.workspace = true +dotenvy.workspace = true + +timescale.workspace = true +sqlx.workspace = true + +# chrono = { version = "0.4", features = ["serde"] } diff --git a/services/analytics/readme.md b/services/analytics/readme.md new file mode 100644 index 00000000..06117559 --- /dev/null +++ b/services/analytics/readme.md @@ -0,0 +1,19 @@ +# analytics + +saves live timing data in a timeseries database and provides analytics of said data + +## usage + +```bash +cargo r -p analytics +``` + +you can set the port, address and log level with these env vars + +```bash +# the address and port where it starts +ANALYTICS_BACKEND_ADDRESS=localhost:4002 + +# sets the rust log level +RUST_LOG="api=debug,info" +``` diff --git a/services/analytics/src/main.rs b/services/analytics/src/main.rs new file mode 100644 index 00000000..a6d8c534 --- /dev/null +++ b/services/analytics/src/main.rs @@ -0,0 +1,74 @@ +use std::{env, sync::Arc}; + +use axum::{ + http::{HeaderValue, Method}, + routing::get, + Router, +}; +use dotenvy::dotenv; +use sqlx::PgPool; +use tokio::net::TcpListener; +use tower_http::cors::CorsLayer; +use tracing::info; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +use timescale::init_timescaledb; + +use server::{gap::get_driver_gap, health::healt_check, laptime::get_driver_laptimes}; + +mod server { + pub mod gap; + pub mod health; + pub mod laptime; +} + +pub struct AppState { + pool: PgPool, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let _ = dotenv(); + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + let default_addr = "0.0.0.0:4002".to_string(); + let addr = env::var("ANALYTICS_ADDRESS").unwrap_or(default_addr); + + info!(addr, "starting analytics service"); + + let pool = init_timescaledb(false).await?; + + let cors = cors_layer()?; + + let app_state = Arc::new(AppState { pool }); + + let app = Router::new() + .route("/api/health", get(healt_check)) + .route("/api/laptime/{driver_nr}", get(get_driver_laptimes)) + .route("/api/gap/{driver_nr}", get(get_driver_gap)) + .layer(cors) + .with_state(app_state); + + let listener = TcpListener::bind(addr).await?; + + axum::serve(listener, app).await?; + + Ok(()) +} + +pub fn cors_layer() -> Result { + let origin = env::var("ORIGIN")?; // origins string split by semicolumn + + let origins = origin + .split(';') + .filter_map(|o| HeaderValue::from_str(o).ok()) + .collect::>(); + + Ok(CorsLayer::new() + .allow_origin(origins) + .allow_methods([Method::GET, Method::CONNECT])) +} diff --git a/services/analytics/src/server/gap.rs b/services/analytics/src/server/gap.rs new file mode 100644 index 00000000..482a3850 --- /dev/null +++ b/services/analytics/src/server/gap.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use timescale::timing::{get_gaps, Gap}; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, +}; + +use tracing::error; + +use crate::AppState; + +#[derive(serde::Deserialize)] +pub struct Params { + driver_nr: String, +} + +pub async fn get_driver_gap( + State(app_state): State>, + Path(Params { driver_nr }): Path, +) -> Result>, StatusCode> { + let gaps = get_gaps(&app_state.pool, &driver_nr).await; + + match gaps { + Ok(gaps) => Ok(Json(gaps)), + Err(error) => { + error!(?error, driver_nr, "failed to get gaps"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } +} diff --git a/services/analytics/src/server/health.rs b/services/analytics/src/server/health.rs new file mode 100644 index 00000000..178ff630 --- /dev/null +++ b/services/analytics/src/server/health.rs @@ -0,0 +1,6 @@ +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde_json::json; + +pub async fn healt_check() -> impl IntoResponse { + (StatusCode::OK, Json(json!({ "success": true }))) +} diff --git a/services/analytics/src/server/laptime.rs b/services/analytics/src/server/laptime.rs new file mode 100644 index 00000000..a4693535 --- /dev/null +++ b/services/analytics/src/server/laptime.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use timescale::timing::{get_laptimes, Laptime}; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, +}; + +use tracing::error; + +use crate::AppState; + +#[derive(serde::Deserialize)] +pub struct Params { + driver_nr: String, +} + +pub async fn get_driver_laptimes( + State(app_state): State>, + Path(Params { driver_nr }): Path, +) -> Result>, StatusCode> { + let laptimes = get_laptimes(&app_state.pool, &driver_nr).await; + + match laptimes { + Ok(laptimes) => Ok(Json(laptimes)), + Err(error) => { + error!(?error, driver_nr, "failed to get laptimes"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } +} diff --git a/services/importer/Cargo.toml b/services/importer/Cargo.toml new file mode 100644 index 00000000..4f51fedb --- /dev/null +++ b/services/importer/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "importer" +version = "0.3.0" +edition = "2021" +publish = false +authors = ["slowlydev"] + +[[bin]] +name = "importer" +path = "src/main.rs" + +[dependencies] +client.workspace = true +timescale.workspace = true + +tokio.workspace = true +tokio-stream.workspace = true + +tracing.workspace = true +tracing-subscriber.workspace = true + +serde.workspace = true +serde_json.workspace = true + +anyhow.workspace = true +dotenvy.workspace = true + +sqlx.workspace = true diff --git a/services/importer/src/main.rs b/services/importer/src/main.rs new file mode 100644 index 00000000..be290a2f --- /dev/null +++ b/services/importer/src/main.rs @@ -0,0 +1,167 @@ +use anyhow::Error; +use dotenvy::dotenv; +use serde_json::Value; +use sqlx::PgPool; +use tracing::{info, trace}; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +use client::message::Message; + +use timescale::{ + app_timing::{insert_tire_driver, TireDriver}, + init_timescaledb, + timing::{insert_timing_driver, TimingDriver}, +}; + +use models::State; +use parsers::{parse_timing_driver, parse_tire_driver}; + +mod models; +mod parsers; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let _ = dotenv(); + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + info!("starting importer service"); + + let pool = init_timescaledb(true).await?; + + let stream = client::manage(); + let (tx, rx) = client::broadcast(stream); + let state = client::keep_state(rx); + + let mut message_rx = tx.subscribe(); + + while let Ok(message) = message_rx.recv().await { + match message { + Message::Updates(updates) => { + trace!(?updates, "recived updates, saving"); + + let currnet_state = state.lock().unwrap().clone(); + + let _ = parse_update(&pool, currnet_state, updates).await; + } + Message::Initial(initial) => { + trace!(?initial, "recived initial, saving"); + + let _ = save_initial_state(&pool, initial).await; + } + } + } + + Ok(()) +} + +async fn parse_update( + pool: &PgPool, + state: Value, + updates: Vec<(String, Value)>, +) -> Result<(), Error> { + // check for TIMING_TOPICS + // check for every driver that has a update and fill the rest + + let state = serde_json::from_value::(state)?; + + for (topic, update) in updates { + match &topic[..] { + "timingData" => { + let Some(drivers) = parse_timing_update(&state, update).await else { + continue; + }; + + for driver in drivers { + let _ = insert_timing_driver(pool, driver).await; + } + } + "timingAppData" => { + let Some(drivers) = parse_tire_update(&state, update).await else { + continue; + }; + + for driver in drivers { + let _ = insert_tire_driver(pool, driver).await; + } + } + _ => {} + } + } + + Ok(()) +} + +async fn parse_timing_update(state: &State, update: Value) -> Option> { + // for every driver in the update parse driver and use state for None values + let timing_data = state.timing_data.as_ref()?; + let lap = state.lap_count.as_ref()?.current_lap; + + let mut drivers = vec![]; + + for (nr, update_driver) in update["lines"].as_object()?.into_iter() { + let Some(driver) = timing_data.lines.get(nr) else { + continue; + }; + + let Some(driver) = parse_timing_driver(nr, Some(lap), driver, Some(update_driver)) else { + continue; + }; + + drivers.push(driver); + } + + Some(drivers) +} + +async fn parse_tire_update(state: &State, update: Value) -> Option> { + let timing_app_data = state.timing_app_data.as_ref()?; + let lap = state.lap_count.as_ref()?.current_lap; + + let mut drivers = vec![]; + + for (nr, update_driver) in update["lines"].as_object()?.into_iter() { + let Some(driver) = timing_app_data.lines.get(nr) else { + continue; + }; + + let Some(driver) = parse_tire_driver(nr, Some(lap), driver, Some(update_driver)) else { + continue; + }; + + drivers.push(driver); + } + + Some(drivers) +} + +async fn save_initial_state(pool: &PgPool, state: Value) -> Result<(), Error> { + let state = serde_json::from_value::(state)?; + + let lap = state.lap_count.and_then(|v| Some(v.current_lap)); + + if let Some(timing_data) = state.timing_data { + for (nr, driver) in timing_data.lines.iter() { + let Some(timing_driver) = parse_timing_driver(&nr, lap, &driver, None) else { + continue; + }; + + let _ = insert_timing_driver(pool, timing_driver).await; + } + } + + if let Some(app_timing_data) = state.timing_app_data { + for (nr, driver) in app_timing_data.lines.iter() { + let Some(tire_driver) = parse_tire_driver(&nr, lap, &driver, None) else { + continue; + }; + + let _ = insert_tire_driver(pool, tire_driver).await; + } + } + + Ok(()) +} diff --git a/services/importer/src/models.rs b/services/importer/src/models.rs new file mode 100644 index 00000000..9108109c --- /dev/null +++ b/services/importer/src/models.rs @@ -0,0 +1,331 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct State { + // pub heartbeat: Option, + // pub extrapolated_clock: Option, + // pub top_three: Option, + // pub timing_stats: Option, + pub timing_app_data: Option, + // pub weather_data: Option, + // pub track_status: Option, + // pub session_status: Option, + // pub driver_list: Option>, + // pub race_control_messages: Option, + // pub session_info: Option, + // pub session_data: Option, + pub lap_count: Option, + pub timing_data: Option, + // pub team_radio: Option, + // pub championship_prediction: Option, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TimingStats { + pub withheld: bool, + pub lines: HashMap, + pub session_type: String, + #[serde(rename = "_kf")] + pub kf: bool, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TimingAppData { + pub lines: HashMap, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TimingAppDataDriver { + pub racing_number: String, + pub stints: Vec, + pub line: i32, + pub grid_pos: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Stint { + pub total_laps: Option, + pub compound: Option, // "SOFT" | "MEDIUM" | "HARD" | "INTERMEDIATE" | "WET" + #[serde(rename = "new")] + pub is_new: Option, // "TRUE" | "FALSE" +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WeatherData { + pub air_temp: String, + pub humidity: String, + pub pressure: String, + pub rainfall: String, + pub track_temp: String, + pub wind_direction: String, + pub wind_speed: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TrackStatus { + pub status: String, + pub message: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionStatus { + pub status: String, // "Started" | "Finished" | "Finalised" | "Ends" +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Driver { + pub racing_number: String, + pub broadcast_name: String, + pub full_name: String, + pub tla: String, + pub line: i32, + pub team_name: String, + pub team_colour: String, + pub first_name: String, + pub last_name: String, + pub reference: String, + pub headshot_url: String, + pub country_code: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LapCount { + pub current_lap: i32, + pub total_laps: i32, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TimingData { + pub no_entries: Option>, + pub session_part: Option, + pub cut_off_time: Option, + pub cut_off_percentage: Option, + pub lines: HashMap, + pub withheld: bool, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TimingDataDriver { + pub stats: Option>, + pub time_diff_to_fastest: Option, + pub time_diff_to_position_ahead: Option, + pub gap_to_leader: String, + pub interval_to_position_ahead: Option, + pub line: i32, + // pub position: String, + // pub show_position: bool, + pub racing_number: String, + // pub retired: bool, + // pub in_pit: bool, + // pub pit_out: bool, + // pub stopped: bool, + // pub status: i32, + pub sectors: Vec, + // pub speeds: Speeds, + pub best_lap_time: PersonalBestLapTime, + pub last_lap_time: I1, + // pub number_of_laps: i32, + // pub knocked_out: Option, + // pub cutoff: Option, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Stats { + pub time_diff_to_fastest: String, + pub time_diff_to_position_ahead: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IntervalToPositionAhead { + pub value: String, + pub catching: bool, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Sector { + pub stopped: bool, + pub value: String, + pub previous_value: Option, + pub status: i32, + pub overall_fastest: bool, + pub personal_fastest: bool, + pub segments: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Segment { + pub status: i32, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Speeds { + pub i1: I1, + pub i2: I1, + pub fl: I1, + pub st: I1, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct I1 { + pub value: String, + pub status: i32, + pub overall_fastest: bool, + pub personal_fastest: bool, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TimingStatsDriver { + pub line: i32, + pub racing_number: String, + pub personal_best_lap_time: PersonalBestLapTime, + pub best_sectors: Vec, + pub best_speeds: BestSpeeds, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BestSpeeds { + pub i1: PersonalBestLapTime, + pub i2: PersonalBestLapTime, + pub fl: PersonalBestLapTime, + pub st: PersonalBestLapTime, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PersonalBestLapTime { + pub value: String, + // pub position: i32, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TopThreeDriver { + pub position: String, + pub show_position: bool, + pub racing_number: String, + pub tla: String, + pub broadcast_name: String, + pub full_name: String, + pub team: String, + pub team_colour: String, + pub lap_time: String, + pub lap_state: i32, + pub diff_to_ahead: String, + pub diff_to_leader: String, + pub overall_fastest: bool, + pub personal_fastest: bool, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TeamRadio { + pub captures: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RadioCapture { + pub utc: String, + pub racing_number: String, + pub path: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChampionshipPrediction { + pub drivers: HashMap, + pub teams: HashMap, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChampionshipDriver { + pub racing_number: String, + pub current_position: i32, + pub predicted_position: i32, + pub current_points: i32, + pub predicted_points: i32, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChampionshipTeam { + pub team_name: String, + pub current_position: i32, + pub predicted_position: i32, + pub current_points: i32, + pub predicted_points: i32, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Position { + pub position: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PositionItem { + pub timestamp: String, + pub entries: HashMap, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PositionCar { + pub status: String, + pub x: f64, + pub y: f64, + pub z: f64, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CarData { + pub entries: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Entry { + pub utc: String, + pub cars: HashMap, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CarDataChannels { + #[serde(rename = "0")] + pub rpm: i32, + #[serde(rename = "2")] + pub speed: i32, + #[serde(rename = "3")] + pub gear: i32, + #[serde(rename = "4")] + pub throttle: i32, + #[serde(rename = "5")] + pub brake: i32, + #[serde(rename = "45")] + pub drs: i32, +} diff --git a/services/importer/src/parsers.rs b/services/importer/src/parsers.rs new file mode 100644 index 00000000..28025dde --- /dev/null +++ b/services/importer/src/parsers.rs @@ -0,0 +1,137 @@ +use serde_json::Value; +use timescale::{app_timing::TireDriver, timing::TimingDriver}; +use tracing::trace; + +use crate::models::{TimingAppDataDriver, TimingDataDriver}; + +// "LAP1" / "" / "+0.273" / "1L" / "20L" +pub fn parse_gap(gap: String) -> i64 { + if gap.is_empty() { + trace!(gap, "gap empty"); + return 0; + } + if gap.contains("L") { + trace!(gap, "gap contains L"); + return 0; + } + if let Ok(ms) = gap.replace("+", "").parse::() { + trace!(gap, ms, "gap parsed"); + return (ms * 1000.0) as i64; + } + + trace!(gap, "gap failed to parse"); + + return 0; +} + +// "1:21.306" / "" +pub fn parse_laptime(lap: String) -> i64 { + if lap.is_empty() { + trace!(lap, "laptime empty"); + return 0; + } + let parts: Vec<&str> = lap.split(':').collect(); + if parts.len() == 2 { + if let (Ok(minutes), Ok(seconds)) = (parts[0].parse::(), parts[1].parse::()) { + trace!(lap, "laptime parsed"); + return minutes * 60_000 + (seconds * 1000.0) as i64; + } + } + trace!(lap, "laptime failed to parse"); + return 0; +} + +// "26.259" / "" +pub fn parse_sector(sector: String) -> i64 { + if sector.is_empty() { + trace!(sector, "sector empty"); + return 0; + } + if let Ok(seconds) = sector.parse::() { + trace!(sector, "sector parsed"); + return (seconds * 1000.0) as i64; + } + trace!(sector, "sector failed to parse"); + return 0; +} + +fn str_pointer<'a>(update: Option<&'a Value>, pointer: &str) -> Option<&'a str> { + update + .and_then(|v| v.pointer(pointer)) + .and_then(|v| v.as_str()) +} + +pub fn parse_timing_driver( + nr: &String, + lap: Option, + driver: &TimingDataDriver, + update: Option<&Value>, +) -> Option { + let gap = str_pointer(update, "/intervalToPositionAhead/value"); + let leader_gap = str_pointer(update, "/gapToLeader"); + + let laptime = str_pointer(update, "/lastLaptime/value"); + + let sector_1 = str_pointer(update, "/sectors/0/value"); + let sector_2 = str_pointer(update, "/sectors/1/value"); + let sector_3 = str_pointer(update, "/sectors/2/value"); + + if gap.is_some() + || leader_gap.is_some() + || laptime.is_some() + || sector_1.is_some() + || sector_2.is_some() + || sector_3.is_some() + { + return None; + } + + Some(TimingDriver { + nr: nr.clone(), + lap, + gap: parse_gap( + gap.unwrap_or(&driver.interval_to_position_ahead.as_ref().unwrap().value) + .to_string(), + ), + leader_gap: parse_gap(leader_gap.unwrap_or(&driver.gap_to_leader).to_string()), + laptime: parse_laptime(laptime.unwrap_or(&driver.last_lap_time.value).to_string()), + sector_1: parse_sector(sector_1.unwrap_or(&driver.sectors[0].value).to_string()), + sector_2: parse_sector(sector_2.unwrap_or(&driver.sectors[1].value).to_string()), + sector_3: parse_sector(sector_3.unwrap_or(&driver.sectors[2].value).to_string()), + }) +} + +pub fn parse_tire_driver( + nr: &String, + lap: Option, + driver: &TimingAppDataDriver, + update: Option<&Value>, +) -> Option { + let update_stint = update + .and_then(|v| v.pointer("/stints")) + .and_then(|v| v.as_array()) + .and_then(|v| v.last()); + + let last_stint = driver.stints.last(); + + let compound = update_stint + .and_then(|v| v.get("compound")) + .and_then(|v| v.as_str()); + + let laps = update_stint + .and_then(|v| v.get("totalLaps")) + .and_then(|v| v.as_i64()); + + if compound.is_some() || laps.is_some() { + return None; + } + + Some(TireDriver { + nr: nr.clone(), + lap, + compound: compound + .unwrap_or(last_stint.unwrap().compound.as_ref().unwrap()) + .to_string(), + laps: laps.unwrap_or(last_stint.unwrap().total_laps.unwrap().clone() as i64) as i32, + }) +} From 14162c63f165197b7abc11d23bb93d801f5ce521 Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Tue, 27 May 2025 19:54:55 +0200 Subject: [PATCH 13/23] fix(timescale): add sqlx cache --- ...2903fb29d257e5edb641211ee1148637b33d5.json | 28 +++++++++++++++ ...c8bce1bd85adf440a424f35ac774b5535cb39.json | 34 +++++++++++++++++++ ...2d507d58a569841601cafcd7d486a245f62fe.json | 21 ++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 crates/timescale/.sqlx/query-242dca2d8a422e6043b05db01d42903fb29d257e5edb641211ee1148637b33d5.json create mode 100644 crates/timescale/.sqlx/query-558382d26b558498dea63c4afdcc8bce1bd85adf440a424f35ac774b5535cb39.json create mode 100644 crates/timescale/.sqlx/query-c899c95cf53a6b6d0d4e3a0b96b2d507d58a569841601cafcd7d486a245f62fe.json diff --git a/crates/timescale/.sqlx/query-242dca2d8a422e6043b05db01d42903fb29d257e5edb641211ee1148637b33d5.json b/crates/timescale/.sqlx/query-242dca2d8a422e6043b05db01d42903fb29d257e5edb641211ee1148637b33d5.json new file mode 100644 index 00000000..ee3769b6 --- /dev/null +++ b/crates/timescale/.sqlx/query-242dca2d8a422e6043b05db01d42903fb29d257e5edb641211ee1148637b33d5.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n select\n gap as \"gap!\",\n time as \"time!\"\n from\n timing_driver\n where\n nr = $1\n and gap != 0\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "gap!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "time!", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true, + false + ] + }, + "hash": "242dca2d8a422e6043b05db01d42903fb29d257e5edb641211ee1148637b33d5" +} diff --git a/crates/timescale/.sqlx/query-558382d26b558498dea63c4afdcc8bce1bd85adf440a424f35ac774b5535cb39.json b/crates/timescale/.sqlx/query-558382d26b558498dea63c4afdcc8bce1bd85adf440a424f35ac774b5535cb39.json new file mode 100644 index 00000000..1de30a0f --- /dev/null +++ b/crates/timescale/.sqlx/query-558382d26b558498dea63c4afdcc8bce1bd85adf440a424f35ac774b5535cb39.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n select\n lap,\n min(laptime) AS \"laptime!\",\n min(time) AS \"time!\"\n from\n timing_driver\n where\n nr = $1\n and laptime != 0\n group by\n lap\n order by\n lap;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "lap", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "laptime!", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "time!", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true, + null, + null + ] + }, + "hash": "558382d26b558498dea63c4afdcc8bce1bd85adf440a424f35ac774b5535cb39" +} diff --git a/crates/timescale/.sqlx/query-c899c95cf53a6b6d0d4e3a0b96b2d507d58a569841601cafcd7d486a245f62fe.json b/crates/timescale/.sqlx/query-c899c95cf53a6b6d0d4e3a0b96b2d507d58a569841601cafcd7d486a245f62fe.json new file mode 100644 index 00000000..d7a17df0 --- /dev/null +++ b/crates/timescale/.sqlx/query-c899c95cf53a6b6d0d4e3a0b96b2d507d58a569841601cafcd7d486a245f62fe.json @@ -0,0 +1,21 @@ +{ + "db_name": "PostgreSQL", + "query": "\n insert into timing_driver (nr, lap, gap, leader_gap, laptime, sector_1, sector_2, sector_3)\n values ($1, $2, $3, $4, $5, $6, $7, $8)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int4", + "Int8", + "Int8", + "Int8", + "Int8", + "Int8", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "c899c95cf53a6b6d0d4e3a0b96b2d507d58a569841601cafcd7d486a245f62fe" +} From fe51fa73ee76beb1981ddb76163422c944b2b211 Mon Sep 17 00:00:00 2001 From: Slowlydev Date: Tue, 27 May 2025 21:29:12 +0200 Subject: [PATCH 14/23] refactor: unify address envs --- .env.example | 10 +++++----- crates/simulator/src/server.rs | 2 +- services/live/src/main.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 5983cd8e..dc28a059 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,17 @@ -# the address of the simulator backend, found in crates/simulator -SIMULATOR_BACKEND_ADDRESS=localhost:8000 - - LIVE_ADDRESS=localhost:4000 API_ADDRESS=localhost:4001 ANALYTICS_ADDRESS=localhost:4002 -# the origin of the frontend, found in dash/ ORIGIN=http://localhost:3000 # by setting WS_URL you are telling the live backend to connect to this address # (preferably from the simualtor) and not to the f1 address # WS_URL=ws://localhost:8000/ws +SIMULATOR_ADDRESS=localhost:8000 + # sets the rust log level, used by all packages (live, api, simulator, saver) RUST_LOG="live=debug,info" + +# timescale database used by importer and analytics +DATABASE_URL= diff --git a/crates/simulator/src/server.rs b/crates/simulator/src/server.rs index da10f84c..67fff7e6 100644 --- a/crates/simulator/src/server.rs +++ b/crates/simulator/src/server.rs @@ -20,7 +20,7 @@ pub struct AppState { } fn addr() -> String { - std::env::var("SIMULATOR_BACKEND_ADDRESS").unwrap_or("0.0.0.0:8000".to_string()) + std::env::var("SIMULATOR_ADDRESS").unwrap_or("0.0.0.0:8000".to_string()) } pub async fn init(tx: broadcast::Sender, mpsc_tx: mpsc::Sender<()>) { diff --git a/services/live/src/main.rs b/services/live/src/main.rs index 93830012..bf659e2c 100644 --- a/services/live/src/main.rs +++ b/services/live/src/main.rs @@ -39,7 +39,7 @@ async fn main() -> Result<(), anyhow::Error> { .init(); let default_addr = "0.0.0.0:4000".to_string(); - let addr = env::var("LIVE_BACKEND_ADDRESS").unwrap_or(default_addr); + let addr = env::var("LIVE_ADDRESS").unwrap_or(default_addr); info!(?addr, "starting live service"); From 5f0c81fa240c34c395982ef69b819f544cd8b74c Mon Sep 17 00:00:00 2001 From: Alexkill536ITA Date: Wed, 28 May 2025 09:37:42 +0200 Subject: [PATCH 15/23] Feat: Remove TeamLogo and use only Image --- dash/src/app/dashboard/standings/page.tsx | 11 ++++++++-- dash/src/components/TeamLogo.tsx | 25 ----------------------- 2 files changed, 9 insertions(+), 27 deletions(-) delete mode 100644 dash/src/components/TeamLogo.tsx diff --git a/dash/src/app/dashboard/standings/page.tsx b/dash/src/app/dashboard/standings/page.tsx index 36786c16..fee8ee63 100644 --- a/dash/src/app/dashboard/standings/page.tsx +++ b/dash/src/app/dashboard/standings/page.tsx @@ -3,7 +3,7 @@ import { useDataStore } from "@/stores/useDataStore"; import NumberDiff from "@/components/NumberDiff"; -import TeamLogo from "@/components/TeamLogo"; +import Image from "next/image"; export default function Standings() { const driverStandings = useDataStore((state) => state?.championshipPrediction?.drivers); @@ -86,7 +86,13 @@ export default function Standings() {

{team.predictedPosition}

- + {team.teamName}

{team.teamName}

@@ -109,6 +115,7 @@ const SkeletonItem = () => { gridTemplateColumns: "2rem 2rem auto 4rem 4rem 4rem", }} > +
diff --git a/dash/src/components/TeamLogo.tsx b/dash/src/components/TeamLogo.tsx deleted file mode 100644 index 039b8896..00000000 --- a/dash/src/components/TeamLogo.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Image from "next/image"; - -type Props = { - teamName: string | undefined; - width: number | undefined; - height: number | undefined; -}; - -export default function TeamLogo({ teamName, width, height }: Props) { - return ( -
- {teamName ? ( - {teamName} - ) : ( -
- )} -
- ); -} From 7f79202dccece258d2f13ceab6534c8e120d62b1 Mon Sep 17 00:00:00 2001 From: Alexkill536ITA Date: Wed, 28 May 2025 09:38:32 +0200 Subject: [PATCH 16/23] Fix: Remove space to logo teams name --- dash/public/team-logos/{aston martin.svg => aston-martin.svg} | 0 dash/public/team-logos/{haas f1 team.svg => haas-f1-team.svg} | 0 dash/public/team-logos/{kick sauber.svg => kick-sauber.svg} | 0 dash/public/team-logos/{racing bulls.svg => racing-bulls.svg} | 0 .../team-logos/{red bull racing.svg => red-bull-racing.svg} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename dash/public/team-logos/{aston martin.svg => aston-martin.svg} (100%) rename dash/public/team-logos/{haas f1 team.svg => haas-f1-team.svg} (100%) rename dash/public/team-logos/{kick sauber.svg => kick-sauber.svg} (100%) rename dash/public/team-logos/{racing bulls.svg => racing-bulls.svg} (100%) rename dash/public/team-logos/{red bull racing.svg => red-bull-racing.svg} (100%) diff --git a/dash/public/team-logos/aston martin.svg b/dash/public/team-logos/aston-martin.svg similarity index 100% rename from dash/public/team-logos/aston martin.svg rename to dash/public/team-logos/aston-martin.svg diff --git a/dash/public/team-logos/haas f1 team.svg b/dash/public/team-logos/haas-f1-team.svg similarity index 100% rename from dash/public/team-logos/haas f1 team.svg rename to dash/public/team-logos/haas-f1-team.svg diff --git a/dash/public/team-logos/kick sauber.svg b/dash/public/team-logos/kick-sauber.svg similarity index 100% rename from dash/public/team-logos/kick sauber.svg rename to dash/public/team-logos/kick-sauber.svg diff --git a/dash/public/team-logos/racing bulls.svg b/dash/public/team-logos/racing-bulls.svg similarity index 100% rename from dash/public/team-logos/racing bulls.svg rename to dash/public/team-logos/racing-bulls.svg diff --git a/dash/public/team-logos/red bull racing.svg b/dash/public/team-logos/red-bull-racing.svg similarity index 100% rename from dash/public/team-logos/red bull racing.svg rename to dash/public/team-logos/red-bull-racing.svg From 66bb8c90f85a23158c4eb54a3eb55555439cb18f Mon Sep 17 00:00:00 2001 From: Constrat <56174894+Constrat@users.noreply.github.com> Date: Sat, 31 May 2025 14:44:46 +0200 Subject: [PATCH 17/23] feat: oled full black background setting toggle --- dash/src/app/dashboard/settings/page.tsx | 5 +++++ dash/src/app/layout.tsx | 9 ++++++--- dash/src/components/OledModeProvider.tsx | 19 +++++++++++++++++++ dash/src/components/Sidebar.tsx | 7 ++++++- dash/src/stores/useSettingsStore.ts | 6 ++++++ 5 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 dash/src/components/OledModeProvider.tsx diff --git a/dash/src/app/dashboard/settings/page.tsx b/dash/src/app/dashboard/settings/page.tsx index 42ca4111..f9d263f4 100644 --- a/dash/src/app/dashboard/settings/page.tsx +++ b/dash/src/app/dashboard/settings/page.tsx @@ -47,6 +47,11 @@ export default function SettingsPage() {

Show Drivers Mini Sectors

+
+ settings.setOledMode(v)} /> +

OLED Mode (Pure Black Background)

+
+

Race Control

diff --git a/dash/src/app/layout.tsx b/dash/src/app/layout.tsx index 448940de..47698516 100644 --- a/dash/src/app/layout.tsx +++ b/dash/src/app/layout.tsx @@ -5,6 +5,7 @@ import "@/styles/globals.css"; import { env } from "@/env"; import EnvScript from "@/env-script"; +import OledModeProvider from "@/components/OledModeProvider"; import { GeistMono } from "geist/font/mono"; import { GeistSans } from "geist/font/sans"; @@ -18,7 +19,7 @@ type Props = Readonly<{ export default function RootLayout({ children }: Props) { return ( - + @@ -34,7 +35,9 @@ export default function RootLayout({ children }: Props) { )} - {children} + + {children} + ); -} +} \ No newline at end of file diff --git a/dash/src/components/OledModeProvider.tsx b/dash/src/components/OledModeProvider.tsx new file mode 100644 index 00000000..0dc274c4 --- /dev/null +++ b/dash/src/components/OledModeProvider.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { useSettingsStore } from "@/stores/useSettingsStore"; +import { useEffect } from "react"; + +type Props = { + children: React.ReactNode; +}; + +export default function OledModeProvider({ children }: Props) { + const oledMode = useSettingsStore((state) => state.oledMode); + + useEffect(() => { + document.documentElement.classList.toggle("bg-zinc-950", !oledMode); + document.documentElement.classList.toggle("bg-black", oledMode); + }, [oledMode]); + + return
{children}
; +} \ No newline at end of file diff --git a/dash/src/components/Sidebar.tsx b/dash/src/components/Sidebar.tsx index cb39b893..925daae5 100644 --- a/dash/src/components/Sidebar.tsx +++ b/dash/src/components/Sidebar.tsx @@ -7,6 +7,7 @@ import Link from "next/link"; import clsx from "clsx"; import { useSidebarStore } from "@/stores/useSidebarStore"; +import { useSettingsStore } from "@/stores/useSettingsStore"; import ConnectionStatus from "@/components/ConnectionStatus"; import DelayInput from "@/components/DelayInput"; @@ -57,6 +58,8 @@ export default function Sidebar({ connected }: Props) { const pin = useSidebarStore((state) => state.pin); const unpin = useSidebarStore((state) => state.unpin); + + const oledMode = useSettingsStore((state) => state.oledMode); useEffect(() => { const handleResize = () => { @@ -97,8 +100,10 @@ export default function Sidebar({ connected }: Props) { transition={{ type: "spring", bounce: 0.1 }} >