diff --git a/README.md b/README.md index 7ae2d38..e98217a 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,15 @@ The integration test starts its own backend on a temporary localhost port and ne - import that package into another installation as a new race entry - JSON export files now include export metadata and a schema version field so future imports can be handled more safely. - CSV imports from the application's own exports preserve IDs. External lists without IDs first match by transponder and then by name + class. Empty fields do not overwrite existing information, and new class names are created automatically. +- Driver imports only require the `name` column. The recommended minimum is `name;transponder;class`. Optional columns such as `brand`, `spoken_name`, `club`, `country`, and `profile_id` may be omitted; unknown columns are ignored and rows without a name are skipped. + +```csv +name;transponder;class;brand;club;country +Anna Andersson;123456;2WD;;JMK;SE +Erik Eriksson;654321;4WD;Associated;; +``` + +- An empty CSV template can be downloaded directly from the driver import area under `Settings -> Storage`. - Create a JSON backup before clearing data. Every clear button requires a separate confirmation and cannot be undone. - Web clearing removes canonical application data but retains the separate raw SQLite `passings` table as an audit log. A physical factory reset of the database must be performed locally on the stopped server, never through an unauthenticated LAN route. diff --git a/README.sv.md b/README.sv.md index 13aa266..513e348 100644 --- a/README.sv.md +++ b/README.sv.md @@ -275,6 +275,15 @@ Integrationstestet startar en egen backend på en temporär localhost-port och l - importera paketet på en annan installation som ett nytt race - JSON-exporter innehåller nu även exportmetadata och ett schema-/versionsfält för säkrare import framåt. - CSV-import från systemets egen export bevarar ID:n. Externa listor utan ID matchas först på transponder och därefter på namn + klass. Tomma fält skriver inte över befintlig information, och nya klassnamn skapas automatiskt. +- Förarimport kräver bara kolumnen `name` eller `namn`. Rekommenderat minimum är `name;transponder;class`. Valfria kolumner som `brand`, `spoken_name`, `club`, `country` och `profile_id` får saknas; okända kolumner ignoreras och rader utan namn hoppas över. + +```csv +name;transponder;class;brand;club;country +Anna Andersson;123456;2WD;;JMK;SE +Erik Eriksson;654321;4WD;Associated;; +``` + +- En tom CSV-mall kan laddas ner direkt vid förarimporten under `Inställningar -> Lagring`. - Ta en JSON-backup före rensning. Varje rensningsknapp kräver en separat bekräftelse och åtgärden kan inte ångras. - Webbrensningen tömmer kanonisk appdata men behåller den separata råa `passings`-tabellen i SQLite som revisionslogg. En fysisk fabriksrensning av databasen ska göras lokalt på servern när processen är stoppad, inte via en oautentiserad LAN-route. diff --git a/src/app.js b/src/app.js index 5c46d24..cf61762 100644 --- a/src/app.js +++ b/src/app.js @@ -1266,6 +1266,12 @@ const TRANSLATIONS = { "settings.export_cars_csv": "Exportera bilar CSV", "settings.import_drivers_csv": "Importera förare CSV", "settings.import_cars_csv": "Importera bilar CSV", + "settings.download_drivers_csv_template": "Ladda ner förarmall", + "settings.driver_csv_guide_title": "Format för förarimport", + "settings.driver_csv_required": "Krav: name eller namn. Rader utan förarnamn hoppas över.", + "settings.driver_csv_columns": "Rekommenderat minimum är namn, transponder och klass. Valfria kolumner är id, brand/märke, spoken_name, club/klubb, country/land och profile_id.", + "settings.driver_csv_behavior": "Valfria kolumner och värden får saknas. De lämnas då tomma och skriver inte över befintliga uppgifter. Okända extra kolumner ignoreras.", + "settings.driver_csv_example": "Exempel med semikolon", "settings.import_csv_success": "{type}: {added} tillagda, {updated} uppdaterade och {skipped} hoppade rader.", "settings.csv_type_drivers": "Förare importerade", "settings.csv_type_cars": "Bilar importerade", @@ -1564,7 +1570,7 @@ const TRANSLATIONS = { "guide.sqlite_2": "API: /api/state och /api/passings", "guide.sqlite_3": "I Inställningar -> Lagring kan du exportera hela racepaket för backup eller flytt till annan server/laptop.", "guide.sqlite_4": "Du kan också exportera eller importera bara klasser, förare och bilar om du vill börja om med race men behålla registret.", - "guide.sqlite_5": "CSV-filer för förare och bilar kan både exporteras och importeras. Egna exporter bevarar ID:n; externa komma-, semikolon- eller tabbseparerade listor slås ihop på transponder eller namn och klass.", + "guide.sqlite_5": "CSV-filer för förare och bilar kan både exporteras och importeras. För förare räcker name/namn, transponder och class/klass; endast namnet är obligatoriskt. Egna exporter bevarar ID:n och externa listor slås ihop på transponder eller namn och klass.", "guide.sqlite_6": "Rensa data kan tömma förare, bilar, klasser, race eller träningsstatistik separat. Rensa all tävlingsdata behåller decoder-, tema-, branding- och serverinställningar. Råa SQLite-passeringar behålls som revisionslogg. Exportera backup först.", "guide.ammc_ref": "AMMC referens: https://www.ammconverter.eu/docs/intro/quick-start/", "error.backend_offline": "Backend offline: {msg}", @@ -2510,6 +2516,12 @@ const TRANSLATIONS = { "settings.export_cars_csv": "Export cars CSV", "settings.import_drivers_csv": "Import drivers CSV", "settings.import_cars_csv": "Import cars CSV", + "settings.download_drivers_csv_template": "Download driver template", + "settings.driver_csv_guide_title": "Driver import format", + "settings.driver_csv_required": "Required: name. Rows without a driver name are skipped.", + "settings.driver_csv_columns": "The recommended minimum is name, transponder, and class. Optional columns are id, brand, spoken_name, club, country, and profile_id.", + "settings.driver_csv_behavior": "Optional columns and values may be omitted. They remain empty and do not overwrite existing data. Unknown extra columns are ignored.", + "settings.driver_csv_example": "Semicolon-delimited example", "settings.import_csv_success": "{type}: {added} added, {updated} updated, and {skipped} skipped rows.", "settings.csv_type_drivers": "Drivers imported", "settings.csv_type_cars": "Cars imported", @@ -2808,7 +2820,7 @@ const TRANSLATIONS = { "guide.sqlite_2": "API endpoints: /api/state and /api/passings", "guide.sqlite_3": "In Settings -> Storage you can export a full race package for backup or moving to another server or laptop.", "guide.sqlite_4": "You can also export or import only classes, drivers and cars if you want to reset race data but keep the directory.", - "guide.sqlite_5": "Driver and car CSV files can both be exported and imported. Native exports preserve IDs; external comma-, semicolon-, or tab-separated lists merge by transponder or by name and class.", + "guide.sqlite_5": "Driver and car CSV files can both be exported and imported. Drivers only need name, transponder, and class; only name is required. Native exports preserve IDs and external lists merge by transponder or by name and class.", "guide.sqlite_6": "Clear data can remove drivers, cars, classes, races, or training statistics separately. Clear all competition data retains decoder, theme, branding, and server settings. Raw SQLite passings remain as an audit log. Export a backup first.", "guide.ammc_ref": "AMMC reference: https://www.ammconverter.eu/docs/intro/quick-start/", "error.backend_offline": "Backend offline: {msg}", diff --git a/src/settings.js b/src/settings.js index 3118be2..0b05bbd 100644 --- a/src/settings.js +++ b/src/settings.js @@ -434,11 +434,24 @@ export function renderSettings(deps) {
${t("settings.driver_csv_required")}
+${t("settings.driver_csv_columns")}
+${t("settings.driver_csv_behavior")}
+ ${t("settings.driver_csv_example")} +name;transponder;class;brand;club;country
+Anna Andersson;123456;2WD;;JMK;SE
+Erik Eriksson;654321;4WD;Associated;;
+ ${escapeHtml(settingsStorageNotice)}
` : ""} @@ -716,6 +729,12 @@ export function renderSettings(deps) { downloadCsvFile("live_rc_drivers.csv", buildDriversCsvRows()); }); + document.getElementById("downloadDriversCsvTemplate")?.addEventListener("click", () => { + downloadCsvFile("racecore_driver_import_template.csv", [ + ["name", "transponder", "class", "brand", "spoken_name", "club", "country"], + ]); + }); + document.getElementById("exportCarsCsv")?.addEventListener("click", () => { downloadCsvFile("live_rc_cars.csv", buildCarsCsvRows()); }); diff --git a/src/styles.css b/src/styles.css index b7bd200..36a3a27 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1510,6 +1510,62 @@ body { grid-column: span 2; } +.csv-import-guide { + margin-top: 12px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--surface-elevated); + overflow: hidden; +} + +.csv-import-guide > summary { + padding: 11px 13px; + color: var(--text); + font-weight: 700; + cursor: pointer; +} + +.csv-import-guide[open] > summary { + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--accent-2) 8%, transparent); +} + +.csv-import-guide-body { + padding: 12px 13px 14px; +} + +.csv-import-guide-body p { + margin: 0 0 8px; + color: var(--muted); + line-height: 1.45; +} + +.csv-import-guide-body p strong { + color: var(--text); +} + +.csv-import-example-label { + display: block; + margin: 12px 0 6px; + color: var(--muted); + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.csv-import-guide pre { + margin: 0; + padding: 11px 12px; + overflow-x: auto; + border: 1px solid var(--line); + border-radius: 9px; + background: var(--field-bg); + color: var(--field-text); + font-size: 0.8rem; + line-height: 1.5; +} + .actions { display: flex; gap: 10px; diff --git a/tests/csv_import.test.js b/tests/csv_import.test.js index f24c9ab..13043b8 100644 --- a/tests/csv_import.test.js +++ b/tests/csv_import.test.js @@ -34,6 +34,27 @@ test("driver CSV import creates classes and merges exported or external rows saf assert.equal(result.drivers.find((item) => item.transponder === "222")?.classId, "class_1"); }); +test("minimal driver CSV ignores missing optional fields and skips nameless rows", () => { + const result = importDriversCsv( + "namn;transponder;klass;fält_från_annat_system\nAnna Andersson;123456;2WD;ignoreras\n;999999;4WD;ignoreras", + { + classes: [], + drivers: [], + uid: makeUid(), + } + ); + + assert.equal(result.added, 1); + assert.equal(result.updated, 0); + assert.equal(result.skipped, 1); + assert.equal(result.drivers[0].name, "Anna Andersson"); + assert.equal(result.drivers[0].transponder, "123456"); + assert.equal(result.drivers[0].brand, ""); + assert.equal(result.drivers[0].club, ""); + assert.equal(result.drivers[0].country, ""); + assert.equal(result.classes[0].name, "2WD"); +}); + test("car CSV import matches transponders and skips nameless rows", () => { const result = importCarsCsv( "name,brand,transponder\nUpdated Car,,123\nNew Car,Schumacher,456\n,Missing,789",