anthodev/lapin-futepublic Fork
⑂ develop
Commits
⬇ Clone ▾
git clone https://git.rickub.com/anthodev/lapin-fute.git
git clone ssh://git@rickub.com/anthodev/lapin-fute.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat: build companion configuration page #13

Merged@github-anthodev wants to merge feature/phone-configuration/configuration/5 into develop
Merged as f7d01bd5b859.
71 files changed
modified fixtures/catalog/idfm-v1.json +6 -6
@@ -22,12 +22,12 @@
2222 { "agency_name": "Fixture IDFM operator", "agency_url": "https://www.iledefrance-mobilites.fr/", "agency_timezone": "Europe/Paris" }
2323 ],
2424 "lines": [
25- { "id_line": "C100", "name_line": "Bus 42", "shortname_line": "42", "transportmode": "bus", "transportsubmode": "", "status": "active" },
26- { "id_line": "C200", "name_line": "Métro 4", "shortname_line": "4", "transportmode": "metro", "transportsubmode": "", "status": "active" },
27- { "id_line": "C300", "name_line": "Tram T3a", "shortname_line": "T3a", "transportmode": "tram", "transportsubmode": "", "status": "active" },
28- { "id_line": "C400", "name_line": "RER A", "shortname_line": "A", "transportmode": "rail", "transportsubmode": "local", "status": "active" },
29- { "id_line": "C500", "name_line": "Transilien L", "shortname_line": "L", "transportmode": "rail", "transportsubmode": "suburbanRailway", "status": "active" },
30- { "id_line": "C600", "name_line": "Funiculaire", "shortname_line": "F", "transportmode": "funicular", "transportsubmode": "", "status": "active" }
25+ { "id_line": "C100", "name_line": "Bus 42", "shortname_line": "42", "transportmode": "bus", "transportsubmode": "", "status": "active", "colourweb_hexa": "E86A10", "textcolourweb_hexa": "FFFFFF" },
26+ { "id_line": "C200", "name_line": "Métro 4", "shortname_line": "4", "transportmode": "metro", "transportsubmode": "", "status": "active", "colourweb_hexa": "#BE418D", "textcolourweb_hexa": "#FFFFFF" },
27+ { "id_line": "C300", "name_line": "Tram T3a", "shortname_line": "T3a", "transportmode": "tram", "transportsubmode": "", "status": "active", "colourweb_hexa": "85216F", "textcolourweb_hexa": "FFFFFF" },
28+ { "id_line": "C400", "name_line": "RER A", "shortname_line": "A", "transportmode": "rail", "transportsubmode": "local", "status": "active", "colourweb_hexa": "E3051C", "textcolourweb_hexa": "FFFFFF" },
29+ { "id_line": "C500", "name_line": "Transilien L", "shortname_line": "L", "transportmode": "rail", "transportsubmode": "suburbanRailway", "status": "active", "colourweb_hexa": "7584BC", "textcolourweb_hexa": "000000" },
30+ { "id_line": "C600", "name_line": "Funiculaire", "shortname_line": "F", "transportmode": "funicular", "transportsubmode": "", "status": "active", "colourweb_hexa": "000000", "textcolourweb_hexa": "FFFFFF" }
3131 ],
3232 "routes": [
3333 { "route_id": "IDFM:C100", "route_short_name": "42", "route_long_name": "Bus 42" },
@@ -22,12 +22,12 @@
22 { "agency_name": "Fixture IDFM operator", "agency_url": "https://www.iledefrance-mobilites.fr/", "agency_timezone": "Europe/Paris" }22 { "agency_name": "Fixture IDFM operator", "agency_url": "https://www.iledefrance-mobilites.fr/", "agency_timezone": "Europe/Paris" }
23 ],23 ],
24 "lines": [24 "lines": [
25- { "id_line": "C100", "name_line": "Bus 42", "shortname_line": "42", "transportmode": "bus", "transportsubmode": "", "status": "active" },25+ { "id_line": "C100", "name_line": "Bus 42", "shortname_line": "42", "transportmode": "bus", "transportsubmode": "", "status": "active", "colourweb_hexa": "E86A10", "textcolourweb_hexa": "FFFFFF" },
26- { "id_line": "C200", "name_line": "Métro 4", "shortname_line": "4", "transportmode": "metro", "transportsubmode": "", "status": "active" },26+ { "id_line": "C200", "name_line": "Métro 4", "shortname_line": "4", "transportmode": "metro", "transportsubmode": "", "status": "active", "colourweb_hexa": "#BE418D", "textcolourweb_hexa": "#FFFFFF" },
27- { "id_line": "C300", "name_line": "Tram T3a", "shortname_line": "T3a", "transportmode": "tram", "transportsubmode": "", "status": "active" },27+ { "id_line": "C300", "name_line": "Tram T3a", "shortname_line": "T3a", "transportmode": "tram", "transportsubmode": "", "status": "active", "colourweb_hexa": "85216F", "textcolourweb_hexa": "FFFFFF" },
28- { "id_line": "C400", "name_line": "RER A", "shortname_line": "A", "transportmode": "rail", "transportsubmode": "local", "status": "active" },28+ { "id_line": "C400", "name_line": "RER A", "shortname_line": "A", "transportmode": "rail", "transportsubmode": "local", "status": "active", "colourweb_hexa": "E3051C", "textcolourweb_hexa": "FFFFFF" },
29- { "id_line": "C500", "name_line": "Transilien L", "shortname_line": "L", "transportmode": "rail", "transportsubmode": "suburbanRailway", "status": "active" },29+ { "id_line": "C500", "name_line": "Transilien L", "shortname_line": "L", "transportmode": "rail", "transportsubmode": "suburbanRailway", "status": "active", "colourweb_hexa": "7584BC", "textcolourweb_hexa": "000000" },
30- { "id_line": "C600", "name_line": "Funiculaire", "shortname_line": "F", "transportmode": "funicular", "transportsubmode": "", "status": "active" }30+ { "id_line": "C600", "name_line": "Funiculaire", "shortname_line": "F", "transportmode": "funicular", "transportsubmode": "", "status": "active", "colourweb_hexa": "000000", "textcolourweb_hexa": "FFFFFF" }
31 ],31 ],
32 "routes": [32 "routes": [
33 { "route_id": "IDFM:C100", "route_short_name": "42", "route_long_name": "Bus 42" },33 { "route_id": "IDFM:C100", "route_short_name": "42", "route_long_name": "Bus 42" },
modified fixtures/departures/foundation.json +3 -0
@@ -7,6 +7,9 @@
77 "stopLabel": "Châtelet",
88 "lineLabel": "Métro 1",
99 "destinationLabel": "La Défense",
10+ "lineMode": "METRO",
11+ "lineColor": "#ffbe00",
12+ "lineTextColor": "#000000",
1013 "sortOrder": 0
1114 },
1215 "result": {
@@ -7,6 +7,9 @@
7 "stopLabel": "Châtelet",7 "stopLabel": "Châtelet",
8 "lineLabel": "Métro 1",8 "lineLabel": "Métro 1",
9 "destinationLabel": "La Défense",9 "destinationLabel": "La Défense",
10+ "lineMode": "METRO",
11+ "lineColor": "#ffbe00",
12+ "lineTextColor": "#000000",
10 "sortOrder": 013 "sortOrder": 0
11 },14 },
12 "result": {15 "result": {
modified package.json +2 -0
@@ -13,8 +13,10 @@
1313 "build": "node scripts/build.mjs",
1414 "catalog:probe": "node scripts/probe-catalog.mjs",
1515 "catalog:refresh": "node scripts/refresh-catalog.mjs",
16+ "badges:sync": "node scripts/sync-line-badges.mjs",
1617 "measure:radio": "node scripts/measure-radio.mjs",
1718 "measure:catalog": "node scripts/measure-catalog.mjs",
19+ "measure:config-page": "node scripts/measure-config-page.mjs",
1820 "test": "node scripts/test.mjs",
1921 "test:contracts": "node --test packages/contracts/test/contracts.test.ts",
2022 "test:watch": "node --test packages/watch/test/*.test.js",
@@ -13,8 +13,10 @@
13 "build": "node scripts/build.mjs",13 "build": "node scripts/build.mjs",
14 "catalog:probe": "node scripts/probe-catalog.mjs",14 "catalog:probe": "node scripts/probe-catalog.mjs",
15 "catalog:refresh": "node scripts/refresh-catalog.mjs",15 "catalog:refresh": "node scripts/refresh-catalog.mjs",
16+ "badges:sync": "node scripts/sync-line-badges.mjs",
16 "measure:radio": "node scripts/measure-radio.mjs",17 "measure:radio": "node scripts/measure-radio.mjs",
17 "measure:catalog": "node scripts/measure-catalog.mjs",18 "measure:catalog": "node scripts/measure-catalog.mjs",
19+ "measure:config-page": "node scripts/measure-config-page.mjs",
18 "test": "node scripts/test.mjs",20 "test": "node scripts/test.mjs",
19 "test:contracts": "node --test packages/contracts/test/contracts.test.ts",21 "test:contracts": "node --test packages/contracts/test/contracts.test.ts",
20 "test:watch": "node --test packages/watch/test/*.test.js",22 "test:watch": "node --test packages/watch/test/*.test.js",
modified packages/backend/src/catalog-import.ts +35 -12
@@ -24,7 +24,7 @@ import {
2424 } from "../../contracts/src/index.ts";
2525 import type { TransportMode } from "../../contracts/src/index.ts";
2626
27-export const CATALOG_VERSION = 1 as const;
27+export const CATALOG_VERSION = 2 as const;
2828 export const DOWNLOAD_TIMEOUT_MS = 15 * 60 * 1_000;
2929 export const MAX_COMPRESSED_DOWNLOAD_BYTES = 2 * 1024 ** 3;
3030 export const MAX_SELECTED_ENTRY_BYTES = 8 * 1024 ** 3;
@@ -224,6 +224,12 @@ function canonicalLineId(value: unknown, field: string): string {
224224 return text.startsWith("IDFM:") ? text.slice("IDFM:".length) : text;
225225 }
226226
227+function normalizeLineColor(value: unknown, field: string): string {
228+ const text = nonEmpty(value, field);
229+ if (!/^#?[0-9a-f]{6}$/iu.test(text)) fail("INVALID_SOURCE", `${field} must be a six-digit hexadecimal color`);
230+ return `#${text.replace(/^#/u, "").toLowerCase()}`;
231+}
232+
227233 function isoTimestamp(value: string, field: string): string {
228234 if (value.length === 0 || !Number.isFinite(Date.parse(value))) {
229235 fail("INVALID_METADATA", `${field} must be an ISO timestamp`);
@@ -500,6 +506,8 @@ function createSchema(database: DatabaseSync): void {
500506 stop_label TEXT NOT NULL,
501507 line_label TEXT NOT NULL,
502508 destination_label TEXT NOT NULL,
509+ line_color TEXT NOT NULL,
510+ line_text_color TEXT NOT NULL,
503511 monitoring_ref TEXT NOT NULL,
504512 line_ref TEXT NOT NULL,
505513 direction_id TEXT NOT NULL,
@@ -575,7 +583,9 @@ function createSchema(database: DatabaseSync): void {
575583 shortname_line TEXT NOT NULL,
576584 transport_mode TEXT NOT NULL,
577585 transport_submode TEXT NOT NULL,
578- status TEXT NOT NULL
586+ status TEXT NOT NULL,
587+ line_color TEXT NOT NULL,
588+ line_text_color TEXT NOT NULL
579589 ) STRICT;
580590 CREATE TABLE stage_arrets_lignes (
581591 route_id TEXT NOT NULL,
@@ -596,7 +606,7 @@ function prepareSpecs(database: DatabaseSync): Readonly<Record<keyof CatalogReco
596606 const arret = database.prepare("INSERT INTO stage_arrets VALUES (?, ?)");
597607 const zone = database.prepare("INSERT INTO stage_zones VALUES (?, ?, ?)");
598608 const relation = database.prepare("INSERT OR IGNORE INTO stage_relations VALUES (?, ?, ?)");
599- const line = database.prepare("INSERT INTO stage_lines VALUES (?, ?, ?, ?, ?, ?)");
609+ const line = database.prepare("INSERT INTO stage_lines VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
600610 const arretLigne = database.prepare("INSERT OR IGNORE INTO stage_arrets_lignes VALUES (?, ?)");
601611
602612 return {
@@ -649,8 +659,8 @@ function prepareSpecs(database: DatabaseSync): Readonly<Record<keyof CatalogReco
649659 insert: (row) => relation.run(nonEmpty(row.zdaid, "relations.zdaid"), nonEmpty(row.arrid, "relations.arrid"), nonEmpty(row.artid, "relations.artid")),
650660 },
651661 lines: {
652- required: ["id_line", "name_line", "shortname_line", "transportmode", "transportsubmode", "status"],
653- insert: (row) => line.run(canonicalLineId(row.id_line, "lines.id_line"), nonEmpty(row.name_line, "lines.name_line"), optionalText(row.shortname_line), nonEmpty(row.transportmode, "lines.transportmode"), optionalText(row.transportsubmode), nonEmpty(row.status, "lines.status")),
662+ required: ["id_line", "name_line", "shortname_line", "transportmode", "transportsubmode", "status", "colourweb_hexa", "textcolourweb_hexa"],
663+ insert: (row) => line.run(canonicalLineId(row.id_line, "lines.id_line"), nonEmpty(row.name_line, "lines.name_line"), optionalText(row.shortname_line), nonEmpty(row.transportmode, "lines.transportmode"), optionalText(row.transportsubmode), nonEmpty(row.status, "lines.status"), normalizeLineColor(row.colourweb_hexa, "lines.colourweb_hexa"), normalizeLineColor(row.textcolourweb_hexa, "lines.textcolourweb_hexa")),
654664 },
655665 arretsLignes: {
656666 required: ["id", "stop_id"],
@@ -959,7 +969,9 @@ function deriveCandidates(database: DatabaseSync): void {
959969 CREATE TABLE stage_line_modes (
960970 id_line TEXT PRIMARY KEY,
961971 mode TEXT NOT NULL,
962- line_label TEXT NOT NULL
972+ line_label TEXT NOT NULL,
973+ line_color TEXT NOT NULL,
974+ line_text_color TEXT NOT NULL
963975 ) STRICT;
964976
965977 INSERT INTO stage_line_modes
@@ -971,7 +983,9 @@ function deriveCandidates(database: DatabaseSync): void {
971983 WHEN lower(trim(transport_mode)) = 'rail' AND lower(trim(transport_submode)) = 'local' THEN 'RER'
972984 WHEN lower(trim(transport_mode)) = 'rail' AND lower(trim(transport_submode)) = 'suburbanrailway' THEN 'TRANSILIEN'
973985 END,
974- coalesce(nullif(trim(shortname_line), ''), trim(name_line))
986+ coalesce(nullif(trim(shortname_line), ''), trim(name_line)),
987+ line_color,
988+ line_text_color
975989 FROM stage_lines
976990 WHERE lower(trim(status)) = 'active'
977991 AND (
@@ -1040,6 +1054,8 @@ function deriveCandidates(database: DatabaseSync): void {
10401054 zone.zdatown AS locality_label,
10411055 line_mode.id_line AS id_line,
10421056 line_mode.line_label AS line_label,
1057+ line_mode.line_color AS line_color,
1058+ line_mode.line_text_color AS line_text_color,
10431059 CASE
10441060 WHEN line_mode.mode IN ('RER', 'TRANSILIEN') THEN terminal.stop_name
10451061 ELSE coalesce(nullif(trim(trip.trip_headsign), ''), terminal.stop_name)
@@ -1089,7 +1105,8 @@ function deriveCandidates(database: DatabaseSync): void {
10891105 AND trim(destination_code.object_code) <> ''
10901106 )
10911107 SELECT mode, zdaid, min(stop_label) AS stop_label, min(locality_label) AS locality_label,
1092- id_line, min(line_label) AS line_label, min(destination_label) AS destination_label,
1108+ id_line, min(line_label) AS line_label, min(line_color) AS line_color,
1109+ min(line_text_color) AS line_text_color, min(destination_label) AS destination_label,
10931110 monitoring_ref, line_ref, direction_id, destination_ref,
10941111 max(CASE WHEN resolution = 'arrets' THEN 1 ELSE 0 END) AS via_arrets
10951112 FROM joined
@@ -1107,7 +1124,7 @@ function insertFinalCatalog(database: DatabaseSync, sourceRevision: string, crea
11071124
11081125 const insertPlace = database.prepare("INSERT INTO places VALUES (?, ?, ?, ?, ?)");
11091126 const insertSearch = database.prepare("INSERT INTO place_search(search_text, place_id) VALUES (?, ?)");
1110- const insertService = database.prepare("INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1127+ const insertService = database.prepare("INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
11111128 const selectPlace = database.prepare("SELECT canonical_tuple FROM places WHERE place_id = ?");
11121129 const selectService = database.prepare("SELECT place_id, canonical_tuple FROM services WHERE service_id = ?");
11131130
@@ -1146,7 +1163,7 @@ function insertFinalCatalog(database: DatabaseSync, sourceRevision: string, crea
11461163
11471164 const candidates = database.prepare(`
11481165 SELECT mode, zdaid, stop_label, line_label, destination_label,
1149- id_line, monitoring_ref, line_ref, direction_id, destination_ref
1166+ line_color, line_text_color, id_line, monitoring_ref, line_ref, direction_id, destination_ref
11501167 FROM stage_candidates
11511168 ORDER BY mode, zdaid, id_line, monitoring_ref, direction_id, destination_ref
11521169 `).iterate() as Iterable<QueryRow>;
@@ -1174,6 +1191,8 @@ function insertFinalCatalog(database: DatabaseSync, sourceRevision: string, crea
11741191 nonEmpty(row.stop_label, "candidate.stop_label"),
11751192 nonEmpty(row.line_label, "candidate.line_label"),
11761193 nonEmpty(row.destination_label, "candidate.destination_label"),
1194+ normalizeLineColor(row.line_color, "candidate.line_color"),
1195+ normalizeLineColor(row.line_text_color, "candidate.line_text_color"),
11771196 nonEmpty(row.monitoring_ref, "candidate.monitoring_ref"),
11781197 nonEmpty(row.line_ref, "candidate.line_ref"),
11791198 nonEmpty(row.direction_id, "candidate.direction_id"),
@@ -1206,6 +1225,8 @@ function validateByteBounds(database: DatabaseSync): void {
12061225 OR length(CAST(stop_label AS BLOB)) > ?
12071226 OR length(CAST(line_label AS BLOB)) > ?
12081227 OR length(CAST(destination_label AS BLOB)) > ?
1228+ OR length(CAST(line_color AS BLOB)) <> 7
1229+ OR length(CAST(line_text_color AS BLOB)) <> 7
12091230 LIMIT 1
12101231 `).get(LIMITS.idUtf8Bytes, LIMITS.labelUtf8Bytes, LIMITS.labelUtf8Bytes, LIMITS.labelUtf8Bytes);
12111232 if (invalidService !== undefined) fail("CATALOG_LIMIT", "service exceeds a shared UTF-8 byte limit");
@@ -1228,7 +1249,7 @@ function validateOpenCatalog(database: DatabaseSync): CatalogBuildResult {
12281249 database.exec("PRAGMA foreign_keys = ON");
12291250 exactColumns(database, "metadata", ["key", "value"]);
12301251 exactColumns(database, "places", ["place_id", "stop_label", "locality_label", "mode", "canonical_tuple"]);
1231- exactColumns(database, "services", ["service_id", "place_id", "stop_label", "line_label", "destination_label", "monitoring_ref", "line_ref", "direction_id", "destination_ref", "canonical_tuple"]);
1252+ exactColumns(database, "services", ["service_id", "place_id", "stop_label", "line_label", "destination_label", "line_color", "line_text_color", "monitoring_ref", "line_ref", "direction_id", "destination_ref", "canonical_tuple"]);
12321253 exactColumns(database, "place_search", ["search_text", "place_id"]);
12331254
12341255 const sourceRevision = metadataValue(database, "source_revision");
@@ -1276,9 +1297,11 @@ function validateOpenCatalog(database: DatabaseSync): CatalogBuildResult {
12761297 const unresolved = database.prepare(`
12771298 SELECT 1 FROM services
12781299 WHERE trim(monitoring_ref) = '' OR trim(line_ref) = '' OR trim(direction_id) = '' OR trim(destination_ref) = ''
1300+ OR line_color NOT GLOB '#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
1301+ OR line_text_color NOT GLOB '#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
12791302 LIMIT 1
12801303 `).get();
1281- if (unresolved !== undefined) fail("CATALOG_UNRESOLVED", "catalog contains an unresolvable offered service");
1304+ if (unresolved !== undefined) fail("CATALOG_UNRESOLVED", "catalog contains an unresolvable or malformed offered service");
12821305
12831306 for (const row of database.prepare("SELECT place_id, canonical_tuple FROM places").iterate() as Iterable<QueryRow>) {
12841307 if (opaqueId("plc_", String(row.canonical_tuple)) !== row.place_id) fail("HASH_COLLISION", "place ID does not match its canonical tuple");
@@ -24,7 +24,7 @@ import {
24 } from "../../contracts/src/index.ts";24 } from "../../contracts/src/index.ts";
25 import type { TransportMode } from "../../contracts/src/index.ts";25 import type { TransportMode } from "../../contracts/src/index.ts";
26 26
27-export const CATALOG_VERSION = 1 as const;27+export const CATALOG_VERSION = 2 as const;
28 export const DOWNLOAD_TIMEOUT_MS = 15 * 60 * 1_000;28 export const DOWNLOAD_TIMEOUT_MS = 15 * 60 * 1_000;
29 export const MAX_COMPRESSED_DOWNLOAD_BYTES = 2 * 1024 ** 3;29 export const MAX_COMPRESSED_DOWNLOAD_BYTES = 2 * 1024 ** 3;
30 export const MAX_SELECTED_ENTRY_BYTES = 8 * 1024 ** 3;30 export const MAX_SELECTED_ENTRY_BYTES = 8 * 1024 ** 3;
@@ -224,6 +224,12 @@ function canonicalLineId(value: unknown, field: string): string {
224 return text.startsWith("IDFM:") ? text.slice("IDFM:".length) : text;224 return text.startsWith("IDFM:") ? text.slice("IDFM:".length) : text;
225 }225 }
226 226
227+function normalizeLineColor(value: unknown, field: string): string {
228+ const text = nonEmpty(value, field);
229+ if (!/^#?[0-9a-f]{6}$/iu.test(text)) fail("INVALID_SOURCE", `${field} must be a six-digit hexadecimal color`);
230+ return `#${text.replace(/^#/u, "").toLowerCase()}`;
231+}
232+
227 function isoTimestamp(value: string, field: string): string {233 function isoTimestamp(value: string, field: string): string {
228 if (value.length === 0 || !Number.isFinite(Date.parse(value))) {234 if (value.length === 0 || !Number.isFinite(Date.parse(value))) {
229 fail("INVALID_METADATA", `${field} must be an ISO timestamp`);235 fail("INVALID_METADATA", `${field} must be an ISO timestamp`);
@@ -500,6 +506,8 @@ function createSchema(database: DatabaseSync): void {
500 stop_label TEXT NOT NULL,506 stop_label TEXT NOT NULL,
501 line_label TEXT NOT NULL,507 line_label TEXT NOT NULL,
502 destination_label TEXT NOT NULL,508 destination_label TEXT NOT NULL,
509+ line_color TEXT NOT NULL,
510+ line_text_color TEXT NOT NULL,
503 monitoring_ref TEXT NOT NULL,511 monitoring_ref TEXT NOT NULL,
504 line_ref TEXT NOT NULL,512 line_ref TEXT NOT NULL,
505 direction_id TEXT NOT NULL,513 direction_id TEXT NOT NULL,
@@ -575,7 +583,9 @@ function createSchema(database: DatabaseSync): void {
575 shortname_line TEXT NOT NULL,583 shortname_line TEXT NOT NULL,
576 transport_mode TEXT NOT NULL,584 transport_mode TEXT NOT NULL,
577 transport_submode TEXT NOT NULL,585 transport_submode TEXT NOT NULL,
578- status TEXT NOT NULL586+ status TEXT NOT NULL,
587+ line_color TEXT NOT NULL,
588+ line_text_color TEXT NOT NULL
579 ) STRICT;589 ) STRICT;
580 CREATE TABLE stage_arrets_lignes (590 CREATE TABLE stage_arrets_lignes (
581 route_id TEXT NOT NULL,591 route_id TEXT NOT NULL,
@@ -596,7 +606,7 @@ function prepareSpecs(database: DatabaseSync): Readonly<Record<keyof CatalogReco
596 const arret = database.prepare("INSERT INTO stage_arrets VALUES (?, ?)");606 const arret = database.prepare("INSERT INTO stage_arrets VALUES (?, ?)");
597 const zone = database.prepare("INSERT INTO stage_zones VALUES (?, ?, ?)");607 const zone = database.prepare("INSERT INTO stage_zones VALUES (?, ?, ?)");
598 const relation = database.prepare("INSERT OR IGNORE INTO stage_relations VALUES (?, ?, ?)");608 const relation = database.prepare("INSERT OR IGNORE INTO stage_relations VALUES (?, ?, ?)");
599- const line = database.prepare("INSERT INTO stage_lines VALUES (?, ?, ?, ?, ?, ?)");609+ const line = database.prepare("INSERT INTO stage_lines VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
600 const arretLigne = database.prepare("INSERT OR IGNORE INTO stage_arrets_lignes VALUES (?, ?)");610 const arretLigne = database.prepare("INSERT OR IGNORE INTO stage_arrets_lignes VALUES (?, ?)");
601 611
602 return {612 return {
@@ -649,8 +659,8 @@ function prepareSpecs(database: DatabaseSync): Readonly<Record<keyof CatalogReco
649 insert: (row) => relation.run(nonEmpty(row.zdaid, "relations.zdaid"), nonEmpty(row.arrid, "relations.arrid"), nonEmpty(row.artid, "relations.artid")),659 insert: (row) => relation.run(nonEmpty(row.zdaid, "relations.zdaid"), nonEmpty(row.arrid, "relations.arrid"), nonEmpty(row.artid, "relations.artid")),
650 },660 },
651 lines: {661 lines: {
652- required: ["id_line", "name_line", "shortname_line", "transportmode", "transportsubmode", "status"],662+ required: ["id_line", "name_line", "shortname_line", "transportmode", "transportsubmode", "status", "colourweb_hexa", "textcolourweb_hexa"],
653- insert: (row) => line.run(canonicalLineId(row.id_line, "lines.id_line"), nonEmpty(row.name_line, "lines.name_line"), optionalText(row.shortname_line), nonEmpty(row.transportmode, "lines.transportmode"), optionalText(row.transportsubmode), nonEmpty(row.status, "lines.status")),663+ insert: (row) => line.run(canonicalLineId(row.id_line, "lines.id_line"), nonEmpty(row.name_line, "lines.name_line"), optionalText(row.shortname_line), nonEmpty(row.transportmode, "lines.transportmode"), optionalText(row.transportsubmode), nonEmpty(row.status, "lines.status"), normalizeLineColor(row.colourweb_hexa, "lines.colourweb_hexa"), normalizeLineColor(row.textcolourweb_hexa, "lines.textcolourweb_hexa")),
654 },664 },
655 arretsLignes: {665 arretsLignes: {
656 required: ["id", "stop_id"],666 required: ["id", "stop_id"],
@@ -959,7 +969,9 @@ function deriveCandidates(database: DatabaseSync): void {
959 CREATE TABLE stage_line_modes (969 CREATE TABLE stage_line_modes (
960 id_line TEXT PRIMARY KEY,970 id_line TEXT PRIMARY KEY,
961 mode TEXT NOT NULL,971 mode TEXT NOT NULL,
962- line_label TEXT NOT NULL972+ line_label TEXT NOT NULL,
973+ line_color TEXT NOT NULL,
974+ line_text_color TEXT NOT NULL
963 ) STRICT;975 ) STRICT;
964 976
965 INSERT INTO stage_line_modes977 INSERT INTO stage_line_modes
@@ -971,7 +983,9 @@ function deriveCandidates(database: DatabaseSync): void {
971 WHEN lower(trim(transport_mode)) = 'rail' AND lower(trim(transport_submode)) = 'local' THEN 'RER'983 WHEN lower(trim(transport_mode)) = 'rail' AND lower(trim(transport_submode)) = 'local' THEN 'RER'
972 WHEN lower(trim(transport_mode)) = 'rail' AND lower(trim(transport_submode)) = 'suburbanrailway' THEN 'TRANSILIEN'984 WHEN lower(trim(transport_mode)) = 'rail' AND lower(trim(transport_submode)) = 'suburbanrailway' THEN 'TRANSILIEN'
973 END,985 END,
974- coalesce(nullif(trim(shortname_line), ''), trim(name_line))986+ coalesce(nullif(trim(shortname_line), ''), trim(name_line)),
987+ line_color,
988+ line_text_color
975 FROM stage_lines989 FROM stage_lines
976 WHERE lower(trim(status)) = 'active'990 WHERE lower(trim(status)) = 'active'
977 AND (991 AND (
@@ -1040,6 +1054,8 @@ function deriveCandidates(database: DatabaseSync): void {
1040 zone.zdatown AS locality_label,1054 zone.zdatown AS locality_label,
1041 line_mode.id_line AS id_line,1055 line_mode.id_line AS id_line,
1042 line_mode.line_label AS line_label,1056 line_mode.line_label AS line_label,
1057+ line_mode.line_color AS line_color,
1058+ line_mode.line_text_color AS line_text_color,
1043 CASE1059 CASE
1044 WHEN line_mode.mode IN ('RER', 'TRANSILIEN') THEN terminal.stop_name1060 WHEN line_mode.mode IN ('RER', 'TRANSILIEN') THEN terminal.stop_name
1045 ELSE coalesce(nullif(trim(trip.trip_headsign), ''), terminal.stop_name)1061 ELSE coalesce(nullif(trim(trip.trip_headsign), ''), terminal.stop_name)
@@ -1089,7 +1105,8 @@ function deriveCandidates(database: DatabaseSync): void {
1089 AND trim(destination_code.object_code) <> ''1105 AND trim(destination_code.object_code) <> ''
1090 )1106 )
1091 SELECT mode, zdaid, min(stop_label) AS stop_label, min(locality_label) AS locality_label,1107 SELECT mode, zdaid, min(stop_label) AS stop_label, min(locality_label) AS locality_label,
1092- id_line, min(line_label) AS line_label, min(destination_label) AS destination_label,1108+ id_line, min(line_label) AS line_label, min(line_color) AS line_color,
1109+ min(line_text_color) AS line_text_color, min(destination_label) AS destination_label,
1093 monitoring_ref, line_ref, direction_id, destination_ref,1110 monitoring_ref, line_ref, direction_id, destination_ref,
1094 max(CASE WHEN resolution = 'arrets' THEN 1 ELSE 0 END) AS via_arrets1111 max(CASE WHEN resolution = 'arrets' THEN 1 ELSE 0 END) AS via_arrets
1095 FROM joined1112 FROM joined
@@ -1107,7 +1124,7 @@ function insertFinalCatalog(database: DatabaseSync, sourceRevision: string, crea
1107 1124
1108 const insertPlace = database.prepare("INSERT INTO places VALUES (?, ?, ?, ?, ?)");1125 const insertPlace = database.prepare("INSERT INTO places VALUES (?, ?, ?, ?, ?)");
1109 const insertSearch = database.prepare("INSERT INTO place_search(search_text, place_id) VALUES (?, ?)");1126 const insertSearch = database.prepare("INSERT INTO place_search(search_text, place_id) VALUES (?, ?)");
1110- const insertService = database.prepare("INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");1127+ const insertService = database.prepare("INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1111 const selectPlace = database.prepare("SELECT canonical_tuple FROM places WHERE place_id = ?");1128 const selectPlace = database.prepare("SELECT canonical_tuple FROM places WHERE place_id = ?");
1112 const selectService = database.prepare("SELECT place_id, canonical_tuple FROM services WHERE service_id = ?");1129 const selectService = database.prepare("SELECT place_id, canonical_tuple FROM services WHERE service_id = ?");
1113 1130
@@ -1146,7 +1163,7 @@ function insertFinalCatalog(database: DatabaseSync, sourceRevision: string, crea
1146 1163
1147 const candidates = database.prepare(`1164 const candidates = database.prepare(`
1148 SELECT mode, zdaid, stop_label, line_label, destination_label,1165 SELECT mode, zdaid, stop_label, line_label, destination_label,
1149- id_line, monitoring_ref, line_ref, direction_id, destination_ref1166+ line_color, line_text_color, id_line, monitoring_ref, line_ref, direction_id, destination_ref
1150 FROM stage_candidates1167 FROM stage_candidates
1151 ORDER BY mode, zdaid, id_line, monitoring_ref, direction_id, destination_ref1168 ORDER BY mode, zdaid, id_line, monitoring_ref, direction_id, destination_ref
1152 `).iterate() as Iterable<QueryRow>;1169 `).iterate() as Iterable<QueryRow>;
@@ -1174,6 +1191,8 @@ function insertFinalCatalog(database: DatabaseSync, sourceRevision: string, crea
1174 nonEmpty(row.stop_label, "candidate.stop_label"),1191 nonEmpty(row.stop_label, "candidate.stop_label"),
1175 nonEmpty(row.line_label, "candidate.line_label"),1192 nonEmpty(row.line_label, "candidate.line_label"),
1176 nonEmpty(row.destination_label, "candidate.destination_label"),1193 nonEmpty(row.destination_label, "candidate.destination_label"),
1194+ normalizeLineColor(row.line_color, "candidate.line_color"),
1195+ normalizeLineColor(row.line_text_color, "candidate.line_text_color"),
1177 nonEmpty(row.monitoring_ref, "candidate.monitoring_ref"),1196 nonEmpty(row.monitoring_ref, "candidate.monitoring_ref"),
1178 nonEmpty(row.line_ref, "candidate.line_ref"),1197 nonEmpty(row.line_ref, "candidate.line_ref"),
1179 nonEmpty(row.direction_id, "candidate.direction_id"),1198 nonEmpty(row.direction_id, "candidate.direction_id"),
@@ -1206,6 +1225,8 @@ function validateByteBounds(database: DatabaseSync): void {
1206 OR length(CAST(stop_label AS BLOB)) > ?1225 OR length(CAST(stop_label AS BLOB)) > ?
1207 OR length(CAST(line_label AS BLOB)) > ?1226 OR length(CAST(line_label AS BLOB)) > ?
1208 OR length(CAST(destination_label AS BLOB)) > ?1227 OR length(CAST(destination_label AS BLOB)) > ?
1228+ OR length(CAST(line_color AS BLOB)) <> 7
1229+ OR length(CAST(line_text_color AS BLOB)) <> 7
1209 LIMIT 11230 LIMIT 1
1210 `).get(LIMITS.idUtf8Bytes, LIMITS.labelUtf8Bytes, LIMITS.labelUtf8Bytes, LIMITS.labelUtf8Bytes);1231 `).get(LIMITS.idUtf8Bytes, LIMITS.labelUtf8Bytes, LIMITS.labelUtf8Bytes, LIMITS.labelUtf8Bytes);
1211 if (invalidService !== undefined) fail("CATALOG_LIMIT", "service exceeds a shared UTF-8 byte limit");1232 if (invalidService !== undefined) fail("CATALOG_LIMIT", "service exceeds a shared UTF-8 byte limit");
@@ -1228,7 +1249,7 @@ function validateOpenCatalog(database: DatabaseSync): CatalogBuildResult {
1228 database.exec("PRAGMA foreign_keys = ON");1249 database.exec("PRAGMA foreign_keys = ON");
1229 exactColumns(database, "metadata", ["key", "value"]);1250 exactColumns(database, "metadata", ["key", "value"]);
1230 exactColumns(database, "places", ["place_id", "stop_label", "locality_label", "mode", "canonical_tuple"]);1251 exactColumns(database, "places", ["place_id", "stop_label", "locality_label", "mode", "canonical_tuple"]);
1231- exactColumns(database, "services", ["service_id", "place_id", "stop_label", "line_label", "destination_label", "monitoring_ref", "line_ref", "direction_id", "destination_ref", "canonical_tuple"]);1252+ exactColumns(database, "services", ["service_id", "place_id", "stop_label", "line_label", "destination_label", "line_color", "line_text_color", "monitoring_ref", "line_ref", "direction_id", "destination_ref", "canonical_tuple"]);
1232 exactColumns(database, "place_search", ["search_text", "place_id"]);1253 exactColumns(database, "place_search", ["search_text", "place_id"]);
1233 1254
1234 const sourceRevision = metadataValue(database, "source_revision");1255 const sourceRevision = metadataValue(database, "source_revision");
@@ -1276,9 +1297,11 @@ function validateOpenCatalog(database: DatabaseSync): CatalogBuildResult {
1276 const unresolved = database.prepare(`1297 const unresolved = database.prepare(`
1277 SELECT 1 FROM services1298 SELECT 1 FROM services
1278 WHERE trim(monitoring_ref) = '' OR trim(line_ref) = '' OR trim(direction_id) = '' OR trim(destination_ref) = ''1299 WHERE trim(monitoring_ref) = '' OR trim(line_ref) = '' OR trim(direction_id) = '' OR trim(destination_ref) = ''
1300+ OR line_color NOT GLOB '#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
1301+ OR line_text_color NOT GLOB '#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
1279 LIMIT 11302 LIMIT 1
1280 `).get();1303 `).get();
1281- if (unresolved !== undefined) fail("CATALOG_UNRESOLVED", "catalog contains an unresolvable offered service");1304+ if (unresolved !== undefined) fail("CATALOG_UNRESOLVED", "catalog contains an unresolvable or malformed offered service");
1282 1305
1283 for (const row of database.prepare("SELECT place_id, canonical_tuple FROM places").iterate() as Iterable<QueryRow>) {1306 for (const row of database.prepare("SELECT place_id, canonical_tuple FROM places").iterate() as Iterable<QueryRow>) {
1284 if (opaqueId("plc_", String(row.canonical_tuple)) !== row.place_id) fail("HASH_COLLISION", "place ID does not match its canonical tuple");1307 if (opaqueId("plc_", String(row.canonical_tuple)) !== row.place_id) fail("HASH_COLLISION", "place ID does not match its canonical tuple");
modified packages/backend/src/catalog.ts +22 -6
@@ -48,6 +48,9 @@ interface ServiceRow {
4848 readonly stopLabel: string;
4949 readonly lineLabel: string;
5050 readonly destinationLabel: string;
51+ readonly lineMode: TransportMode;
52+ readonly lineColor: string;
53+ readonly lineTextColor: string;
5154 }
5255
5356 interface ResolutionRow {
@@ -87,7 +90,7 @@ interface ExpectedColumn {
8790 const CLOSE_READER = Symbol("closeCatalogReader");
8891 const OPAQUE_ID_BYTES = 47;
8992 const CATALOG_SCHEMA_VERSION = "1";
90-const CATALOG_FORMAT_VERSION = "1";
93+const CATALOG_FORMAT_VERSION = "2";
9194 const REQUIRED_METADATA = [
9295 "schema_version",
9396 "catalog_version",
@@ -123,6 +126,8 @@ const TABLE_COLUMNS: Readonly<Record<string, readonly ExpectedColumn[]>> = {
123126 { name: "stop_label", type: "TEXT", notNull: true },
124127 { name: "line_label", type: "TEXT", notNull: true },
125128 { name: "destination_label", type: "TEXT", notNull: true },
129+ { name: "line_color", type: "TEXT", notNull: true },
130+ { name: "line_text_color", type: "TEXT", notNull: true },
126131 { name: "monitoring_ref", type: "TEXT", notNull: true },
127132 { name: "line_ref", type: "TEXT", notNull: true },
128133 { name: "direction_id", type: "TEXT", notNull: true },
@@ -322,6 +327,10 @@ function validateData(database: DatabaseSync): void {
322327 OR length(CAST(stop_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes}
323328 OR length(CAST(line_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes}
324329 OR length(CAST(destination_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes}
330+ OR length(CAST(line_color AS BLOB)) <> 7
331+ OR line_color NOT GLOB '#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
332+ OR length(CAST(line_text_color AS BLOB)) <> 7
333+ OR line_text_color NOT GLOB '#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
325334 OR length(CAST(monitoring_ref AS BLOB)) < 1
326335 OR length(CAST(line_ref AS BLOB)) < 1
327336 OR length(CAST(direction_id AS BLOB)) < 1
@@ -482,12 +491,16 @@ export class SqliteCatalogReader implements CatalogReader {
482491 `);
483492 this.#listServicesStatement = database.prepare(`
484493 SELECT
485- service_id AS serviceId,
486- stop_label AS stopLabel,
487- line_label AS lineLabel,
488- destination_label AS destinationLabel
494+ services.service_id AS serviceId,
495+ services.stop_label AS stopLabel,
496+ services.line_label AS lineLabel,
497+ services.destination_label AS destinationLabel,
498+ places.mode AS lineMode,
499+ services.line_color AS lineColor,
500+ services.line_text_color AS lineTextColor
489501 FROM services
490- WHERE place_id = ?
502+ JOIN places ON places.place_id = services.place_id
503+ WHERE services.place_id = ?
491504 ORDER BY
492505 line_label COLLATE NOCASE,
493506 destination_label COLLATE NOCASE,
@@ -544,6 +557,9 @@ export class SqliteCatalogReader implements CatalogReader {
544557 stopLabel: entry.stopLabel,
545558 lineLabel: entry.lineLabel,
546559 destinationLabel: entry.destinationLabel,
560+ lineMode: entry.lineMode,
561+ lineColor: entry.lineColor,
562+ lineTextColor: entry.lineTextColor,
547563 }));
548564 }
549565
@@ -48,6 +48,9 @@ interface ServiceRow {
48 readonly stopLabel: string;48 readonly stopLabel: string;
49 readonly lineLabel: string;49 readonly lineLabel: string;
50 readonly destinationLabel: string;50 readonly destinationLabel: string;
51+ readonly lineMode: TransportMode;
52+ readonly lineColor: string;
53+ readonly lineTextColor: string;
51 }54 }
52 55
53 interface ResolutionRow {56 interface ResolutionRow {
@@ -87,7 +90,7 @@ interface ExpectedColumn {
87 const CLOSE_READER = Symbol("closeCatalogReader");90 const CLOSE_READER = Symbol("closeCatalogReader");
88 const OPAQUE_ID_BYTES = 47;91 const OPAQUE_ID_BYTES = 47;
89 const CATALOG_SCHEMA_VERSION = "1";92 const CATALOG_SCHEMA_VERSION = "1";
90-const CATALOG_FORMAT_VERSION = "1";93+const CATALOG_FORMAT_VERSION = "2";
91 const REQUIRED_METADATA = [94 const REQUIRED_METADATA = [
92 "schema_version",95 "schema_version",
93 "catalog_version",96 "catalog_version",
@@ -123,6 +126,8 @@ const TABLE_COLUMNS: Readonly<Record<string, readonly ExpectedColumn[]>> = {
123 { name: "stop_label", type: "TEXT", notNull: true },126 { name: "stop_label", type: "TEXT", notNull: true },
124 { name: "line_label", type: "TEXT", notNull: true },127 { name: "line_label", type: "TEXT", notNull: true },
125 { name: "destination_label", type: "TEXT", notNull: true },128 { name: "destination_label", type: "TEXT", notNull: true },
129+ { name: "line_color", type: "TEXT", notNull: true },
130+ { name: "line_text_color", type: "TEXT", notNull: true },
126 { name: "monitoring_ref", type: "TEXT", notNull: true },131 { name: "monitoring_ref", type: "TEXT", notNull: true },
127 { name: "line_ref", type: "TEXT", notNull: true },132 { name: "line_ref", type: "TEXT", notNull: true },
128 { name: "direction_id", type: "TEXT", notNull: true },133 { name: "direction_id", type: "TEXT", notNull: true },
@@ -322,6 +327,10 @@ function validateData(database: DatabaseSync): void {
322 OR length(CAST(stop_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes}327 OR length(CAST(stop_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes}
323 OR length(CAST(line_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes}328 OR length(CAST(line_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes}
324 OR length(CAST(destination_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes}329 OR length(CAST(destination_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes}
330+ OR length(CAST(line_color AS BLOB)) <> 7
331+ OR line_color NOT GLOB '#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
332+ OR length(CAST(line_text_color AS BLOB)) <> 7
333+ OR line_text_color NOT GLOB '#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
325 OR length(CAST(monitoring_ref AS BLOB)) < 1334 OR length(CAST(monitoring_ref AS BLOB)) < 1
326 OR length(CAST(line_ref AS BLOB)) < 1335 OR length(CAST(line_ref AS BLOB)) < 1
327 OR length(CAST(direction_id AS BLOB)) < 1336 OR length(CAST(direction_id AS BLOB)) < 1
@@ -482,12 +491,16 @@ export class SqliteCatalogReader implements CatalogReader {
482 `);491 `);
483 this.#listServicesStatement = database.prepare(`492 this.#listServicesStatement = database.prepare(`
484 SELECT493 SELECT
485- service_id AS serviceId,494+ services.service_id AS serviceId,
486- stop_label AS stopLabel,495+ services.stop_label AS stopLabel,
487- line_label AS lineLabel,496+ services.line_label AS lineLabel,
488- destination_label AS destinationLabel497+ services.destination_label AS destinationLabel,
498+ places.mode AS lineMode,
499+ services.line_color AS lineColor,
500+ services.line_text_color AS lineTextColor
489 FROM services501 FROM services
490- WHERE place_id = ?502+ JOIN places ON places.place_id = services.place_id
503+ WHERE services.place_id = ?
491 ORDER BY504 ORDER BY
492 line_label COLLATE NOCASE,505 line_label COLLATE NOCASE,
493 destination_label COLLATE NOCASE,506 destination_label COLLATE NOCASE,
@@ -544,6 +557,9 @@ export class SqliteCatalogReader implements CatalogReader {
544 stopLabel: entry.stopLabel,557 stopLabel: entry.stopLabel,
545 lineLabel: entry.lineLabel,558 lineLabel: entry.lineLabel,
546 destinationLabel: entry.destinationLabel,559 destinationLabel: entry.destinationLabel,
560+ lineMode: entry.lineMode,
561+ lineColor: entry.lineColor,
562+ lineTextColor: entry.lineTextColor,
547 }));563 }));
548 }564 }
549 565
modified packages/backend/test/catalog-http.test.ts +5 -5
@@ -55,20 +55,20 @@ function fixtureManager(): CatalogManager {
5555 PRAGMA foreign_keys = ON;
5656 CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL);
5757 CREATE TABLE places(place_id TEXT PRIMARY KEY, stop_label TEXT NOT NULL, locality_label TEXT, mode TEXT NOT NULL, canonical_tuple TEXT NOT NULL UNIQUE);
58- CREATE TABLE services(service_id TEXT PRIMARY KEY, place_id TEXT NOT NULL REFERENCES places(place_id), stop_label TEXT NOT NULL, line_label TEXT NOT NULL, destination_label TEXT NOT NULL, monitoring_ref TEXT NOT NULL, line_ref TEXT NOT NULL, direction_id TEXT NOT NULL, destination_ref TEXT NOT NULL, canonical_tuple TEXT NOT NULL UNIQUE);
58+ CREATE TABLE services(service_id TEXT PRIMARY KEY, place_id TEXT NOT NULL REFERENCES places(place_id), stop_label TEXT NOT NULL, line_label TEXT NOT NULL, destination_label TEXT NOT NULL, line_color TEXT NOT NULL, line_text_color TEXT NOT NULL, monitoring_ref TEXT NOT NULL, line_ref TEXT NOT NULL, direction_id TEXT NOT NULL, destination_ref TEXT NOT NULL, canonical_tuple TEXT NOT NULL UNIQUE);
5959 CREATE VIRTUAL TABLE place_search USING fts5(search_text, place_id UNINDEXED);
6060 `);
6161 const metadata = database.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)");
62- metadata.run("schema_version", "1"); metadata.run("catalog_version", "1");
62+ metadata.run("schema_version", "1"); metadata.run("catalog_version", "2");
6363 metadata.run("source_revision", "http-fixture-revision");
6464 metadata.run("created_at", "2026-08-30T12:00:00.000Z");
6565 const place = database.prepare("INSERT INTO places VALUES (?, ?, ?, ?, ?)");
6666 const search = database.prepare("INSERT INTO place_search(search_text, place_id) VALUES (?, ?)");
67- const service = database.prepare("INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
67+ const service = database.prepare("INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
6868 FIXTURE.forEach((entry, index) => {
6969 place.run(entry.id, entry.label, entry.locality, entry.mode, `place:v1|${index}:${entry.id}`);
7070 search.run(normalizedSearchText(entry), entry.id);
71- service.run(entry.serviceId, entry.id, entry.label, entry.line, entry.destination, entry.monitoringRef, entry.lineRef, `direction-${index}`, `raw-terminal-${index}`, `service:v1|${index}:${entry.serviceId}`);
71+ service.run(entry.serviceId, entry.id, entry.label, entry.line, entry.destination, "#123456", "#ffffff", entry.monitoringRef, entry.lineRef, `direction-${index}`, `raw-terminal-${index}`, `service:v1|${index}:${entry.serviceId}`);
7272 });
7373 database.close();
7474 const manager = new CatalogManager();
@@ -100,7 +100,7 @@ test("GET routes return exact public response bodies without source identifiers
100100 assert.equal(services.status, 200);
101101 assert.ok(isServiceOptionsResult(services.body));
102102 assert.deepEqual(services.body, { schemaVersion: SCHEMA_VERSION, placeId: PLACE_ID, services: [
103- { serviceId: SERVICE_ID, stopLabel: "Châtelet", lineLabel: "4", destinationLabel: "Bagneux – Lucie Aubrac" },
103+ { serviceId: SERVICE_ID, stopLabel: "Châtelet", lineLabel: "4", destinationLabel: "Bagneux – Lucie Aubrac", lineMode: "METRO", lineColor: "#123456", lineTextColor: "#ffffff" },
104104 ] });
105105 const publicJson = JSON.stringify([places.body, services.body]);
106106 for (const forbidden of [PERSONAL_KEY, "raw-monitoring-metro-chatelet", "raw-line-metro-4", "raw-terminal-0", "direction-0"]) {
@@ -55,20 +55,20 @@ function fixtureManager(): CatalogManager {
55 PRAGMA foreign_keys = ON;55 PRAGMA foreign_keys = ON;
56 CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL);56 CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL);
57 CREATE TABLE places(place_id TEXT PRIMARY KEY, stop_label TEXT NOT NULL, locality_label TEXT, mode TEXT NOT NULL, canonical_tuple TEXT NOT NULL UNIQUE);57 CREATE TABLE places(place_id TEXT PRIMARY KEY, stop_label TEXT NOT NULL, locality_label TEXT, mode TEXT NOT NULL, canonical_tuple TEXT NOT NULL UNIQUE);
58- CREATE TABLE services(service_id TEXT PRIMARY KEY, place_id TEXT NOT NULL REFERENCES places(place_id), stop_label TEXT NOT NULL, line_label TEXT NOT NULL, destination_label TEXT NOT NULL, monitoring_ref TEXT NOT NULL, line_ref TEXT NOT NULL, direction_id TEXT NOT NULL, destination_ref TEXT NOT NULL, canonical_tuple TEXT NOT NULL UNIQUE);58+ CREATE TABLE services(service_id TEXT PRIMARY KEY, place_id TEXT NOT NULL REFERENCES places(place_id), stop_label TEXT NOT NULL, line_label TEXT NOT NULL, destination_label TEXT NOT NULL, line_color TEXT NOT NULL, line_text_color TEXT NOT NULL, monitoring_ref TEXT NOT NULL, line_ref TEXT NOT NULL, direction_id TEXT NOT NULL, destination_ref TEXT NOT NULL, canonical_tuple TEXT NOT NULL UNIQUE);
59 CREATE VIRTUAL TABLE place_search USING fts5(search_text, place_id UNINDEXED);59 CREATE VIRTUAL TABLE place_search USING fts5(search_text, place_id UNINDEXED);
60 `);60 `);
61 const metadata = database.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)");61 const metadata = database.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)");
62- metadata.run("schema_version", "1"); metadata.run("catalog_version", "1");62+ metadata.run("schema_version", "1"); metadata.run("catalog_version", "2");
63 metadata.run("source_revision", "http-fixture-revision");63 metadata.run("source_revision", "http-fixture-revision");
64 metadata.run("created_at", "2026-08-30T12:00:00.000Z");64 metadata.run("created_at", "2026-08-30T12:00:00.000Z");
65 const place = database.prepare("INSERT INTO places VALUES (?, ?, ?, ?, ?)");65 const place = database.prepare("INSERT INTO places VALUES (?, ?, ?, ?, ?)");
66 const search = database.prepare("INSERT INTO place_search(search_text, place_id) VALUES (?, ?)");66 const search = database.prepare("INSERT INTO place_search(search_text, place_id) VALUES (?, ?)");
67- const service = database.prepare("INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");67+ const service = database.prepare("INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
68 FIXTURE.forEach((entry, index) => {68 FIXTURE.forEach((entry, index) => {
69 place.run(entry.id, entry.label, entry.locality, entry.mode, `place:v1|${index}:${entry.id}`);69 place.run(entry.id, entry.label, entry.locality, entry.mode, `place:v1|${index}:${entry.id}`);
70 search.run(normalizedSearchText(entry), entry.id);70 search.run(normalizedSearchText(entry), entry.id);
71- service.run(entry.serviceId, entry.id, entry.label, entry.line, entry.destination, entry.monitoringRef, entry.lineRef, `direction-${index}`, `raw-terminal-${index}`, `service:v1|${index}:${entry.serviceId}`);71+ service.run(entry.serviceId, entry.id, entry.label, entry.line, entry.destination, "#123456", "#ffffff", entry.monitoringRef, entry.lineRef, `direction-${index}`, `raw-terminal-${index}`, `service:v1|${index}:${entry.serviceId}`);
72 });72 });
73 database.close();73 database.close();
74 const manager = new CatalogManager();74 const manager = new CatalogManager();
@@ -100,7 +100,7 @@ test("GET routes return exact public response bodies without source identifiers
100 assert.equal(services.status, 200);100 assert.equal(services.status, 200);
101 assert.ok(isServiceOptionsResult(services.body));101 assert.ok(isServiceOptionsResult(services.body));
102 assert.deepEqual(services.body, { schemaVersion: SCHEMA_VERSION, placeId: PLACE_ID, services: [102 assert.deepEqual(services.body, { schemaVersion: SCHEMA_VERSION, placeId: PLACE_ID, services: [
103- { serviceId: SERVICE_ID, stopLabel: "Châtelet", lineLabel: "4", destinationLabel: "Bagneux – Lucie Aubrac" },103+ { serviceId: SERVICE_ID, stopLabel: "Châtelet", lineLabel: "4", destinationLabel: "Bagneux – Lucie Aubrac", lineMode: "METRO", lineColor: "#123456", lineTextColor: "#ffffff" },
104 ] });104 ] });
105 const publicJson = JSON.stringify([places.body, services.body]);105 const publicJson = JSON.stringify([places.body, services.body]);
106 for (const forbidden of [PERSONAL_KEY, "raw-monitoring-metro-chatelet", "raw-line-metro-4", "raw-terminal-0", "direction-0"]) {106 for (const forbidden of [PERSONAL_KEY, "raw-monitoring-metro-chatelet", "raw-line-metro-4", "raw-terminal-0", "direction-0"]) {
modified packages/backend/test/catalog-import.test.ts +47 -0
@@ -107,6 +107,13 @@ test("import covers five modes, gates perimeter and unsupported modes, preserves
107107 const metroServices = manager.listServices(metro.placeId) ?? [];
108108 assert.equal(metroServices.length, 2);
109109 assert.notEqual(metroServices[0]?.serviceId, metroServices[1]?.serviceId);
110+ assert.deepEqual(
111+ metroServices.map(({ lineMode, lineColor, lineTextColor }) => ({ lineMode, lineColor, lineTextColor })),
112+ [
113+ { lineMode: "METRO", lineColor: "#be418d", lineTextColor: "#ffffff" },
114+ { lineMode: "METRO", lineColor: "#be418d", lineTextColor: "#ffffff" },
115+ ],
116+ );
110117 const rer = place(manager, "chatelet les halles", "RER");
111118 const east = service(manager, rer.placeId, "Marne-la-Vallée Chessy");
112119 const west = service(manager, rer.placeId, "Saint-Germain-en-Laye");
@@ -129,6 +136,46 @@ test("import covers five modes, gates perimeter and unsupported modes, preserves
129136 assert.doesNotMatch(JSON.stringify([...manager.searchPlaces("chatelet"), ...metroServices, ...busServices]), /STIF:|IDFM:|DEST:/u);
130137 });
131138
139+test("import normalizes valid colors and rejects invalid or missing authoritative colors", async (t) => {
140+ const directory = workspace(t);
141+ const validSources = sourcesFor("fixture-2026-08-a");
142+ const invalidSources: CatalogRecordSourceSet = {
143+ ...validSources,
144+ lines: validSources.lines.map((line, index) => index === 0
145+ ? { ...line, colourweb_hexa: "not-a-color" }
146+ : line),
147+ };
148+ const invalidCandidate = join(directory, "invalid-color.sqlite");
149+ await assert.rejects(
150+ buildCatalogCandidateFromRecords({
151+ candidatePath: invalidCandidate,
152+ sourceRevision: "fixture-invalid-color",
153+ sources: invalidSources,
154+ }),
155+ /six-digit hexadecimal color/u,
156+ );
157+ assert.equal(existsSync(invalidCandidate), false);
158+
159+ const missingSources: CatalogRecordSourceSet = {
160+ ...validSources,
161+ lines: validSources.lines.map((line, index) => {
162+ if (index !== 0) return line;
163+ const { textcolourweb_hexa: _missing, ...withoutTextColor } = line;
164+ return withoutTextColor;
165+ }),
166+ };
167+ const missingCandidate = join(directory, "missing-color.sqlite");
168+ await assert.rejects(
169+ buildCatalogCandidateFromRecords({
170+ candidatePath: missingCandidate,
171+ sourceRevision: "fixture-missing-color",
172+ sources: missingSources,
173+ }),
174+ /missing textcolourweb_hexa/u,
175+ );
176+ assert.equal(existsSync(missingCandidate), false);
177+});
178+
132179 test("activation preserves stable services, removes stale IDs, and failed replacement leaves the active file untouched", async (t) => {
133180 const directory = workspace(t);
134181 const active = join(directory, "catalog.sqlite");
@@ -107,6 +107,13 @@ test("import covers five modes, gates perimeter and unsupported modes, preserves
107 const metroServices = manager.listServices(metro.placeId) ?? [];107 const metroServices = manager.listServices(metro.placeId) ?? [];
108 assert.equal(metroServices.length, 2);108 assert.equal(metroServices.length, 2);
109 assert.notEqual(metroServices[0]?.serviceId, metroServices[1]?.serviceId);109 assert.notEqual(metroServices[0]?.serviceId, metroServices[1]?.serviceId);
110+ assert.deepEqual(
111+ metroServices.map(({ lineMode, lineColor, lineTextColor }) => ({ lineMode, lineColor, lineTextColor })),
112+ [
113+ { lineMode: "METRO", lineColor: "#be418d", lineTextColor: "#ffffff" },
114+ { lineMode: "METRO", lineColor: "#be418d", lineTextColor: "#ffffff" },
115+ ],
116+ );
110 const rer = place(manager, "chatelet les halles", "RER");117 const rer = place(manager, "chatelet les halles", "RER");
111 const east = service(manager, rer.placeId, "Marne-la-Vallée Chessy");118 const east = service(manager, rer.placeId, "Marne-la-Vallée Chessy");
112 const west = service(manager, rer.placeId, "Saint-Germain-en-Laye");119 const west = service(manager, rer.placeId, "Saint-Germain-en-Laye");
@@ -129,6 +136,46 @@ test("import covers five modes, gates perimeter and unsupported modes, preserves
129 assert.doesNotMatch(JSON.stringify([...manager.searchPlaces("chatelet"), ...metroServices, ...busServices]), /STIF:|IDFM:|DEST:/u);136 assert.doesNotMatch(JSON.stringify([...manager.searchPlaces("chatelet"), ...metroServices, ...busServices]), /STIF:|IDFM:|DEST:/u);
130 });137 });
131 138
139+test("import normalizes valid colors and rejects invalid or missing authoritative colors", async (t) => {
140+ const directory = workspace(t);
141+ const validSources = sourcesFor("fixture-2026-08-a");
142+ const invalidSources: CatalogRecordSourceSet = {
143+ ...validSources,
144+ lines: validSources.lines.map((line, index) => index === 0
145+ ? { ...line, colourweb_hexa: "not-a-color" }
146+ : line),
147+ };
148+ const invalidCandidate = join(directory, "invalid-color.sqlite");
149+ await assert.rejects(
150+ buildCatalogCandidateFromRecords({
151+ candidatePath: invalidCandidate,
152+ sourceRevision: "fixture-invalid-color",
153+ sources: invalidSources,
154+ }),
155+ /six-digit hexadecimal color/u,
156+ );
157+ assert.equal(existsSync(invalidCandidate), false);
158+
159+ const missingSources: CatalogRecordSourceSet = {
160+ ...validSources,
161+ lines: validSources.lines.map((line, index) => {
162+ if (index !== 0) return line;
163+ const { textcolourweb_hexa: _missing, ...withoutTextColor } = line;
164+ return withoutTextColor;
165+ }),
166+ };
167+ const missingCandidate = join(directory, "missing-color.sqlite");
168+ await assert.rejects(
169+ buildCatalogCandidateFromRecords({
170+ candidatePath: missingCandidate,
171+ sourceRevision: "fixture-missing-color",
172+ sources: missingSources,
173+ }),
174+ /missing textcolourweb_hexa/u,
175+ );
176+ assert.equal(existsSync(missingCandidate), false);
177+});
178+
132 test("activation preserves stable services, removes stale IDs, and failed replacement leaves the active file untouched", async (t) => {179 test("activation preserves stable services, removes stale IDs, and failed replacement leaves the active file untouched", async (t) => {
133 const directory = workspace(t);180 const directory = workspace(t);
134 const active = join(directory, "catalog.sqlite");181 const active = join(directory, "catalog.sqlite");
modified packages/backend/test/catalog.test.ts +31 -3
@@ -23,6 +23,8 @@ interface ServiceSeed {
2323 readonly stopLabel: string;
2424 readonly lineLabel: string;
2525 readonly destinationLabel: string;
26+ readonly lineColor?: string;
27+ readonly lineTextColor?: string;
2628 readonly monitoringRef: string;
2729 readonly lineRef: string;
2830 readonly directionId: string;
@@ -208,6 +210,8 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog {
208210 stop_label TEXT NOT NULL,
209211 line_label TEXT NOT NULL,
210212 destination_label TEXT NOT NULL,
213+ line_color TEXT NOT NULL,
214+ line_text_color TEXT NOT NULL,
211215 monitoring_ref TEXT NOT NULL,
212216 line_ref TEXT NOT NULL,
213217 direction_id TEXT NOT NULL,
@@ -219,7 +223,7 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog {
219223
220224 const metadata = database.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)");
221225 metadata.run("schema_version", "1");
222- metadata.run("catalog_version", seed.catalogVersion ?? "1");
226+ metadata.run("catalog_version", seed.catalogVersion ?? "2");
223227 metadata.run("source_revision", seed.revision ?? "fixture-revision-a");
224228 metadata.run("created_at", "2026-08-30T12:00:00.000Z");
225229
@@ -258,12 +262,14 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog {
258262 stop_label,
259263 line_label,
260264 destination_label,
265+ line_color,
266+ line_text_color,
261267 monitoring_ref,
262268 line_ref,
263269 direction_id,
264270 destination_ref,
265271 canonical_tuple
266- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
272+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
267273 `);
268274 for (const service of services) {
269275 const identity = serviceIdentity(service);
@@ -274,6 +280,8 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog {
274280 service.stopLabel,
275281 service.lineLabel,
276282 service.destinationLabel,
283+ service.lineColor ?? "#123456",
284+ service.lineTextColor ?? "#ffffff",
277285 service.monitoringRef,
278286 service.lineRef,
279287 service.directionId,
@@ -424,7 +432,7 @@ test("reload preserves stable IDs, distinguishes directions, rejects invalid can
424432 destinationLabel: "Saint-Rémy-lès-Chevreuse",
425433 });
426434
427- const invalid = buildCatalog({ catalogVersion: "2" });
435+ const invalid = buildCatalog({ catalogVersion: "1" });
428436 assert.throws(() => manager.reload(invalid.path), /Invalid catalog candidate/u);
429437 assert.equal(manager.resolveService(southId).status, "RESOLVED");
430438
@@ -455,13 +463,33 @@ test("reload preserves stable IDs, distinguishes directions, rejects invalid can
455463 assert.equal(manager.listServices("plc_missing"), undefined);
456464 assert.deepEqual(Object.keys(manager.listServices(replacement.placeIds.metro)![0]!).sort(), [
457465 "destinationLabel",
466+ "lineColor",
458467 "lineLabel",
468+ "lineMode",
469+ "lineTextColor",
459470 "serviceId",
460471 "stopLabel",
461472 ]);
473+ assert.deepEqual(manager.listServices(replacement.placeIds.metro)![0], {
474+ serviceId: replacement.serviceIds["metro-south"],
475+ stopLabel: "Châtelet",
476+ lineLabel: "4",
477+ destinationLabel: "Bagneux – Lucie Aubrac",
478+ lineMode: "METRO",
479+ lineColor: "#123456",
480+ lineTextColor: "#ffffff",
481+ });
462482 });
463483
464484 test("candidate validation rejects incomplete FTS coverage", () => {
465485 const invalid = buildCatalog({ omitSearchFor: "metro" });
466486 assert.throws(() => SqliteCatalogReader.open(invalid.path), /search consistency/u);
467487 });
488+
489+test("candidate validation rejects non-normalized presentation colors", () => {
490+ const invalid = buildCatalog({
491+ omitServiceKeys: ["metro-south"],
492+ services: [{ ...BASE_SERVICES.find((service) => service.key === "metro-south")!, lineColor: "#ABCDEF" }],
493+ });
494+ assert.throws(() => SqliteCatalogReader.open(invalid.path), /service data/u);
495+});
@@ -23,6 +23,8 @@ interface ServiceSeed {
23 readonly stopLabel: string;23 readonly stopLabel: string;
24 readonly lineLabel: string;24 readonly lineLabel: string;
25 readonly destinationLabel: string;25 readonly destinationLabel: string;
26+ readonly lineColor?: string;
27+ readonly lineTextColor?: string;
26 readonly monitoringRef: string;28 readonly monitoringRef: string;
27 readonly lineRef: string;29 readonly lineRef: string;
28 readonly directionId: string;30 readonly directionId: string;
@@ -208,6 +210,8 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog {
208 stop_label TEXT NOT NULL,210 stop_label TEXT NOT NULL,
209 line_label TEXT NOT NULL,211 line_label TEXT NOT NULL,
210 destination_label TEXT NOT NULL,212 destination_label TEXT NOT NULL,
213+ line_color TEXT NOT NULL,
214+ line_text_color TEXT NOT NULL,
211 monitoring_ref TEXT NOT NULL,215 monitoring_ref TEXT NOT NULL,
212 line_ref TEXT NOT NULL,216 line_ref TEXT NOT NULL,
213 direction_id TEXT NOT NULL,217 direction_id TEXT NOT NULL,
@@ -219,7 +223,7 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog {
219 223
220 const metadata = database.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)");224 const metadata = database.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)");
221 metadata.run("schema_version", "1");225 metadata.run("schema_version", "1");
222- metadata.run("catalog_version", seed.catalogVersion ?? "1");226+ metadata.run("catalog_version", seed.catalogVersion ?? "2");
223 metadata.run("source_revision", seed.revision ?? "fixture-revision-a");227 metadata.run("source_revision", seed.revision ?? "fixture-revision-a");
224 metadata.run("created_at", "2026-08-30T12:00:00.000Z");228 metadata.run("created_at", "2026-08-30T12:00:00.000Z");
225 229
@@ -258,12 +262,14 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog {
258 stop_label,262 stop_label,
259 line_label,263 line_label,
260 destination_label,264 destination_label,
265+ line_color,
266+ line_text_color,
261 monitoring_ref,267 monitoring_ref,
262 line_ref,268 line_ref,
263 direction_id,269 direction_id,
264 destination_ref,270 destination_ref,
265 canonical_tuple271 canonical_tuple
266- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)272+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
267 `);273 `);
268 for (const service of services) {274 for (const service of services) {
269 const identity = serviceIdentity(service);275 const identity = serviceIdentity(service);
@@ -274,6 +280,8 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog {
274 service.stopLabel,280 service.stopLabel,
275 service.lineLabel,281 service.lineLabel,
276 service.destinationLabel,282 service.destinationLabel,
283+ service.lineColor ?? "#123456",
284+ service.lineTextColor ?? "#ffffff",
277 service.monitoringRef,285 service.monitoringRef,
278 service.lineRef,286 service.lineRef,
279 service.directionId,287 service.directionId,
@@ -424,7 +432,7 @@ test("reload preserves stable IDs, distinguishes directions, rejects invalid can
424 destinationLabel: "Saint-Rémy-lès-Chevreuse",432 destinationLabel: "Saint-Rémy-lès-Chevreuse",
425 });433 });
426 434
427- const invalid = buildCatalog({ catalogVersion: "2" });435+ const invalid = buildCatalog({ catalogVersion: "1" });
428 assert.throws(() => manager.reload(invalid.path), /Invalid catalog candidate/u);436 assert.throws(() => manager.reload(invalid.path), /Invalid catalog candidate/u);
429 assert.equal(manager.resolveService(southId).status, "RESOLVED");437 assert.equal(manager.resolveService(southId).status, "RESOLVED");
430 438
@@ -455,13 +463,33 @@ test("reload preserves stable IDs, distinguishes directions, rejects invalid can
455 assert.equal(manager.listServices("plc_missing"), undefined);463 assert.equal(manager.listServices("plc_missing"), undefined);
456 assert.deepEqual(Object.keys(manager.listServices(replacement.placeIds.metro)![0]!).sort(), [464 assert.deepEqual(Object.keys(manager.listServices(replacement.placeIds.metro)![0]!).sort(), [
457 "destinationLabel",465 "destinationLabel",
466+ "lineColor",
458 "lineLabel",467 "lineLabel",
468+ "lineMode",
469+ "lineTextColor",
459 "serviceId",470 "serviceId",
460 "stopLabel",471 "stopLabel",
461 ]);472 ]);
473+ assert.deepEqual(manager.listServices(replacement.placeIds.metro)![0], {
474+ serviceId: replacement.serviceIds["metro-south"],
475+ stopLabel: "Châtelet",
476+ lineLabel: "4",
477+ destinationLabel: "Bagneux – Lucie Aubrac",
478+ lineMode: "METRO",
479+ lineColor: "#123456",
480+ lineTextColor: "#ffffff",
481+ });
462 });482 });
463 483
464 test("candidate validation rejects incomplete FTS coverage", () => {484 test("candidate validation rejects incomplete FTS coverage", () => {
465 const invalid = buildCatalog({ omitSearchFor: "metro" });485 const invalid = buildCatalog({ omitSearchFor: "metro" });
466 assert.throws(() => SqliteCatalogReader.open(invalid.path), /search consistency/u);486 assert.throws(() => SqliteCatalogReader.open(invalid.path), /search consistency/u);
467 });487 });
488+
489+test("candidate validation rejects non-normalized presentation colors", () => {
490+ const invalid = buildCatalog({
491+ omitServiceKeys: ["metro-south"],
492+ services: [{ ...BASE_SERVICES.find((service) => service.key === "metro-south")!, lineColor: "#ABCDEF" }],
493+ });
494+ assert.throws(() => SqliteCatalogReader.open(invalid.path), /service data/u);
495+});
modified packages/companion/src/configuration.js +4 -1
@@ -11,7 +11,10 @@ var FAVORITE_STRING_KEYS = [
1111 "displayName",
1212 "stopLabel",
1313 "lineLabel",
14- "destinationLabel"
14+ "destinationLabel",
15+ "lineMode",
16+ "lineColor",
17+ "lineTextColor"
1518 ];
1619
1720 function emptyConfiguration() {
@@ -11,7 +11,10 @@ var FAVORITE_STRING_KEYS = [
11 "displayName",11 "displayName",
12 "stopLabel",12 "stopLabel",
13 "lineLabel",13 "lineLabel",
14- "destinationLabel"14+ "destinationLabel",
15+ "lineMode",
16+ "lineColor",
17+ "lineTextColor"
15 ];18 ];
16 19
17 function emptyConfiguration() {20 function emptyConfiguration() {
modified packages/companion/src/contracts.js +31 -5
@@ -25,6 +25,7 @@ var ERROR_CODE = Object.freeze([
2525 "RATE_LIMITED",
2626 "INVALID_RESPONSE"
2727 ]);
28+var TRANSPORT_MODE = Object.freeze(["BUS", "METRO", "TRAM", "RER", "TRANSILIEN"]);
2829 var API_KEY_ACTION = Object.freeze(["KEEP", "REPLACE", "REMOVE"]);
2930 var KEY_STATUS = Object.freeze({ MISSING: 0, CONFIGURED: 1, INVALID: 2 });
3031 var REQUEST_TRIGGER = Object.freeze({ APP_OPEN: 0, FAVORITE_SELECTION: 1, MANUAL_SELECT: 2 });
@@ -103,6 +104,9 @@ var FAVORITE_KEYS = [
103104 "stopLabel",
104105 "lineLabel",
105106 "destinationLabel",
107+ "lineMode",
108+ "lineColor",
109+ "lineTextColor",
106110 "sortOrder"
107111 ];
108112 var DEPARTURE_KEYS = ["expectedAt", "aimedAt", "minutes", "status", "nextIntervalMinutes"];
@@ -169,6 +173,10 @@ function boundedString(value, maximum) {
169173 return size >= 1 && size <= maximum;
170174 }
171175
176+function isLineColor(value) {
177+ return typeof value === "string" && /^#[0-9a-f]{6}$/.test(value);
178+}
179+
172180 function uint32(value) {
173181 return typeof value === "number" && isFinite(value) && Math.floor(value) === value && value >= 0 && value <= UINT32_MAX;
174182 }
@@ -178,15 +186,26 @@ function optionalUint32(value) {
178186 }
179187
180188 function isFavorite(value) {
181- return isObject(value)
182- && hasOnlyKeys(value, FAVORITE_KEYS)
183- && value.schemaVersion === SCHEMA_VERSION
189+ var hasLineColor;
190+ var hasLineMode;
191+ var hasLineTextColor;
192+ if (!isObject(value) || !hasOnlyKeys(value, FAVORITE_KEYS)) return false;
193+ hasLineMode = Object.prototype.hasOwnProperty.call(value, "lineMode");
194+ hasLineColor = Object.prototype.hasOwnProperty.call(value, "lineColor");
195+ hasLineTextColor = Object.prototype.hasOwnProperty.call(value, "lineTextColor");
196+ if (hasLineMode !== hasLineColor || hasLineMode !== hasLineTextColor) return false;
197+ return value.schemaVersion === SCHEMA_VERSION
184198 && boundedString(value.id, LIMITS.idUtf8Bytes)
185199 && boundedString(value.serviceId, LIMITS.idUtf8Bytes)
186200 && (typeof value.displayName === "undefined" || boundedString(value.displayName, LIMITS.labelUtf8Bytes))
187201 && boundedString(value.stopLabel, LIMITS.labelUtf8Bytes)
188202 && boundedString(value.lineLabel, LIMITS.labelUtf8Bytes)
189203 && boundedString(value.destinationLabel, LIMITS.labelUtf8Bytes)
204+ && (!hasLineMode || (
205+ TRANSPORT_MODE.indexOf(value.lineMode) !== -1
206+ && isLineColor(value.lineColor)
207+ && isLineColor(value.lineTextColor)
208+ ))
190209 && typeof value.sortOrder === "number"
191210 && Math.floor(value.sortOrder) === value.sortOrder
192211 && value.sortOrder >= 0
@@ -262,9 +281,14 @@ function copyFavorite(favorite) {
262281 serviceId: favorite.serviceId,
263282 stopLabel: favorite.stopLabel,
264283 lineLabel: favorite.lineLabel,
265- destinationLabel: favorite.destinationLabel,
266- sortOrder: favorite.sortOrder
284+ destinationLabel: favorite.destinationLabel
267285 };
286+ if (Object.prototype.hasOwnProperty.call(favorite, "lineMode")) {
287+ copy.lineMode = favorite.lineMode;
288+ copy.lineColor = favorite.lineColor;
289+ copy.lineTextColor = favorite.lineTextColor;
290+ }
291+ copy.sortOrder = favorite.sortOrder;
268292 if (typeof favorite.displayName !== "undefined") copy.displayName = favorite.displayName;
269293 return copy;
270294 }
@@ -301,6 +325,7 @@ module.exports = {
301325 FRESHNESS: FRESHNESS,
302326 DEPARTURE_STATUS: DEPARTURE_STATUS,
303327 ERROR_CODE: ERROR_CODE,
328+ TRANSPORT_MODE: TRANSPORT_MODE,
304329 API_KEY_ACTION: API_KEY_ACTION,
305330 KEY_STATUS: KEY_STATUS,
306331 REQUEST_TRIGGER: REQUEST_TRIGGER,
@@ -313,6 +338,7 @@ module.exports = {
313338 isObject: isObject,
314339 hasOnlyKeys: hasOnlyKeys,
315340 boundedString: boundedString,
341+ isLineColor: isLineColor,
316342 isFavorite: isFavorite,
317343 isFavoriteList: isFavoriteList,
318344 isDeparture: isDeparture,
@@ -25,6 +25,7 @@ var ERROR_CODE = Object.freeze([
25 "RATE_LIMITED",25 "RATE_LIMITED",
26 "INVALID_RESPONSE"26 "INVALID_RESPONSE"
27 ]);27 ]);
28+var TRANSPORT_MODE = Object.freeze(["BUS", "METRO", "TRAM", "RER", "TRANSILIEN"]);
28 var API_KEY_ACTION = Object.freeze(["KEEP", "REPLACE", "REMOVE"]);29 var API_KEY_ACTION = Object.freeze(["KEEP", "REPLACE", "REMOVE"]);
29 var KEY_STATUS = Object.freeze({ MISSING: 0, CONFIGURED: 1, INVALID: 2 });30 var KEY_STATUS = Object.freeze({ MISSING: 0, CONFIGURED: 1, INVALID: 2 });
30 var REQUEST_TRIGGER = Object.freeze({ APP_OPEN: 0, FAVORITE_SELECTION: 1, MANUAL_SELECT: 2 });31 var REQUEST_TRIGGER = Object.freeze({ APP_OPEN: 0, FAVORITE_SELECTION: 1, MANUAL_SELECT: 2 });
@@ -103,6 +104,9 @@ var FAVORITE_KEYS = [
103 "stopLabel",104 "stopLabel",
104 "lineLabel",105 "lineLabel",
105 "destinationLabel",106 "destinationLabel",
107+ "lineMode",
108+ "lineColor",
109+ "lineTextColor",
106 "sortOrder"110 "sortOrder"
107 ];111 ];
108 var DEPARTURE_KEYS = ["expectedAt", "aimedAt", "minutes", "status", "nextIntervalMinutes"];112 var DEPARTURE_KEYS = ["expectedAt", "aimedAt", "minutes", "status", "nextIntervalMinutes"];
@@ -169,6 +173,10 @@ function boundedString(value, maximum) {
169 return size >= 1 && size <= maximum;173 return size >= 1 && size <= maximum;
170 }174 }
171 175
176+function isLineColor(value) {
177+ return typeof value === "string" && /^#[0-9a-f]{6}$/.test(value);
178+}
179+
172 function uint32(value) {180 function uint32(value) {
173 return typeof value === "number" && isFinite(value) && Math.floor(value) === value && value >= 0 && value <= UINT32_MAX;181 return typeof value === "number" && isFinite(value) && Math.floor(value) === value && value >= 0 && value <= UINT32_MAX;
174 }182 }
@@ -178,15 +186,26 @@ function optionalUint32(value) {
178 }186 }
179 187
180 function isFavorite(value) {188 function isFavorite(value) {
181- return isObject(value)189+ var hasLineColor;
182- && hasOnlyKeys(value, FAVORITE_KEYS)190+ var hasLineMode;
183- && value.schemaVersion === SCHEMA_VERSION191+ var hasLineTextColor;
192+ if (!isObject(value) || !hasOnlyKeys(value, FAVORITE_KEYS)) return false;
193+ hasLineMode = Object.prototype.hasOwnProperty.call(value, "lineMode");
194+ hasLineColor = Object.prototype.hasOwnProperty.call(value, "lineColor");
195+ hasLineTextColor = Object.prototype.hasOwnProperty.call(value, "lineTextColor");
196+ if (hasLineMode !== hasLineColor || hasLineMode !== hasLineTextColor) return false;
197+ return value.schemaVersion === SCHEMA_VERSION
184 && boundedString(value.id, LIMITS.idUtf8Bytes)198 && boundedString(value.id, LIMITS.idUtf8Bytes)
185 && boundedString(value.serviceId, LIMITS.idUtf8Bytes)199 && boundedString(value.serviceId, LIMITS.idUtf8Bytes)
186 && (typeof value.displayName === "undefined" || boundedString(value.displayName, LIMITS.labelUtf8Bytes))200 && (typeof value.displayName === "undefined" || boundedString(value.displayName, LIMITS.labelUtf8Bytes))
187 && boundedString(value.stopLabel, LIMITS.labelUtf8Bytes)201 && boundedString(value.stopLabel, LIMITS.labelUtf8Bytes)
188 && boundedString(value.lineLabel, LIMITS.labelUtf8Bytes)202 && boundedString(value.lineLabel, LIMITS.labelUtf8Bytes)
189 && boundedString(value.destinationLabel, LIMITS.labelUtf8Bytes)203 && boundedString(value.destinationLabel, LIMITS.labelUtf8Bytes)
204+ && (!hasLineMode || (
205+ TRANSPORT_MODE.indexOf(value.lineMode) !== -1
206+ && isLineColor(value.lineColor)
207+ && isLineColor(value.lineTextColor)
208+ ))
190 && typeof value.sortOrder === "number"209 && typeof value.sortOrder === "number"
191 && Math.floor(value.sortOrder) === value.sortOrder210 && Math.floor(value.sortOrder) === value.sortOrder
192 && value.sortOrder >= 0211 && value.sortOrder >= 0
@@ -262,9 +281,14 @@ function copyFavorite(favorite) {
262 serviceId: favorite.serviceId,281 serviceId: favorite.serviceId,
263 stopLabel: favorite.stopLabel,282 stopLabel: favorite.stopLabel,
264 lineLabel: favorite.lineLabel,283 lineLabel: favorite.lineLabel,
265- destinationLabel: favorite.destinationLabel,284+ destinationLabel: favorite.destinationLabel
266- sortOrder: favorite.sortOrder
267 };285 };
286+ if (Object.prototype.hasOwnProperty.call(favorite, "lineMode")) {
287+ copy.lineMode = favorite.lineMode;
288+ copy.lineColor = favorite.lineColor;
289+ copy.lineTextColor = favorite.lineTextColor;
290+ }
291+ copy.sortOrder = favorite.sortOrder;
268 if (typeof favorite.displayName !== "undefined") copy.displayName = favorite.displayName;292 if (typeof favorite.displayName !== "undefined") copy.displayName = favorite.displayName;
269 return copy;293 return copy;
270 }294 }
@@ -301,6 +325,7 @@ module.exports = {
301 FRESHNESS: FRESHNESS,325 FRESHNESS: FRESHNESS,
302 DEPARTURE_STATUS: DEPARTURE_STATUS,326 DEPARTURE_STATUS: DEPARTURE_STATUS,
303 ERROR_CODE: ERROR_CODE,327 ERROR_CODE: ERROR_CODE,
328+ TRANSPORT_MODE: TRANSPORT_MODE,
304 API_KEY_ACTION: API_KEY_ACTION,329 API_KEY_ACTION: API_KEY_ACTION,
305 KEY_STATUS: KEY_STATUS,330 KEY_STATUS: KEY_STATUS,
306 REQUEST_TRIGGER: REQUEST_TRIGGER,331 REQUEST_TRIGGER: REQUEST_TRIGGER,
@@ -313,6 +338,7 @@ module.exports = {
313 isObject: isObject,338 isObject: isObject,
314 hasOnlyKeys: hasOnlyKeys,339 hasOnlyKeys: hasOnlyKeys,
315 boundedString: boundedString,340 boundedString: boundedString,
341+ isLineColor: isLineColor,
316 isFavorite: isFavorite,342 isFavorite: isFavorite,
317 isFavoriteList: isFavoriteList,343 isFavoriteList: isFavoriteList,
318 isDeparture: isDeparture,344 isDeparture: isDeparture,
modified packages/companion/test/companion.test.js +28 -0
@@ -126,6 +126,34 @@ test("close fragment atomically stores one favorites-and-key record and exposes
126126 target.xhr.instances[0].respond(200, responseFor("secret-boundary"));
127127 assert.equal(JSON.stringify(target.Pebble.sent).includes(TEST_KEY), false);
128128 });
129+test("a pre-presentation v1 record keeps its API key and legacy favorite on load", function () {
130+ var storage = new fakes.FakeStorage();
131+ var legacyRecord = {
132+ schemaVersion: contracts.SCHEMA_VERSION,
133+ favorites: [{
134+ schemaVersion: contracts.SCHEMA_VERSION,
135+ id: "legacy-home",
136+ serviceId: "opaque:legacy-service",
137+ displayName: "Maison",
138+ stopLabel: "Châtelet",
139+ lineLabel: "1",
140+ destinationLabel: "La Défense",
141+ sortOrder: 0
142+ }],
143+ apiKey: TEST_KEY
144+ };
145+ var loaded;
146+
147+ storage.setItem(configuration.STORAGE_KEY, JSON.stringify(legacyRecord));
148+ loaded = configuration.loadConfiguration(storage);
149+
150+ assert.deepEqual(loaded, legacyRecord);
151+ assert.equal(loaded.apiKey, TEST_KEY);
152+ assert.equal(loaded.favorites.length, 1);
153+ ["lineMode", "lineColor", "lineTextColor"].forEach(function (field) {
154+ assert.equal(Object.prototype.hasOwnProperty.call(loaded.favorites[0], field), false);
155+ });
156+});
129157 test("configuration opening uses the active watch language and safely falls back to English", function () {
130158 var target = harness();
131159 var state;
@@ -126,6 +126,34 @@ test("close fragment atomically stores one favorites-and-key record and exposes
126 target.xhr.instances[0].respond(200, responseFor("secret-boundary"));126 target.xhr.instances[0].respond(200, responseFor("secret-boundary"));
127 assert.equal(JSON.stringify(target.Pebble.sent).includes(TEST_KEY), false);127 assert.equal(JSON.stringify(target.Pebble.sent).includes(TEST_KEY), false);
128 });128 });
129+test("a pre-presentation v1 record keeps its API key and legacy favorite on load", function () {
130+ var storage = new fakes.FakeStorage();
131+ var legacyRecord = {
132+ schemaVersion: contracts.SCHEMA_VERSION,
133+ favorites: [{
134+ schemaVersion: contracts.SCHEMA_VERSION,
135+ id: "legacy-home",
136+ serviceId: "opaque:legacy-service",
137+ displayName: "Maison",
138+ stopLabel: "Châtelet",
139+ lineLabel: "1",
140+ destinationLabel: "La Défense",
141+ sortOrder: 0
142+ }],
143+ apiKey: TEST_KEY
144+ };
145+ var loaded;
146+
147+ storage.setItem(configuration.STORAGE_KEY, JSON.stringify(legacyRecord));
148+ loaded = configuration.loadConfiguration(storage);
149+
150+ assert.deepEqual(loaded, legacyRecord);
151+ assert.equal(loaded.apiKey, TEST_KEY);
152+ assert.equal(loaded.favorites.length, 1);
153+ ["lineMode", "lineColor", "lineTextColor"].forEach(function (field) {
154+ assert.equal(Object.prototype.hasOwnProperty.call(loaded.favorites[0], field), false);
155+ });
156+});
129 test("configuration opening uses the active watch language and safely falls back to English", function () {157 test("configuration opening uses the active watch language and safely falls back to English", function () {
130 var target = harness();158 var target = harness();
131 var state;159 var state;
modified packages/companion/test/protocol.test.js +39 -0
@@ -23,6 +23,7 @@ test("PebbleKit constants exactly mirror the canonical alias and numeric contrac
2323 assert.deepEqual(contracts.MESSAGE_TYPE, shared.MESSAGE_TYPE);
2424 assert.deepEqual(contracts.KEY_STATUS, shared.KEY_STATUS);
2525 assert.deepEqual(contracts.REQUEST_TRIGGER, shared.REQUEST_TRIGGER);
26+ assert.deepEqual(contracts.TRANSPORT_MODE, shared.TRANSPORT_MODE);
2627 assert.deepEqual(contracts.WIRE_LANGUAGE, shared.WIRE_LANGUAGE);
2728 assert.deepEqual(contracts.LIMITS, shared.LIMITS);
2829 assert.equal(contracts.SCHEMA_VERSION, shared.SCHEMA_VERSION);
@@ -30,6 +31,44 @@ test("PebbleKit constants exactly mirror the canonical alias and numeric contrac
3031 assert.equal(contracts.FAVORITE_SETTLE_MS, shared.FAVORITE_SETTLE_MS);
3132 });
3233
34+test("presentation fields form an optional complete group and remain outside AppMessage", function () {
35+ var copied = contracts.copyFavorite(fixture.favorite);
36+ var legacy;
37+ var legacyCopy;
38+ var presentationFields = ["lineMode", "lineColor", "lineTextColor"];
39+ assert.deepEqual(copied, fixture.favorite);
40+ assert.equal(contracts.isFavorite(copied), true);
41+ assert.equal(contracts.isFavorite(Object.assign({}, copied, { lineColor: "#FFBE00" })), false);
42+ assert.equal(contracts.isFavorite(Object.assign({}, copied, { lineMode: "metro" })), false);
43+ delete copied.lineTextColor;
44+ assert.equal(contracts.isFavorite(copied), false);
45+
46+ legacy = contracts.copyFavorite(fixture.favorite);
47+ presentationFields.forEach(function (field) { delete legacy[field]; });
48+ assert.equal(contracts.isFavorite(legacy), true);
49+ assert.equal(contracts.isFavorite(Object.assign({}, legacy, { lineMode: "METRO" })), false);
50+ assert.equal(contracts.isFavorite(Object.assign({}, legacy, {
51+ lineMode: "METRO",
52+ lineColor: "#ffbe00"
53+ })), false);
54+ legacyCopy = contracts.copyFavorite(legacy);
55+ assert.deepEqual(legacyCopy, legacy);
56+ presentationFields.forEach(function (field) {
57+ assert.equal(Object.prototype.hasOwnProperty.call(legacyCopy, field), false);
58+ });
59+
60+ assert.equal(codec.encodeConfiguration(
61+ "presentation-wire",
62+ [fixture.favorite],
63+ contracts.KEY_STATUS.CONFIGURED,
64+ "en"
65+ ).some(function (message) {
66+ return Object.prototype.hasOwnProperty.call(message, "lineMode")
67+ || Object.prototype.hasOwnProperty.call(message, "lineColor")
68+ || Object.prototype.hasOwnProperty.call(message, "lineTextColor");
69+ }), false);
70+});
71+
3372 test("recorded fixture round trips through symbolic configuration and result payloads", async function () {
3473 var shared = await sharedContracts();
3574 var receiver = new shared.ProtocolReceiver();
@@ -23,6 +23,7 @@ test("PebbleKit constants exactly mirror the canonical alias and numeric contrac
23 assert.deepEqual(contracts.MESSAGE_TYPE, shared.MESSAGE_TYPE);23 assert.deepEqual(contracts.MESSAGE_TYPE, shared.MESSAGE_TYPE);
24 assert.deepEqual(contracts.KEY_STATUS, shared.KEY_STATUS);24 assert.deepEqual(contracts.KEY_STATUS, shared.KEY_STATUS);
25 assert.deepEqual(contracts.REQUEST_TRIGGER, shared.REQUEST_TRIGGER);25 assert.deepEqual(contracts.REQUEST_TRIGGER, shared.REQUEST_TRIGGER);
26+ assert.deepEqual(contracts.TRANSPORT_MODE, shared.TRANSPORT_MODE);
26 assert.deepEqual(contracts.WIRE_LANGUAGE, shared.WIRE_LANGUAGE);27 assert.deepEqual(contracts.WIRE_LANGUAGE, shared.WIRE_LANGUAGE);
27 assert.deepEqual(contracts.LIMITS, shared.LIMITS);28 assert.deepEqual(contracts.LIMITS, shared.LIMITS);
28 assert.equal(contracts.SCHEMA_VERSION, shared.SCHEMA_VERSION);29 assert.equal(contracts.SCHEMA_VERSION, shared.SCHEMA_VERSION);
@@ -30,6 +31,44 @@ test("PebbleKit constants exactly mirror the canonical alias and numeric contrac
30 assert.equal(contracts.FAVORITE_SETTLE_MS, shared.FAVORITE_SETTLE_MS);31 assert.equal(contracts.FAVORITE_SETTLE_MS, shared.FAVORITE_SETTLE_MS);
31 });32 });
32 33
34+test("presentation fields form an optional complete group and remain outside AppMessage", function () {
35+ var copied = contracts.copyFavorite(fixture.favorite);
36+ var legacy;
37+ var legacyCopy;
38+ var presentationFields = ["lineMode", "lineColor", "lineTextColor"];
39+ assert.deepEqual(copied, fixture.favorite);
40+ assert.equal(contracts.isFavorite(copied), true);
41+ assert.equal(contracts.isFavorite(Object.assign({}, copied, { lineColor: "#FFBE00" })), false);
42+ assert.equal(contracts.isFavorite(Object.assign({}, copied, { lineMode: "metro" })), false);
43+ delete copied.lineTextColor;
44+ assert.equal(contracts.isFavorite(copied), false);
45+
46+ legacy = contracts.copyFavorite(fixture.favorite);
47+ presentationFields.forEach(function (field) { delete legacy[field]; });
48+ assert.equal(contracts.isFavorite(legacy), true);
49+ assert.equal(contracts.isFavorite(Object.assign({}, legacy, { lineMode: "METRO" })), false);
50+ assert.equal(contracts.isFavorite(Object.assign({}, legacy, {
51+ lineMode: "METRO",
52+ lineColor: "#ffbe00"
53+ })), false);
54+ legacyCopy = contracts.copyFavorite(legacy);
55+ assert.deepEqual(legacyCopy, legacy);
56+ presentationFields.forEach(function (field) {
57+ assert.equal(Object.prototype.hasOwnProperty.call(legacyCopy, field), false);
58+ });
59+
60+ assert.equal(codec.encodeConfiguration(
61+ "presentation-wire",
62+ [fixture.favorite],
63+ contracts.KEY_STATUS.CONFIGURED,
64+ "en"
65+ ).some(function (message) {
66+ return Object.prototype.hasOwnProperty.call(message, "lineMode")
67+ || Object.prototype.hasOwnProperty.call(message, "lineColor")
68+ || Object.prototype.hasOwnProperty.call(message, "lineTextColor");
69+ }), false);
70+});
71+
33 test("recorded fixture round trips through symbolic configuration and result payloads", async function () {72 test("recorded fixture round trips through symbolic configuration and result payloads", async function () {
34 var shared = await sharedContracts();73 var shared = await sharedContracts();
35 var receiver = new shared.ProtocolReceiver();74 var receiver = new shared.ProtocolReceiver();
added packages/config-page/assets/line-badges/attribution.json +371 -0
Large diff collapsed to keep the page fast — load it when you need it.
added packages/config-page/assets/line-badges/metro-1.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-1.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-1.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-1.png differ
added packages/config-page/assets/line-badges/metro-10.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-10.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-10.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-10.png differ
added packages/config-page/assets/line-badges/metro-11.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-11.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-11.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-11.png differ
added packages/config-page/assets/line-badges/metro-12.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-12.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-12.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-12.png differ
added packages/config-page/assets/line-badges/metro-13.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-13.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-13.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-13.png differ
added packages/config-page/assets/line-badges/metro-14.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-14.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-14.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-14.png differ
added packages/config-page/assets/line-badges/metro-2.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-2.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-2.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-2.png differ
added packages/config-page/assets/line-badges/metro-3.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-3.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-3.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-3.png differ
added packages/config-page/assets/line-badges/metro-3b.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-3b.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-3b.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-3b.png differ
added packages/config-page/assets/line-badges/metro-4.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-4.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-4.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-4.png differ
added packages/config-page/assets/line-badges/metro-5.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-5.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-5.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-5.png differ
added packages/config-page/assets/line-badges/metro-6.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-6.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-6.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-6.png differ
added packages/config-page/assets/line-badges/metro-7.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-7.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-7.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-7.png differ
added packages/config-page/assets/line-badges/metro-7b.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-7b.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-7b.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-7b.png differ
added packages/config-page/assets/line-badges/metro-8.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-8.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-8.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-8.png differ
added packages/config-page/assets/line-badges/metro-9.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-9.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/metro-9.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/metro-9.png differ
added packages/config-page/assets/line-badges/rer-a.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-a.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-a.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/rer-a.png differ
added packages/config-page/assets/line-badges/rer-b.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-b.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-b.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/rer-b.png differ
added packages/config-page/assets/line-badges/rer-c.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-c.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-c.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/rer-c.png differ
added packages/config-page/assets/line-badges/rer-d.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-d.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-d.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/rer-d.png differ
added packages/config-page/assets/line-badges/rer-e.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-e.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/rer-e.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/rer-e.png differ
added packages/config-page/assets/line-badges/tram-t1.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t1.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t1.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t1.png differ
added packages/config-page/assets/line-badges/tram-t10.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t10.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t10.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t10.png differ
added packages/config-page/assets/line-badges/tram-t11.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t11.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t11.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t11.png differ
added packages/config-page/assets/line-badges/tram-t12.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t12.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t12.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t12.png differ
added packages/config-page/assets/line-badges/tram-t13.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t13.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t13.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t13.png differ
added packages/config-page/assets/line-badges/tram-t14.svg +24 -0
new file mode 100644
@@ -0,0 +1,24 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24.01">
3+ <defs>
4+ <style>
5+ .cls-1 {
6+ fill: #00a092;
7+ }
8+
9+ .cls-2 {
10+ fill: #fff;
11+ }
12+ </style>
13+ </defs>
14+ <path class="cls-2" d="M2.18,24.01c-1.2,0-2.18-.98-2.18-2.18V2.18C0,.98.98,0,2.18,0h19.64c1.2,0,2.18.98,2.18,2.18v19.64c0,1.2-.98,2.18-2.18,2.18H2.18Z"/>
15+ <g id="Layer_1-2" data-name="Layer 1">
16+ <g>
17+ <path class="cls-1" d="M21.83,4.34H2.19c-.6,0-1.09-.49-1.09-1.09v-1.09c0-.6.49-1.09,1.09-1.09h19.64c.6,0,1.09.49,1.09,1.09v1.09c0,.6-.49,1.09-1.09,1.09M22.91,21.81v-1.09c0-.6-.49-1.09-1.09-1.09H2.19c-.6,0-1.09.49-1.09,1.09v1.09c0,.6.49,1.09,1.09,1.09h19.64c.6,0,1.09-.49,1.09-1.09"/>
18+ <g>
19+ <polygon points="6.25 9.34 6.25 16.24 4.4 16.24 4.4 9.34 1.81 9.34 1.81 7.83 8.84 7.83 8.84 9.34 6.25 9.34"/>
20+ <path d="M13.03,16.24v-6.61c-.51.41-1.32.77-1.88,1.05l-.57-1.29c.88-.37,1.94-.93,2.77-1.56h1.46v8.41h-1.79ZM21.21,14.48v1.75h-1.67v-1.75h-3.79v-1.56l3.26-5.1h2.19v5.34h.99v1.31h-.99ZM19.54,9.31h-.02c-.04.14-.28.65-.61,1.18l-1.72,2.69h2.35v-3.87Z"/>
21+ </g>
22+ </g>
23+ </g>
24+</svg>
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,24 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24.01">
3+ <defs>
4+ <style>
5+ .cls-1 {
6+ fill: #00a092;
7+ }
8+
9+ .cls-2 {
10+ fill: #fff;
11+ }
12+ </style>
13+ </defs>
14+ <path class="cls-2" d="M2.18,24.01c-1.2,0-2.18-.98-2.18-2.18V2.18C0,.98.98,0,2.18,0h19.64c1.2,0,2.18.98,2.18,2.18v19.64c0,1.2-.98,2.18-2.18,2.18H2.18Z"/>
15+ <g id="Layer_1-2" data-name="Layer 1">
16+ <g>
17+ <path class="cls-1" d="M21.83,4.34H2.19c-.6,0-1.09-.49-1.09-1.09v-1.09c0-.6.49-1.09,1.09-1.09h19.64c.6,0,1.09.49,1.09,1.09v1.09c0,.6-.49,1.09-1.09,1.09M22.91,21.81v-1.09c0-.6-.49-1.09-1.09-1.09H2.19c-.6,0-1.09.49-1.09,1.09v1.09c0,.6.49,1.09,1.09,1.09h19.64c.6,0,1.09-.49,1.09-1.09"/>
18+ <g>
19+ <polygon points="6.25 9.34 6.25 16.24 4.4 16.24 4.4 9.34 1.81 9.34 1.81 7.83 8.84 7.83 8.84 9.34 6.25 9.34"/>
20+ <path d="M13.03,16.24v-6.61c-.51.41-1.32.77-1.88,1.05l-.57-1.29c.88-.37,1.94-.93,2.77-1.56h1.46v8.41h-1.79ZM21.21,14.48v1.75h-1.67v-1.75h-3.79v-1.56l3.26-5.1h2.19v5.34h.99v1.31h-.99ZM19.54,9.31h-.02c-.04.14-.28.65-.61,1.18l-1.72,2.69h2.35v-3.87Z"/>
21+ </g>
22+ </g>
23+ </g>
24+</svg>
\ No newline at end of file\ No newline at end of file
added packages/config-page/assets/line-badges/tram-t2.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t2.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t2.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t2.png differ
added packages/config-page/assets/line-badges/tram-t3a.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t3a.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t3a.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t3a.png differ
added packages/config-page/assets/line-badges/tram-t3b.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t3b.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t3b.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t3b.png differ
added packages/config-page/assets/line-badges/tram-t4.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t4.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t4.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t4.png differ
added packages/config-page/assets/line-badges/tram-t5.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t5.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t5.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t5.png differ
added packages/config-page/assets/line-badges/tram-t6.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t6.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t6.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t6.png differ
added packages/config-page/assets/line-badges/tram-t7.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t7.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t7.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t7.png differ
added packages/config-page/assets/line-badges/tram-t8.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t8.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t8.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t8.png differ
added packages/config-page/assets/line-badges/tram-t9.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t9.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/tram-t9.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/tram-t9.png differ
added packages/config-page/assets/line-badges/transilien-h.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-h.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-h.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/transilien-h.png differ
added packages/config-page/assets/line-badges/transilien-j.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-j.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-j.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/transilien-j.png differ
added packages/config-page/assets/line-badges/transilien-k.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-k.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-k.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/transilien-k.png differ
added packages/config-page/assets/line-badges/transilien-l.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-l.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-l.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/transilien-l.png differ
added packages/config-page/assets/line-badges/transilien-n.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-n.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-n.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/transilien-n.png differ
added packages/config-page/assets/line-badges/transilien-p.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-p.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-p.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/transilien-p.png differ
added packages/config-page/assets/line-badges/transilien-r.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-r.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-r.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/transilien-r.png differ
added packages/config-page/assets/line-badges/transilien-u.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-u.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-u.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/transilien-u.png differ
added packages/config-page/assets/line-badges/transilien-v.png +0 -0
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-v.png differ
new file mode 100644
Binary files /dev/null and b/packages/config-page/assets/line-badges/transilien-v.png differBinary files /dev/null and b/packages/config-page/assets/line-badges/transilien-v.png differ
modified packages/config-page/index.html +109 -12
Large diff collapsed to keep the page fast — load it when you need it.
added packages/config-page/src/catalog-client.js +98 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified packages/config-page/src/config-core.js +193 -27
Large diff collapsed to keep the page fast — load it when you need it.
modified packages/config-page/src/config-page.js +350 -128
Large diff collapsed to keep the page fast — load it when you need it.
added packages/config-page/src/line-badge-assets.js +188 -0
Large diff collapsed to keep the page fast — load it when you need it.
added packages/config-page/src/preview-fixture.js +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+// Public, credential-free projection of fixtures/departures/foundation.json.
2+// The test suite keeps this display-only preview synchronized with the recorded
3+// departure fixture without making live departure normalization a page dependency.
4+export const RECORDED_PREVIEW = Object.freeze([
5+ Object.freeze({ minutes: 2, status: "DELAYED" }),
6+ Object.freeze({ minutes: 6, status: "ON_TIME" }),
7+]);
new file mode 100644
@@ -0,0 +1,7 @@
1+// Public, credential-free projection of fixtures/departures/foundation.json.
2+// The test suite keeps this display-only preview synchronized with the recorded
3+// departure fixture without making live departure normalization a page dependency.
4+export const RECORDED_PREVIEW = Object.freeze([
5+ Object.freeze({ minutes: 2, status: "DELAYED" }),
6+ Object.freeze({ minutes: 6, status: "ON_TIME" }),
7+]);
modified packages/config-page/styles/config-page.css +189 -138
Large diff collapsed to keep the page fast — load it when you need it.
modified packages/config-page/test/config-page.test.ts +245 -6
Large diff collapsed to keep the page fast — load it when you need it.
modified packages/contracts/src/index.ts +35 -5
Large diff collapsed to keep the page fast — load it when you need it.
modified packages/contracts/test/contracts.test.ts +32 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified packages/watch/.lock-waf_linux_build +1 -1
@@ -1,6 +1,6 @@
11 argv = ['/home/anthodev/.local/share/pebble-sdk/SDKs/current/sdk-core/pebble/waf', 'configure']
22 config_cmd = 'configure'
3-environ = {'IMSETTINGS_INTEGRATE_DESKTOP': 'yes', 'npm_command': 'run', 'SESSION_MANAGER': 'local/unix:@/tmp/.ICE-unix/2942,unix/unix:/tmp/.ICE-unix/2942', 'npm_config_yes': 'true', 'GHOSTTY_BIN_DIR': '/usr/bin', 'npm_config_userconfig': '/home/anthodev/.npmrc', 'COLORTERM': 'truecolor', 'HERDR_PANE_ID': 'w6:p2', 'XDG_CONFIG_DIRS': '/home/anthodev/.config/kdedefaults:/etc/xdg:/usr/share/kde-settings/kde-profile/default/xdg', 'npm_config_cache': '/home/anthodev/.npm', 'PYTHONUNBUFFERED': '1', 'PIP_NO_INPUT': '1', 'LESS': 'FRX', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session1', 'HISTCONTROL': 'ignoredups', 'XDG_MENU_PREFIX': 'plasma-', 'TERM_PROGRAM_VERSION': '1.3.1-4.fc44', 'COMPOSER_NO_INTERACTION': '1', 'OMPCODE': '1', 'HISTSIZE': '1000', 'TF_IN_AUTOMATION': '1', 'ICEAUTHORITY': '/run/user/1000/iceauth_KXfteX', 'PNPM_UPDATE_NOTIFIER': 'false', 'NODE': '/usr/bin/node-24', 'GLAB_PAGER': 'cat', 'SSH_AUTH_SOCK': '/home/anthodev/.1password/agent.sock', 'CLOUDSDK_CORE_DISABLE_PROMPTS': '1', 'DOCKER_USER': '1000:1000', '__ABBR_TIPS_VALUES': 'git rebase -i HEAD~2 git rebase --continue git rebase develop gitmoji -c helium-browser --disable-features=WaylandWpColorManagerV1 just eza -al --icons', 'AGENT': '1', 'MEMORY_PRESSURE_WRITE': 'c29tZSAyMDAwMDAgMjAwMDAwMAA=', 'COLOR': '0', 'npm_config_local_prefix': '/home/anthodev/dev/perso/js/lapin-fute', 'XMODIFIERS': '@im=none', 'HOMEBREW_PREFIX': '/home/linuxbrew/.linuxbrew', 'DESKTOP_SESSION': '/usr/share/wayland-sessions/plasma.desktop', 'GTK_RC_FILES': '/etc/gtk/gtkrc:/home/anthodev/.gtkrc:/home/anthodev/.config/gtkrc', 'GDK_CORE_DEVICE_EVENTS': '1', 'npm_config_globalconfig': '/etc/npmrc', 'GPG_TTY': 'not a tty', 'EDITOR': 'true', 'GH_PAGER': 'cat', 'XDG_SEAT': 'seat0', 'GH_PROMPT_DISABLED': '1', 'PWD': '/home/anthodev/dev/perso/js/lapin-fute', 'XDG_SESSION_DESKTOP': 'KDE', 'LOGNAME': 'anthodev', 'XDG_SESSION_TYPE': 'wayland', 'PNPM_HOME': '/home/anthodev/.local/share/pnpm', 'npm_config_init_module': '/home/anthodev/.npm-init.js', 'SYSTEMD_EXEC_PID': '6424', '_': '/usr/bin/node', 'XAUTHORITY': '/run/user/1000/xauth_XpcTHG', 'KWIN_DISABLE_TONEMAPPING': '1', 'SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS': '0', 'POSH_SHELL': 'fish', 'ABBR_TIPS_PROMPT': '\\n\U0001f4a1 \\e[1m{{ .abbr }}\\e[0m => {{ .cmd }}', 'CLAUDECODE': '1', 'XKB_DEFAULT_MODEL': 'pc105', 'GTK2_RC_FILES': '/etc/gtk-2.0/gtkrc:/home/anthodev/.gtkrc-2.0:/home/anthodev/.config/gtkrc-2.0', '__ABBR_TIPS_KEYS': 'a__gr2 a__grc a__grd a__gtc a__helium-browser a__j a__xl', 'GHOSTTY_SHELL_FEATURES': 'path,sudo,title', 'HOME': '/home/anthodev', '__PI_NATIVE_VARIANT_CACHE': 'modern', 'DELTA_PAGER': 'cat', 'SSH_ASKPASS': '/usr/bin/false', 'LANG': 'fr_FR.UTF-8', '_JAVA_AWT_WM_NONREPARENTING': '1', 'LS_COLORS': '', 'XDG_CURRENT_DESKTOP': 'KDE', 'POSH_SHELL_VERSION': '4.8.1', 'npm_package_version': '0.1.0', 'POSH_SESSION_ID': 'aed4658f-12ea-4aca-a036-894bd3ef6bd0', 'MEMORY_PRESSURE_WATCH': '/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-com.mitchellh.ghostty.service/memory.pressure', 'WAYLAND_DISPLAY': 'wayland-0', 'AWS_PAGER': '', 'YARN_ENABLE_PROGRESS_BARS': '0', '_fish_env_initialized': '1', 'CONDA_PROMPT_MODIFIER': 'false', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'MANAGERPID': '2300', 'IMSETTINGS_MODULE': 'none', 'INIT_CWD': '/home/anthodev/dev/perso/js/lapin-fute', 'POSH_CONFIG': '/home/anthodev/.config/.zsh/anthodev.omp.json', 'STEAM_FRAME_FORCE_CLOSE': '1', 'KDE_SESSION_UID': '1000', 'PSQL_PAGER': 'cat', 'INFOPATH': '/home/linuxbrew/.linuxbrew/share/info:', 'npm_lifecycle_script': 'node scripts/build.mjs', 'NVM_DIR': '/home/anthodev/.nvm', 'MOZ_GMP_PATH': '/usr/lib64/mozilla/plugins/gmp-gmpopenh264/system-installed', 'XKB_DEFAULT_LAYOUT': 'us', 'npm_config_npm_version': '11.16.0', 'GHOSTTY_RESOURCES_DIR': '/usr/share/ghostty', 'XDG_SESSION_CLASS': 'user', 'TERMINFO': '/usr/share/terminfo', 'TERM': 'dumb', 'npm_package_name': 'lapin-fute', 'BAT_PAGER': 'cat', 'npm_config_prefix': '/home/anthodev/.npm-global', 'LESSOPEN': '||/usr/bin/lesspipe.sh %s', 'USER': 'anthodev', 'YARN_ENABLE_TELEMETRY': '0', 'GIT_PAGER': 'cat', 'PIP_DISABLE_PIP_VERSION_CHECK': '1', 'HERDR_BIN_PATH': '/home/linuxbrew/.linuxbrew/Cellar/herdr/0.8.2/bin/herdr', 'GID': '1000', 'HOMEBREW_CELLAR': '/home/linuxbrew/.linuxbrew/Cellar', 'QT_WAYLAND_RECONNECT': '1', 'KDE_SESSION_VERSION': '6', 'PAM_KWALLET5_LOGIN': '/run/user/1000/kwallet5.socket', 'MANPAGER': 'cat', 'VISUAL': 'true', 'DISPLAY': ':0', 'npm_lifecycle_event': 'build', 'SHLVL': '1', 'NVM_CD_FLAGS': '', 'GIT_EDITOR': 'true', 'PAGER': 'cat', 'HOMEBREW_REPOSITORY': '/home/linuxbrew/.linuxbrew/Homebrew', 'npm_config_fund': 'false', 'SYSTEMD_PAGER': 'cat', 'XDG_VTNR': '2', 'XDG_SESSION_ID': '2', 'ATUIN_SESSION': '01a0523b089c70c29608f76da5ef3ae9', 'MANAGERPIDFDID': '8555', 'npm_config_user_agent': 'npm/11.16.0 node/v24.18.0 linux x64 workspaces/false', 'NO_COLOR': '1', 'TF_INPUT': '0', 'npm_execpath': '/usr/lib/node_modules_24/npm/bin/npm-cli.js', 'npm_config_progress': 'false', 'XDG_RUNTIME_DIR': '/run/user/1000', 'MYSQL_PAGER': 'cat', 'HOMEBREW_PAGER': 'cat', 'PNPM_DISABLE_SELF_UPDATE_CHECK': 'true', 'DEBUGINFOD_URLS': 'ima:enforcing https://debuginfod.fedoraproject.org/ ima:ignore ', 'npm_package_json': '/home/anthodev/dev/perso/js/lapin-fute/package.json', 'DEBUGINFOD_IMA_CERT_PATH': '/etc/keys/ima:', 'LC_ALL': 'C.UTF-8', 'KDEDIRS': '/usr', 'XKB_DEFAULT_VARIANT': 'intl', 'CARGO_TERM_PROGRESS_WHEN': 'never', 'HERDR_ENV': '1', 'XDG_DATA_DIRS': '/home/anthodev/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share', 'KDE_FULL_SESSION': 'true', 'npm_config_allow_scripts': '', 'npm_config_noproxy': '', 'PATH': '/var/tmp/pebble-sdk/SDKs/4.33.1/toolchain/moddable-tools:/home/anthodev/.local/share/pebble-sdk/SDKs/4.33.1/toolchain/arm-none-eabi/bin:/home/anthodev/.local/share/uv/tools/pebble-tool/lib64/python3.14/site-packages/pebble_tool/commands/sdk:/home/anthodev/.local/share/pebble-sdk/SDKs/4.33.1/toolchain/bin:/home/anthodev/dev/perso/js/lapin-fute/node_modules/.bin:/home/anthodev/dev/perso/js/node_modules/.bin:/home/anthodev/dev/perso/node_modules/.bin:/home/anthodev/dev/node_modules/.bin:/home/anthodev/node_modules/.bin:/home/node_modules/.bin:/node_modules/.bin:/usr/lib/node_modules_24/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/anthodev/.local/bin:/home/anthodev/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/anthodev/.atuin/bin:/home/anthodev/.local/bin:/home/anthodev/.bun/bin:/home/anthodev/go/bin:/usr/local/bin:/usr/bin:/sbin:/home/anthodev/.npm-global/bin:/home/anthodev/.local/share/pnpm/bin:/home/anthodev/.cargo/bin:/usr/lib64/ccache:/bin', 'HERDR_TAB_ID': 'w6:t2', 'npm_config_node_gyp': '/usr/lib/node_modules_24/npm/node_modules/node-gyp/bin/node-gyp.js', 'CI': 'true', 'DBUS_SESSION_BUS_ADDRESS': 'unix:path=/run/user/1000/bus', 'npm_config_global_prefix': '/home/anthodev/.npm-global', 'npm_config_update_notifier': 'false', 'KDE_APPLICATIONS_AS_SCOPE': '1', 'HERDR_SOCKET_PATH': '/home/anthodev/.config/herdr/herdr.sock', 'HERDR_WORKSPACE_ID': 'w6', 'MAIL': '/var/spool/mail/anthodev', 'POWERLINE_COMMAND': 'oh-my-posh', 'npm_config_audit': 'false', 'ABBR_TIPS_REGEXES': '(^(\\w+\\s+)+(-{1,2})\\w+)(\\s\\S+) (^(\\s?(\\w-?)+){3}).* (^(\\s?(\\w-?)+){2}).* (^(\\s?(\\w-?)+){1}).*', 'DEBIAN_FRONTEND': 'noninteractive', 'GIT_TERMINAL_PROMPT': '0', 'npm_node_execpath': '/usr/bin/node-24', 'npm_package_engines_node': '24.18.0', 'TERM_PROGRAM': 'ghostty', 'MODDABLE': '/var/tmp/pebble-sdk/SDKs/4.33.1/toolchain/moddable', 'PYTHONHOME': '/home/anthodev/.local/share/pebble-sdk/SDKs/current/.venv', 'PYTHONPATH': '/home/anthodev/.local/share/uv/tools/pebble-tool/bin:/usr/lib64/python314.zip:/usr/lib64/python3.14:/usr/lib64/python3.14/lib-dynload:/home/anthodev/.local/share/uv/tools/pebble-tool/lib64/python3.14/site-packages:/home/anthodev/.local/share/uv/tools/pebble-tool/lib/python3.14/site-packages', 'NODE_PATH': '/home/anthodev/.local/share/pebble-sdk/SDKs/current/sdk-core/../node_modules', 'NOCLIMB': '1'}
3+environ = {'IMSETTINGS_INTEGRATE_DESKTOP': 'yes', 'npm_command': 'run', 'SESSION_MANAGER': 'local/unix:@/tmp/.ICE-unix/2937,unix/unix:/tmp/.ICE-unix/2937', 'npm_config_yes': 'true', 'GHOSTTY_BIN_DIR': '/usr/bin', 'npm_config_userconfig': '/home/anthodev/.npmrc', 'COLORTERM': 'truecolor', 'HERDR_PANE_ID': 'w6:p2', 'XDG_CONFIG_DIRS': '/home/anthodev/.config/kdedefaults:/etc/xdg:/usr/share/kde-settings/kde-profile/default/xdg', 'npm_config_cache': '/home/anthodev/.npm', 'PYTHONUNBUFFERED': '1', 'PIP_NO_INPUT': '1', 'LESS': 'FRX', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session1', 'HISTCONTROL': 'ignoredups', 'XDG_MENU_PREFIX': 'plasma-', 'TERM_PROGRAM_VERSION': '1.3.1-4.fc44', 'COMPOSER_NO_INTERACTION': '1', 'OMPCODE': '1', 'HISTSIZE': '1000', 'TF_IN_AUTOMATION': '1', 'ICEAUTHORITY': '/run/user/1000/iceauth_aLtUGr', 'PNPM_UPDATE_NOTIFIER': 'false', 'NODE': '/usr/bin/node-24', 'GLAB_PAGER': 'cat', 'SSH_AUTH_SOCK': '/home/anthodev/.1password/agent.sock', 'CLOUDSDK_CORE_DISABLE_PROMPTS': '1', 'DOCKER_USER': '1000:1000', '__ABBR_TIPS_VALUES': 'git rebase -i HEAD~2 git rebase --continue git rebase develop gitmoji -c helium-browser --disable-features=WaylandWpColorManagerV1 just eza -al --icons', 'AGENT': '1', 'MEMORY_PRESSURE_WRITE': 'c29tZSAyMDAwMDAgMjAwMDAwMAA=', 'COLOR': '0', 'npm_config_local_prefix': '/home/anthodev/dev/perso/js/lapin-fute', 'XMODIFIERS': '@im=none', 'HOMEBREW_PREFIX': '/home/linuxbrew/.linuxbrew', 'DESKTOP_SESSION': '/usr/share/wayland-sessions/plasma.desktop', 'GTK_RC_FILES': '/etc/gtk/gtkrc:/home/anthodev/.gtkrc:/home/anthodev/.config/gtkrc', 'GDK_CORE_DEVICE_EVENTS': '1', 'npm_config_globalconfig': '/etc/npmrc', 'GPG_TTY': 'not a tty', 'EDITOR': 'true', 'GH_PAGER': 'cat', 'XDG_SEAT': 'seat0', 'GH_PROMPT_DISABLED': '1', 'PWD': '/home/anthodev/dev/perso/js/lapin-fute', 'XDG_SESSION_DESKTOP': 'KDE', 'LOGNAME': 'anthodev', 'XDG_SESSION_TYPE': 'wayland', 'PNPM_HOME': '/home/anthodev/.local/share/pnpm', 'npm_config_init_module': '/home/anthodev/.npm-init.js', 'SYSTEMD_EXEC_PID': '14866', '_': '/usr/bin/node', 'XAUTHORITY': '/run/user/1000/xauth_qgtLXn', 'KWIN_DISABLE_TONEMAPPING': '1', 'SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS': '0', 'POSH_SHELL': 'fish', 'ABBR_TIPS_PROMPT': '\\n\U0001f4a1 \\e[1m{{ .abbr }}\\e[0m => {{ .cmd }}', 'CLAUDECODE': '1', 'XKB_DEFAULT_MODEL': 'pc105', 'GTK2_RC_FILES': '/etc/gtk-2.0/gtkrc:/home/anthodev/.gtkrc-2.0:/home/anthodev/.config/gtkrc-2.0', '__ABBR_TIPS_KEYS': 'a__gr2 a__grc a__grd a__gtc a__helium-browser a__j a__xl', 'GHOSTTY_SHELL_FEATURES': 'path,sudo,title', 'HOME': '/home/anthodev', '__PI_NATIVE_VARIANT_CACHE': 'modern', 'DELTA_PAGER': 'cat', 'SSH_ASKPASS': '/usr/bin/false', 'LANG': 'fr_FR.UTF-8', '_JAVA_AWT_WM_NONREPARENTING': '1', 'LS_COLORS': '', 'XDG_CURRENT_DESKTOP': 'KDE', 'POSH_SHELL_VERSION': '4.8.1', 'npm_package_version': '0.1.0', 'POSH_SESSION_ID': 'aed4658f-12ea-4aca-a036-894bd3ef6bd0', 'MEMORY_PRESSURE_WATCH': '/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-com.mitchellh.ghostty.service/memory.pressure', 'WAYLAND_DISPLAY': 'wayland-0', 'AWS_PAGER': '', 'YARN_ENABLE_PROGRESS_BARS': '0', '_fish_env_initialized': '1', 'CONDA_PROMPT_MODIFIER': 'false', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'MANAGERPID': '2302', 'IMSETTINGS_MODULE': 'none', 'INIT_CWD': '/home/anthodev/dev/perso/js/lapin-fute', 'POSH_CONFIG': '/home/anthodev/.config/.zsh/anthodev.omp.json', 'STEAM_FRAME_FORCE_CLOSE': '1', 'KDE_SESSION_UID': '1000', 'PSQL_PAGER': 'cat', 'INFOPATH': '/home/linuxbrew/.linuxbrew/share/info:', 'npm_lifecycle_script': 'node scripts/build.mjs', 'NVM_DIR': '/home/anthodev/.nvm', 'MOZ_GMP_PATH': '/usr/lib64/mozilla/plugins/gmp-gmpopenh264/system-installed', 'XKB_DEFAULT_LAYOUT': 'us', 'npm_config_npm_version': '11.16.0', 'HERDR_STARTUP_CWD': '/home/anthodev', 'GHOSTTY_RESOURCES_DIR': '/usr/share/ghostty', 'XDG_SESSION_CLASS': 'user', 'TERMINFO': '/usr/share/terminfo', 'TERM': 'dumb', 'npm_package_name': 'lapin-fute', 'BAT_PAGER': 'cat', 'npm_config_prefix': '/home/anthodev/.npm-global', 'LESSOPEN': '||/usr/bin/lesspipe.sh %s', 'USER': 'anthodev', 'YARN_ENABLE_TELEMETRY': '0', 'GIT_PAGER': 'cat', 'PIP_DISABLE_PIP_VERSION_CHECK': '1', 'HERDR_BIN_PATH': '/home/linuxbrew/.linuxbrew/Cellar/herdr/0.8.2/bin/herdr', 'GID': '1000', 'HOMEBREW_CELLAR': '/home/linuxbrew/.linuxbrew/Cellar', 'QT_WAYLAND_RECONNECT': '1', 'KDE_SESSION_VERSION': '6', 'PAM_KWALLET5_LOGIN': '/run/user/1000/kwallet5.socket', 'MANPAGER': 'cat', 'VISUAL': 'true', 'DISPLAY': ':0', 'npm_lifecycle_event': 'build', 'SHLVL': '1', 'NVM_CD_FLAGS': '', 'GIT_EDITOR': 'true', 'PAGER': 'cat', 'HOMEBREW_REPOSITORY': '/home/linuxbrew/.linuxbrew/Homebrew', 'npm_config_fund': 'false', 'SYSTEMD_PAGER': 'cat', 'XDG_VTNR': '2', 'XDG_SESSION_ID': '2', 'ATUIN_SESSION': '01a05e273bfb7d2196e6386c6401cc46', 'MANAGERPIDFDID': '393', 'npm_config_user_agent': 'npm/11.16.0 node/v24.18.0 linux x64 workspaces/false', 'NO_COLOR': '1', 'TF_INPUT': '0', 'npm_execpath': '/usr/lib/node_modules_24/npm/bin/npm-cli.js', 'npm_config_progress': 'false', 'XDG_RUNTIME_DIR': '/run/user/1000', 'MYSQL_PAGER': 'cat', 'HOMEBREW_PAGER': 'cat', 'PNPM_DISABLE_SELF_UPDATE_CHECK': 'true', 'DEBUGINFOD_URLS': 'ima:enforcing https://debuginfod.fedoraproject.org/ ima:ignore ', 'npm_package_json': '/home/anthodev/dev/perso/js/lapin-fute/package.json', 'DEBUGINFOD_IMA_CERT_PATH': '/etc/keys/ima:', 'LC_ALL': 'C.UTF-8', 'KDEDIRS': '/usr', 'XKB_DEFAULT_VARIANT': 'intl', 'CARGO_TERM_PROGRESS_WHEN': 'never', 'HERDR_ENV': '1', 'XDG_DATA_DIRS': '/home/anthodev/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share', 'KDE_FULL_SESSION': 'true', 'npm_config_allow_scripts': '', 'npm_config_noproxy': '', 'PATH': '/var/tmp/pebble-sdk/SDKs/4.33.1/toolchain/moddable-tools:/home/anthodev/.local/share/pebble-sdk/SDKs/4.33.1/toolchain/arm-none-eabi/bin:/home/anthodev/.local/share/uv/tools/pebble-tool/lib64/python3.14/site-packages/pebble_tool/commands/sdk:/home/anthodev/.local/share/pebble-sdk/SDKs/4.33.1/toolchain/bin:/home/anthodev/dev/perso/js/lapin-fute/node_modules/.bin:/home/anthodev/dev/perso/js/node_modules/.bin:/home/anthodev/dev/perso/node_modules/.bin:/home/anthodev/dev/node_modules/.bin:/home/anthodev/node_modules/.bin:/home/node_modules/.bin:/node_modules/.bin:/usr/lib/node_modules_24/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/anthodev/.local/bin:/home/anthodev/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/anthodev/.atuin/bin:/home/anthodev/.local/bin:/home/anthodev/.bun/bin:/home/anthodev/go/bin:/usr/local/bin:/usr/bin:/sbin:/home/anthodev/.npm-global/bin:/home/anthodev/.local/share/pnpm/bin:/home/anthodev/.cargo/bin:/usr/lib64/ccache:/bin', 'HERDR_TAB_ID': 'w6:t2', 'npm_config_node_gyp': '/usr/lib/node_modules_24/npm/node_modules/node-gyp/bin/node-gyp.js', 'CI': 'true', 'DBUS_SESSION_BUS_ADDRESS': 'unix:path=/run/user/1000/bus', 'npm_config_global_prefix': '/home/anthodev/.npm-global', 'npm_config_update_notifier': 'false', 'KDE_APPLICATIONS_AS_SCOPE': '1', 'HERDR_SOCKET_PATH': '/home/anthodev/.config/herdr/herdr.sock', 'HERDR_WORKSPACE_ID': 'w6', 'MAIL': '/var/spool/mail/anthodev', 'POWERLINE_COMMAND': 'oh-my-posh', 'npm_config_audit': 'false', 'ABBR_TIPS_REGEXES': '(^(\\w+\\s+)+(-{1,2})\\w+)(\\s\\S+) (^(\\s?(\\w-?)+){3}).* (^(\\s?(\\w-?)+){2}).* (^(\\s?(\\w-?)+){1}).*', 'DEBIAN_FRONTEND': 'noninteractive', 'GIT_TERMINAL_PROMPT': '0', 'npm_node_execpath': '/usr/bin/node-24', 'npm_package_engines_node': '24.18.0', 'TERM_PROGRAM': 'ghostty', 'MODDABLE': '/var/tmp/pebble-sdk/SDKs/4.33.1/toolchain/moddable', 'PYTHONHOME': '/home/anthodev/.local/share/pebble-sdk/SDKs/current/.venv', 'PYTHONPATH': '/home/anthodev/.local/share/uv/tools/pebble-tool/bin:/usr/lib64/python314.zip:/usr/lib64/python3.14:/usr/lib64/python3.14/lib-dynload:/home/anthodev/.local/share/uv/tools/pebble-tool/lib64/python3.14/site-packages:/home/anthodev/.local/share/uv/tools/pebble-tool/lib/python3.14/site-packages', 'NODE_PATH': '/home/anthodev/.local/share/pebble-sdk/SDKs/current/sdk-core/../node_modules', 'NOCLIMB': '1'}
44 files = ['/home/anthodev/dev/perso/js/lapin-fute/packages/watch/wscript']
55 hash = b'\xc6\xb0\xec\xd9\xadu[\xb0m\xd2\xe0\x84N\xf01%'
66 launch_dir = '/home/anthodev/dev/perso/js/lapin-fute/packages/watch'
@@ -1,6 +1,6 @@
1 argv = ['/home/anthodev/.local/share/pebble-sdk/SDKs/current/sdk-core/pebble/waf', 'configure']1 argv = ['/home/anthodev/.local/share/pebble-sdk/SDKs/current/sdk-core/pebble/waf', 'configure']
2 config_cmd = 'configure'2 config_cmd = 'configure'
3-environ = {'IMSETTINGS_INTEGRATE_DESKTOP': 'yes', 'npm_command': 'run', 'SESSION_MANAGER': 'local/unix:@/tmp/.ICE-unix/2942,unix/unix:/tmp/.ICE-unix/2942', 'npm_config_yes': 'true', 'GHOSTTY_BIN_DIR': '/usr/bin', 'npm_config_userconfig': '/home/anthodev/.npmrc', 'COLORTERM': 'truecolor', 'HERDR_PANE_ID': 'w6:p2', 'XDG_CONFIG_DIRS': '/home/anthodev/.config/kdedefaults:/etc/xdg:/usr/share/kde-settings/kde-profile/default/xdg', 'npm_config_cache': '/home/anthodev/.npm', 'PYTHONUNBUFFERED': '1', 'PIP_NO_INPUT': '1', 'LESS': 'FRX', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session1', 'HISTCONTROL': 'ignoredups', 'XDG_MENU_PREFIX': 'plasma-', 'TERM_PROGRAM_VERSION': '1.3.1-4.fc44', 'COMPOSER_NO_INTERACTION': '1', 'OMPCODE': '1', 'HISTSIZE': '1000', 'TF_IN_AUTOMATION': '1', 'ICEAUTHORITY': '/run/user/1000/iceauth_KXfteX', 'PNPM_UPDATE_NOTIFIER': 'false', 'NODE': '/usr/bin/node-24', 'GLAB_PAGER': 'cat', 'SSH_AUTH_SOCK': '/home/anthodev/.1password/agent.sock', 'CLOUDSDK_CORE_DISABLE_PROMPTS': '1', 'DOCKER_USER': '1000:1000', '__ABBR_TIPS_VALUES': 'git rebase -i HEAD~2 git rebase --continue git rebase develop gitmoji -c helium-browser --disable-features=WaylandWpColorManagerV1 just eza -al --icons', 'AGENT': '1', 'MEMORY_PRESSURE_WRITE': 'c29tZSAyMDAwMDAgMjAwMDAwMAA=', 'COLOR': '0', 'npm_config_local_prefix': '/home/anthodev/dev/perso/js/lapin-fute', 'XMODIFIERS': '@im=none', 'HOMEBREW_PREFIX': '/home/linuxbrew/.linuxbrew', 'DESKTOP_SESSION': '/usr/share/wayland-sessions/plasma.desktop', 'GTK_RC_FILES': '/etc/gtk/gtkrc:/home/anthodev/.gtkrc:/home/anthodev/.config/gtkrc', 'GDK_CORE_DEVICE_EVENTS': '1', 'npm_config_globalconfig': '/etc/npmrc', 'GPG_TTY': 'not a tty', 'EDITOR': 'true', 'GH_PAGER': 'cat', 'XDG_SEAT': 'seat0', 'GH_PROMPT_DISABLED': '1', 'PWD': '/home/anthodev/dev/perso/js/lapin-fute', 'XDG_SESSION_DESKTOP': 'KDE', 'LOGNAME': 'anthodev', 'XDG_SESSION_TYPE': 'wayland', 'PNPM_HOME': '/home/anthodev/.local/share/pnpm', 'npm_config_init_module': '/home/anthodev/.npm-init.js', 'SYSTEMD_EXEC_PID': '6424', '_': '/usr/bin/node', 'XAUTHORITY': '/run/user/1000/xauth_XpcTHG', 'KWIN_DISABLE_TONEMAPPING': '1', 'SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS': '0', 'POSH_SHELL': 'fish', 'ABBR_TIPS_PROMPT': '\\n\U0001f4a1 \\e[1m{{ .abbr }}\\e[0m => {{ .cmd }}', 'CLAUDECODE': '1', 'XKB_DEFAULT_MODEL': 'pc105', 'GTK2_RC_FILES': '/etc/gtk-2.0/gtkrc:/home/anthodev/.gtkrc-2.0:/home/anthodev/.config/gtkrc-2.0', '__ABBR_TIPS_KEYS': 'a__gr2 a__grc a__grd a__gtc a__helium-browser a__j a__xl', 'GHOSTTY_SHELL_FEATURES': 'path,sudo,title', 'HOME': '/home/anthodev', '__PI_NATIVE_VARIANT_CACHE': 'modern', 'DELTA_PAGER': 'cat', 'SSH_ASKPASS': '/usr/bin/false', 'LANG': 'fr_FR.UTF-8', '_JAVA_AWT_WM_NONREPARENTING': '1', 'LS_COLORS': '', 'XDG_CURRENT_DESKTOP': 'KDE', 'POSH_SHELL_VERSION': '4.8.1', 'npm_package_version': '0.1.0', 'POSH_SESSION_ID': 'aed4658f-12ea-4aca-a036-894bd3ef6bd0', 'MEMORY_PRESSURE_WATCH': '/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-com.mitchellh.ghostty.service/memory.pressure', 'WAYLAND_DISPLAY': 'wayland-0', 'AWS_PAGER': '', 'YARN_ENABLE_PROGRESS_BARS': '0', '_fish_env_initialized': '1', 'CONDA_PROMPT_MODIFIER': 'false', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'MANAGERPID': '2300', 'IMSETTINGS_MODULE': 'none', 'INIT_CWD': '/home/anthodev/dev/perso/js/lapin-fute', 'POSH_CONFIG': '/home/anthodev/.config/.zsh/anthodev.omp.json', 'STEAM_FRAME_FORCE_CLOSE': '1', 'KDE_SESSION_UID': '1000', 'PSQL_PAGER': 'cat', 'INFOPATH': '/home/linuxbrew/.linuxbrew/share/info:', 'npm_lifecycle_script': 'node scripts/build.mjs', 'NVM_DIR': '/home/anthodev/.nvm', 'MOZ_GMP_PATH': '/usr/lib64/mozilla/plugins/gmp-gmpopenh264/system-installed', 'XKB_DEFAULT_LAYOUT': 'us', 'npm_config_npm_version': '11.16.0', 'GHOSTTY_RESOURCES_DIR': '/usr/share/ghostty', 'XDG_SESSION_CLASS': 'user', 'TERMINFO': '/usr/share/terminfo', 'TERM': 'dumb', 'npm_package_name': 'lapin-fute', 'BAT_PAGER': 'cat', 'npm_config_prefix': '/home/anthodev/.npm-global', 'LESSOPEN': '||/usr/bin/lesspipe.sh %s', 'USER': 'anthodev', 'YARN_ENABLE_TELEMETRY': '0', 'GIT_PAGER': 'cat', 'PIP_DISABLE_PIP_VERSION_CHECK': '1', 'HERDR_BIN_PATH': '/home/linuxbrew/.linuxbrew/Cellar/herdr/0.8.2/bin/herdr', 'GID': '1000', 'HOMEBREW_CELLAR': '/home/linuxbrew/.linuxbrew/Cellar', 'QT_WAYLAND_RECONNECT': '1', 'KDE_SESSION_VERSION': '6', 'PAM_KWALLET5_LOGIN': '/run/user/1000/kwallet5.socket', 'MANPAGER': 'cat', 'VISUAL': 'true', 'DISPLAY': ':0', 'npm_lifecycle_event': 'build', 'SHLVL': '1', 'NVM_CD_FLAGS': '', 'GIT_EDITOR': 'true', 'PAGER': 'cat', 'HOMEBREW_REPOSITORY': '/home/linuxbrew/.linuxbrew/Homebrew', 'npm_config_fund': 'false', 'SYSTEMD_PAGER': 'cat', 'XDG_VTNR': '2', 'XDG_SESSION_ID': '2', 'ATUIN_SESSION': '01a0523b089c70c29608f76da5ef3ae9', 'MANAGERPIDFDID': '8555', 'npm_config_user_agent': 'npm/11.16.0 node/v24.18.0 linux x64 workspaces/false', 'NO_COLOR': '1', 'TF_INPUT': '0', 'npm_execpath': '/usr/lib/node_modules_24/npm/bin/npm-cli.js', 'npm_config_progress': 'false', 'XDG_RUNTIME_DIR': '/run/user/1000', 'MYSQL_PAGER': 'cat', 'HOMEBREW_PAGER': 'cat', 'PNPM_DISABLE_SELF_UPDATE_CHECK': 'true', 'DEBUGINFOD_URLS': 'ima:enforcing https://debuginfod.fedoraproject.org/ ima:ignore ', 'npm_package_json': '/home/anthodev/dev/perso/js/lapin-fute/package.json', 'DEBUGINFOD_IMA_CERT_PATH': '/etc/keys/ima:', 'LC_ALL': 'C.UTF-8', 'KDEDIRS': '/usr', 'XKB_DEFAULT_VARIANT': 'intl', 'CARGO_TERM_PROGRESS_WHEN': 'never', 'HERDR_ENV': '1', 'XDG_DATA_DIRS': '/home/anthodev/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share', 'KDE_FULL_SESSION': 'true', 'npm_config_allow_scripts': '', 'npm_config_noproxy': '', 'PATH': '/var/tmp/pebble-sdk/SDKs/4.33.1/toolchain/moddable-tools:/home/anthodev/.local/share/pebble-sdk/SDKs/4.33.1/toolchain/arm-none-eabi/bin:/home/anthodev/.local/share/uv/tools/pebble-tool/lib64/python3.14/site-packages/pebble_tool/commands/sdk:/home/anthodev/.local/share/pebble-sdk/SDKs/4.33.1/toolchain/bin:/home/anthodev/dev/perso/js/lapin-fute/node_modules/.bin:/home/anthodev/dev/perso/js/node_modules/.bin:/home/anthodev/dev/perso/node_modules/.bin:/home/anthodev/dev/node_modules/.bin:/home/anthodev/node_modules/.bin:/home/node_modules/.bin:/node_modules/.bin:/usr/lib/node_modules_24/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/anthodev/.local/bin:/home/anthodev/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/anthodev/.atuin/bin:/home/anthodev/.local/bin:/home/anthodev/.bun/bin:/home/anthodev/go/bin:/usr/local/bin:/usr/bin:/sbin:/home/anthodev/.npm-global/bin:/home/anthodev/.local/share/pnpm/bin:/home/anthodev/.cargo/bin:/usr/lib64/ccache:/bin', 'HERDR_TAB_ID': 'w6:t2', 'npm_config_node_gyp': '/usr/lib/node_modules_24/npm/node_modules/node-gyp/bin/node-gyp.js', 'CI': 'true', 'DBUS_SESSION_BUS_ADDRESS': 'unix:path=/run/user/1000/bus', 'npm_config_global_prefix': '/home/anthodev/.npm-global', 'npm_config_update_notifier': 'false', 'KDE_APPLICATIONS_AS_SCOPE': '1', 'HERDR_SOCKET_PATH': '/home/anthodev/.config/herdr/herdr.sock', 'HERDR_WORKSPACE_ID': 'w6', 'MAIL': '/var/spool/mail/anthodev', 'POWERLINE_COMMAND': 'oh-my-posh', 'npm_config_audit': 'false', 'ABBR_TIPS_REGEXES': '(^(\\w+\\s+)+(-{1,2})\\w+)(\\s\\S+) (^(\\s?(\\w-?)+){3}).* (^(\\s?(\\w-?)+){2}).* (^(\\s?(\\w-?)+){1}).*', 'DEBIAN_FRONTEND': 'noninteractive', 'GIT_TERMINAL_PROMPT': '0', 'npm_node_execpath': '/usr/bin/node-24', 'npm_package_engines_node': '24.18.0', 'TERM_PROGRAM': 'ghostty', 'MODDABLE': '/var/tmp/pebble-sdk/SDKs/4.33.1/toolchain/moddable', 'PYTHONHOME': '/home/anthodev/.local/share/pebble-sdk/SDKs/current/.venv', 'PYTHONPATH': '/home/anthodev/.local/share/uv/tools/pebble-tool/bin:/usr/lib64/python314.zip:/usr/lib64/python3.14:/usr/lib64/python3.14/lib-dynload:/home/anthodev/.local/share/uv/tools/pebble-tool/lib64/python3.14/site-packages:/home/anthodev/.local/share/uv/tools/pebble-tool/lib/python3.14/site-packages', 'NODE_PATH': '/home/anthodev/.local/share/pebble-sdk/SDKs/current/sdk-core/../node_modules', 'NOCLIMB': '1'}3+environ = {'IMSETTINGS_INTEGRATE_DESKTOP': 'yes', 'npm_command': 'run', 'SESSION_MANAGER': 'local/unix:@/tmp/.ICE-unix/2937,unix/unix:/tmp/.ICE-unix/2937', 'npm_config_yes': 'true', 'GHOSTTY_BIN_DIR': '/usr/bin', 'npm_config_userconfig': '/home/anthodev/.npmrc', 'COLORTERM': 'truecolor', 'HERDR_PANE_ID': 'w6:p2', 'XDG_CONFIG_DIRS': '/home/anthodev/.config/kdedefaults:/etc/xdg:/usr/share/kde-settings/kde-profile/default/xdg', 'npm_config_cache': '/home/anthodev/.npm', 'PYTHONUNBUFFERED': '1', 'PIP_NO_INPUT': '1', 'LESS': 'FRX', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session1', 'HISTCONTROL': 'ignoredups', 'XDG_MENU_PREFIX': 'plasma-', 'TERM_PROGRAM_VERSION': '1.3.1-4.fc44', 'COMPOSER_NO_INTERACTION': '1', 'OMPCODE': '1', 'HISTSIZE': '1000', 'TF_IN_AUTOMATION': '1', 'ICEAUTHORITY': '/run/user/1000/iceauth_aLtUGr', 'PNPM_UPDATE_NOTIFIER': 'false', 'NODE': '/usr/bin/node-24', 'GLAB_PAGER': 'cat', 'SSH_AUTH_SOCK': '/home/anthodev/.1password/agent.sock', 'CLOUDSDK_CORE_DISABLE_PROMPTS': '1', 'DOCKER_USER': '1000:1000', '__ABBR_TIPS_VALUES': 'git rebase -i HEAD~2 git rebase --continue git rebase develop gitmoji -c helium-browser --disable-features=WaylandWpColorManagerV1 just eza -al --icons', 'AGENT': '1', 'MEMORY_PRESSURE_WRITE': 'c29tZSAyMDAwMDAgMjAwMDAwMAA=', 'COLOR': '0', 'npm_config_local_prefix': '/home/anthodev/dev/perso/js/lapin-fute', 'XMODIFIERS': '@im=none', 'HOMEBREW_PREFIX': '/home/linuxbrew/.linuxbrew', 'DESKTOP_SESSION': '/usr/share/wayland-sessions/plasma.desktop', 'GTK_RC_FILES': '/etc/gtk/gtkrc:/home/anthodev/.gtkrc:/home/anthodev/.config/gtkrc', 'GDK_CORE_DEVICE_EVENTS': '1', 'npm_config_globalconfig': '/etc/npmrc', 'GPG_TTY': 'not a tty', 'EDITOR': 'true', 'GH_PAGER': 'cat', 'XDG_SEAT': 'seat0', 'GH_PROMPT_DISABLED': '1', 'PWD': '/home/anthodev/dev/perso/js/lapin-fute', 'XDG_SESSION_DESKTOP': 'KDE', 'LOGNAME': 'anthodev', 'XDG_SESSION_TYPE': 'wayland', 'PNPM_HOME': '/home/anthodev/.local/share/pnpm', 'npm_config_init_module': '/home/anthodev/.npm-init.js', 'SYSTEMD_EXEC_PID': '14866', '_': '/usr/bin/node', 'XAUTHORITY': '/run/user/1000/xauth_qgtLXn', 'KWIN_DISABLE_TONEMAPPING': '1', 'SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS': '0', 'POSH_SHELL': 'fish', 'ABBR_TIPS_PROMPT': '\\n\U0001f4a1 \\e[1m{{ .abbr }}\\e[0m => {{ .cmd }}', 'CLAUDECODE': '1', 'XKB_DEFAULT_MODEL': 'pc105', 'GTK2_RC_FILES': '/etc/gtk-2.0/gtkrc:/home/anthodev/.gtkrc-2.0:/home/anthodev/.config/gtkrc-2.0', '__ABBR_TIPS_KEYS': 'a__gr2 a__grc a__grd a__gtc a__helium-browser a__j a__xl', 'GHOSTTY_SHELL_FEATURES': 'path,sudo,title', 'HOME': '/home/anthodev', '__PI_NATIVE_VARIANT_CACHE': 'modern', 'DELTA_PAGER': 'cat', 'SSH_ASKPASS': '/usr/bin/false', 'LANG': 'fr_FR.UTF-8', '_JAVA_AWT_WM_NONREPARENTING': '1', 'LS_COLORS': '', 'XDG_CURRENT_DESKTOP': 'KDE', 'POSH_SHELL_VERSION': '4.8.1', 'npm_package_version': '0.1.0', 'POSH_SESSION_ID': 'aed4658f-12ea-4aca-a036-894bd3ef6bd0', 'MEMORY_PRESSURE_WATCH': '/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-com.mitchellh.ghostty.service/memory.pressure', 'WAYLAND_DISPLAY': 'wayland-0', 'AWS_PAGER': '', 'YARN_ENABLE_PROGRESS_BARS': '0', '_fish_env_initialized': '1', 'CONDA_PROMPT_MODIFIER': 'false', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'MANAGERPID': '2302', 'IMSETTINGS_MODULE': 'none', 'INIT_CWD': '/home/anthodev/dev/perso/js/lapin-fute', 'POSH_CONFIG': '/home/anthodev/.config/.zsh/anthodev.omp.json', 'STEAM_FRAME_FORCE_CLOSE': '1', 'KDE_SESSION_UID': '1000', 'PSQL_PAGER': 'cat', 'INFOPATH': '/home/linuxbrew/.linuxbrew/share/info:', 'npm_lifecycle_script': 'node scripts/build.mjs', 'NVM_DIR': '/home/anthodev/.nvm', 'MOZ_GMP_PATH': '/usr/lib64/mozilla/plugins/gmp-gmpopenh264/system-installed', 'XKB_DEFAULT_LAYOUT': 'us', 'npm_config_npm_version': '11.16.0', 'HERDR_STARTUP_CWD': '/home/anthodev', 'GHOSTTY_RESOURCES_DIR': '/usr/share/ghostty', 'XDG_SESSION_CLASS': 'user', 'TERMINFO': '/usr/share/terminfo', 'TERM': 'dumb', 'npm_package_name': 'lapin-fute', 'BAT_PAGER': 'cat', 'npm_config_prefix': '/home/anthodev/.npm-global', 'LESSOPEN': '||/usr/bin/lesspipe.sh %s', 'USER': 'anthodev', 'YARN_ENABLE_TELEMETRY': '0', 'GIT_PAGER': 'cat', 'PIP_DISABLE_PIP_VERSION_CHECK': '1', 'HERDR_BIN_PATH': '/home/linuxbrew/.linuxbrew/Cellar/herdr/0.8.2/bin/herdr', 'GID': '1000', 'HOMEBREW_CELLAR': '/home/linuxbrew/.linuxbrew/Cellar', 'QT_WAYLAND_RECONNECT': '1', 'KDE_SESSION_VERSION': '6', 'PAM_KWALLET5_LOGIN': '/run/user/1000/kwallet5.socket', 'MANPAGER': 'cat', 'VISUAL': 'true', 'DISPLAY': ':0', 'npm_lifecycle_event': 'build', 'SHLVL': '1', 'NVM_CD_FLAGS': '', 'GIT_EDITOR': 'true', 'PAGER': 'cat', 'HOMEBREW_REPOSITORY': '/home/linuxbrew/.linuxbrew/Homebrew', 'npm_config_fund': 'false', 'SYSTEMD_PAGER': 'cat', 'XDG_VTNR': '2', 'XDG_SESSION_ID': '2', 'ATUIN_SESSION': '01a05e273bfb7d2196e6386c6401cc46', 'MANAGERPIDFDID': '393', 'npm_config_user_agent': 'npm/11.16.0 node/v24.18.0 linux x64 workspaces/false', 'NO_COLOR': '1', 'TF_INPUT': '0', 'npm_execpath': '/usr/lib/node_modules_24/npm/bin/npm-cli.js', 'npm_config_progress': 'false', 'XDG_RUNTIME_DIR': '/run/user/1000', 'MYSQL_PAGER': 'cat', 'HOMEBREW_PAGER': 'cat', 'PNPM_DISABLE_SELF_UPDATE_CHECK': 'true', 'DEBUGINFOD_URLS': 'ima:enforcing https://debuginfod.fedoraproject.org/ ima:ignore ', 'npm_package_json': '/home/anthodev/dev/perso/js/lapin-fute/package.json', 'DEBUGINFOD_IMA_CERT_PATH': '/etc/keys/ima:', 'LC_ALL': 'C.UTF-8', 'KDEDIRS': '/usr', 'XKB_DEFAULT_VARIANT': 'intl', 'CARGO_TERM_PROGRESS_WHEN': 'never', 'HERDR_ENV': '1', 'XDG_DATA_DIRS': '/home/anthodev/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share', 'KDE_FULL_SESSION': 'true', 'npm_config_allow_scripts': '', 'npm_config_noproxy': '', 'PATH': '/var/tmp/pebble-sdk/SDKs/4.33.1/toolchain/moddable-tools:/home/anthodev/.local/share/pebble-sdk/SDKs/4.33.1/toolchain/arm-none-eabi/bin:/home/anthodev/.local/share/uv/tools/pebble-tool/lib64/python3.14/site-packages/pebble_tool/commands/sdk:/home/anthodev/.local/share/pebble-sdk/SDKs/4.33.1/toolchain/bin:/home/anthodev/dev/perso/js/lapin-fute/node_modules/.bin:/home/anthodev/dev/perso/js/node_modules/.bin:/home/anthodev/dev/perso/node_modules/.bin:/home/anthodev/dev/node_modules/.bin:/home/anthodev/node_modules/.bin:/home/node_modules/.bin:/node_modules/.bin:/usr/lib/node_modules_24/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/anthodev/.local/bin:/home/anthodev/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/anthodev/.atuin/bin:/home/anthodev/.local/bin:/home/anthodev/.bun/bin:/home/anthodev/go/bin:/usr/local/bin:/usr/bin:/sbin:/home/anthodev/.npm-global/bin:/home/anthodev/.local/share/pnpm/bin:/home/anthodev/.cargo/bin:/usr/lib64/ccache:/bin', 'HERDR_TAB_ID': 'w6:t2', 'npm_config_node_gyp': '/usr/lib/node_modules_24/npm/node_modules/node-gyp/bin/node-gyp.js', 'CI': 'true', 'DBUS_SESSION_BUS_ADDRESS': 'unix:path=/run/user/1000/bus', 'npm_config_global_prefix': '/home/anthodev/.npm-global', 'npm_config_update_notifier': 'false', 'KDE_APPLICATIONS_AS_SCOPE': '1', 'HERDR_SOCKET_PATH': '/home/anthodev/.config/herdr/herdr.sock', 'HERDR_WORKSPACE_ID': 'w6', 'MAIL': '/var/spool/mail/anthodev', 'POWERLINE_COMMAND': 'oh-my-posh', 'npm_config_audit': 'false', 'ABBR_TIPS_REGEXES': '(^(\\w+\\s+)+(-{1,2})\\w+)(\\s\\S+) (^(\\s?(\\w-?)+){3}).* (^(\\s?(\\w-?)+){2}).* (^(\\s?(\\w-?)+){1}).*', 'DEBIAN_FRONTEND': 'noninteractive', 'GIT_TERMINAL_PROMPT': '0', 'npm_node_execpath': '/usr/bin/node-24', 'npm_package_engines_node': '24.18.0', 'TERM_PROGRAM': 'ghostty', 'MODDABLE': '/var/tmp/pebble-sdk/SDKs/4.33.1/toolchain/moddable', 'PYTHONHOME': '/home/anthodev/.local/share/pebble-sdk/SDKs/current/.venv', 'PYTHONPATH': '/home/anthodev/.local/share/uv/tools/pebble-tool/bin:/usr/lib64/python314.zip:/usr/lib64/python3.14:/usr/lib64/python3.14/lib-dynload:/home/anthodev/.local/share/uv/tools/pebble-tool/lib64/python3.14/site-packages:/home/anthodev/.local/share/uv/tools/pebble-tool/lib/python3.14/site-packages', 'NODE_PATH': '/home/anthodev/.local/share/pebble-sdk/SDKs/current/sdk-core/../node_modules', 'NOCLIMB': '1'}
4 files = ['/home/anthodev/dev/perso/js/lapin-fute/packages/watch/wscript']4 files = ['/home/anthodev/dev/perso/js/lapin-fute/packages/watch/wscript']
5 hash = b'\xc6\xb0\xec\xd9\xadu[\xb0m\xd2\xe0\x84N\xf01%'5 hash = b'\xc6\xb0\xec\xd9\xadu[\xb0m\xd2\xe0\x84N\xf01%'
6 launch_dir = '/home/anthodev/dev/perso/js/lapin-fute/packages/watch'6 launch_dir = '/home/anthodev/dev/perso/js/lapin-fute/packages/watch'
added scripts/measure-config-page.mjs +25 -0
Large diff collapsed to keep the page fast — load it when you need it.
added scripts/sync-line-badges.mjs +173 -0
Large diff collapsed to keep the page fast — load it when you need it.