+2
BACKLOG.md
+2
BACKLOG.md
+30
-9
default.nix
+30
-9
default.nix
···
1
-
{ lib, buildGoModule, tailwindcss }:
2
3
-
buildGoModule rec {
4
pname = "arabica";
5
version = "0.1.0";
6
src = ./.;
7
-
vendorHash = "sha256-mrIFu5c2EuGvYHyjJVqC8WzlsmUJYCm/6yUpJ0IGPlA=";
8
-
9
-
nativeBuildInputs = [ tailwindcss ];
10
11
preBuild = ''
12
-
tailwindcss -i web/static/css/style.css -o web/static/css/output.css --minify
13
'';
14
15
buildPhase = ''
···
39
mkdir -p $out/bin
40
mkdir -p $out/share/arabica
41
42
-
# Copy static files and templates
43
-
cp -r web $out/share/arabica/
44
-
cp -r templates $out/share/arabica/
45
cp arabica $out/bin/arabica-unwrapped
46
cat > $out/bin/arabica <<'WRAPPER'
47
${wrapperScript}
···
1
+
{ lib, buildGoModule, buildNpmPackage }:
2
3
+
let
4
+
frontend = buildNpmPackage {
5
+
pname = "arabica-frontend";
6
+
version = "0.1.0";
7
+
src = ./frontend;
8
+
npmDepsHash = "sha256-zCQiB+NV3iIxZtZ/hHKZ23FbLzBDJmmngBJ4s3QPhyk=";
9
+
10
+
preBuild = ''
11
+
mkdir -p ../static
12
+
'';
13
+
14
+
buildPhase = ''
15
+
npm run build
16
+
'';
17
+
18
+
installPhase = ''
19
+
mkdir -p $out
20
+
cp -r ../static/app $out/
21
+
'';
22
+
};
23
+
in buildGoModule {
24
pname = "arabica";
25
version = "0.1.0";
26
src = ./.;
27
+
vendorHash = "sha256-xgxoI2tmT4tVjgy+dv96ptI2YSU8T+Yq+rzApAiJ3yw=";
28
29
preBuild = ''
30
+
echo "Copying pre-built frontend..."
31
+
mkdir -p static/app
32
+
cp -r ${frontend}/app/* static/app/ || true
33
+
cp -r ${frontend}/* static/app/ || true
34
+
ls -la static/app/ || true
35
'';
36
37
buildPhase = ''
···
61
mkdir -p $out/bin
62
mkdir -p $out/share/arabica
63
64
+
# Copy static files
65
+
cp -r static $out/share/arabica/
66
cp arabica $out/bin/arabica-unwrapped
67
cat > $out/bin/arabica <<'WRAPPER'
68
${wrapperScript}
+1
-8
flake.nix
+1
-8
flake.nix
···
8
(system: function nixpkgs.legacyPackages.${system} system);
9
in {
10
devShells = forAllSystems (pkgs: system: {
11
-
default = pkgs.mkShell { packages = with pkgs; [ go tailwindcss ]; };
12
});
13
14
packages = forAllSystems (pkgs: system: rec {
···
21
type = "app";
22
program = "${self.packages.${system}.arabica}/bin/arabica";
23
};
24
-
tailwind = {
25
-
type = "app";
26
-
program = toString (pkgs.writeShellScript "tailwind-build" ''
27
-
cd ${./.}
28
-
${pkgs.tailwindcss}/bin/tailwindcss -i web/static/css/style.css -o web/static/css/output.css --minify
29
-
'');
30
-
};
31
});
32
33
nixosModules.default = import ./module.nix;
···
8
(system: function nixpkgs.legacyPackages.${system} system);
9
in {
10
devShells = forAllSystems (pkgs: system: {
11
+
default = pkgs.mkShell { packages = with pkgs; [ go nodejs ]; };
12
});
13
14
packages = forAllSystems (pkgs: system: rec {
···
21
type = "app";
22
program = "${self.packages.${system}.arabica}/bin/arabica";
23
};
24
});
25
26
nixosModules.default = import ./module.nix;
+14
-25
frontend/index.html
+14
-25
frontend/index.html
···
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8" />
5
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
<title>Arabica - Coffee Brew Tracker</title>
7
<meta
8
name="description"
9
content="Track your coffee brewing journey with detailed logs stored in your Personal Data Server"
10
/>
11
12
-
<!-- Tailwind CSS -->
13
-
<link rel="stylesheet" href="/static/css/output.css?v=0.1.4" />
14
-
15
-
<!-- Favicon -->
16
-
<link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg" />
17
-
<link
18
-
rel="icon"
19
-
type="image/png"
20
-
sizes="32x32"
21
-
href="/static/images/favicon-32x32.png"
22
-
/>
23
-
<link
24
-
rel="icon"
25
-
type="image/png"
26
-
sizes="16x16"
27
-
href="/static/images/favicon-16x16.png"
28
-
/>
29
-
<link
30
-
rel="apple-touch-icon"
31
-
sizes="180x180"
32
-
href="/static/images/apple-touch-icon.png"
33
-
/>
34
35
-
<!-- Web Manifest -->
36
<link rel="manifest" href="/static/manifest.json" />
37
-
<meta name="theme-color" content="#78350f" />
38
</head>
39
<body class="bg-brown-50 text-brown-900 min-h-screen">
40
<div id="app"></div>
41
<script type="module" src="/src/main.js"></script>
42
</body>
43
</html>
···
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8" />
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+
<meta name="mobile-web-app-capable" content="yes" />
7
+
<meta name="apple-mobile-web-app-capable" content="yes" />
8
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
9
+
<meta name="apple-mobile-web-app-title" content="Arabica" />
10
+
<meta name="theme-color" content="#78350f" />
11
+
<meta name="color-scheme" content="light" />
12
+
13
<title>Arabica - Coffee Brew Tracker</title>
14
<meta
15
name="description"
16
content="Track your coffee brewing journey with detailed logs stored in your Personal Data Server"
17
/>
18
19
+
<!-- Favicon & Icons -->
20
+
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
21
+
<link rel="apple-touch-icon" href="/static/icon-192.svg" />
22
23
+
<!-- Web Manifest for PWA -->
24
<link rel="manifest" href="/static/manifest.json" />
25
</head>
26
<body class="bg-brown-50 text-brown-900 min-h-screen">
27
<div id="app"></div>
28
<script type="module" src="/src/main.js"></script>
29
+
<!-- Service Worker Registration (external script for CSP compliance) -->
30
+
<script defer src="/static/register-sw.js"></script>
31
</body>
32
</html>
+888
frontend/package-lock.json
+888
frontend/package-lock.json
···
12
},
13
"devDependencies": {
14
"@sveltejs/vite-plugin-svelte": "^3.0.0",
15
"svelte": "^4.2.0",
16
"vite": "^5.0.0"
17
}
18
},
19
"node_modules/@ampproject/remapping": {
20
"version": "2.3.0",
21
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
···
460
"@jridgewell/sourcemap-codec": "^1.4.14"
461
}
462
},
463
"node_modules/@rollup/rollup-android-arm-eabi": {
464
"version": "4.56.0",
465
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz",
···
872
"node": ">=0.4.0"
873
}
874
},
875
"node_modules/aria-query": {
876
"version": "5.3.2",
877
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
···
892
"node": ">= 0.4"
893
}
894
},
895
"node_modules/code-red": {
896
"version": "1.0.4",
897
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
···
906
"periscopic": "^3.1.0"
907
}
908
},
909
"node_modules/css-tree": {
910
"version": "2.3.1",
911
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
···
920
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
921
}
922
},
923
"node_modules/debug": {
924
"version": "4.4.3",
925
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
···
948
"node": ">=0.10.0"
949
}
950
},
951
"node_modules/esbuild": {
952
"version": "0.21.5",
953
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
···
997
"@types/estree": "^1.0.0"
998
}
999
},
1000
"node_modules/fsevents": {
1001
"version": "2.3.3",
1002
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
···
1012
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1013
}
1014
},
1015
"node_modules/is-reference": {
1016
"version": "3.0.3",
1017
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
···
1022
"@types/estree": "^1.0.6"
1023
}
1024
},
1025
"node_modules/kleur": {
1026
"version": "4.1.5",
1027
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
···
1032
"node": ">=6"
1033
}
1034
},
1035
"node_modules/locate-character": {
1036
"version": "3.0.0",
1037
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
···
1056
"dev": true,
1057
"license": "CC0-1.0"
1058
},
1059
"node_modules/ms": {
1060
"version": "2.1.3",
1061
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
···
1063
"dev": true,
1064
"license": "MIT"
1065
},
1066
"node_modules/nanoid": {
1067
"version": "3.3.11",
1068
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
···
1094
"node": ">= 6"
1095
}
1096
},
1097
"node_modules/periscopic": {
1098
"version": "3.1.0",
1099
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
···
1113
"dev": true,
1114
"license": "ISC"
1115
},
1116
"node_modules/postcss": {
1117
"version": "8.5.6",
1118
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
···
1133
}
1134
],
1135
"license": "MIT",
1136
"dependencies": {
1137
"nanoid": "^3.3.11",
1138
"picocolors": "^1.1.1",
···
1142
"node": "^10 || ^12 || >=14"
1143
}
1144
},
1145
"node_modules/regexparam": {
1146
"version": "1.3.0",
1147
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz",
···
1151
"node": ">=6"
1152
}
1153
},
1154
"node_modules/rollup": {
1155
"version": "4.56.0",
1156
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
···
1196
"fsevents": "~2.3.2"
1197
}
1198
},
1199
"node_modules/source-map-js": {
1200
"version": "1.2.1",
1201
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
···
1206
"node": ">=0.10.0"
1207
}
1208
},
1209
"node_modules/svelte": {
1210
"version": "4.2.20",
1211
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.20.tgz",
···
1246
"svelte": "^3.19.0 || ^4.0.0"
1247
}
1248
},
1249
"node_modules/vite": {
1250
"version": "5.4.21",
1251
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
···
12
},
13
"devDependencies": {
14
"@sveltejs/vite-plugin-svelte": "^3.0.0",
15
+
"postcss": "^8.4.0",
16
"svelte": "^4.2.0",
17
+
"tailwindcss": "^3.4.0",
18
"vite": "^5.0.0"
19
}
20
},
21
+
"node_modules/@alloc/quick-lru": {
22
+
"version": "5.2.0",
23
+
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
24
+
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
25
+
"dev": true,
26
+
"license": "MIT",
27
+
"engines": {
28
+
"node": ">=10"
29
+
},
30
+
"funding": {
31
+
"url": "https://github.com/sponsors/sindresorhus"
32
+
}
33
+
},
34
"node_modules/@ampproject/remapping": {
35
"version": "2.3.0",
36
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
···
475
"@jridgewell/sourcemap-codec": "^1.4.14"
476
}
477
},
478
+
"node_modules/@nodelib/fs.scandir": {
479
+
"version": "2.1.5",
480
+
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
481
+
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
482
+
"dev": true,
483
+
"license": "MIT",
484
+
"dependencies": {
485
+
"@nodelib/fs.stat": "2.0.5",
486
+
"run-parallel": "^1.1.9"
487
+
},
488
+
"engines": {
489
+
"node": ">= 8"
490
+
}
491
+
},
492
+
"node_modules/@nodelib/fs.stat": {
493
+
"version": "2.0.5",
494
+
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
495
+
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
496
+
"dev": true,
497
+
"license": "MIT",
498
+
"engines": {
499
+
"node": ">= 8"
500
+
}
501
+
},
502
+
"node_modules/@nodelib/fs.walk": {
503
+
"version": "1.2.8",
504
+
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
505
+
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
506
+
"dev": true,
507
+
"license": "MIT",
508
+
"dependencies": {
509
+
"@nodelib/fs.scandir": "2.1.5",
510
+
"fastq": "^1.6.0"
511
+
},
512
+
"engines": {
513
+
"node": ">= 8"
514
+
}
515
+
},
516
"node_modules/@rollup/rollup-android-arm-eabi": {
517
"version": "4.56.0",
518
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz",
···
925
"node": ">=0.4.0"
926
}
927
},
928
+
"node_modules/any-promise": {
929
+
"version": "1.3.0",
930
+
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
931
+
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
932
+
"dev": true,
933
+
"license": "MIT"
934
+
},
935
+
"node_modules/anymatch": {
936
+
"version": "3.1.3",
937
+
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
938
+
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
939
+
"dev": true,
940
+
"license": "ISC",
941
+
"dependencies": {
942
+
"normalize-path": "^3.0.0",
943
+
"picomatch": "^2.0.4"
944
+
},
945
+
"engines": {
946
+
"node": ">= 8"
947
+
}
948
+
},
949
+
"node_modules/arg": {
950
+
"version": "5.0.2",
951
+
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
952
+
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
953
+
"dev": true,
954
+
"license": "MIT"
955
+
},
956
"node_modules/aria-query": {
957
"version": "5.3.2",
958
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
···
973
"node": ">= 0.4"
974
}
975
},
976
+
"node_modules/binary-extensions": {
977
+
"version": "2.3.0",
978
+
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
979
+
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
980
+
"dev": true,
981
+
"license": "MIT",
982
+
"engines": {
983
+
"node": ">=8"
984
+
},
985
+
"funding": {
986
+
"url": "https://github.com/sponsors/sindresorhus"
987
+
}
988
+
},
989
+
"node_modules/braces": {
990
+
"version": "3.0.3",
991
+
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
992
+
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
993
+
"dev": true,
994
+
"license": "MIT",
995
+
"dependencies": {
996
+
"fill-range": "^7.1.1"
997
+
},
998
+
"engines": {
999
+
"node": ">=8"
1000
+
}
1001
+
},
1002
+
"node_modules/camelcase-css": {
1003
+
"version": "2.0.1",
1004
+
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
1005
+
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
1006
+
"dev": true,
1007
+
"license": "MIT",
1008
+
"engines": {
1009
+
"node": ">= 6"
1010
+
}
1011
+
},
1012
+
"node_modules/chokidar": {
1013
+
"version": "3.6.0",
1014
+
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
1015
+
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
1016
+
"dev": true,
1017
+
"license": "MIT",
1018
+
"dependencies": {
1019
+
"anymatch": "~3.1.2",
1020
+
"braces": "~3.0.2",
1021
+
"glob-parent": "~5.1.2",
1022
+
"is-binary-path": "~2.1.0",
1023
+
"is-glob": "~4.0.1",
1024
+
"normalize-path": "~3.0.0",
1025
+
"readdirp": "~3.6.0"
1026
+
},
1027
+
"engines": {
1028
+
"node": ">= 8.10.0"
1029
+
},
1030
+
"funding": {
1031
+
"url": "https://paulmillr.com/funding/"
1032
+
},
1033
+
"optionalDependencies": {
1034
+
"fsevents": "~2.3.2"
1035
+
}
1036
+
},
1037
+
"node_modules/chokidar/node_modules/glob-parent": {
1038
+
"version": "5.1.2",
1039
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1040
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1041
+
"dev": true,
1042
+
"license": "ISC",
1043
+
"dependencies": {
1044
+
"is-glob": "^4.0.1"
1045
+
},
1046
+
"engines": {
1047
+
"node": ">= 6"
1048
+
}
1049
+
},
1050
"node_modules/code-red": {
1051
"version": "1.0.4",
1052
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
···
1061
"periscopic": "^3.1.0"
1062
}
1063
},
1064
+
"node_modules/commander": {
1065
+
"version": "4.1.1",
1066
+
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
1067
+
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
1068
+
"dev": true,
1069
+
"license": "MIT",
1070
+
"engines": {
1071
+
"node": ">= 6"
1072
+
}
1073
+
},
1074
"node_modules/css-tree": {
1075
"version": "2.3.1",
1076
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
···
1085
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
1086
}
1087
},
1088
+
"node_modules/cssesc": {
1089
+
"version": "3.0.0",
1090
+
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
1091
+
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
1092
+
"dev": true,
1093
+
"license": "MIT",
1094
+
"bin": {
1095
+
"cssesc": "bin/cssesc"
1096
+
},
1097
+
"engines": {
1098
+
"node": ">=4"
1099
+
}
1100
+
},
1101
"node_modules/debug": {
1102
"version": "4.4.3",
1103
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
···
1126
"node": ">=0.10.0"
1127
}
1128
},
1129
+
"node_modules/didyoumean": {
1130
+
"version": "1.2.2",
1131
+
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
1132
+
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
1133
+
"dev": true,
1134
+
"license": "Apache-2.0"
1135
+
},
1136
+
"node_modules/dlv": {
1137
+
"version": "1.1.3",
1138
+
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
1139
+
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
1140
+
"dev": true,
1141
+
"license": "MIT"
1142
+
},
1143
"node_modules/esbuild": {
1144
"version": "0.21.5",
1145
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
···
1189
"@types/estree": "^1.0.0"
1190
}
1191
},
1192
+
"node_modules/fast-glob": {
1193
+
"version": "3.3.3",
1194
+
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
1195
+
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
1196
+
"dev": true,
1197
+
"license": "MIT",
1198
+
"dependencies": {
1199
+
"@nodelib/fs.stat": "^2.0.2",
1200
+
"@nodelib/fs.walk": "^1.2.3",
1201
+
"glob-parent": "^5.1.2",
1202
+
"merge2": "^1.3.0",
1203
+
"micromatch": "^4.0.8"
1204
+
},
1205
+
"engines": {
1206
+
"node": ">=8.6.0"
1207
+
}
1208
+
},
1209
+
"node_modules/fast-glob/node_modules/glob-parent": {
1210
+
"version": "5.1.2",
1211
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1212
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1213
+
"dev": true,
1214
+
"license": "ISC",
1215
+
"dependencies": {
1216
+
"is-glob": "^4.0.1"
1217
+
},
1218
+
"engines": {
1219
+
"node": ">= 6"
1220
+
}
1221
+
},
1222
+
"node_modules/fastq": {
1223
+
"version": "1.20.1",
1224
+
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
1225
+
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
1226
+
"dev": true,
1227
+
"license": "ISC",
1228
+
"dependencies": {
1229
+
"reusify": "^1.0.4"
1230
+
}
1231
+
},
1232
+
"node_modules/fill-range": {
1233
+
"version": "7.1.1",
1234
+
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
1235
+
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
1236
+
"dev": true,
1237
+
"license": "MIT",
1238
+
"dependencies": {
1239
+
"to-regex-range": "^5.0.1"
1240
+
},
1241
+
"engines": {
1242
+
"node": ">=8"
1243
+
}
1244
+
},
1245
"node_modules/fsevents": {
1246
"version": "2.3.3",
1247
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
···
1257
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1258
}
1259
},
1260
+
"node_modules/function-bind": {
1261
+
"version": "1.1.2",
1262
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1263
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1264
+
"dev": true,
1265
+
"license": "MIT",
1266
+
"funding": {
1267
+
"url": "https://github.com/sponsors/ljharb"
1268
+
}
1269
+
},
1270
+
"node_modules/glob-parent": {
1271
+
"version": "6.0.2",
1272
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1273
+
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1274
+
"dev": true,
1275
+
"license": "ISC",
1276
+
"dependencies": {
1277
+
"is-glob": "^4.0.3"
1278
+
},
1279
+
"engines": {
1280
+
"node": ">=10.13.0"
1281
+
}
1282
+
},
1283
+
"node_modules/hasown": {
1284
+
"version": "2.0.2",
1285
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1286
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1287
+
"dev": true,
1288
+
"license": "MIT",
1289
+
"dependencies": {
1290
+
"function-bind": "^1.1.2"
1291
+
},
1292
+
"engines": {
1293
+
"node": ">= 0.4"
1294
+
}
1295
+
},
1296
+
"node_modules/is-binary-path": {
1297
+
"version": "2.1.0",
1298
+
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1299
+
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1300
+
"dev": true,
1301
+
"license": "MIT",
1302
+
"dependencies": {
1303
+
"binary-extensions": "^2.0.0"
1304
+
},
1305
+
"engines": {
1306
+
"node": ">=8"
1307
+
}
1308
+
},
1309
+
"node_modules/is-core-module": {
1310
+
"version": "2.16.1",
1311
+
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
1312
+
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
1313
+
"dev": true,
1314
+
"license": "MIT",
1315
+
"dependencies": {
1316
+
"hasown": "^2.0.2"
1317
+
},
1318
+
"engines": {
1319
+
"node": ">= 0.4"
1320
+
},
1321
+
"funding": {
1322
+
"url": "https://github.com/sponsors/ljharb"
1323
+
}
1324
+
},
1325
+
"node_modules/is-extglob": {
1326
+
"version": "2.1.1",
1327
+
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1328
+
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1329
+
"dev": true,
1330
+
"license": "MIT",
1331
+
"engines": {
1332
+
"node": ">=0.10.0"
1333
+
}
1334
+
},
1335
+
"node_modules/is-glob": {
1336
+
"version": "4.0.3",
1337
+
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1338
+
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1339
+
"dev": true,
1340
+
"license": "MIT",
1341
+
"dependencies": {
1342
+
"is-extglob": "^2.1.1"
1343
+
},
1344
+
"engines": {
1345
+
"node": ">=0.10.0"
1346
+
}
1347
+
},
1348
+
"node_modules/is-number": {
1349
+
"version": "7.0.0",
1350
+
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1351
+
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1352
+
"dev": true,
1353
+
"license": "MIT",
1354
+
"engines": {
1355
+
"node": ">=0.12.0"
1356
+
}
1357
+
},
1358
"node_modules/is-reference": {
1359
"version": "3.0.3",
1360
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
···
1365
"@types/estree": "^1.0.6"
1366
}
1367
},
1368
+
"node_modules/jiti": {
1369
+
"version": "1.21.7",
1370
+
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1371
+
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1372
+
"dev": true,
1373
+
"license": "MIT",
1374
+
"peer": true,
1375
+
"bin": {
1376
+
"jiti": "bin/jiti.js"
1377
+
}
1378
+
},
1379
"node_modules/kleur": {
1380
"version": "4.1.5",
1381
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
···
1386
"node": ">=6"
1387
}
1388
},
1389
+
"node_modules/lilconfig": {
1390
+
"version": "3.1.3",
1391
+
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
1392
+
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
1393
+
"dev": true,
1394
+
"license": "MIT",
1395
+
"engines": {
1396
+
"node": ">=14"
1397
+
},
1398
+
"funding": {
1399
+
"url": "https://github.com/sponsors/antonk52"
1400
+
}
1401
+
},
1402
+
"node_modules/lines-and-columns": {
1403
+
"version": "1.2.4",
1404
+
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
1405
+
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
1406
+
"dev": true,
1407
+
"license": "MIT"
1408
+
},
1409
"node_modules/locate-character": {
1410
"version": "3.0.0",
1411
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
···
1430
"dev": true,
1431
"license": "CC0-1.0"
1432
},
1433
+
"node_modules/merge2": {
1434
+
"version": "1.4.1",
1435
+
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1436
+
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
1437
+
"dev": true,
1438
+
"license": "MIT",
1439
+
"engines": {
1440
+
"node": ">= 8"
1441
+
}
1442
+
},
1443
+
"node_modules/micromatch": {
1444
+
"version": "4.0.8",
1445
+
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
1446
+
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
1447
+
"dev": true,
1448
+
"license": "MIT",
1449
+
"dependencies": {
1450
+
"braces": "^3.0.3",
1451
+
"picomatch": "^2.3.1"
1452
+
},
1453
+
"engines": {
1454
+
"node": ">=8.6"
1455
+
}
1456
+
},
1457
"node_modules/ms": {
1458
"version": "2.1.3",
1459
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
···
1461
"dev": true,
1462
"license": "MIT"
1463
},
1464
+
"node_modules/mz": {
1465
+
"version": "2.7.0",
1466
+
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
1467
+
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
1468
+
"dev": true,
1469
+
"license": "MIT",
1470
+
"dependencies": {
1471
+
"any-promise": "^1.0.0",
1472
+
"object-assign": "^4.0.1",
1473
+
"thenify-all": "^1.0.0"
1474
+
}
1475
+
},
1476
"node_modules/nanoid": {
1477
"version": "3.3.11",
1478
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
···
1504
"node": ">= 6"
1505
}
1506
},
1507
+
"node_modules/normalize-path": {
1508
+
"version": "3.0.0",
1509
+
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1510
+
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1511
+
"dev": true,
1512
+
"license": "MIT",
1513
+
"engines": {
1514
+
"node": ">=0.10.0"
1515
+
}
1516
+
},
1517
+
"node_modules/object-assign": {
1518
+
"version": "4.1.1",
1519
+
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1520
+
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1521
+
"dev": true,
1522
+
"license": "MIT",
1523
+
"engines": {
1524
+
"node": ">=0.10.0"
1525
+
}
1526
+
},
1527
+
"node_modules/object-hash": {
1528
+
"version": "3.0.0",
1529
+
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
1530
+
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
1531
+
"dev": true,
1532
+
"license": "MIT",
1533
+
"engines": {
1534
+
"node": ">= 6"
1535
+
}
1536
+
},
1537
+
"node_modules/path-parse": {
1538
+
"version": "1.0.7",
1539
+
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1540
+
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1541
+
"dev": true,
1542
+
"license": "MIT"
1543
+
},
1544
"node_modules/periscopic": {
1545
"version": "3.1.0",
1546
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
···
1560
"dev": true,
1561
"license": "ISC"
1562
},
1563
+
"node_modules/picomatch": {
1564
+
"version": "2.3.1",
1565
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1566
+
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1567
+
"dev": true,
1568
+
"license": "MIT",
1569
+
"engines": {
1570
+
"node": ">=8.6"
1571
+
},
1572
+
"funding": {
1573
+
"url": "https://github.com/sponsors/jonschlinkert"
1574
+
}
1575
+
},
1576
+
"node_modules/pify": {
1577
+
"version": "2.3.0",
1578
+
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
1579
+
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
1580
+
"dev": true,
1581
+
"license": "MIT",
1582
+
"engines": {
1583
+
"node": ">=0.10.0"
1584
+
}
1585
+
},
1586
+
"node_modules/pirates": {
1587
+
"version": "4.0.7",
1588
+
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
1589
+
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
1590
+
"dev": true,
1591
+
"license": "MIT",
1592
+
"engines": {
1593
+
"node": ">= 6"
1594
+
}
1595
+
},
1596
"node_modules/postcss": {
1597
"version": "8.5.6",
1598
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
···
1613
}
1614
],
1615
"license": "MIT",
1616
+
"peer": true,
1617
"dependencies": {
1618
"nanoid": "^3.3.11",
1619
"picocolors": "^1.1.1",
···
1623
"node": "^10 || ^12 || >=14"
1624
}
1625
},
1626
+
"node_modules/postcss-import": {
1627
+
"version": "15.1.0",
1628
+
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
1629
+
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
1630
+
"dev": true,
1631
+
"license": "MIT",
1632
+
"dependencies": {
1633
+
"postcss-value-parser": "^4.0.0",
1634
+
"read-cache": "^1.0.0",
1635
+
"resolve": "^1.1.7"
1636
+
},
1637
+
"engines": {
1638
+
"node": ">=14.0.0"
1639
+
},
1640
+
"peerDependencies": {
1641
+
"postcss": "^8.0.0"
1642
+
}
1643
+
},
1644
+
"node_modules/postcss-js": {
1645
+
"version": "4.1.0",
1646
+
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
1647
+
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
1648
+
"dev": true,
1649
+
"funding": [
1650
+
{
1651
+
"type": "opencollective",
1652
+
"url": "https://opencollective.com/postcss/"
1653
+
},
1654
+
{
1655
+
"type": "github",
1656
+
"url": "https://github.com/sponsors/ai"
1657
+
}
1658
+
],
1659
+
"license": "MIT",
1660
+
"dependencies": {
1661
+
"camelcase-css": "^2.0.1"
1662
+
},
1663
+
"engines": {
1664
+
"node": "^12 || ^14 || >= 16"
1665
+
},
1666
+
"peerDependencies": {
1667
+
"postcss": "^8.4.21"
1668
+
}
1669
+
},
1670
+
"node_modules/postcss-load-config": {
1671
+
"version": "6.0.1",
1672
+
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
1673
+
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
1674
+
"dev": true,
1675
+
"funding": [
1676
+
{
1677
+
"type": "opencollective",
1678
+
"url": "https://opencollective.com/postcss/"
1679
+
},
1680
+
{
1681
+
"type": "github",
1682
+
"url": "https://github.com/sponsors/ai"
1683
+
}
1684
+
],
1685
+
"license": "MIT",
1686
+
"dependencies": {
1687
+
"lilconfig": "^3.1.1"
1688
+
},
1689
+
"engines": {
1690
+
"node": ">= 18"
1691
+
},
1692
+
"peerDependencies": {
1693
+
"jiti": ">=1.21.0",
1694
+
"postcss": ">=8.0.9",
1695
+
"tsx": "^4.8.1",
1696
+
"yaml": "^2.4.2"
1697
+
},
1698
+
"peerDependenciesMeta": {
1699
+
"jiti": {
1700
+
"optional": true
1701
+
},
1702
+
"postcss": {
1703
+
"optional": true
1704
+
},
1705
+
"tsx": {
1706
+
"optional": true
1707
+
},
1708
+
"yaml": {
1709
+
"optional": true
1710
+
}
1711
+
}
1712
+
},
1713
+
"node_modules/postcss-nested": {
1714
+
"version": "6.2.0",
1715
+
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
1716
+
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
1717
+
"dev": true,
1718
+
"funding": [
1719
+
{
1720
+
"type": "opencollective",
1721
+
"url": "https://opencollective.com/postcss/"
1722
+
},
1723
+
{
1724
+
"type": "github",
1725
+
"url": "https://github.com/sponsors/ai"
1726
+
}
1727
+
],
1728
+
"license": "MIT",
1729
+
"dependencies": {
1730
+
"postcss-selector-parser": "^6.1.1"
1731
+
},
1732
+
"engines": {
1733
+
"node": ">=12.0"
1734
+
},
1735
+
"peerDependencies": {
1736
+
"postcss": "^8.2.14"
1737
+
}
1738
+
},
1739
+
"node_modules/postcss-selector-parser": {
1740
+
"version": "6.1.2",
1741
+
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
1742
+
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
1743
+
"dev": true,
1744
+
"license": "MIT",
1745
+
"dependencies": {
1746
+
"cssesc": "^3.0.0",
1747
+
"util-deprecate": "^1.0.2"
1748
+
},
1749
+
"engines": {
1750
+
"node": ">=4"
1751
+
}
1752
+
},
1753
+
"node_modules/postcss-value-parser": {
1754
+
"version": "4.2.0",
1755
+
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
1756
+
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
1757
+
"dev": true,
1758
+
"license": "MIT"
1759
+
},
1760
+
"node_modules/queue-microtask": {
1761
+
"version": "1.2.3",
1762
+
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
1763
+
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
1764
+
"dev": true,
1765
+
"funding": [
1766
+
{
1767
+
"type": "github",
1768
+
"url": "https://github.com/sponsors/feross"
1769
+
},
1770
+
{
1771
+
"type": "patreon",
1772
+
"url": "https://www.patreon.com/feross"
1773
+
},
1774
+
{
1775
+
"type": "consulting",
1776
+
"url": "https://feross.org/support"
1777
+
}
1778
+
],
1779
+
"license": "MIT"
1780
+
},
1781
+
"node_modules/read-cache": {
1782
+
"version": "1.0.0",
1783
+
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
1784
+
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
1785
+
"dev": true,
1786
+
"license": "MIT",
1787
+
"dependencies": {
1788
+
"pify": "^2.3.0"
1789
+
}
1790
+
},
1791
+
"node_modules/readdirp": {
1792
+
"version": "3.6.0",
1793
+
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1794
+
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1795
+
"dev": true,
1796
+
"license": "MIT",
1797
+
"dependencies": {
1798
+
"picomatch": "^2.2.1"
1799
+
},
1800
+
"engines": {
1801
+
"node": ">=8.10.0"
1802
+
}
1803
+
},
1804
"node_modules/regexparam": {
1805
"version": "1.3.0",
1806
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz",
···
1810
"node": ">=6"
1811
}
1812
},
1813
+
"node_modules/resolve": {
1814
+
"version": "1.22.11",
1815
+
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
1816
+
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
1817
+
"dev": true,
1818
+
"license": "MIT",
1819
+
"dependencies": {
1820
+
"is-core-module": "^2.16.1",
1821
+
"path-parse": "^1.0.7",
1822
+
"supports-preserve-symlinks-flag": "^1.0.0"
1823
+
},
1824
+
"bin": {
1825
+
"resolve": "bin/resolve"
1826
+
},
1827
+
"engines": {
1828
+
"node": ">= 0.4"
1829
+
},
1830
+
"funding": {
1831
+
"url": "https://github.com/sponsors/ljharb"
1832
+
}
1833
+
},
1834
+
"node_modules/reusify": {
1835
+
"version": "1.1.0",
1836
+
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
1837
+
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
1838
+
"dev": true,
1839
+
"license": "MIT",
1840
+
"engines": {
1841
+
"iojs": ">=1.0.0",
1842
+
"node": ">=0.10.0"
1843
+
}
1844
+
},
1845
"node_modules/rollup": {
1846
"version": "4.56.0",
1847
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz",
···
1887
"fsevents": "~2.3.2"
1888
}
1889
},
1890
+
"node_modules/run-parallel": {
1891
+
"version": "1.2.0",
1892
+
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
1893
+
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
1894
+
"dev": true,
1895
+
"funding": [
1896
+
{
1897
+
"type": "github",
1898
+
"url": "https://github.com/sponsors/feross"
1899
+
},
1900
+
{
1901
+
"type": "patreon",
1902
+
"url": "https://www.patreon.com/feross"
1903
+
},
1904
+
{
1905
+
"type": "consulting",
1906
+
"url": "https://feross.org/support"
1907
+
}
1908
+
],
1909
+
"license": "MIT",
1910
+
"dependencies": {
1911
+
"queue-microtask": "^1.2.2"
1912
+
}
1913
+
},
1914
"node_modules/source-map-js": {
1915
"version": "1.2.1",
1916
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
···
1921
"node": ">=0.10.0"
1922
}
1923
},
1924
+
"node_modules/sucrase": {
1925
+
"version": "3.35.1",
1926
+
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
1927
+
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
1928
+
"dev": true,
1929
+
"license": "MIT",
1930
+
"dependencies": {
1931
+
"@jridgewell/gen-mapping": "^0.3.2",
1932
+
"commander": "^4.0.0",
1933
+
"lines-and-columns": "^1.1.6",
1934
+
"mz": "^2.7.0",
1935
+
"pirates": "^4.0.1",
1936
+
"tinyglobby": "^0.2.11",
1937
+
"ts-interface-checker": "^0.1.9"
1938
+
},
1939
+
"bin": {
1940
+
"sucrase": "bin/sucrase",
1941
+
"sucrase-node": "bin/sucrase-node"
1942
+
},
1943
+
"engines": {
1944
+
"node": ">=16 || 14 >=14.17"
1945
+
}
1946
+
},
1947
+
"node_modules/supports-preserve-symlinks-flag": {
1948
+
"version": "1.0.0",
1949
+
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
1950
+
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
1951
+
"dev": true,
1952
+
"license": "MIT",
1953
+
"engines": {
1954
+
"node": ">= 0.4"
1955
+
},
1956
+
"funding": {
1957
+
"url": "https://github.com/sponsors/ljharb"
1958
+
}
1959
+
},
1960
"node_modules/svelte": {
1961
"version": "4.2.20",
1962
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.20.tgz",
···
1997
"svelte": "^3.19.0 || ^4.0.0"
1998
}
1999
},
2000
+
"node_modules/tailwindcss": {
2001
+
"version": "3.4.19",
2002
+
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
2003
+
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
2004
+
"dev": true,
2005
+
"license": "MIT",
2006
+
"dependencies": {
2007
+
"@alloc/quick-lru": "^5.2.0",
2008
+
"arg": "^5.0.2",
2009
+
"chokidar": "^3.6.0",
2010
+
"didyoumean": "^1.2.2",
2011
+
"dlv": "^1.1.3",
2012
+
"fast-glob": "^3.3.2",
2013
+
"glob-parent": "^6.0.2",
2014
+
"is-glob": "^4.0.3",
2015
+
"jiti": "^1.21.7",
2016
+
"lilconfig": "^3.1.3",
2017
+
"micromatch": "^4.0.8",
2018
+
"normalize-path": "^3.0.0",
2019
+
"object-hash": "^3.0.0",
2020
+
"picocolors": "^1.1.1",
2021
+
"postcss": "^8.4.47",
2022
+
"postcss-import": "^15.1.0",
2023
+
"postcss-js": "^4.0.1",
2024
+
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
2025
+
"postcss-nested": "^6.2.0",
2026
+
"postcss-selector-parser": "^6.1.2",
2027
+
"resolve": "^1.22.8",
2028
+
"sucrase": "^3.35.0"
2029
+
},
2030
+
"bin": {
2031
+
"tailwind": "lib/cli.js",
2032
+
"tailwindcss": "lib/cli.js"
2033
+
},
2034
+
"engines": {
2035
+
"node": ">=14.0.0"
2036
+
}
2037
+
},
2038
+
"node_modules/thenify": {
2039
+
"version": "3.3.1",
2040
+
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
2041
+
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
2042
+
"dev": true,
2043
+
"license": "MIT",
2044
+
"dependencies": {
2045
+
"any-promise": "^1.0.0"
2046
+
}
2047
+
},
2048
+
"node_modules/thenify-all": {
2049
+
"version": "1.6.0",
2050
+
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
2051
+
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
2052
+
"dev": true,
2053
+
"license": "MIT",
2054
+
"dependencies": {
2055
+
"thenify": ">= 3.1.0 < 4"
2056
+
},
2057
+
"engines": {
2058
+
"node": ">=0.8"
2059
+
}
2060
+
},
2061
+
"node_modules/tinyglobby": {
2062
+
"version": "0.2.15",
2063
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
2064
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
2065
+
"dev": true,
2066
+
"license": "MIT",
2067
+
"dependencies": {
2068
+
"fdir": "^6.5.0",
2069
+
"picomatch": "^4.0.3"
2070
+
},
2071
+
"engines": {
2072
+
"node": ">=12.0.0"
2073
+
},
2074
+
"funding": {
2075
+
"url": "https://github.com/sponsors/SuperchupuDev"
2076
+
}
2077
+
},
2078
+
"node_modules/tinyglobby/node_modules/fdir": {
2079
+
"version": "6.5.0",
2080
+
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
2081
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2082
+
"dev": true,
2083
+
"license": "MIT",
2084
+
"engines": {
2085
+
"node": ">=12.0.0"
2086
+
},
2087
+
"peerDependencies": {
2088
+
"picomatch": "^3 || ^4"
2089
+
},
2090
+
"peerDependenciesMeta": {
2091
+
"picomatch": {
2092
+
"optional": true
2093
+
}
2094
+
}
2095
+
},
2096
+
"node_modules/tinyglobby/node_modules/picomatch": {
2097
+
"version": "4.0.3",
2098
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
2099
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
2100
+
"dev": true,
2101
+
"license": "MIT",
2102
+
"peer": true,
2103
+
"engines": {
2104
+
"node": ">=12"
2105
+
},
2106
+
"funding": {
2107
+
"url": "https://github.com/sponsors/jonschlinkert"
2108
+
}
2109
+
},
2110
+
"node_modules/to-regex-range": {
2111
+
"version": "5.0.1",
2112
+
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
2113
+
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2114
+
"dev": true,
2115
+
"license": "MIT",
2116
+
"dependencies": {
2117
+
"is-number": "^7.0.0"
2118
+
},
2119
+
"engines": {
2120
+
"node": ">=8.0"
2121
+
}
2122
+
},
2123
+
"node_modules/ts-interface-checker": {
2124
+
"version": "0.1.13",
2125
+
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
2126
+
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
2127
+
"dev": true,
2128
+
"license": "Apache-2.0"
2129
+
},
2130
+
"node_modules/util-deprecate": {
2131
+
"version": "1.0.2",
2132
+
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2133
+
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2134
+
"dev": true,
2135
+
"license": "MIT"
2136
+
},
2137
"node_modules/vite": {
2138
"version": "5.4.21",
2139
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+2
frontend/package.json
+2
frontend/package.json
+5
frontend/postcss.config.js
+5
frontend/postcss.config.js
+5
frontend/src/App.svelte
+5
frontend/src/App.svelte
···
17
18
import Header from "./components/Header.svelte";
19
import Footer from "./components/Footer.svelte";
20
21
let currentRoute = null;
22
let params = {};
···
83
</script>
84
85
<div class="flex flex-col min-h-screen">
86
<Header />
87
88
<main class="flex-1 container mx-auto px-3 md:px-4 py-4 md:py-8">
···
17
18
import Header from "./components/Header.svelte";
19
import Footer from "./components/Footer.svelte";
20
+
import OfflineIndicator from "./components/OfflineIndicator.svelte";
21
+
import UpdateNotification from "./components/UpdateNotification.svelte";
22
23
let currentRoute = null;
24
let params = {};
···
85
</script>
86
87
<div class="flex flex-col min-h-screen">
88
+
<OfflineIndicator />
89
+
<UpdateNotification />
90
+
91
<Header />
92
93
<main class="flex-1 container mx-auto px-3 md:px-4 py-4 md:py-8">
+36
frontend/src/components/OfflineIndicator.svelte
+36
frontend/src/components/OfflineIndicator.svelte
···
···
1
+
<script>
2
+
import { isOnline } from "../stores/pwa.js";
3
+
</script>
4
+
5
+
{#if !$isOnline}
6
+
<div
7
+
class="fixed top-0 left-0 right-0 bg-red-500 text-white px-4 py-2 text-center text-sm font-medium z-50 animate-pulse"
8
+
>
9
+
<div class="flex items-center justify-center gap-2">
10
+
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
11
+
<path
12
+
fill-rule="evenodd"
13
+
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
14
+
clip-rule="evenodd"
15
+
/>
16
+
</svg>
17
+
<span>You're offline. Some features may be limited.</span>
18
+
</div>
19
+
</div>
20
+
{/if}
21
+
22
+
<style>
23
+
@keyframes animate-pulse {
24
+
0%,
25
+
100% {
26
+
opacity: 1;
27
+
}
28
+
50% {
29
+
opacity: 0.5;
30
+
}
31
+
}
32
+
33
+
:global(.animate-pulse) {
34
+
animation: animate-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
35
+
}
36
+
</style>
+60
frontend/src/components/UpdateNotification.svelte
+60
frontend/src/components/UpdateNotification.svelte
···
···
1
+
<script>
2
+
import { updateAvailable, updateServiceWorker } from '../stores/pwa.js';
3
+
4
+
let showNotification = false;
5
+
6
+
$: if ($updateAvailable) {
7
+
showNotification = true;
8
+
}
9
+
10
+
function handleUpdate() {
11
+
showNotification = false;
12
+
updateServiceWorker();
13
+
}
14
+
15
+
function handleDismiss() {
16
+
showNotification = false;
17
+
}
18
+
</script>
19
+
20
+
{#if showNotification}
21
+
<div class="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:max-w-md bg-amber-100 border border-amber-400 rounded-lg shadow-lg p-4">
22
+
<div class="flex items-start gap-3">
23
+
<div class="flex-shrink-0">
24
+
<svg class="h-5 w-5 text-amber-600" fill="currentColor" viewBox="0 0 20 20">
25
+
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
26
+
</svg>
27
+
</div>
28
+
<div class="flex-1">
29
+
<h3 class="text-sm font-medium text-amber-900">Update Available</h3>
30
+
<p class="mt-1 text-sm text-amber-800">
31
+
A new version of Arabica is available.
32
+
</p>
33
+
</div>
34
+
<div class="flex gap-2 flex-shrink-0">
35
+
<button
36
+
on:click={handleUpdate}
37
+
class="inline-flex items-center px-2.5 py-1.5 rounded text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 transition-colors"
38
+
>
39
+
Update
40
+
</button>
41
+
<button
42
+
on:click={handleDismiss}
43
+
class="inline-flex items-center px-2.5 py-1.5 rounded text-sm font-medium text-amber-900 bg-transparent hover:bg-amber-200 transition-colors"
44
+
>
45
+
Dismiss
46
+
</button>
47
+
</div>
48
+
</div>
49
+
</div>
50
+
{/if}
51
+
52
+
<style>
53
+
/* Mobile-friendly positioning */
54
+
@media (max-width: 640px) {
55
+
div {
56
+
margin: 0 1rem;
57
+
max-width: calc(100vw - 2rem);
58
+
}
59
+
}
60
+
</style>
+42
frontend/src/main.js
+42
frontend/src/main.js
···
1
+
import './styles.css';
2
import App from './App.svelte';
3
4
+
// Register service worker for PWA functionality
5
+
// Note: Service workers require a secure context (HTTPS) or localhost/127.0.0.1 for development
6
+
if ('serviceWorker' in navigator) {
7
+
// Wait for page load before registering
8
+
window.addEventListener('load', () => {
9
+
navigator.serviceWorker
10
+
.register('/static/service-worker.js', { scope: '/' })
11
+
.then((registration) => {
12
+
console.log('[App] Service Worker registered:', registration);
13
+
14
+
// Listen for updates
15
+
registration.addEventListener('updatefound', () => {
16
+
const newWorker = registration.installing;
17
+
newWorker.addEventListener('statechange', () => {
18
+
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
19
+
// New service worker available - notify user
20
+
console.log('[App] New service worker available');
21
+
// Dispatch event to show update notification
22
+
window.dispatchEvent(new CustomEvent('sw-update-available'));
23
+
}
24
+
});
25
+
});
26
+
27
+
// Check for updates periodically (every 60 seconds)
28
+
setInterval(() => {
29
+
registration.update();
30
+
}, 60000);
31
+
})
32
+
.catch((err) => {
33
+
console.error('[App] Service Worker registration failed:', err);
34
+
// Log additional context for debugging
35
+
if (err instanceof DOMException && err.name === 'SecurityError') {
36
+
console.warn('[App] Service Worker requires a secure context (HTTPS) or localhost');
37
+
console.warn('[App] For development, access via http://localhost:18910 instead of 127.0.0.1');
38
+
}
39
+
});
40
+
});
41
+
} else {
42
+
console.warn('[App] Service Workers not supported in this browser');
43
+
}
44
+
45
const app = new App({
46
target: document.getElementById('app'),
47
});
+47
frontend/src/stores/pwa.js
+47
frontend/src/stores/pwa.js
···
···
1
+
import { writable } from 'svelte/store';
2
+
3
+
// Track online/offline status
4
+
export const isOnline = writable(
5
+
typeof navigator !== 'undefined' ? navigator.onLine : true
6
+
);
7
+
8
+
// Track service worker update availability
9
+
export const updateAvailable = writable(false);
10
+
11
+
// Initialize online/offline detection
12
+
if (typeof window !== 'undefined') {
13
+
window.addEventListener('online', () => isOnline.set(true));
14
+
window.addEventListener('offline', () => isOnline.set(false));
15
+
16
+
// Listen for service worker updates
17
+
window.addEventListener('sw-update-available', () => {
18
+
console.log('[PWA] Update available');
19
+
updateAvailable.set(true);
20
+
});
21
+
}
22
+
23
+
// Trigger service worker update
24
+
export function updateServiceWorker() {
25
+
if ('serviceWorker' in navigator) {
26
+
navigator.serviceWorker.getRegistration().then((registration) => {
27
+
if (registration) {
28
+
registration.unregister().then(() => {
29
+
window.location.reload();
30
+
});
31
+
}
32
+
});
33
+
}
34
+
}
35
+
36
+
// Request for notification permission (future use for push notifications)
37
+
export function requestNotificationPermission() {
38
+
if ('Notification' in window && 'serviceWorker' in navigator) {
39
+
if (Notification.permission === 'granted') {
40
+
return Promise.resolve();
41
+
}
42
+
if (Notification.permission !== 'denied') {
43
+
return Notification.requestPermission();
44
+
}
45
+
}
46
+
return Promise.reject('Notifications not supported');
47
+
}
static/css/style.css
frontend/src/styles.css
static/css/style.css
frontend/src/styles.css
+5
-2
tailwind.config.js
frontend/tailwind.config.js
+5
-2
tailwind.config.js
frontend/tailwind.config.js
+7
-5
internal/middleware/security.go
+7
-5
internal/middleware/security.go
···
26
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
27
28
// Content Security Policy
29
-
// Allows: self for scripts/styles, inline styles (for Tailwind), jsdelivr for HTMX/Alpine
30
-
// Note: unsafe-eval required for Alpine.js standard build (CSP build has CDN MIME type issues)
31
// Note: form-action allows https: for OAuth redirects to external authorization servers
32
csp := strings.Join([]string{
33
"default-src 'self'",
34
-
"script-src 'self' 'unsafe-eval' https://cdn.jsdelivr.net",
35
-
"style-src 'self' 'unsafe-inline'", // unsafe-inline needed for Tailwind
36
-
"img-src 'self' https: data:", // Allow external images (avatars) and data URIs
37
"font-src 'self'",
38
"connect-src 'self' https:", // Allow connections to external APIs (OAuth, PDS)
39
"frame-ancestors 'none'",
40
"base-uri 'self'",
41
"form-action 'self' https:", // Allow form submissions to external OAuth servers
···
26
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
27
28
// Content Security Policy
29
+
// Allows: self for scripts/styles, inline styles (for Tailwind), external scripts from CDN
30
// Note: form-action allows https: for OAuth redirects to external authorization servers
31
+
// Note: script-src includes https://cdn.jsdelivr.net for external libraries and /static/ for service worker registration
32
csp := strings.Join([]string{
33
"default-src 'self'",
34
+
"script-src 'self' 'unsafe-eval' https://cdn.jsdelivr.net", // External scripts must be listed explicitly
35
+
"script-src-elem 'self' https://cdn.jsdelivr.net", // External script tags (register-sw.js)
36
+
"style-src 'self' 'unsafe-inline'", // unsafe-inline needed for Tailwind
37
+
"img-src 'self' https: data:", // Allow external images (avatars) and data URIs
38
"font-src 'self'",
39
"connect-src 'self' https:", // Allow connections to external APIs (OAuth, PDS)
40
+
"worker-src 'self'", // Service workers must be same-origin
41
"frame-ancestors 'none'",
42
"base-uri 'self'",
43
"form-action 'self' https:", // Allow form submissions to external OAuth servers
+2
-8
justfile
+2
-8
justfile
···
1
run:
2
@LOG_LEVEL=debug LOG_FORMAT=console go run cmd/arabica-server/main.go -known-dids known-dids.txt
3
4
-
run-production:
5
-
@LOG_FORMAT=json SERVER_PUBLIC_URL=https://arabica.example.com go run cmd/arabica-server/main.go
6
7
test:
8
@go test ./... -cover -coverprofile=cover.out
9
-
10
-
style:
11
-
@nix develop --command tailwindcss -i static/css/style.css -o static/css/output.css --minify
12
-
13
-
build-ui:
14
-
@pushd frontend || exit 1 && npm run build && popd || exit 1
+16
-27
static/app/index.html
+16
-27
static/app/index.html
···
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8" />
5
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
<title>Arabica - Coffee Brew Tracker</title>
7
<meta
8
name="description"
9
content="Track your coffee brewing journey with detailed logs stored in your Personal Data Server"
10
/>
11
12
-
<!-- Tailwind CSS -->
13
-
<link rel="stylesheet" href="/static/css/output.css?v=0.1.4" />
14
-
15
-
<!-- Favicon -->
16
-
<link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg" />
17
-
<link
18
-
rel="icon"
19
-
type="image/png"
20
-
sizes="32x32"
21
-
href="/static/images/favicon-32x32.png"
22
-
/>
23
-
<link
24
-
rel="icon"
25
-
type="image/png"
26
-
sizes="16x16"
27
-
href="/static/images/favicon-16x16.png"
28
-
/>
29
-
<link
30
-
rel="apple-touch-icon"
31
-
sizes="180x180"
32
-
href="/static/images/apple-touch-icon.png"
33
-
/>
34
35
-
<!-- Web Manifest -->
36
<link rel="manifest" href="/static/manifest.json" />
37
-
<meta name="theme-color" content="#78350f" />
38
-
<script type="module" crossorigin src="/static/app/assets/index-COWWPLMS.js"></script>
39
-
<link rel="stylesheet" crossorigin href="/static/app/assets/index-DUcERGgO.css">
40
</head>
41
<body class="bg-brown-50 text-brown-900 min-h-screen">
42
<div id="app"></div>
43
</body>
44
</html>
···
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8" />
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+
<meta name="mobile-web-app-capable" content="yes" />
7
+
<meta name="apple-mobile-web-app-capable" content="yes" />
8
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
9
+
<meta name="apple-mobile-web-app-title" content="Arabica" />
10
+
<meta name="theme-color" content="#78350f" />
11
+
<meta name="color-scheme" content="light" />
12
+
13
<title>Arabica - Coffee Brew Tracker</title>
14
<meta
15
name="description"
16
content="Track your coffee brewing journey with detailed logs stored in your Personal Data Server"
17
/>
18
19
+
<!-- Favicon & Icons -->
20
+
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
21
+
<link rel="apple-touch-icon" href="/static/icon-192.svg" />
22
23
+
<!-- Web Manifest for PWA -->
24
<link rel="manifest" href="/static/manifest.json" />
25
+
<script type="module" crossorigin src="/static/app/assets/index-B1vlIgXx.js"></script>
26
+
<link rel="stylesheet" crossorigin href="/static/app/assets/index-C7VTH3Mq.css">
27
</head>
28
<body class="bg-brown-50 text-brown-900 min-h-screen">
29
<div id="app"></div>
30
+
<!-- Service Worker Registration (external script for CSP compliance) -->
31
+
<script defer src="/static/register-sw.js"></script>
32
</body>
33
</html>
-1
static/css/output.css
-1
static/css/output.css
···
1
-
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}button,input[type=button],input[type=submit]{min-height:44px;min-width:44px}@media (max-width:768px){input,select,textarea{font-size:16px}}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.z-50{z-index:50}.-mx-3{margin-left:-.75rem;margin-right:-.75rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-2{height:.5rem}.h-20{height:5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.max-h-60{max-height:15rem}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-1\/6{width:16.666667%}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-20{width:5rem}.w-3\/4{width:75%}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[50px\]{min-width:50px}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-6xl{max-width:72rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-180{--tw-rotate:180deg}.rotate-180,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-0\.5{row-gap:.125rem}.gap-y-1{row-gap:.25rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-brown-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(234 221 215/var(--tw-divide-opacity,1))}.divide-brown-300>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(224 206 199/var(--tw-divide-opacity,1))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-r-lg{border-bottom-right-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l-2{border-left-width:2px}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-amber-400{--tw-border-opacity:1;border-color:rgb(251 191 36/var(--tw-border-opacity,1))}.border-brown-100{--tw-border-opacity:1;border-color:rgb(242 232 229/var(--tw-border-opacity,1))}.border-brown-200{--tw-border-opacity:1;border-color:rgb(234 221 215/var(--tw-border-opacity,1))}.border-brown-300{--tw-border-opacity:1;border-color:rgb(224 206 199/var(--tw-border-opacity,1))}.border-brown-600{--tw-border-opacity:1;border-color:rgb(127 85 57/var(--tw-border-opacity,1))}.border-brown-700{--tw-border-opacity:1;border-color:rgb(107 68 35/var(--tw-border-opacity,1))}.border-brown-800{--tw-border-opacity:1;border-color:rgb(74 44 42/var(--tw-border-opacity,1))}.border-brown-900{--tw-border-opacity:1;border-color:rgb(61 35 25/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity,1))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity,1))}.bg-amber-400{--tw-bg-opacity:1;background-color:rgb(251 191 36/var(--tw-bg-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-brown-200{--tw-bg-opacity:1;background-color:rgb(234 221 215/var(--tw-bg-opacity,1))}.bg-brown-200\/80{background-color:hsla(19,31%,88%,.8)}.bg-brown-300{--tw-bg-opacity:1;background-color:rgb(224 206 199/var(--tw-bg-opacity,1))}.bg-brown-50{--tw-bg-opacity:1;background-color:rgb(253 248 246/var(--tw-bg-opacity,1))}.bg-brown-50\/60{background-color:hsla(17,64%,98%,.6)}.bg-brown-600{--tw-bg-opacity:1;background-color:rgb(127 85 57/var(--tw-bg-opacity,1))}.bg-brown-700{--tw-bg-opacity:1;background-color:rgb(107 68 35/var(--tw-bg-opacity,1))}.bg-brown-800{--tw-bg-opacity:1;background-color:rgb(74 44 42/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/60{background-color:hsla(0,0%,100%,.6)}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-amber-50{--tw-gradient-from:#fffbeb var(--tw-gradient-from-position);--tw-gradient-to:rgba(255,251,235,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-100{--tw-gradient-from:#f2e8e5 var(--tw-gradient-from-position);--tw-gradient-to:hsla(14,33%,92%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-50{--tw-gradient-from:#fdf8f6 var(--tw-gradient-from-position);--tw-gradient-to:hsla(17,64%,98%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-500{--tw-gradient-from:#bfa094 var(--tw-gradient-from-position);--tw-gradient-to:hsla(17,25%,66%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-700{--tw-gradient-from:#6b4423 var(--tw-gradient-from-position);--tw-gradient-to:rgba(107,68,35,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-800{--tw-gradient-from:#4a2c2a var(--tw-gradient-from-position);--tw-gradient-to:rgba(74,44,42,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-brown-100{--tw-gradient-to:#f2e8e5 var(--tw-gradient-to-position)}.to-brown-200{--tw-gradient-to:#eaddd7 var(--tw-gradient-to-position)}.to-brown-600{--tw-gradient-to:#7f5539 var(--tw-gradient-to-position)}.to-brown-800{--tw-gradient-to:#4a2c2a var(--tw-gradient-to-position)}.to-brown-900{--tw-gradient-to:#3d2319 var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-bottom:.125rem;padding-top:.125rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-12{padding-bottom:3rem;padding-top:3rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pl-3{padding-left:.75rem}.pt-1{padding-top:.25rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-6xl{font-size:3.75rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wider{letter-spacing:.05em}.text-amber-900{--tw-text-opacity:1;color:rgb(120 53 15/var(--tw-text-opacity,1))}.text-brown-100{--tw-text-opacity:1;color:rgb(242 232 229/var(--tw-text-opacity,1))}.text-brown-300{--tw-text-opacity:1;color:rgb(224 206 199/var(--tw-text-opacity,1))}.text-brown-400{--tw-text-opacity:1;color:rgb(210 186 176/var(--tw-text-opacity,1))}.text-brown-500{--tw-text-opacity:1;color:rgb(191 160 148/var(--tw-text-opacity,1))}.text-brown-600{--tw-text-opacity:1;color:rgb(127 85 57/var(--tw-text-opacity,1))}.text-brown-700{--tw-text-opacity:1;color:rgb(107 68 35/var(--tw-text-opacity,1))}.text-brown-800{--tw-text-opacity:1;color:rgb(74 44 42/var(--tw-text-opacity,1))}.text-brown-900{--tw-text-opacity:1;color:rgb(61 35 25/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.accent-brown-700{accent-color:#6b4423}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-brown-500{--tw-ring-opacity:1;--tw-ring-color:rgb(191 160 148/var(--tw-ring-opacity,1))}.ring-brown-600{--tw-ring-opacity:1;--tw-ring-color:rgb(127 85 57/var(--tw-ring-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.htmx-swapping{opacity:0;transition:opacity .3s ease-out}.hover\:bg-brown-100:hover{--tw-bg-opacity:1;background-color:rgb(242 232 229/var(--tw-bg-opacity,1))}.hover\:bg-brown-100\/60:hover{background-color:hsla(14,33%,92%,.6)}.hover\:bg-brown-300:hover{--tw-bg-opacity:1;background-color:rgb(224 206 199/var(--tw-bg-opacity,1))}.hover\:bg-brown-400:hover{--tw-bg-opacity:1;background-color:rgb(210 186 176/var(--tw-bg-opacity,1))}.hover\:bg-brown-50:hover{--tw-bg-opacity:1;background-color:rgb(253 248 246/var(--tw-bg-opacity,1))}.hover\:bg-brown-800:hover{--tw-bg-opacity:1;background-color:rgb(74 44 42/var(--tw-bg-opacity,1))}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity,1))}.hover\:from-brown-600:hover{--tw-gradient-from:#7f5539 var(--tw-gradient-from-position);--tw-gradient-to:rgba(127,85,57,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:from-brown-800:hover{--tw-gradient-from:#4a2c2a var(--tw-gradient-from-position);--tw-gradient-to:rgba(74,44,42,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:to-brown-700:hover{--tw-gradient-to:#6b4423 var(--tw-gradient-to-position)}.hover\:to-brown-900:hover{--tw-gradient-to:#3d2319 var(--tw-gradient-to-position)}.hover\:text-brown-700:hover{--tw-text-opacity:1;color:rgb(107 68 35/var(--tw-text-opacity,1))}.hover\:text-brown-800:hover{--tw-text-opacity:1;color:rgb(74 44 42/var(--tw-text-opacity,1))}.hover\:text-brown-900:hover{--tw-text-opacity:1;color:rgb(61 35 25/var(--tw-text-opacity,1))}.hover\:text-red-800:hover{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.hover\:shadow-lg:hover,.hover\:shadow-xl:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.hover\:ring-2:hover{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.hover\:ring-brown-600:hover{--tw-ring-opacity:1;--tw-ring-color:rgb(127 85 57/var(--tw-ring-opacity,1))}.focus\:border-brown-600:focus{--tw-border-opacity:1;border-color:rgb(127 85 57/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-brown-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(127 85 57/var(--tw-ring-opacity,1))}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:640px){.sm\:inline{display:inline}}@media (min-width:768px){.md\:mx-0{margin-left:0;margin-right:0}.md\:mb-6{margin-bottom:1.5rem}.md\:line-clamp-2{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-20{width:5rem}.md\:min-w-\[60px\]{min-width:60px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:items-start{align-items:flex-start}.md\:items-end{align-items:flex-end}.md\:justify-between{justify-content:space-between}.md\:gap-4{gap:1rem}.md\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.md\:p-3{padding:.75rem}.md\:p-4{padding:1rem}.md\:p-5{padding:1.25rem}.md\:p-6{padding:1.5rem}.md\:p-8{padding:2rem}.md\:px-4{padding-left:1rem;padding-right:1rem}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}.md\:py-3{padding-bottom:.75rem;padding-top:.75rem}.md\:py-8{padding-bottom:2rem;padding-top:2rem}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-base{font-size:1rem;line-height:1.5rem}.md\:text-sm{font-size:.875rem;line-height:1.25rem}}
···
-7
static/js/sw-register.js
-7
static/js/sw-register.js
+48
-4
static/manifest.json
+48
-4
static/manifest.json
···
1
{
2
"name": "Arabica - Coffee Brew Tracker",
3
"short_name": "Arabica",
4
-
"description": "Track your coffee brewing journey",
5
"start_url": "/",
6
"display": "standalone",
7
-
"background_color": "#4a2c2a",
8
-
"theme_color": "#4a2c2a",
9
-
"orientation": "portrait",
10
"icons": [
11
{
12
"src": "/static/favicon.svg",
···
26
"type": "image/svg+xml",
27
"purpose": "any maskable"
28
}
29
]
30
}
···
1
{
2
"name": "Arabica - Coffee Brew Tracker",
3
"short_name": "Arabica",
4
+
"description": "Track your coffee brewing journey with detailed logs stored in your Personal Data Server",
5
"start_url": "/",
6
"display": "standalone",
7
+
"orientation": "portrait-primary",
8
+
"scope": "/",
9
+
"background_color": "#faf8f3",
10
+
"theme_color": "#78350f",
11
+
"categories": ["productivity", "lifestyle"],
12
+
"screenshots": [
13
+
{
14
+
"src": "/static/icon-192.svg",
15
+
"sizes": "192x192",
16
+
"type": "image/svg+xml",
17
+
"form_factor": "narrow"
18
+
},
19
+
{
20
+
"src": "/static/icon-512.svg",
21
+
"sizes": "512x512",
22
+
"type": "image/svg+xml",
23
+
"form_factor": "wide"
24
+
}
25
+
],
26
"icons": [
27
{
28
"src": "/static/favicon.svg",
···
42
"type": "image/svg+xml",
43
"purpose": "any maskable"
44
}
45
+
],
46
+
"shortcuts": [
47
+
{
48
+
"name": "Log a Brew",
49
+
"short_name": "New Brew",
50
+
"description": "Create a new brew entry",
51
+
"url": "/brews?action=new",
52
+
"icons": [
53
+
{
54
+
"src": "/static/icon-192.svg",
55
+
"sizes": "192x192",
56
+
"type": "image/svg+xml"
57
+
}
58
+
]
59
+
},
60
+
{
61
+
"name": "View Brews",
62
+
"short_name": "My Brews",
63
+
"description": "View your brew history",
64
+
"url": "/brews",
65
+
"icons": [
66
+
{
67
+
"src": "/static/icon-192.svg",
68
+
"sizes": "192x192",
69
+
"type": "image/svg+xml"
70
+
}
71
+
]
72
+
}
73
]
74
}
+29
static/register-sw.js
+29
static/register-sw.js
···
···
1
+
// Service Worker Registration
2
+
// This is loaded as an external script to comply with strict CSP
3
+
// Note: Service workers require a secure context (HTTPS) or localhost/127.0.0.1 for development
4
+
5
+
if ('serviceWorker' in navigator) {
6
+
window.addEventListener('load', () => {
7
+
navigator.serviceWorker
8
+
.register('/static/service-worker.js', { scope: '/' })
9
+
.then((registration) => {
10
+
console.log('[SW] Service Worker registered:', registration);
11
+
12
+
// Check for updates periodically (every 60 seconds)
13
+
setInterval(() => {
14
+
registration.update();
15
+
}, 60000);
16
+
})
17
+
.catch((err) => {
18
+
console.error('[SW] Service Worker registration failed:', err);
19
+
20
+
// Log additional context for debugging
21
+
if (err instanceof DOMException && err.name === 'SecurityError') {
22
+
console.warn('[SW] Service Worker requires a secure context (HTTPS) or localhost');
23
+
console.warn('[SW] For development, access via http://localhost:18910 instead of 127.0.0.1');
24
+
}
25
+
});
26
+
});
27
+
} else {
28
+
console.warn('[SW] Service Workers not supported in this browser');
29
+
}
+128
-31
static/service-worker.js
+128
-31
static/service-worker.js
···
1
-
const CACHE_NAME = 'arabica-v1';
2
-
const urlsToCache = [
3
-
'/',
4
-
'/static/css/style.css',
5
-
'https://unpkg.com/htmx.org@1.9.10',
6
-
'https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js'
7
];
8
9
-
// Install service worker and cache resources
10
-
self.addEventListener('install', (event) => {
11
event.waitUntil(
12
-
caches.open(CACHE_NAME)
13
-
.then((cache) => cache.addAll(urlsToCache))
14
);
15
});
16
17
-
// Fetch from cache, fallback to network
18
-
self.addEventListener('fetch', (event) => {
19
-
event.respondWith(
20
-
caches.match(event.request)
21
-
.then((response) => {
22
-
// Cache hit - return response
23
-
if (response) {
24
-
return response;
25
-
}
26
-
return fetch(event.request);
27
-
}
28
-
)
29
-
);
30
});
31
32
-
// Update service worker
33
-
self.addEventListener('activate', (event) => {
34
-
const cacheWhitelist = [CACHE_NAME];
35
event.waitUntil(
36
caches.keys().then((cacheNames) => {
37
return Promise.all(
38
-
cacheNames.map((cacheName) => {
39
-
if (cacheWhitelist.indexOf(cacheName) === -1) {
40
return caches.delete(cacheName);
41
-
}
42
-
})
43
);
44
-
})
45
);
46
});
···
1
+
const CACHE_VERSION = "v1";
2
+
const CACHE_NAMES = {
3
+
static: `arabica-static-${CACHE_VERSION}`,
4
+
dynamic: `arabica-dynamic-${CACHE_VERSION}`,
5
+
api: `arabica-api-${CACHE_VERSION}`,
6
+
};
7
+
8
+
// Resources to cache on install
9
+
const staticAssets = [
10
+
"/",
11
+
"/static/app/index.html",
12
+
"/static/manifest.json",
13
+
"/static/favicon.svg",
14
+
"/static/icon-192.svg",
15
+
"/static/icon-512.svg",
16
];
17
18
+
// Install service worker - cache static assets
19
+
self.addEventListener("install", (event) => {
20
+
console.log("[SW] Installing service worker");
21
event.waitUntil(
22
+
caches.open(CACHE_NAMES.static).then((cache) => {
23
+
console.log("[SW] Caching static assets");
24
+
return cache.addAll(staticAssets).catch((err) => {
25
+
console.warn("[SW] Failed to cache some assets:", err);
26
+
// Don't fail install if some assets can't be cached
27
+
return Promise.resolve();
28
+
});
29
+
}),
30
);
31
+
self.skipWaiting(); // Activate new service worker immediately
32
});
33
34
+
// Fetch event - implement cache strategies
35
+
self.addEventListener("fetch", (event) => {
36
+
const { request } = event;
37
+
const url = new URL(request.url);
38
+
39
+
// Skip cross-origin requests
40
+
if (url.origin !== self.location.origin) {
41
+
return;
42
+
}
43
+
44
+
// API requests: network-first with cache fallback
45
+
if (url.pathname.startsWith("/api/")) {
46
+
event.respondWith(networkFirstStrategy(request, CACHE_NAMES.api));
47
+
return;
48
+
}
49
+
50
+
// Static assets: cache-first with network fallback
51
+
if (
52
+
url.pathname.includes("/assets/") ||
53
+
url.pathname.endsWith(".svg") ||
54
+
url.pathname.endsWith(".css") ||
55
+
url.pathname.endsWith(".js")
56
+
) {
57
+
event.respondWith(cacheFirstStrategy(request, CACHE_NAMES.static));
58
+
return;
59
+
}
60
+
61
+
// HTML documents: network-first with cache fallback
62
+
if (request.method === "GET" && request.headers.get("accept")?.includes("text/html")) {
63
+
event.respondWith(networkFirstStrategy(request, CACHE_NAMES.dynamic));
64
+
return;
65
+
}
66
+
67
+
// Default: try network, fallback to cache
68
+
event.respondWith(networkFirstStrategy(request, CACHE_NAMES.dynamic));
69
});
70
71
+
// Activate service worker - clean up old caches
72
+
self.addEventListener("activate", (event) => {
73
+
console.log("[SW] Activating service worker");
74
event.waitUntil(
75
caches.keys().then((cacheNames) => {
76
+
const cacheWhitelist = Object.values(CACHE_NAMES);
77
return Promise.all(
78
+
cacheNames
79
+
.filter((cacheName) => !cacheWhitelist.includes(cacheName))
80
+
.map((cacheName) => {
81
+
console.log("[SW] Deleting old cache:", cacheName);
82
return caches.delete(cacheName);
83
+
}),
84
);
85
+
}),
86
);
87
+
self.clients.claim(); // Take control of all pages immediately
88
});
89
+
90
+
// Cache-first strategy: use cache, fallback to network
91
+
async function cacheFirstStrategy(request, cacheName) {
92
+
const cache = await caches.open(cacheName);
93
+
const cached = await cache.match(request);
94
+
95
+
if (cached) {
96
+
return cached;
97
+
}
98
+
99
+
try {
100
+
const response = await fetch(request);
101
+
if (response.ok) {
102
+
cache.put(request, response.clone());
103
+
}
104
+
return response;
105
+
} catch (error) {
106
+
console.warn("[SW] Fetch failed for", request.url, error);
107
+
// Return offline page if available
108
+
return cache.match("/") || new Response("Offline - content not available", {
109
+
status: 503,
110
+
statusText: "Service Unavailable",
111
+
headers: { "Content-Type": "text/plain" },
112
+
});
113
+
}
114
+
}
115
+
116
+
// Network-first strategy: try network, fallback to cache
117
+
async function networkFirstStrategy(request, cacheName) {
118
+
const cache = await caches.open(cacheName);
119
+
120
+
try {
121
+
const response = await fetch(request);
122
+
if (response.ok) {
123
+
cache.put(request, response.clone());
124
+
}
125
+
return response;
126
+
} catch (error) {
127
+
console.warn("[SW] Network request failed for", request.url, error);
128
+
const cached = await cache.match(request);
129
+
if (cached) {
130
+
return cached;
131
+
}
132
+
133
+
// Fallback for failed requests
134
+
return new Response(
135
+
JSON.stringify({ error: "Offline - unable to fetch data" }),
136
+
{
137
+
status: 503,
138
+
statusText: "Service Unavailable",
139
+
headers: { "Content-Type": "application/json" },
140
+
},
141
+
);
142
+
}
143
+
}
History
1 round
0 comments
pdewey.com
submitted
#0
1 commit
expand
collapse
feat: pwa and tailwind move to vite
expand 0 comments
closed without merging