gablilli commited on
Commit
4cf80fe
Β·
1 Parent(s): 71e8598

feat: add web ui for ourbooks (#2)

Browse files
Files changed (6) hide show
  1. package-lock.json +603 -7
  2. package.json +5 -2
  3. server.js +165 -0
  4. ui/app.js +227 -0
  5. ui/index.html +91 -0
  6. ui/style.css +555 -0
package-lock.json CHANGED
@@ -12,6 +12,7 @@
12
  "adm-zip": "^0.5.12",
13
  "aes-js": "^3.1.2",
14
  "better-sqlite3": "^12.4.1",
 
15
  "fs-extra": "^11.2.0",
16
  "inquirer": "^8.2.7",
17
  "md5": "^2.3.0",
@@ -25,6 +26,7 @@
25
  "sanitize-filename": "^1.6.3",
26
  "svg-to-pdfkit": "^0.1.8",
27
  "sync-request": "^6.1.0",
 
28
  "xml2js": "^0.6.2",
29
  "yargs": "^17.5.1",
30
  "yauzl": "^3.1.3"
@@ -93,6 +95,44 @@
93
  "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
94
  "license": "MIT"
95
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  "node_modules/adm-zip": {
97
  "version": "0.5.16",
98
  "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
@@ -213,6 +253,30 @@
213
  "readable-stream": "^3.4.0"
214
  }
215
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  "node_modules/brotli": {
217
  "version": "1.3.3",
218
  "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
@@ -261,6 +325,15 @@
261
  "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
262
  "license": "MIT"
263
  },
 
 
 
 
 
 
 
 
 
264
  "node_modules/call-bind-apply-helpers": {
265
  "version": "1.0.2",
266
  "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -494,6 +567,46 @@
494
  "safe-buffer": "~5.1.0"
495
  }
496
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  "node_modules/core-util-is": {
498
  "version": "1.0.3",
499
  "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -524,6 +637,23 @@
524
  "node": ">= 12"
525
  }
526
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  "node_modules/decompress-response": {
528
  "version": "6.0.0",
529
  "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -569,6 +699,15 @@
569
  "node": ">=0.4.0"
570
  }
571
  },
 
 
 
 
 
 
 
 
 
572
  "node_modules/detect-libc": {
573
  "version": "2.1.2",
574
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -598,12 +737,27 @@
598
  "node": ">= 0.4"
599
  }
600
  },
 
 
 
 
 
 
601
  "node_modules/emoji-regex": {
602
  "version": "8.0.0",
603
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
604
  "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
605
  "license": "MIT"
606
  },
 
 
 
 
 
 
 
 
 
607
  "node_modules/end-of-stream": {
608
  "version": "1.4.5",
609
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -667,6 +821,12 @@
667
  "node": ">=6"
668
  }
669
  },
 
 
 
 
 
 
670
  "node_modules/escape-string-regexp": {
671
  "version": "1.0.5",
672
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -676,6 +836,15 @@
676
  "node": ">=0.8.0"
677
  }
678
  },
 
 
 
 
 
 
 
 
 
679
  "node_modules/event-lite": {
680
  "version": "0.1.3",
681
  "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
@@ -691,6 +860,74 @@
691
  "node": ">=6"
692
  }
693
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  "node_modules/fast-deep-equal": {
695
  "version": "3.1.3",
696
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -741,6 +978,27 @@
741
  "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
742
  "license": "MIT"
743
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
  "node_modules/fontkit": {
745
  "version": "2.0.4",
746
  "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
@@ -796,6 +1054,24 @@
796
  "node": ">=12.20.0"
797
  }
798
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  "node_modules/fs-constants": {
800
  "version": "1.0.0",
801
  "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -967,6 +1243,26 @@
967
  "node": ">=6.0.0"
968
  }
969
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
970
  "node_modules/http-response-object": {
971
  "version": "3.0.2",
972
  "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
@@ -1112,6 +1408,15 @@
1112
  "integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
1113
  "license": "MIT"
1114
  },
 
 
 
 
 
 
 
 
 
1115
  "node_modules/is-buffer": {
1116
  "version": "1.1.6",
1117
  "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
@@ -1136,6 +1441,12 @@
1136
  "node": ">=8"
1137
  }
1138
  },
 
 
 
 
 
 
1139
  "node_modules/is-unicode-supported": {
1140
  "version": "0.1.0",
1141
  "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -1234,6 +1545,27 @@
1234
  "is-buffer": "~1.1.6"
1235
  }
1236
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1237
  "node_modules/mime-db": {
1238
  "version": "1.52.0",
1239
  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -1291,6 +1623,12 @@
1291
  "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
1292
  "license": "MIT"
1293
  },
 
 
 
 
 
 
1294
  "node_modules/msgpack-lite": {
1295
  "version": "0.1.26",
1296
  "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
@@ -1318,6 +1656,15 @@
1318
  "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
1319
  "license": "MIT"
1320
  },
 
 
 
 
 
 
 
 
 
1321
  "node_modules/node-abi": {
1322
  "version": "3.87.0",
1323
  "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
@@ -1389,6 +1736,18 @@
1389
  "url": "https://github.com/sponsors/ljharb"
1390
  }
1391
  },
 
 
 
 
 
 
 
 
 
 
 
 
1392
  "node_modules/once": {
1393
  "version": "1.4.0",
1394
  "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -1483,6 +1842,25 @@
1483
  "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
1484
  "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
1485
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1486
  "node_modules/pdf-lib": {
1487
  "version": "1.17.1",
1488
  "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
@@ -1586,6 +1964,19 @@
1586
  "strip-ansi": "^5.0.0"
1587
  }
1588
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
1589
  "node_modules/pump": {
1590
  "version": "3.0.3",
1591
  "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -1611,6 +2002,30 @@
1611
  "url": "https://github.com/sponsors/ljharb"
1612
  }
1613
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1614
  "node_modules/rc": {
1615
  "version": "1.2.8",
1616
  "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -1668,6 +2083,22 @@
1668
  "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
1669
  "license": "MIT"
1670
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1671
  "node_modules/run-async": {
1672
  "version": "2.4.1",
1673
  "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@@ -1748,6 +2179,82 @@
1748
  "node": ">=10"
1749
  }
1750
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1751
  "node_modules/side-channel": {
1752
  "version": "1.1.0",
1753
  "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -1871,6 +2378,15 @@
1871
  "simple-concat": "^1.0.0"
1872
  }
1873
  },
 
 
 
 
 
 
 
 
 
1874
  "node_modules/string_decoder": {
1875
  "version": "1.3.0",
1876
  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -2048,6 +2564,15 @@
2048
  "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
2049
  "license": "MIT"
2050
  },
 
 
 
 
 
 
 
 
 
2051
  "node_modules/truncate-utf8-bytes": {
2052
  "version": "1.0.2",
2053
  "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@@ -2087,19 +2612,51 @@
2087
  "url": "https://github.com/sponsors/sindresorhus"
2088
  }
2089
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2090
  "node_modules/typedarray": {
2091
  "version": "0.0.6",
2092
  "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
2093
  "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
2094
  "license": "MIT"
2095
  },
2096
- "node_modules/undici-types": {
2097
- "version": "7.18.2",
2098
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
2099
- "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
2100
- "license": "MIT",
2101
- "optional": true
2102
- },
2103
  "node_modules/unicode-properties": {
2104
  "version": "1.4.1",
2105
  "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
@@ -2135,6 +2692,15 @@
2135
  "node": ">= 10.0.0"
2136
  }
2137
  },
 
 
 
 
 
 
 
 
 
2138
  "node_modules/utf8-byte-length": {
2139
  "version": "1.0.5",
2140
  "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
@@ -2147,6 +2713,15 @@
2147
  "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2148
  "license": "MIT"
2149
  },
 
 
 
 
 
 
 
 
 
2150
  "node_modules/wcwidth": {
2151
  "version": "1.0.1",
2152
  "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -2209,6 +2784,27 @@
2209
  "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
2210
  "license": "ISC"
2211
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2212
  "node_modules/xml2js": {
2213
  "version": "0.6.2",
2214
  "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
 
12
  "adm-zip": "^0.5.12",
13
  "aes-js": "^3.1.2",
14
  "better-sqlite3": "^12.4.1",
15
+ "express": "^5.2.1",
16
  "fs-extra": "^11.2.0",
17
  "inquirer": "^8.2.7",
18
  "md5": "^2.3.0",
 
26
  "sanitize-filename": "^1.6.3",
27
  "svg-to-pdfkit": "^0.1.8",
28
  "sync-request": "^6.1.0",
29
+ "ws": "^8.19.0",
30
  "xml2js": "^0.6.2",
31
  "yargs": "^17.5.1",
32
  "yauzl": "^3.1.3"
 
95
  "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
96
  "license": "MIT"
97
  },
98
+ "node_modules/accepts": {
99
+ "version": "2.0.0",
100
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
101
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
102
+ "license": "MIT",
103
+ "dependencies": {
104
+ "mime-types": "^3.0.0",
105
+ "negotiator": "^1.0.0"
106
+ },
107
+ "engines": {
108
+ "node": ">= 0.6"
109
+ }
110
+ },
111
+ "node_modules/accepts/node_modules/mime-db": {
112
+ "version": "1.54.0",
113
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
114
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
115
+ "license": "MIT",
116
+ "engines": {
117
+ "node": ">= 0.6"
118
+ }
119
+ },
120
+ "node_modules/accepts/node_modules/mime-types": {
121
+ "version": "3.0.2",
122
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
123
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
124
+ "license": "MIT",
125
+ "dependencies": {
126
+ "mime-db": "^1.54.0"
127
+ },
128
+ "engines": {
129
+ "node": ">=18"
130
+ },
131
+ "funding": {
132
+ "type": "opencollective",
133
+ "url": "https://opencollective.com/express"
134
+ }
135
+ },
136
  "node_modules/adm-zip": {
137
  "version": "0.5.16",
138
  "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
 
253
  "readable-stream": "^3.4.0"
254
  }
255
  },
256
+ "node_modules/body-parser": {
257
+ "version": "2.2.2",
258
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
259
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
260
+ "license": "MIT",
261
+ "dependencies": {
262
+ "bytes": "^3.1.2",
263
+ "content-type": "^1.0.5",
264
+ "debug": "^4.4.3",
265
+ "http-errors": "^2.0.0",
266
+ "iconv-lite": "^0.7.0",
267
+ "on-finished": "^2.4.1",
268
+ "qs": "^6.14.1",
269
+ "raw-body": "^3.0.1",
270
+ "type-is": "^2.0.1"
271
+ },
272
+ "engines": {
273
+ "node": ">=18"
274
+ },
275
+ "funding": {
276
+ "type": "opencollective",
277
+ "url": "https://opencollective.com/express"
278
+ }
279
+ },
280
  "node_modules/brotli": {
281
  "version": "1.3.3",
282
  "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
 
325
  "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
326
  "license": "MIT"
327
  },
328
+ "node_modules/bytes": {
329
+ "version": "3.1.2",
330
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
331
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
332
+ "license": "MIT",
333
+ "engines": {
334
+ "node": ">= 0.8"
335
+ }
336
+ },
337
  "node_modules/call-bind-apply-helpers": {
338
  "version": "1.0.2",
339
  "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
 
567
  "safe-buffer": "~5.1.0"
568
  }
569
  },
570
+ "node_modules/content-disposition": {
571
+ "version": "1.0.1",
572
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
573
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
574
+ "license": "MIT",
575
+ "engines": {
576
+ "node": ">=18"
577
+ },
578
+ "funding": {
579
+ "type": "opencollective",
580
+ "url": "https://opencollective.com/express"
581
+ }
582
+ },
583
+ "node_modules/content-type": {
584
+ "version": "1.0.5",
585
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
586
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
587
+ "license": "MIT",
588
+ "engines": {
589
+ "node": ">= 0.6"
590
+ }
591
+ },
592
+ "node_modules/cookie": {
593
+ "version": "0.7.2",
594
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
595
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
596
+ "license": "MIT",
597
+ "engines": {
598
+ "node": ">= 0.6"
599
+ }
600
+ },
601
+ "node_modules/cookie-signature": {
602
+ "version": "1.2.2",
603
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
604
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
605
+ "license": "MIT",
606
+ "engines": {
607
+ "node": ">=6.6.0"
608
+ }
609
+ },
610
  "node_modules/core-util-is": {
611
  "version": "1.0.3",
612
  "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
 
637
  "node": ">= 12"
638
  }
639
  },
640
+ "node_modules/debug": {
641
+ "version": "4.4.3",
642
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
643
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
644
+ "license": "MIT",
645
+ "dependencies": {
646
+ "ms": "^2.1.3"
647
+ },
648
+ "engines": {
649
+ "node": ">=6.0"
650
+ },
651
+ "peerDependenciesMeta": {
652
+ "supports-color": {
653
+ "optional": true
654
+ }
655
+ }
656
+ },
657
  "node_modules/decompress-response": {
658
  "version": "6.0.0",
659
  "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
 
699
  "node": ">=0.4.0"
700
  }
701
  },
702
+ "node_modules/depd": {
703
+ "version": "2.0.0",
704
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
705
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
706
+ "license": "MIT",
707
+ "engines": {
708
+ "node": ">= 0.8"
709
+ }
710
+ },
711
  "node_modules/detect-libc": {
712
  "version": "2.1.2",
713
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
 
737
  "node": ">= 0.4"
738
  }
739
  },
740
+ "node_modules/ee-first": {
741
+ "version": "1.1.1",
742
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
743
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
744
+ "license": "MIT"
745
+ },
746
  "node_modules/emoji-regex": {
747
  "version": "8.0.0",
748
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
749
  "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
750
  "license": "MIT"
751
  },
752
+ "node_modules/encodeurl": {
753
+ "version": "2.0.0",
754
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
755
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
756
+ "license": "MIT",
757
+ "engines": {
758
+ "node": ">= 0.8"
759
+ }
760
+ },
761
  "node_modules/end-of-stream": {
762
  "version": "1.4.5",
763
  "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
 
821
  "node": ">=6"
822
  }
823
  },
824
+ "node_modules/escape-html": {
825
+ "version": "1.0.3",
826
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
827
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
828
+ "license": "MIT"
829
+ },
830
  "node_modules/escape-string-regexp": {
831
  "version": "1.0.5",
832
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
 
836
  "node": ">=0.8.0"
837
  }
838
  },
839
+ "node_modules/etag": {
840
+ "version": "1.8.1",
841
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
842
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
843
+ "license": "MIT",
844
+ "engines": {
845
+ "node": ">= 0.6"
846
+ }
847
+ },
848
  "node_modules/event-lite": {
849
  "version": "0.1.3",
850
  "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
 
860
  "node": ">=6"
861
  }
862
  },
863
+ "node_modules/express": {
864
+ "version": "5.2.1",
865
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
866
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
867
+ "license": "MIT",
868
+ "dependencies": {
869
+ "accepts": "^2.0.0",
870
+ "body-parser": "^2.2.1",
871
+ "content-disposition": "^1.0.0",
872
+ "content-type": "^1.0.5",
873
+ "cookie": "^0.7.1",
874
+ "cookie-signature": "^1.2.1",
875
+ "debug": "^4.4.0",
876
+ "depd": "^2.0.0",
877
+ "encodeurl": "^2.0.0",
878
+ "escape-html": "^1.0.3",
879
+ "etag": "^1.8.1",
880
+ "finalhandler": "^2.1.0",
881
+ "fresh": "^2.0.0",
882
+ "http-errors": "^2.0.0",
883
+ "merge-descriptors": "^2.0.0",
884
+ "mime-types": "^3.0.0",
885
+ "on-finished": "^2.4.1",
886
+ "once": "^1.4.0",
887
+ "parseurl": "^1.3.3",
888
+ "proxy-addr": "^2.0.7",
889
+ "qs": "^6.14.0",
890
+ "range-parser": "^1.2.1",
891
+ "router": "^2.2.0",
892
+ "send": "^1.1.0",
893
+ "serve-static": "^2.2.0",
894
+ "statuses": "^2.0.1",
895
+ "type-is": "^2.0.1",
896
+ "vary": "^1.1.2"
897
+ },
898
+ "engines": {
899
+ "node": ">= 18"
900
+ },
901
+ "funding": {
902
+ "type": "opencollective",
903
+ "url": "https://opencollective.com/express"
904
+ }
905
+ },
906
+ "node_modules/express/node_modules/mime-db": {
907
+ "version": "1.54.0",
908
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
909
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
910
+ "license": "MIT",
911
+ "engines": {
912
+ "node": ">= 0.6"
913
+ }
914
+ },
915
+ "node_modules/express/node_modules/mime-types": {
916
+ "version": "3.0.2",
917
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
918
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
919
+ "license": "MIT",
920
+ "dependencies": {
921
+ "mime-db": "^1.54.0"
922
+ },
923
+ "engines": {
924
+ "node": ">=18"
925
+ },
926
+ "funding": {
927
+ "type": "opencollective",
928
+ "url": "https://opencollective.com/express"
929
+ }
930
+ },
931
  "node_modules/fast-deep-equal": {
932
  "version": "3.1.3",
933
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 
978
  "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
979
  "license": "MIT"
980
  },
981
+ "node_modules/finalhandler": {
982
+ "version": "2.1.1",
983
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
984
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
985
+ "license": "MIT",
986
+ "dependencies": {
987
+ "debug": "^4.4.0",
988
+ "encodeurl": "^2.0.0",
989
+ "escape-html": "^1.0.3",
990
+ "on-finished": "^2.4.1",
991
+ "parseurl": "^1.3.3",
992
+ "statuses": "^2.0.1"
993
+ },
994
+ "engines": {
995
+ "node": ">= 18.0.0"
996
+ },
997
+ "funding": {
998
+ "type": "opencollective",
999
+ "url": "https://opencollective.com/express"
1000
+ }
1001
+ },
1002
  "node_modules/fontkit": {
1003
  "version": "2.0.4",
1004
  "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
 
1054
  "node": ">=12.20.0"
1055
  }
1056
  },
1057
+ "node_modules/forwarded": {
1058
+ "version": "0.2.0",
1059
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
1060
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
1061
+ "license": "MIT",
1062
+ "engines": {
1063
+ "node": ">= 0.6"
1064
+ }
1065
+ },
1066
+ "node_modules/fresh": {
1067
+ "version": "2.0.0",
1068
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
1069
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
1070
+ "license": "MIT",
1071
+ "engines": {
1072
+ "node": ">= 0.8"
1073
+ }
1074
+ },
1075
  "node_modules/fs-constants": {
1076
  "version": "1.0.0",
1077
  "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
 
1243
  "node": ">=6.0.0"
1244
  }
1245
  },
1246
+ "node_modules/http-errors": {
1247
+ "version": "2.0.1",
1248
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
1249
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
1250
+ "license": "MIT",
1251
+ "dependencies": {
1252
+ "depd": "~2.0.0",
1253
+ "inherits": "~2.0.4",
1254
+ "setprototypeof": "~1.2.0",
1255
+ "statuses": "~2.0.2",
1256
+ "toidentifier": "~1.0.1"
1257
+ },
1258
+ "engines": {
1259
+ "node": ">= 0.8"
1260
+ },
1261
+ "funding": {
1262
+ "type": "opencollective",
1263
+ "url": "https://opencollective.com/express"
1264
+ }
1265
+ },
1266
  "node_modules/http-response-object": {
1267
  "version": "3.0.2",
1268
  "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
 
1408
  "integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
1409
  "license": "MIT"
1410
  },
1411
+ "node_modules/ipaddr.js": {
1412
+ "version": "1.9.1",
1413
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1414
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1415
+ "license": "MIT",
1416
+ "engines": {
1417
+ "node": ">= 0.10"
1418
+ }
1419
+ },
1420
  "node_modules/is-buffer": {
1421
  "version": "1.1.6",
1422
  "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
 
1441
  "node": ">=8"
1442
  }
1443
  },
1444
+ "node_modules/is-promise": {
1445
+ "version": "4.0.0",
1446
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
1447
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
1448
+ "license": "MIT"
1449
+ },
1450
  "node_modules/is-unicode-supported": {
1451
  "version": "0.1.0",
1452
  "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
 
1545
  "is-buffer": "~1.1.6"
1546
  }
1547
  },
1548
+ "node_modules/media-typer": {
1549
+ "version": "1.1.0",
1550
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
1551
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
1552
+ "license": "MIT",
1553
+ "engines": {
1554
+ "node": ">= 0.8"
1555
+ }
1556
+ },
1557
+ "node_modules/merge-descriptors": {
1558
+ "version": "2.0.0",
1559
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
1560
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
1561
+ "license": "MIT",
1562
+ "engines": {
1563
+ "node": ">=18"
1564
+ },
1565
+ "funding": {
1566
+ "url": "https://github.com/sponsors/sindresorhus"
1567
+ }
1568
+ },
1569
  "node_modules/mime-db": {
1570
  "version": "1.52.0",
1571
  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
 
1623
  "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
1624
  "license": "MIT"
1625
  },
1626
+ "node_modules/ms": {
1627
+ "version": "2.1.3",
1628
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1629
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1630
+ "license": "MIT"
1631
+ },
1632
  "node_modules/msgpack-lite": {
1633
  "version": "0.1.26",
1634
  "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
 
1656
  "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
1657
  "license": "MIT"
1658
  },
1659
+ "node_modules/negotiator": {
1660
+ "version": "1.0.0",
1661
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
1662
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
1663
+ "license": "MIT",
1664
+ "engines": {
1665
+ "node": ">= 0.6"
1666
+ }
1667
+ },
1668
  "node_modules/node-abi": {
1669
  "version": "3.87.0",
1670
  "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
 
1736
  "url": "https://github.com/sponsors/ljharb"
1737
  }
1738
  },
1739
+ "node_modules/on-finished": {
1740
+ "version": "2.4.1",
1741
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1742
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1743
+ "license": "MIT",
1744
+ "dependencies": {
1745
+ "ee-first": "1.1.1"
1746
+ },
1747
+ "engines": {
1748
+ "node": ">= 0.8"
1749
+ }
1750
+ },
1751
  "node_modules/once": {
1752
  "version": "1.4.0",
1753
  "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
 
1842
  "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
1843
  "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="
1844
  },
1845
+ "node_modules/parseurl": {
1846
+ "version": "1.3.3",
1847
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1848
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1849
+ "license": "MIT",
1850
+ "engines": {
1851
+ "node": ">= 0.8"
1852
+ }
1853
+ },
1854
+ "node_modules/path-to-regexp": {
1855
+ "version": "8.3.0",
1856
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
1857
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
1858
+ "license": "MIT",
1859
+ "funding": {
1860
+ "type": "opencollective",
1861
+ "url": "https://opencollective.com/express"
1862
+ }
1863
+ },
1864
  "node_modules/pdf-lib": {
1865
  "version": "1.17.1",
1866
  "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
 
1964
  "strip-ansi": "^5.0.0"
1965
  }
1966
  },
1967
+ "node_modules/proxy-addr": {
1968
+ "version": "2.0.7",
1969
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1970
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1971
+ "license": "MIT",
1972
+ "dependencies": {
1973
+ "forwarded": "0.2.0",
1974
+ "ipaddr.js": "1.9.1"
1975
+ },
1976
+ "engines": {
1977
+ "node": ">= 0.10"
1978
+ }
1979
+ },
1980
  "node_modules/pump": {
1981
  "version": "3.0.3",
1982
  "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
 
2002
  "url": "https://github.com/sponsors/ljharb"
2003
  }
2004
  },
2005
+ "node_modules/range-parser": {
2006
+ "version": "1.2.1",
2007
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
2008
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
2009
+ "license": "MIT",
2010
+ "engines": {
2011
+ "node": ">= 0.6"
2012
+ }
2013
+ },
2014
+ "node_modules/raw-body": {
2015
+ "version": "3.0.2",
2016
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
2017
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
2018
+ "license": "MIT",
2019
+ "dependencies": {
2020
+ "bytes": "~3.1.2",
2021
+ "http-errors": "~2.0.1",
2022
+ "iconv-lite": "~0.7.0",
2023
+ "unpipe": "~1.0.0"
2024
+ },
2025
+ "engines": {
2026
+ "node": ">= 0.10"
2027
+ }
2028
+ },
2029
  "node_modules/rc": {
2030
  "version": "1.2.8",
2031
  "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
 
2083
  "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
2084
  "license": "MIT"
2085
  },
2086
+ "node_modules/router": {
2087
+ "version": "2.2.0",
2088
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
2089
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
2090
+ "license": "MIT",
2091
+ "dependencies": {
2092
+ "debug": "^4.4.0",
2093
+ "depd": "^2.0.0",
2094
+ "is-promise": "^4.0.0",
2095
+ "parseurl": "^1.3.3",
2096
+ "path-to-regexp": "^8.0.0"
2097
+ },
2098
+ "engines": {
2099
+ "node": ">= 18"
2100
+ }
2101
+ },
2102
  "node_modules/run-async": {
2103
  "version": "2.4.1",
2104
  "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
 
2179
  "node": ">=10"
2180
  }
2181
  },
2182
+ "node_modules/send": {
2183
+ "version": "1.2.1",
2184
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
2185
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
2186
+ "license": "MIT",
2187
+ "dependencies": {
2188
+ "debug": "^4.4.3",
2189
+ "encodeurl": "^2.0.0",
2190
+ "escape-html": "^1.0.3",
2191
+ "etag": "^1.8.1",
2192
+ "fresh": "^2.0.0",
2193
+ "http-errors": "^2.0.1",
2194
+ "mime-types": "^3.0.2",
2195
+ "ms": "^2.1.3",
2196
+ "on-finished": "^2.4.1",
2197
+ "range-parser": "^1.2.1",
2198
+ "statuses": "^2.0.2"
2199
+ },
2200
+ "engines": {
2201
+ "node": ">= 18"
2202
+ },
2203
+ "funding": {
2204
+ "type": "opencollective",
2205
+ "url": "https://opencollective.com/express"
2206
+ }
2207
+ },
2208
+ "node_modules/send/node_modules/mime-db": {
2209
+ "version": "1.54.0",
2210
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
2211
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
2212
+ "license": "MIT",
2213
+ "engines": {
2214
+ "node": ">= 0.6"
2215
+ }
2216
+ },
2217
+ "node_modules/send/node_modules/mime-types": {
2218
+ "version": "3.0.2",
2219
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
2220
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
2221
+ "license": "MIT",
2222
+ "dependencies": {
2223
+ "mime-db": "^1.54.0"
2224
+ },
2225
+ "engines": {
2226
+ "node": ">=18"
2227
+ },
2228
+ "funding": {
2229
+ "type": "opencollective",
2230
+ "url": "https://opencollective.com/express"
2231
+ }
2232
+ },
2233
+ "node_modules/serve-static": {
2234
+ "version": "2.2.1",
2235
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
2236
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
2237
+ "license": "MIT",
2238
+ "dependencies": {
2239
+ "encodeurl": "^2.0.0",
2240
+ "escape-html": "^1.0.3",
2241
+ "parseurl": "^1.3.3",
2242
+ "send": "^1.2.0"
2243
+ },
2244
+ "engines": {
2245
+ "node": ">= 18"
2246
+ },
2247
+ "funding": {
2248
+ "type": "opencollective",
2249
+ "url": "https://opencollective.com/express"
2250
+ }
2251
+ },
2252
+ "node_modules/setprototypeof": {
2253
+ "version": "1.2.0",
2254
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
2255
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
2256
+ "license": "ISC"
2257
+ },
2258
  "node_modules/side-channel": {
2259
  "version": "1.1.0",
2260
  "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
 
2378
  "simple-concat": "^1.0.0"
2379
  }
2380
  },
2381
+ "node_modules/statuses": {
2382
+ "version": "2.0.2",
2383
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
2384
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
2385
+ "license": "MIT",
2386
+ "engines": {
2387
+ "node": ">= 0.8"
2388
+ }
2389
+ },
2390
  "node_modules/string_decoder": {
2391
  "version": "1.3.0",
2392
  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
 
2564
  "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
2565
  "license": "MIT"
2566
  },
2567
+ "node_modules/toidentifier": {
2568
+ "version": "1.0.1",
2569
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
2570
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
2571
+ "license": "MIT",
2572
+ "engines": {
2573
+ "node": ">=0.6"
2574
+ }
2575
+ },
2576
  "node_modules/truncate-utf8-bytes": {
2577
  "version": "1.0.2",
2578
  "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
 
2612
  "url": "https://github.com/sponsors/sindresorhus"
2613
  }
2614
  },
2615
+ "node_modules/type-is": {
2616
+ "version": "2.0.1",
2617
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
2618
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
2619
+ "license": "MIT",
2620
+ "dependencies": {
2621
+ "content-type": "^1.0.5",
2622
+ "media-typer": "^1.1.0",
2623
+ "mime-types": "^3.0.0"
2624
+ },
2625
+ "engines": {
2626
+ "node": ">= 0.6"
2627
+ }
2628
+ },
2629
+ "node_modules/type-is/node_modules/mime-db": {
2630
+ "version": "1.54.0",
2631
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
2632
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
2633
+ "license": "MIT",
2634
+ "engines": {
2635
+ "node": ">= 0.6"
2636
+ }
2637
+ },
2638
+ "node_modules/type-is/node_modules/mime-types": {
2639
+ "version": "3.0.2",
2640
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
2641
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
2642
+ "license": "MIT",
2643
+ "dependencies": {
2644
+ "mime-db": "^1.54.0"
2645
+ },
2646
+ "engines": {
2647
+ "node": ">=18"
2648
+ },
2649
+ "funding": {
2650
+ "type": "opencollective",
2651
+ "url": "https://opencollective.com/express"
2652
+ }
2653
+ },
2654
  "node_modules/typedarray": {
2655
  "version": "0.0.6",
2656
  "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
2657
  "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
2658
  "license": "MIT"
2659
  },
 
 
 
 
 
 
 
2660
  "node_modules/unicode-properties": {
2661
  "version": "1.4.1",
2662
  "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
 
2692
  "node": ">= 10.0.0"
2693
  }
2694
  },
2695
+ "node_modules/unpipe": {
2696
+ "version": "1.0.0",
2697
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
2698
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
2699
+ "license": "MIT",
2700
+ "engines": {
2701
+ "node": ">= 0.8"
2702
+ }
2703
+ },
2704
  "node_modules/utf8-byte-length": {
2705
  "version": "1.0.5",
2706
  "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
 
2713
  "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2714
  "license": "MIT"
2715
  },
2716
+ "node_modules/vary": {
2717
+ "version": "1.1.2",
2718
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2719
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2720
+ "license": "MIT",
2721
+ "engines": {
2722
+ "node": ">= 0.8"
2723
+ }
2724
+ },
2725
  "node_modules/wcwidth": {
2726
  "version": "1.0.1",
2727
  "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
 
2784
  "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
2785
  "license": "ISC"
2786
  },
2787
+ "node_modules/ws": {
2788
+ "version": "8.19.0",
2789
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
2790
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
2791
+ "license": "MIT",
2792
+ "engines": {
2793
+ "node": ">=10.0.0"
2794
+ },
2795
+ "peerDependencies": {
2796
+ "bufferutil": "^4.0.1",
2797
+ "utf-8-validate": ">=5.0.2"
2798
+ },
2799
+ "peerDependenciesMeta": {
2800
+ "bufferutil": {
2801
+ "optional": true
2802
+ },
2803
+ "utf-8-validate": {
2804
+ "optional": true
2805
+ }
2806
+ }
2807
+ },
2808
  "node_modules/xml2js": {
2809
  "version": "0.6.2",
2810
  "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
package.json CHANGED
@@ -6,7 +6,8 @@
6
  "scripts": {
7
  "cli": "node cli.js",
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
- "hub:docenti": "node hubscuola-docenti.js"
 
10
  },
11
  "repository": {
12
  "type": "git",
@@ -23,19 +24,21 @@
23
  "adm-zip": "^0.5.12",
24
  "aes-js": "^3.1.2",
25
  "better-sqlite3": "^12.4.1",
 
26
  "fs-extra": "^11.2.0",
27
  "inquirer": "^8.2.7",
28
  "md5": "^2.3.0",
29
  "msgpack-lite": "^0.1.26",
30
  "node-fetch": "^3.3.2",
31
  "node-forge": "^1.3.3",
 
32
  "pdf-lib": "^1.17.1",
33
  "pdf-merger-js": "^5.1.1",
34
- "p-limit": "^6.2.0",
35
  "prompt-sync": "^4.2.0",
36
  "sanitize-filename": "^1.6.3",
37
  "svg-to-pdfkit": "^0.1.8",
38
  "sync-request": "^6.1.0",
 
39
  "xml2js": "^0.6.2",
40
  "yargs": "^17.5.1",
41
  "yauzl": "^3.1.3"
 
6
  "scripts": {
7
  "cli": "node cli.js",
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
+ "hub:docenti": "node hubscuola-docenti.js",
10
+ "ui": "node server.js"
11
  },
12
  "repository": {
13
  "type": "git",
 
24
  "adm-zip": "^0.5.12",
25
  "aes-js": "^3.1.2",
26
  "better-sqlite3": "^12.4.1",
27
+ "express": "^5.2.1",
28
  "fs-extra": "^11.2.0",
29
  "inquirer": "^8.2.7",
30
  "md5": "^2.3.0",
31
  "msgpack-lite": "^0.1.26",
32
  "node-fetch": "^3.3.2",
33
  "node-forge": "^1.3.3",
34
+ "p-limit": "^6.2.0",
35
  "pdf-lib": "^1.17.1",
36
  "pdf-merger-js": "^5.1.1",
 
37
  "prompt-sync": "^4.2.0",
38
  "sanitize-filename": "^1.6.3",
39
  "svg-to-pdfkit": "^0.1.8",
40
  "sync-request": "^6.1.0",
41
+ "ws": "^8.19.0",
42
  "xml2js": "^0.6.2",
43
  "yargs": "^17.5.1",
44
  "yauzl": "^3.1.3"
server.js ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { WebSocketServer } from 'ws';
3
+ import { createServer } from 'http';
4
+ import { spawn } from 'child_process';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ const app = express();
11
+ app.use(express.json());
12
+ app.use(express.static(path.join(__dirname, 'ui')));
13
+
14
+ const server = createServer(app);
15
+ const wss = new WebSocketServer({ server });
16
+
17
+ const PROVIDERS = {
18
+ sanoma: {
19
+ label: 'Sanoma',
20
+ emoji: 'πŸ“™',
21
+ fields: [
22
+ { name: 'id', label: 'Email account', type: 'text', required: true, placeholder: 'user@email.com' },
23
+ { name: 'password', label: 'Password', type: 'password', required: true, placeholder: 'β€’β€’β€’β€’β€’β€’β€’β€’' },
24
+ { name: 'gedi', label: 'GEDI libro', type: 'text', required: false, placeholder: 'Es: 123456' },
25
+ { name: 'output', label: 'Nome file output', type: 'text', required: false, placeholder: 'libro.pdf' },
26
+ ]
27
+ },
28
+ hubscuola: {
29
+ label: 'HubScuola',
30
+ emoji: 'πŸ“˜',
31
+ fields: [
32
+ { name: 'platform', label: 'Piattaforma', type: 'select', required: true, options: ['hubyoung', 'hubkids'] },
33
+ { name: 'volumeId', label: 'Volume ID', type: 'text', required: true, placeholder: 'Es: 12345' },
34
+ { name: 'token', label: 'Token sessione', type: 'text', required: true, placeholder: 'Token-Session' },
35
+ { name: 'file', label: 'Nome file output', type: 'text', required: false, placeholder: 'libro.pdf' },
36
+ ]
37
+ },
38
+ dibooklaterza: {
39
+ label: 'Laterza',
40
+ emoji: 'πŸ“—',
41
+ fields: [
42
+ { name: 'jwt', label: 'jwtToken (da localStorage)', type: 'text', required: true, placeholder: 'eyJ...' },
43
+ { name: 'isbn', label: 'ISBN', type: 'text', required: true, placeholder: '978...' },
44
+ { name: 'output', label: 'Nome file output', type: 'text', required: false, placeholder: 'libro.pdf' },
45
+ ]
46
+ },
47
+ zanichelli: {
48
+ label: 'Zanichelli',
49
+ emoji: 'πŸ“•',
50
+ fields: [
51
+ { name: 'username', label: 'Email account', type: 'text', required: true, placeholder: 'user@email.com' },
52
+ { name: 'password', label: 'Password', type: 'password', required: true, placeholder: 'β€’β€’β€’β€’β€’β€’β€’β€’' },
53
+ { name: 'isbn', label: 'ISBN', type: 'text', required: false, placeholder: '978...' },
54
+ ]
55
+ },
56
+ bsmart: {
57
+ label: 'Bsmart / Digibook24',
58
+ emoji: 'πŸ“”',
59
+ fields: [
60
+ { name: 'site', label: 'Sito', type: 'select', required: true, options: ['bsmart', 'digibook24'] },
61
+ { name: 'cookie', label: 'Cookie _bsw_session_v1_production', type: 'text', required: true, placeholder: 'Incolla il cookie qui' },
62
+ { name: 'bookId', label: 'Book ID', type: 'text', required: false, placeholder: 'Es: 123456' },
63
+ { name: 'output', label: 'Nome file output', type: 'text', required: false, placeholder: 'libro.pdf' },
64
+ ]
65
+ }
66
+ };
67
+
68
+ app.get('/api/providers', (req, res) => {
69
+ res.json(PROVIDERS);
70
+ });
71
+
72
+ const activeProcesses = new Map();
73
+
74
+ wss.on('connection', (ws) => {
75
+ let activeProcess = null;
76
+
77
+ ws.on('message', (data) => {
78
+ let msg;
79
+ try {
80
+ msg = JSON.parse(data);
81
+ } catch {
82
+ return;
83
+ }
84
+
85
+ if (msg.type === 'start') {
86
+ const { provider, options } = msg;
87
+
88
+ if (!PROVIDERS[provider]) {
89
+ ws.send(JSON.stringify({ type: 'error', text: 'Provider non valido.' }));
90
+ return;
91
+ }
92
+
93
+ /* Validate that option keys are known fields for this provider */
94
+ const knownFields = new Set(PROVIDERS[provider].fields.map(f => f.name));
95
+ const safeOptions = {};
96
+ for (const [key, value] of Object.entries(options || {})) {
97
+ if (!knownFields.has(key)) continue;
98
+ const str = String(value);
99
+ /* Reject values that contain shell metacharacters */
100
+ if (/[\r\n\0]/.test(str)) continue;
101
+ safeOptions[key] = str;
102
+ }
103
+
104
+ const args = ['cli.js', '--provider', provider];
105
+
106
+ for (const [key, value] of Object.entries(safeOptions)) {
107
+ if (value !== '') {
108
+ args.push(`--${key}`, value);
109
+ }
110
+ }
111
+
112
+ ws.send(JSON.stringify({ type: 'started', text: `β–Ά Avvio provider: ${provider}\n` }));
113
+
114
+ activeProcess = spawn('node', args, {
115
+ cwd: __dirname,
116
+ env: { ...process.env }
117
+ });
118
+
119
+ activeProcesses.set(ws, activeProcess);
120
+
121
+ activeProcess.stdout.on('data', (chunk) => {
122
+ ws.send(JSON.stringify({ type: 'stdout', text: chunk.toString() }));
123
+ });
124
+
125
+ activeProcess.stderr.on('data', (chunk) => {
126
+ ws.send(JSON.stringify({ type: 'stderr', text: chunk.toString() }));
127
+ });
128
+
129
+ activeProcess.on('close', (code) => {
130
+ activeProcesses.delete(ws);
131
+ activeProcess = null;
132
+ ws.send(JSON.stringify({
133
+ type: 'done',
134
+ text: `\nβœ… Processo terminato con codice ${code}\n`,
135
+ code
136
+ }));
137
+ });
138
+
139
+ activeProcess.on('error', (err) => {
140
+ ws.send(JSON.stringify({ type: 'error', text: `\n❌ Errore: ${err.message}\n` }));
141
+ });
142
+ }
143
+
144
+ if (msg.type === 'stop') {
145
+ if (activeProcess) {
146
+ activeProcess.kill('SIGTERM');
147
+ activeProcess = null;
148
+ ws.send(JSON.stringify({ type: 'stopped', text: '\nβ›” Processo interrotto.\n' }));
149
+ }
150
+ }
151
+ });
152
+
153
+ ws.on('close', () => {
154
+ const proc = activeProcesses.get(ws);
155
+ if (proc) {
156
+ proc.kill('SIGTERM');
157
+ activeProcesses.delete(ws);
158
+ }
159
+ });
160
+ });
161
+
162
+ const PORT = process.env.PORT || 3000;
163
+ server.listen(PORT, () => {
164
+ console.log(`πŸ“š ourbooks UI disponibile su http://localhost:${PORT}`);
165
+ });
ui/app.js ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* global state */
2
+ let providers = {};
3
+ let selectedProvider = null;
4
+ let ws = null;
5
+ let running = false;
6
+
7
+ /* DOM refs */
8
+ const providerList = document.getElementById('providerList');
9
+ const providerGrid = document.getElementById('providerGrid');
10
+ const welcomeState = document.getElementById('welcomeState');
11
+ const downloadForm = document.getElementById('downloadForm');
12
+ const formFields = document.getElementById('formFields');
13
+ const providerEmoji = document.getElementById('providerEmoji');
14
+ const providerTitle = document.getElementById('providerTitle');
15
+ const downloadFormEl = document.getElementById('downloadFormEl');
16
+ const startBtn = document.getElementById('startBtn');
17
+ const stopBtn = document.getElementById('stopBtn');
18
+ const terminalSection = document.getElementById('terminalSection');
19
+ const terminal = document.getElementById('terminal');
20
+ const clearBtn = document.getElementById('clearBtn');
21
+
22
+ /* ─── Fetch providers ─── */
23
+ async function init() {
24
+ try {
25
+ const res = await fetch('/api/providers');
26
+ providers = await res.json();
27
+ renderSidebar();
28
+ renderGrid();
29
+ connectWS();
30
+ } catch (err) {
31
+ appendTerminal(`\nErrore di connessione al server: ${err.message}\n`, 'stderr');
32
+ terminalSection.classList.remove('hidden');
33
+ }
34
+ }
35
+
36
+ /* ─── Sidebar ─── */
37
+ function renderSidebar() {
38
+ providerList.innerHTML = '';
39
+ for (const [id, p] of Object.entries(providers)) {
40
+ const btn = document.createElement('button');
41
+ btn.className = 'provider-btn';
42
+ btn.dataset.id = id;
43
+ btn.innerHTML = `<span class="p-emoji">${p.emoji}</span><span class="p-label">${p.label}</span>`;
44
+ btn.addEventListener('click', () => selectProvider(id));
45
+ providerList.appendChild(btn);
46
+ }
47
+ }
48
+
49
+ /* ─── Welcome grid ─── */
50
+ function renderGrid() {
51
+ providerGrid.innerHTML = '';
52
+ for (const [id, p] of Object.entries(providers)) {
53
+ const card = document.createElement('div');
54
+ card.className = 'provider-card';
55
+ card.innerHTML = `<span class="pc-emoji">${p.emoji}</span><span class="pc-label">${p.label}</span>`;
56
+ card.addEventListener('click', () => selectProvider(id));
57
+ providerGrid.appendChild(card);
58
+ }
59
+ }
60
+
61
+ /* ─── Select provider ─── */
62
+ function selectProvider(id) {
63
+ selectedProvider = id;
64
+
65
+ /* update sidebar active state */
66
+ document.querySelectorAll('.provider-btn').forEach(btn => {
67
+ btn.classList.toggle('active', btn.dataset.id === id);
68
+ });
69
+
70
+ const p = providers[id];
71
+
72
+ /* update form header */
73
+ providerEmoji.textContent = p.emoji;
74
+ providerTitle.textContent = p.label;
75
+
76
+ /* render fields */
77
+ formFields.innerHTML = '';
78
+ for (const field of p.fields) {
79
+ formFields.appendChild(buildField(field));
80
+ }
81
+
82
+ /* show form, hide welcome */
83
+ welcomeState.classList.add('hidden');
84
+ downloadForm.classList.remove('hidden');
85
+ }
86
+
87
+ /* ─── Build a form field ─── */
88
+ function buildField(field) {
89
+ const group = document.createElement('div');
90
+ group.className = 'field-group';
91
+
92
+ const label = document.createElement('label');
93
+ label.setAttribute('for', `field-${field.name}`);
94
+ label.innerHTML = field.label +
95
+ (field.required
96
+ ? `<span class="required-badge">*</span>`
97
+ : `<span class="optional-badge">(opzionale)</span>`);
98
+
99
+ let input;
100
+ if (field.type === 'select') {
101
+ input = document.createElement('select');
102
+ for (const opt of field.options) {
103
+ const o = document.createElement('option');
104
+ o.value = o.textContent = opt;
105
+ input.appendChild(o);
106
+ }
107
+ } else {
108
+ input = document.createElement('input');
109
+ input.type = field.type;
110
+ input.placeholder = field.placeholder || '';
111
+ if (field.required) input.required = true;
112
+ }
113
+
114
+ input.id = `field-${field.name}`;
115
+ input.name = field.name;
116
+
117
+ group.appendChild(label);
118
+ group.appendChild(input);
119
+ return group;
120
+ }
121
+
122
+ /* ─── WebSocket ─── */
123
+ function connectWS() {
124
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
125
+ ws = new WebSocket(`${protocol}//${location.host}`);
126
+
127
+ ws.onopen = () => {
128
+ appendTerminal('Connesso al server.\n', 'muted');
129
+ terminalSection.classList.remove('hidden');
130
+ };
131
+
132
+ ws.onmessage = (e) => {
133
+ let msg;
134
+ try { msg = JSON.parse(e.data); } catch { return; }
135
+
136
+ switch (msg.type) {
137
+ case 'started':
138
+ setRunning(true);
139
+ appendTerminal(msg.text, 'blue');
140
+ break;
141
+ case 'stdout':
142
+ appendTerminal(msg.text, 'normal');
143
+ break;
144
+ case 'stderr':
145
+ appendTerminal(msg.text, 'stderr');
146
+ break;
147
+ case 'done':
148
+ setRunning(false);
149
+ appendTerminal(msg.text, msg.code === 0 ? 'green' : 'red');
150
+ break;
151
+ case 'stopped':
152
+ setRunning(false);
153
+ appendTerminal(msg.text, 'yellow');
154
+ break;
155
+ case 'error':
156
+ setRunning(false);
157
+ appendTerminal(msg.text, 'stderr');
158
+ break;
159
+ }
160
+ };
161
+
162
+ ws.onclose = () => {
163
+ appendTerminal('\nConnessione chiusa. Ricarica la pagina per riconnetterti.\n', 'muted');
164
+ setRunning(false);
165
+ };
166
+
167
+ ws.onerror = () => {
168
+ appendTerminal('\nErrore WebSocket.\n', 'stderr');
169
+ };
170
+ }
171
+
172
+ /* ─── Start download ─── */
173
+ downloadFormEl.addEventListener('submit', (e) => {
174
+ e.preventDefault();
175
+ if (!selectedProvider || !ws || ws.readyState !== WebSocket.OPEN) return;
176
+
177
+ const formData = new FormData(downloadFormEl);
178
+ const options = {};
179
+ for (const [k, v] of formData.entries()) {
180
+ if (v) options[k] = v;
181
+ }
182
+
183
+ terminal.textContent = '';
184
+ ws.send(JSON.stringify({ type: 'start', provider: selectedProvider, options }));
185
+ });
186
+
187
+ /* ─── Stop ─── */
188
+ stopBtn.addEventListener('click', () => {
189
+ if (ws && ws.readyState === WebSocket.OPEN) {
190
+ ws.send(JSON.stringify({ type: 'stop' }));
191
+ }
192
+ });
193
+
194
+ /* ─── Clear terminal ─── */
195
+ clearBtn.addEventListener('click', () => {
196
+ terminal.textContent = '';
197
+ });
198
+
199
+ /* ─── Helpers ─── */
200
+ function setRunning(state) {
201
+ running = state;
202
+ startBtn.disabled = state;
203
+ stopBtn.classList.toggle('hidden', !state);
204
+ }
205
+
206
+ function appendTerminal(text, style) {
207
+ const span = document.createElement('span');
208
+
209
+ if (style === 'stderr' || style === 'red') {
210
+ span.className = 't-red';
211
+ } else if (style === 'green') {
212
+ span.className = 't-green';
213
+ } else if (style === 'yellow') {
214
+ span.className = 't-yellow';
215
+ } else if (style === 'blue') {
216
+ span.className = 't-blue';
217
+ } else if (style === 'muted') {
218
+ span.className = 't-muted';
219
+ }
220
+
221
+ span.textContent = text;
222
+ terminal.appendChild(span);
223
+ terminal.scrollTop = terminal.scrollHeight;
224
+ }
225
+
226
+ /* ─── Boot ─── */
227
+ init();
ui/index.html ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="it">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ourbooks</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ </head>
9
+ <body>
10
+ <div class="layout">
11
+ <!-- Sidebar -->
12
+ <aside class="sidebar">
13
+ <div class="logo">
14
+ <span class="logo-icon">πŸ“š</span>
15
+ <span class="logo-text">ourbooks</span>
16
+ </div>
17
+ <p class="sidebar-subtitle">Scarica i tuoi ebook dalle piattaforme degli editori</p>
18
+
19
+ <nav class="provider-list" id="providerList">
20
+ <!-- Populated dynamically -->
21
+ </nav>
22
+
23
+ <div class="sidebar-footer">
24
+ <a href="https://github.com/gablilli/ourbooks" target="_blank" rel="noopener" class="github-link">
25
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
26
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
27
+ </svg>
28
+ GitHub
29
+ </a>
30
+ </div>
31
+ </aside>
32
+
33
+ <!-- Main content -->
34
+ <main class="main">
35
+ <div class="main-inner">
36
+ <!-- Welcome state -->
37
+ <div id="welcomeState" class="welcome-state">
38
+ <div class="welcome-icon">πŸ“–</div>
39
+ <h2>Seleziona un editore</h2>
40
+ <p>Scegli una piattaforma dalla barra laterale per iniziare il download del tuo ebook.</p>
41
+ <div class="provider-grid" id="providerGrid">
42
+ <!-- Populated dynamically -->
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Download form -->
47
+ <div id="downloadForm" class="download-form hidden">
48
+ <div class="form-header">
49
+ <div class="form-title-row">
50
+ <span id="providerEmoji" class="form-emoji"></span>
51
+ <h2 id="providerTitle" class="form-title"></h2>
52
+ </div>
53
+ <p id="providerDesc" class="form-desc"></p>
54
+ </div>
55
+
56
+ <form id="downloadFormEl" class="form-fields" autocomplete="off">
57
+ <div id="formFields"></div>
58
+
59
+ <div class="form-actions">
60
+ <button type="submit" id="startBtn" class="btn btn-primary">
61
+ <span class="btn-icon">β–Ά</span>
62
+ <span>Avvia download</span>
63
+ </button>
64
+ <button type="button" id="stopBtn" class="btn btn-danger hidden">
65
+ <span class="btn-icon">β›”</span>
66
+ <span>Interrompi</span>
67
+ </button>
68
+ </div>
69
+ </form>
70
+ </div>
71
+
72
+ <!-- Terminal output -->
73
+ <div id="terminalSection" class="terminal-section hidden">
74
+ <div class="terminal-header">
75
+ <div class="terminal-dots">
76
+ <span class="dot dot-red"></span>
77
+ <span class="dot dot-yellow"></span>
78
+ <span class="dot dot-green"></span>
79
+ </div>
80
+ <span class="terminal-title">Output</span>
81
+ <button id="clearBtn" class="terminal-clear" title="Pulisci output">βœ•</button>
82
+ </div>
83
+ <pre id="terminal" class="terminal-body"></pre>
84
+ </div>
85
+ </div>
86
+ </main>
87
+ </div>
88
+
89
+ <script src="app.js"></script>
90
+ </body>
91
+ </html>
ui/style.css ADDED
@@ -0,0 +1,555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ─── Reset & base ─── */
2
+ *, *::before, *::after {
3
+ box-sizing: border-box;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ :root {
9
+ --bg: #0d1117;
10
+ --surface: #161b22;
11
+ --surface2: #1c2128;
12
+ --border: #30363d;
13
+ --text: #e6edf3;
14
+ --text-muted: #8b949e;
15
+ --accent: #58a6ff;
16
+ --accent-hover:#79b8ff;
17
+ --green: #3fb950;
18
+ --red: #f85149;
19
+ --yellow: #d29922;
20
+ --purple: #bc8cff;
21
+ --radius: 10px;
22
+ --font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, 'Courier New', monospace;
23
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
24
+ --sidebar-w: 260px;
25
+ --transition: 0.15s ease;
26
+ }
27
+
28
+ html, body {
29
+ height: 100%;
30
+ background: var(--bg);
31
+ color: var(--text);
32
+ font-family: var(--font-sans);
33
+ font-size: 14px;
34
+ line-height: 1.6;
35
+ }
36
+
37
+ /* ─── Layout ─── */
38
+ .layout {
39
+ display: flex;
40
+ height: 100vh;
41
+ overflow: hidden;
42
+ }
43
+
44
+ /* ─── Sidebar ─── */
45
+ .sidebar {
46
+ width: var(--sidebar-w);
47
+ background: var(--surface);
48
+ border-right: 1px solid var(--border);
49
+ display: flex;
50
+ flex-direction: column;
51
+ padding: 20px 14px;
52
+ overflow-y: auto;
53
+ flex-shrink: 0;
54
+ }
55
+
56
+ .logo {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 10px;
60
+ margin-bottom: 6px;
61
+ }
62
+
63
+ .logo-icon {
64
+ font-size: 24px;
65
+ }
66
+
67
+ .logo-text {
68
+ font-size: 20px;
69
+ font-weight: 700;
70
+ color: var(--text);
71
+ letter-spacing: -0.5px;
72
+ }
73
+
74
+ .sidebar-subtitle {
75
+ color: var(--text-muted);
76
+ font-size: 12px;
77
+ line-height: 1.5;
78
+ margin-bottom: 24px;
79
+ }
80
+
81
+ .provider-list {
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 4px;
85
+ flex: 1;
86
+ }
87
+
88
+ .provider-btn {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 10px;
92
+ width: 100%;
93
+ padding: 10px 12px;
94
+ border: none;
95
+ border-radius: var(--radius);
96
+ background: transparent;
97
+ color: var(--text);
98
+ font-size: 14px;
99
+ font-family: var(--font-sans);
100
+ cursor: pointer;
101
+ text-align: left;
102
+ transition: background var(--transition), color var(--transition);
103
+ }
104
+
105
+ .provider-btn:hover {
106
+ background: var(--surface2);
107
+ }
108
+
109
+ .provider-btn.active {
110
+ background: rgba(88, 166, 255, 0.12);
111
+ color: var(--accent);
112
+ font-weight: 600;
113
+ }
114
+
115
+ .provider-btn .p-emoji {
116
+ font-size: 18px;
117
+ flex-shrink: 0;
118
+ }
119
+
120
+ .provider-btn .p-label {
121
+ flex: 1;
122
+ overflow: hidden;
123
+ text-overflow: ellipsis;
124
+ white-space: nowrap;
125
+ }
126
+
127
+ .sidebar-footer {
128
+ margin-top: 24px;
129
+ padding-top: 16px;
130
+ border-top: 1px solid var(--border);
131
+ }
132
+
133
+ .github-link {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 8px;
137
+ color: var(--text-muted);
138
+ text-decoration: none;
139
+ font-size: 13px;
140
+ padding: 8px 12px;
141
+ border-radius: 8px;
142
+ transition: background var(--transition), color var(--transition);
143
+ }
144
+
145
+ .github-link:hover {
146
+ background: var(--surface2);
147
+ color: var(--text);
148
+ }
149
+
150
+ /* ─── Main ─── */
151
+ .main {
152
+ flex: 1;
153
+ overflow-y: auto;
154
+ padding: 32px;
155
+ }
156
+
157
+ .main-inner {
158
+ max-width: 720px;
159
+ margin: 0 auto;
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: 24px;
163
+ }
164
+
165
+ /* ─── Welcome state ─── */
166
+ .welcome-state {
167
+ display: flex;
168
+ flex-direction: column;
169
+ align-items: center;
170
+ text-align: center;
171
+ padding: 60px 24px;
172
+ gap: 12px;
173
+ }
174
+
175
+ .welcome-icon {
176
+ font-size: 64px;
177
+ margin-bottom: 8px;
178
+ }
179
+
180
+ .welcome-state h2 {
181
+ font-size: 24px;
182
+ font-weight: 700;
183
+ color: var(--text);
184
+ }
185
+
186
+ .welcome-state p {
187
+ color: var(--text-muted);
188
+ max-width: 400px;
189
+ font-size: 14px;
190
+ }
191
+
192
+ .provider-grid {
193
+ display: grid;
194
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
195
+ gap: 12px;
196
+ width: 100%;
197
+ max-width: 520px;
198
+ margin-top: 24px;
199
+ }
200
+
201
+ .provider-card {
202
+ display: flex;
203
+ flex-direction: column;
204
+ align-items: center;
205
+ gap: 8px;
206
+ padding: 20px 12px;
207
+ background: var(--surface);
208
+ border: 1px solid var(--border);
209
+ border-radius: var(--radius);
210
+ cursor: pointer;
211
+ transition: border-color var(--transition), background var(--transition), transform 0.1s ease;
212
+ text-align: center;
213
+ }
214
+
215
+ .provider-card:hover {
216
+ border-color: var(--accent);
217
+ background: var(--surface2);
218
+ transform: translateY(-2px);
219
+ }
220
+
221
+ .provider-card .pc-emoji {
222
+ font-size: 32px;
223
+ }
224
+
225
+ .provider-card .pc-label {
226
+ font-size: 13px;
227
+ font-weight: 500;
228
+ color: var(--text);
229
+ }
230
+
231
+ /* ─── Download form ─── */
232
+ .download-form {
233
+ background: var(--surface);
234
+ border: 1px solid var(--border);
235
+ border-radius: var(--radius);
236
+ overflow: hidden;
237
+ }
238
+
239
+ .form-header {
240
+ padding: 20px 24px 16px;
241
+ border-bottom: 1px solid var(--border);
242
+ background: linear-gradient(135deg, rgba(88, 166, 255, 0.05) 0%, transparent 100%);
243
+ }
244
+
245
+ .form-title-row {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 12px;
249
+ margin-bottom: 6px;
250
+ }
251
+
252
+ .form-emoji {
253
+ font-size: 28px;
254
+ }
255
+
256
+ .form-title {
257
+ font-size: 20px;
258
+ font-weight: 700;
259
+ }
260
+
261
+ .form-desc {
262
+ color: var(--text-muted);
263
+ font-size: 13px;
264
+ }
265
+
266
+ .form-fields {
267
+ padding: 20px 24px 24px;
268
+ }
269
+
270
+ .field-group {
271
+ margin-bottom: 16px;
272
+ }
273
+
274
+ .field-group label {
275
+ display: block;
276
+ font-size: 13px;
277
+ font-weight: 500;
278
+ color: var(--text);
279
+ margin-bottom: 6px;
280
+ }
281
+
282
+ .field-group label .required-badge {
283
+ color: var(--red);
284
+ margin-left: 2px;
285
+ font-size: 11px;
286
+ }
287
+
288
+ .field-group label .optional-badge {
289
+ color: var(--text-muted);
290
+ margin-left: 4px;
291
+ font-size: 11px;
292
+ font-weight: 400;
293
+ }
294
+
295
+ .field-group input,
296
+ .field-group select {
297
+ width: 100%;
298
+ padding: 9px 12px;
299
+ background: var(--bg);
300
+ border: 1px solid var(--border);
301
+ border-radius: 8px;
302
+ color: var(--text);
303
+ font-size: 14px;
304
+ font-family: var(--font-sans);
305
+ outline: none;
306
+ transition: border-color var(--transition), box-shadow var(--transition);
307
+ }
308
+
309
+ .field-group input::placeholder {
310
+ color: var(--text-muted);
311
+ }
312
+
313
+ .field-group input:focus,
314
+ .field-group select:focus {
315
+ border-color: var(--accent);
316
+ box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.12);
317
+ }
318
+
319
+ .field-group select {
320
+ appearance: none;
321
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%238b949e' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
322
+ background-repeat: no-repeat;
323
+ background-position: right 12px center;
324
+ padding-right: 32px;
325
+ cursor: pointer;
326
+ }
327
+
328
+ .field-group select option {
329
+ background: var(--surface2);
330
+ color: var(--text);
331
+ }
332
+
333
+ /* ─── Buttons ─── */
334
+ .form-actions {
335
+ display: flex;
336
+ gap: 10px;
337
+ margin-top: 24px;
338
+ padding-top: 20px;
339
+ border-top: 1px solid var(--border);
340
+ }
341
+
342
+ .btn {
343
+ display: inline-flex;
344
+ align-items: center;
345
+ gap: 7px;
346
+ padding: 10px 20px;
347
+ border: none;
348
+ border-radius: 8px;
349
+ font-size: 14px;
350
+ font-weight: 600;
351
+ font-family: var(--font-sans);
352
+ cursor: pointer;
353
+ transition: background var(--transition), opacity var(--transition), transform 0.1s ease;
354
+ text-decoration: none;
355
+ }
356
+
357
+ .btn:active {
358
+ transform: scale(0.97);
359
+ }
360
+
361
+ .btn-icon {
362
+ font-size: 13px;
363
+ }
364
+
365
+ .btn-primary {
366
+ background: var(--accent);
367
+ color: #0d1117;
368
+ }
369
+
370
+ .btn-primary:hover {
371
+ background: var(--accent-hover);
372
+ }
373
+
374
+ .btn-primary:disabled {
375
+ opacity: 0.5;
376
+ cursor: not-allowed;
377
+ }
378
+
379
+ .btn-danger {
380
+ background: rgba(248, 81, 73, 0.15);
381
+ color: var(--red);
382
+ border: 1px solid rgba(248, 81, 73, 0.3);
383
+ }
384
+
385
+ .btn-danger:hover {
386
+ background: rgba(248, 81, 73, 0.25);
387
+ }
388
+
389
+ /* ─── Terminal ─── */
390
+ .terminal-section {
391
+ background: #010409;
392
+ border: 1px solid var(--border);
393
+ border-radius: var(--radius);
394
+ overflow: hidden;
395
+ }
396
+
397
+ .terminal-header {
398
+ display: flex;
399
+ align-items: center;
400
+ gap: 8px;
401
+ padding: 10px 16px;
402
+ background: var(--surface);
403
+ border-bottom: 1px solid var(--border);
404
+ user-select: none;
405
+ }
406
+
407
+ .terminal-dots {
408
+ display: flex;
409
+ gap: 6px;
410
+ margin-right: 4px;
411
+ }
412
+
413
+ .dot {
414
+ width: 11px;
415
+ height: 11px;
416
+ border-radius: 50%;
417
+ }
418
+
419
+ .dot-red { background: #ff5f57; }
420
+ .dot-yellow { background: #febc2e; }
421
+ .dot-green { background: #28c840; }
422
+
423
+ .terminal-title {
424
+ font-size: 12px;
425
+ color: var(--text-muted);
426
+ font-weight: 500;
427
+ flex: 1;
428
+ text-align: center;
429
+ }
430
+
431
+ .terminal-clear {
432
+ background: none;
433
+ border: none;
434
+ color: var(--text-muted);
435
+ cursor: pointer;
436
+ font-size: 12px;
437
+ padding: 2px 6px;
438
+ border-radius: 4px;
439
+ transition: color var(--transition), background var(--transition);
440
+ }
441
+
442
+ .terminal-clear:hover {
443
+ color: var(--text);
444
+ background: var(--surface2);
445
+ }
446
+
447
+ .terminal-body {
448
+ padding: 16px 20px;
449
+ font-family: var(--font-mono);
450
+ font-size: 13px;
451
+ line-height: 1.7;
452
+ white-space: pre-wrap;
453
+ word-break: break-word;
454
+ min-height: 240px;
455
+ max-height: 480px;
456
+ overflow-y: auto;
457
+ color: #c9d1d9;
458
+ scrollbar-width: thin;
459
+ scrollbar-color: var(--border) transparent;
460
+ }
461
+
462
+ .terminal-body::-webkit-scrollbar {
463
+ width: 6px;
464
+ }
465
+
466
+ .terminal-body::-webkit-scrollbar-track {
467
+ background: transparent;
468
+ }
469
+
470
+ .terminal-body::-webkit-scrollbar-thumb {
471
+ background: var(--border);
472
+ border-radius: 3px;
473
+ }
474
+
475
+ /* Terminal text colours */
476
+ .t-green { color: var(--green); }
477
+ .t-red { color: var(--red); }
478
+ .t-yellow { color: var(--yellow); }
479
+ .t-blue { color: var(--accent); }
480
+ .t-purple { color: var(--purple); }
481
+ .t-muted { color: var(--text-muted); }
482
+
483
+ /* Status indicator */
484
+ .status-dot {
485
+ display: inline-block;
486
+ width: 8px;
487
+ height: 8px;
488
+ border-radius: 50%;
489
+ margin-right: 6px;
490
+ }
491
+
492
+ .status-dot.running {
493
+ background: var(--green);
494
+ box-shadow: 0 0 6px var(--green);
495
+ animation: pulse 1.5s infinite;
496
+ }
497
+
498
+ .status-dot.idle {
499
+ background: var(--text-muted);
500
+ }
501
+
502
+ @keyframes pulse {
503
+ 0%, 100% { opacity: 1; }
504
+ 50% { opacity: 0.4; }
505
+ }
506
+
507
+ /* ─── Utilities ─── */
508
+ .hidden {
509
+ display: none !important;
510
+ }
511
+
512
+ /* ─── Scrollbar ─── */
513
+ ::-webkit-scrollbar {
514
+ width: 8px;
515
+ }
516
+
517
+ ::-webkit-scrollbar-track {
518
+ background: transparent;
519
+ }
520
+
521
+ ::-webkit-scrollbar-thumb {
522
+ background: var(--border);
523
+ border-radius: 4px;
524
+ }
525
+
526
+ /* ─���─ Responsive ─── */
527
+ @media (max-width: 700px) {
528
+ .layout {
529
+ flex-direction: column;
530
+ height: auto;
531
+ overflow: visible;
532
+ }
533
+
534
+ .sidebar {
535
+ width: 100%;
536
+ border-right: none;
537
+ border-bottom: 1px solid var(--border);
538
+ padding: 16px 12px;
539
+ }
540
+
541
+ .main {
542
+ padding: 16px;
543
+ }
544
+
545
+ .provider-list {
546
+ flex-direction: row;
547
+ flex-wrap: wrap;
548
+ }
549
+
550
+ .provider-btn {
551
+ width: auto;
552
+ flex: 1;
553
+ min-width: 120px;
554
+ }
555
+ }