feat: build companion configuration page #13
Merged@github-anthodev wants to merge
feature/phone-configuration/configuration/5 into developMerged as
f7d01bd5b859.71 files changed
modified
fixtures/catalog/idfm-v1.json +6 -6 | @@ -22,12 +22,12 @@ | ||
| 22 | 22 | { "agency_name": "Fixture IDFM operator", "agency_url": "https://www.iledefrance-mobilites.fr/", "agency_timezone": "Europe/Paris" } |
| 23 | 23 | ], |
| 24 | 24 | "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" } | |
| 31 | 31 | ], |
| 32 | 32 | "routes": [ |
| 33 | 33 | { "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 @@ | ||
| 7 | 7 | "stopLabel": "Châtelet", |
| 8 | 8 | "lineLabel": "Métro 1", |
| 9 | 9 | "destinationLabel": "La Défense", |
| 10 | + "lineMode": "METRO", | |
| 11 | + "lineColor": "#ffbe00", | |
| 12 | + "lineTextColor": "#000000", | |
| 10 | 13 | "sortOrder": 0 |
| 11 | 14 | }, |
| 12 | 15 | "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": 0 | 13 | "sortOrder": 0 |
| 11 | }, | 14 | }, |
| 12 | "result": { | 15 | "result": { |
modified
package.json +2 -0 | @@ -13,8 +13,10 @@ | ||
| 13 | 13 | "build": "node scripts/build.mjs", |
| 14 | 14 | "catalog:probe": "node scripts/probe-catalog.mjs", |
| 15 | 15 | "catalog:refresh": "node scripts/refresh-catalog.mjs", |
| 16 | + "badges:sync": "node scripts/sync-line-badges.mjs", | |
| 16 | 17 | "measure:radio": "node scripts/measure-radio.mjs", |
| 17 | 18 | "measure:catalog": "node scripts/measure-catalog.mjs", |
| 19 | + "measure:config-page": "node scripts/measure-config-page.mjs", | |
| 18 | 20 | "test": "node scripts/test.mjs", |
| 19 | 21 | "test:contracts": "node --test packages/contracts/test/contracts.test.ts", |
| 20 | 22 | "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 { | ||
| 24 | 24 | } from "../../contracts/src/index.ts"; |
| 25 | 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 | 28 | export const DOWNLOAD_TIMEOUT_MS = 15 * 60 * 1_000; |
| 29 | 29 | export const MAX_COMPRESSED_DOWNLOAD_BYTES = 2 * 1024 ** 3; |
| 30 | 30 | export const MAX_SELECTED_ENTRY_BYTES = 8 * 1024 ** 3; |
| @@ -224,6 +224,12 @@ function canonicalLineId(value: unknown, field: string): string { | ||
| 224 | 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 | 233 | function isoTimestamp(value: string, field: string): string { |
| 228 | 234 | if (value.length === 0 || !Number.isFinite(Date.parse(value))) { |
| 229 | 235 | fail("INVALID_METADATA", `${field} must be an ISO timestamp`); |
| @@ -500,6 +506,8 @@ function createSchema(database: DatabaseSync): void { | ||
| 500 | 506 | stop_label TEXT NOT NULL, |
| 501 | 507 | line_label TEXT NOT NULL, |
| 502 | 508 | destination_label TEXT NOT NULL, |
| 509 | + line_color TEXT NOT NULL, | |
| 510 | + line_text_color TEXT NOT NULL, | |
| 503 | 511 | monitoring_ref TEXT NOT NULL, |
| 504 | 512 | line_ref TEXT NOT NULL, |
| 505 | 513 | direction_id TEXT NOT NULL, |
| @@ -575,7 +583,9 @@ function createSchema(database: DatabaseSync): void { | ||
| 575 | 583 | shortname_line TEXT NOT NULL, |
| 576 | 584 | transport_mode TEXT NOT NULL, |
| 577 | 585 | 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 | |
| 579 | 589 | ) STRICT; |
| 580 | 590 | CREATE TABLE stage_arrets_lignes ( |
| 581 | 591 | route_id TEXT NOT NULL, |
| @@ -596,7 +606,7 @@ function prepareSpecs(database: DatabaseSync): Readonly<Record<keyof CatalogReco | ||
| 596 | 606 | const arret = database.prepare("INSERT INTO stage_arrets VALUES (?, ?)"); |
| 597 | 607 | const zone = database.prepare("INSERT INTO stage_zones VALUES (?, ?, ?)"); |
| 598 | 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 | 610 | const arretLigne = database.prepare("INSERT OR IGNORE INTO stage_arrets_lignes VALUES (?, ?)"); |
| 601 | 611 | |
| 602 | 612 | return { |
| @@ -649,8 +659,8 @@ function prepareSpecs(database: DatabaseSync): Readonly<Record<keyof CatalogReco | ||
| 649 | 659 | insert: (row) => relation.run(nonEmpty(row.zdaid, "relations.zdaid"), nonEmpty(row.arrid, "relations.arrid"), nonEmpty(row.artid, "relations.artid")), |
| 650 | 660 | }, |
| 651 | 661 | 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")), | |
| 654 | 664 | }, |
| 655 | 665 | arretsLignes: { |
| 656 | 666 | required: ["id", "stop_id"], |
| @@ -959,7 +969,9 @@ function deriveCandidates(database: DatabaseSync): void { | ||
| 959 | 969 | CREATE TABLE stage_line_modes ( |
| 960 | 970 | id_line TEXT PRIMARY KEY, |
| 961 | 971 | 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 | |
| 963 | 975 | ) STRICT; |
| 964 | 976 | |
| 965 | 977 | INSERT INTO stage_line_modes |
| @@ -971,7 +983,9 @@ function deriveCandidates(database: DatabaseSync): void { | ||
| 971 | 983 | WHEN lower(trim(transport_mode)) = 'rail' AND lower(trim(transport_submode)) = 'local' THEN 'RER' |
| 972 | 984 | WHEN lower(trim(transport_mode)) = 'rail' AND lower(trim(transport_submode)) = 'suburbanrailway' THEN 'TRANSILIEN' |
| 973 | 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 | 989 | FROM stage_lines |
| 976 | 990 | WHERE lower(trim(status)) = 'active' |
| 977 | 991 | AND ( |
| @@ -1040,6 +1054,8 @@ function deriveCandidates(database: DatabaseSync): void { | ||
| 1040 | 1054 | zone.zdatown AS locality_label, |
| 1041 | 1055 | line_mode.id_line AS id_line, |
| 1042 | 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 | 1059 | CASE |
| 1044 | 1060 | WHEN line_mode.mode IN ('RER', 'TRANSILIEN') THEN terminal.stop_name |
| 1045 | 1061 | ELSE coalesce(nullif(trim(trip.trip_headsign), ''), terminal.stop_name) |
| @@ -1089,7 +1105,8 @@ function deriveCandidates(database: DatabaseSync): void { | ||
| 1089 | 1105 | AND trim(destination_code.object_code) <> '' |
| 1090 | 1106 | ) |
| 1091 | 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 | 1110 | monitoring_ref, line_ref, direction_id, destination_ref, |
| 1094 | 1111 | max(CASE WHEN resolution = 'arrets' THEN 1 ELSE 0 END) AS via_arrets |
| 1095 | 1112 | FROM joined |
| @@ -1107,7 +1124,7 @@ function insertFinalCatalog(database: DatabaseSync, sourceRevision: string, crea | ||
| 1107 | 1124 | |
| 1108 | 1125 | const insertPlace = database.prepare("INSERT INTO places VALUES (?, ?, ?, ?, ?)"); |
| 1109 | 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 | 1128 | const selectPlace = database.prepare("SELECT canonical_tuple FROM places WHERE place_id = ?"); |
| 1112 | 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 | 1164 | const candidates = database.prepare(` |
| 1148 | 1165 | 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 | |
| 1150 | 1167 | FROM stage_candidates |
| 1151 | 1168 | ORDER BY mode, zdaid, id_line, monitoring_ref, direction_id, destination_ref |
| 1152 | 1169 | `).iterate() as Iterable<QueryRow>; |
| @@ -1174,6 +1191,8 @@ function insertFinalCatalog(database: DatabaseSync, sourceRevision: string, crea | ||
| 1174 | 1191 | nonEmpty(row.stop_label, "candidate.stop_label"), |
| 1175 | 1192 | nonEmpty(row.line_label, "candidate.line_label"), |
| 1176 | 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 | 1196 | nonEmpty(row.monitoring_ref, "candidate.monitoring_ref"), |
| 1178 | 1197 | nonEmpty(row.line_ref, "candidate.line_ref"), |
| 1179 | 1198 | nonEmpty(row.direction_id, "candidate.direction_id"), |
| @@ -1206,6 +1225,8 @@ function validateByteBounds(database: DatabaseSync): void { | ||
| 1206 | 1225 | OR length(CAST(stop_label AS BLOB)) > ? |
| 1207 | 1226 | OR length(CAST(line_label AS BLOB)) > ? |
| 1208 | 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 | 1230 | LIMIT 1 |
| 1210 | 1231 | `).get(LIMITS.idUtf8Bytes, LIMITS.labelUtf8Bytes, LIMITS.labelUtf8Bytes, LIMITS.labelUtf8Bytes); |
| 1211 | 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 | 1249 | database.exec("PRAGMA foreign_keys = ON"); |
| 1229 | 1250 | exactColumns(database, "metadata", ["key", "value"]); |
| 1230 | 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 | 1253 | exactColumns(database, "place_search", ["search_text", "place_id"]); |
| 1233 | 1254 | |
| 1234 | 1255 | const sourceRevision = metadataValue(database, "source_revision"); |
| @@ -1276,9 +1297,11 @@ function validateOpenCatalog(database: DatabaseSync): CatalogBuildResult { | ||
| 1276 | 1297 | const unresolved = database.prepare(` |
| 1277 | 1298 | SELECT 1 FROM services |
| 1278 | 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 | 1302 | LIMIT 1 |
| 1280 | 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 | 1306 | for (const row of database.prepare("SELECT place_id, canonical_tuple FROM places").iterate() as Iterable<QueryRow>) { |
| 1284 | 1307 | 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 NULL | 586 | + 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 NULL | 972 | + 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_modes | 977 | 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_lines | 989 | 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 | CASE | 1059 | CASE |
| 1044 | WHEN line_mode.mode IN ('RER', 'TRANSILIEN') THEN terminal.stop_name | 1060 | 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_arrets | 1111 | max(CASE WHEN resolution = 'arrets' THEN 1 ELSE 0 END) AS via_arrets |
| 1095 | FROM joined | 1112 | 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_ref | 1166 | + line_color, line_text_color, id_line, monitoring_ref, line_ref, direction_id, destination_ref |
| 1150 | FROM stage_candidates | 1167 | FROM stage_candidates |
| 1151 | ORDER BY mode, zdaid, id_line, monitoring_ref, direction_id, destination_ref | 1168 | 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 1 | 1230 | 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 services | 1298 | 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 1 | 1302 | 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 { | ||
| 48 | 48 | readonly stopLabel: string; |
| 49 | 49 | readonly lineLabel: string; |
| 50 | 50 | readonly destinationLabel: string; |
| 51 | + readonly lineMode: TransportMode; | |
| 52 | + readonly lineColor: string; | |
| 53 | + readonly lineTextColor: string; | |
| 51 | 54 | } |
| 52 | 55 | |
| 53 | 56 | interface ResolutionRow { |
| @@ -87,7 +90,7 @@ interface ExpectedColumn { | ||
| 87 | 90 | const CLOSE_READER = Symbol("closeCatalogReader"); |
| 88 | 91 | const OPAQUE_ID_BYTES = 47; |
| 89 | 92 | const CATALOG_SCHEMA_VERSION = "1"; |
| 90 | -const CATALOG_FORMAT_VERSION = "1"; | |
| 93 | +const CATALOG_FORMAT_VERSION = "2"; | |
| 91 | 94 | const REQUIRED_METADATA = [ |
| 92 | 95 | "schema_version", |
| 93 | 96 | "catalog_version", |
| @@ -123,6 +126,8 @@ const TABLE_COLUMNS: Readonly<Record<string, readonly ExpectedColumn[]>> = { | ||
| 123 | 126 | { name: "stop_label", type: "TEXT", notNull: true }, |
| 124 | 127 | { name: "line_label", type: "TEXT", notNull: true }, |
| 125 | 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 | 131 | { name: "monitoring_ref", type: "TEXT", notNull: true }, |
| 127 | 132 | { name: "line_ref", type: "TEXT", notNull: true }, |
| 128 | 133 | { name: "direction_id", type: "TEXT", notNull: true }, |
| @@ -322,6 +327,10 @@ function validateData(database: DatabaseSync): void { | ||
| 322 | 327 | OR length(CAST(stop_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes} |
| 323 | 328 | OR length(CAST(line_label AS BLOB)) NOT BETWEEN 1 AND ${LIMITS.labelUtf8Bytes} |
| 324 | 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 | 334 | OR length(CAST(monitoring_ref AS BLOB)) < 1 |
| 326 | 335 | OR length(CAST(line_ref AS BLOB)) < 1 |
| 327 | 336 | OR length(CAST(direction_id AS BLOB)) < 1 |
| @@ -482,12 +491,16 @@ export class SqliteCatalogReader implements CatalogReader { | ||
| 482 | 491 | `); |
| 483 | 492 | this.#listServicesStatement = database.prepare(` |
| 484 | 493 | 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 | |
| 489 | 501 | FROM services |
| 490 | - WHERE place_id = ? | |
| 502 | + JOIN places ON places.place_id = services.place_id | |
| 503 | + WHERE services.place_id = ? | |
| 491 | 504 | ORDER BY |
| 492 | 505 | line_label COLLATE NOCASE, |
| 493 | 506 | destination_label COLLATE NOCASE, |
| @@ -544,6 +557,9 @@ export class SqliteCatalogReader implements CatalogReader { | ||
| 544 | 557 | stopLabel: entry.stopLabel, |
| 545 | 558 | lineLabel: entry.lineLabel, |
| 546 | 559 | destinationLabel: entry.destinationLabel, |
| 560 | + lineMode: entry.lineMode, | |
| 561 | + lineColor: entry.lineColor, | |
| 562 | + lineTextColor: entry.lineTextColor, | |
| 547 | 563 | })); |
| 548 | 564 | } |
| 549 | 565 | |
| @@ -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)) < 1 | 334 | OR length(CAST(monitoring_ref AS BLOB)) < 1 |
| 326 | OR length(CAST(line_ref AS BLOB)) < 1 | 335 | OR length(CAST(line_ref AS BLOB)) < 1 |
| 327 | OR length(CAST(direction_id AS BLOB)) < 1 | 336 | 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 | SELECT | 493 | 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 destinationLabel | 497 | + 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 services | 501 | FROM services |
| 490 | - WHERE place_id = ? | 502 | + JOIN places ON places.place_id = services.place_id |
| 503 | + WHERE services.place_id = ? | ||
| 491 | ORDER BY | 504 | 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 { | ||
| 55 | 55 | PRAGMA foreign_keys = ON; |
| 56 | 56 | CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL); |
| 57 | 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 | 59 | CREATE VIRTUAL TABLE place_search USING fts5(search_text, place_id UNINDEXED); |
| 60 | 60 | `); |
| 61 | 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 | 63 | metadata.run("source_revision", "http-fixture-revision"); |
| 64 | 64 | metadata.run("created_at", "2026-08-30T12:00:00.000Z"); |
| 65 | 65 | const place = database.prepare("INSERT INTO places VALUES (?, ?, ?, ?, ?)"); |
| 66 | 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 | 68 | FIXTURE.forEach((entry, index) => { |
| 69 | 69 | place.run(entry.id, entry.label, entry.locality, entry.mode, `place:v1|${index}:${entry.id}`); |
| 70 | 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 | 73 | database.close(); |
| 74 | 74 | const manager = new CatalogManager(); |
| @@ -100,7 +100,7 @@ test("GET routes return exact public response bodies without source identifiers | ||
| 100 | 100 | assert.equal(services.status, 200); |
| 101 | 101 | assert.ok(isServiceOptionsResult(services.body)); |
| 102 | 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 | 105 | const publicJson = JSON.stringify([places.body, services.body]); |
| 106 | 106 | 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 | ||
| 107 | 107 | const metroServices = manager.listServices(metro.placeId) ?? []; |
| 108 | 108 | assert.equal(metroServices.length, 2); |
| 109 | 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 | 117 | const rer = place(manager, "chatelet les halles", "RER"); |
| 111 | 118 | const east = service(manager, rer.placeId, "Marne-la-Vallée Chessy"); |
| 112 | 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 | 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 | 179 | test("activation preserves stable services, removes stale IDs, and failed replacement leaves the active file untouched", async (t) => { |
| 133 | 180 | const directory = workspace(t); |
| 134 | 181 | 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 { | ||
| 23 | 23 | readonly stopLabel: string; |
| 24 | 24 | readonly lineLabel: string; |
| 25 | 25 | readonly destinationLabel: string; |
| 26 | + readonly lineColor?: string; | |
| 27 | + readonly lineTextColor?: string; | |
| 26 | 28 | readonly monitoringRef: string; |
| 27 | 29 | readonly lineRef: string; |
| 28 | 30 | readonly directionId: string; |
| @@ -208,6 +210,8 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog { | ||
| 208 | 210 | stop_label TEXT NOT NULL, |
| 209 | 211 | line_label TEXT NOT NULL, |
| 210 | 212 | destination_label TEXT NOT NULL, |
| 213 | + line_color TEXT NOT NULL, | |
| 214 | + line_text_color TEXT NOT NULL, | |
| 211 | 215 | monitoring_ref TEXT NOT NULL, |
| 212 | 216 | line_ref TEXT NOT NULL, |
| 213 | 217 | direction_id TEXT NOT NULL, |
| @@ -219,7 +223,7 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog { | ||
| 219 | 223 | |
| 220 | 224 | const metadata = database.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)"); |
| 221 | 225 | metadata.run("schema_version", "1"); |
| 222 | - metadata.run("catalog_version", seed.catalogVersion ?? "1"); | |
| 226 | + metadata.run("catalog_version", seed.catalogVersion ?? "2"); | |
| 223 | 227 | metadata.run("source_revision", seed.revision ?? "fixture-revision-a"); |
| 224 | 228 | metadata.run("created_at", "2026-08-30T12:00:00.000Z"); |
| 225 | 229 | |
| @@ -258,12 +262,14 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog { | ||
| 258 | 262 | stop_label, |
| 259 | 263 | line_label, |
| 260 | 264 | destination_label, |
| 265 | + line_color, | |
| 266 | + line_text_color, | |
| 261 | 267 | monitoring_ref, |
| 262 | 268 | line_ref, |
| 263 | 269 | direction_id, |
| 264 | 270 | destination_ref, |
| 265 | 271 | canonical_tuple |
| 266 | - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| 272 | + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| 267 | 273 | `); |
| 268 | 274 | for (const service of services) { |
| 269 | 275 | const identity = serviceIdentity(service); |
| @@ -274,6 +280,8 @@ function buildCatalog(seed: CatalogSeed = {}): BuiltCatalog { | ||
| 274 | 280 | service.stopLabel, |
| 275 | 281 | service.lineLabel, |
| 276 | 282 | service.destinationLabel, |
| 283 | + service.lineColor ?? "#123456", | |
| 284 | + service.lineTextColor ?? "#ffffff", | |
| 277 | 285 | service.monitoringRef, |
| 278 | 286 | service.lineRef, |
| 279 | 287 | service.directionId, |
| @@ -424,7 +432,7 @@ test("reload preserves stable IDs, distinguishes directions, rejects invalid can | ||
| 424 | 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 | 436 | assert.throws(() => manager.reload(invalid.path), /Invalid catalog candidate/u); |
| 429 | 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 | 463 | assert.equal(manager.listServices("plc_missing"), undefined); |
| 456 | 464 | assert.deepEqual(Object.keys(manager.listServices(replacement.placeIds.metro)![0]!).sort(), [ |
| 457 | 465 | "destinationLabel", |
| 466 | + "lineColor", | |
| 458 | 467 | "lineLabel", |
| 468 | + "lineMode", | |
| 469 | + "lineTextColor", | |
| 459 | 470 | "serviceId", |
| 460 | 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 | 484 | test("candidate validation rejects incomplete FTS coverage", () => { |
| 465 | 485 | const invalid = buildCatalog({ omitSearchFor: "metro" }); |
| 466 | 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 | +}); | |
| @@ -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_tuple | 271 | 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 = [ | ||
| 11 | 11 | "displayName", |
| 12 | 12 | "stopLabel", |
| 13 | 13 | "lineLabel", |
| 14 | - "destinationLabel" | |
| 14 | + "destinationLabel", | |
| 15 | + "lineMode", | |
| 16 | + "lineColor", | |
| 17 | + "lineTextColor" | |
| 15 | 18 | ]; |
| 16 | 19 | |
| 17 | 20 | 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([ | ||
| 25 | 25 | "RATE_LIMITED", |
| 26 | 26 | "INVALID_RESPONSE" |
| 27 | 27 | ]); |
| 28 | +var TRANSPORT_MODE = Object.freeze(["BUS", "METRO", "TRAM", "RER", "TRANSILIEN"]); | |
| 28 | 29 | var API_KEY_ACTION = Object.freeze(["KEEP", "REPLACE", "REMOVE"]); |
| 29 | 30 | var KEY_STATUS = Object.freeze({ MISSING: 0, CONFIGURED: 1, INVALID: 2 }); |
| 30 | 31 | var REQUEST_TRIGGER = Object.freeze({ APP_OPEN: 0, FAVORITE_SELECTION: 1, MANUAL_SELECT: 2 }); |
| @@ -103,6 +104,9 @@ var FAVORITE_KEYS = [ | ||
| 103 | 104 | "stopLabel", |
| 104 | 105 | "lineLabel", |
| 105 | 106 | "destinationLabel", |
| 107 | + "lineMode", | |
| 108 | + "lineColor", | |
| 109 | + "lineTextColor", | |
| 106 | 110 | "sortOrder" |
| 107 | 111 | ]; |
| 108 | 112 | var DEPARTURE_KEYS = ["expectedAt", "aimedAt", "minutes", "status", "nextIntervalMinutes"]; |
| @@ -169,6 +173,10 @@ function boundedString(value, maximum) { | ||
| 169 | 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 | 180 | function uint32(value) { |
| 173 | 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 | 188 | 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 | |
| 184 | 198 | && boundedString(value.id, LIMITS.idUtf8Bytes) |
| 185 | 199 | && boundedString(value.serviceId, LIMITS.idUtf8Bytes) |
| 186 | 200 | && (typeof value.displayName === "undefined" || boundedString(value.displayName, LIMITS.labelUtf8Bytes)) |
| 187 | 201 | && boundedString(value.stopLabel, LIMITS.labelUtf8Bytes) |
| 188 | 202 | && boundedString(value.lineLabel, LIMITS.labelUtf8Bytes) |
| 189 | 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 | 209 | && typeof value.sortOrder === "number" |
| 191 | 210 | && Math.floor(value.sortOrder) === value.sortOrder |
| 192 | 211 | && value.sortOrder >= 0 |
| @@ -262,9 +281,14 @@ function copyFavorite(favorite) { | ||
| 262 | 281 | serviceId: favorite.serviceId, |
| 263 | 282 | stopLabel: favorite.stopLabel, |
| 264 | 283 | lineLabel: favorite.lineLabel, |
| 265 | - destinationLabel: favorite.destinationLabel, | |
| 266 | - sortOrder: favorite.sortOrder | |
| 284 | + destinationLabel: favorite.destinationLabel | |
| 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 | 292 | if (typeof favorite.displayName !== "undefined") copy.displayName = favorite.displayName; |
| 269 | 293 | return copy; |
| 270 | 294 | } |
| @@ -301,6 +325,7 @@ module.exports = { | ||
| 301 | 325 | FRESHNESS: FRESHNESS, |
| 302 | 326 | DEPARTURE_STATUS: DEPARTURE_STATUS, |
| 303 | 327 | ERROR_CODE: ERROR_CODE, |
| 328 | + TRANSPORT_MODE: TRANSPORT_MODE, | |
| 304 | 329 | API_KEY_ACTION: API_KEY_ACTION, |
| 305 | 330 | KEY_STATUS: KEY_STATUS, |
| 306 | 331 | REQUEST_TRIGGER: REQUEST_TRIGGER, |
| @@ -313,6 +338,7 @@ module.exports = { | ||
| 313 | 338 | isObject: isObject, |
| 314 | 339 | hasOnlyKeys: hasOnlyKeys, |
| 315 | 340 | boundedString: boundedString, |
| 341 | + isLineColor: isLineColor, | |
| 316 | 342 | isFavorite: isFavorite, |
| 317 | 343 | isFavoriteList: isFavoriteList, |
| 318 | 344 | 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_VERSION | 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 | ||
| 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.sortOrder | 210 | && Math.floor(value.sortOrder) === value.sortOrder |
| 192 | && value.sortOrder >= 0 | 211 | && 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 | ||
| 126 | 126 | target.xhr.instances[0].respond(200, responseFor("secret-boundary")); |
| 127 | 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 | 157 | test("configuration opening uses the active watch language and safely falls back to English", function () { |
| 130 | 158 | var target = harness(); |
| 131 | 159 | 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 | ||
| 23 | 23 | assert.deepEqual(contracts.MESSAGE_TYPE, shared.MESSAGE_TYPE); |
| 24 | 24 | assert.deepEqual(contracts.KEY_STATUS, shared.KEY_STATUS); |
| 25 | 25 | assert.deepEqual(contracts.REQUEST_TRIGGER, shared.REQUEST_TRIGGER); |
| 26 | + assert.deepEqual(contracts.TRANSPORT_MODE, shared.TRANSPORT_MODE); | |
| 26 | 27 | assert.deepEqual(contracts.WIRE_LANGUAGE, shared.WIRE_LANGUAGE); |
| 27 | 28 | assert.deepEqual(contracts.LIMITS, shared.LIMITS); |
| 28 | 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 | 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 | 72 | test("recorded fixture round trips through symbolic configuration and result payloads", async function () { |
| 34 | 73 | var shared = await sharedContracts(); |
| 35 | 74 | 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 differ | Binary 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 @@ | ||
| 1 | 1 | argv = ['/home/anthodev/.local/share/pebble-sdk/SDKs/current/sdk-core/pebble/waf', 'configure'] |
| 2 | 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 | 4 | files = ['/home/anthodev/dev/perso/js/lapin-fute/packages/watch/wscript'] |
| 5 | 5 | hash = b'\xc6\xb0\xec\xd9\xadu[\xb0m\xd2\xe0\x84N\xf01%' |
| 6 | 6 | 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.