Implement Fare v2 p1 #95

Closed
opened 2026-04-21 23:35:34 +00:00 by maxtkc · 1 comment
Owner

Summary

Add GTFS Fares V2 support for the three "simple" tables: fare_media.txt, fare_products.txt, and rider_categories.txt. The GTFS spec files and TypeScript entity types are already defined in the codebase; what was missing was the database schema registration and all UI. The approach is a modal-based CRUD interface accessible from a new "Fares" section on the Browse home page. The modal supports add/edit/delete for all three tables with schema-driven field rendering (tooltips, presence asterisks, spec links) matching the rest of the app.

Relevant Context

Files modified on this branch:

  • src/modules/gtfs-database.ts — added three new IDB store registrations and indexes (schema v9)
  • src/modules/fares-modal.ts — new file (~800 lines), full CRUD modal for all three V2 tables
  • src/modules/page-content-renderer.ts — added Fares section to home page, wired to showFaresModal
  • src/modules/modal-utils.ts — added optional boxClassName to showModal for wider modals

Key utilities used:

  • generateFieldConfigsFromSchema(schema, data, tableName) + renderFormFields(configs) in src/utils/field-component.ts — drives all form field rendering with tooltips/asterisks/spec links
  • generateCompositeKeyFromRecord('fare_products', record) in src/utils/gtfs-primary-keys.ts — builds the IDB key string for the composite-key fare_products table
  • GTFS_ENUMS in src/types/gtfs-enums.ts — auto-derived from spec enumValues, so fare_media_type and is_default_fare_container automatically render as selects

Architecture note: The fares modal does NOT use the form-patch-bridge. Fields are rendered with data-field attributes (for visual consistency), but values are read manually in each Save handler and routed through patchManager.record*() directly.


Phase 1: Database Schema Registration

Register the three new tables in the IndexedDB schema.

  • Bump dbVersion from 8 to 9 in src/modules/gtfs-database.ts
  • Add | 'rider_categories' | 'fare_media' | 'fare_products' to GTFSStoreName union
  • Add three entries to GTFSDBSchema interface
  • Import RiderCategories, FareMedia, FareProducts from gtfs-entities
  • Add index cases in addIndexesForTable() for FK lookups on fare_products and name indexes on fare_media/rider_categories

Discovery: Version bump does a clean-slate wipe of all IDB stores (intentional per project's no-migration policy).


Phase 2: Fares Modal UI

Create src/modules/fares-modal.ts with a single exported showFaresModal(deps) function. Three DaisyUI tab panels, one per table. Each tab shows a list of existing records with add/edit/delete.

  • Define FaresModalDeps interface
  • Implement renderRiderCategoriesPanel, renderFareMediaPanel, renderFareProductsPanel — table of rows with edit/delete buttons per row and "Add" button at top
  • Implement showAddEditRiderCategoryModal, showAddEditFareMediaModal, showAddEditFareProductModal — nested showModal() with form HTML, validation, and patchManager.record*() calls
  • Implement showDeleteConfirmModal — nested confirmation dialog
  • Implement showFaresModal — tabs + event delegation + refreshPanel() helper

Discovery: fare_products composite key fields (fare_product_id, rider_category_id, fare_media_id) are disabled during edit since changing them would create key mismatch in IDB. Users must delete and re-add to change key fields. noImplicitReturns tsconfig rule required explicit return; at end of each async onClick handler.


Phase 3: Entry Point on Home Page

  • Add Fares section to renderHome() in page-content-renderer.ts with a "Manage Fares (V2)" button
  • Wire .fares-btn click to showFaresModal in addEventListeners()
  • Import showFaresModal in page-content-renderer.ts

Phase 4: Polish — Abstracted Field Rendering, Bigger Modal, V2 Note

Bring the fares forms in line with how all other browse panels render GTFS fields. The rest of the app uses generateFieldConfigsFromSchema + renderFormFields from src/utils/field-component.ts, which auto-generates labels with tooltips, colored presence asterisks (*), and spec links. The original modal used hardcoded HTML form fields with no tooltips or asterisks.

  • Add boxClassName?: string to showModal options in src/modules/modal-utils.ts; append it to the modal-box div class
  • In fares-modal.ts: update readFormValues to query by [data-field="${field}"] instead of [name="${field}"] (required because renderFormField outputs data-field, not name)
  • Add imports: generateFieldConfigsFromSchema, renderFormFields, FieldConfig from field-component.js; GTFSSchemas, GTFS_TABLES from gtfs.js; type { z } from zod
  • Refactor showAddEditRiderCategoryModal to use generateFieldConfigsFromSchema(GTFSSchemas[GTFS_TABLES.RIDER_CATEGORIES], existing ?? {}, GTFS_TABLES.RIDER_CATEGORIES) + renderFormFields
  • Refactor showAddEditFareMediaModal similarly with GTFS_TABLES.FARE_MEDIAfare_media_type auto-renders as a select via GTFS_ENUMS
  • Refactor showAddEditFareProductModal: generate base configs from GTFS_TABLES.FARE_PRODUCTS, then post-process rider_category_id and fare_media_id to custom selects (populated from loaded rows); set readonly: isEdit on those two FK fields in edit mode (they're part of the composite key but not marked isPrimaryKey in the spec)
  • Pass boxClassName: 'max-w-3xl' to the outer showFaresModal call for wider modal
  • Add "limited Fares V2" note above the tabs: <p class="text-xs text-base-content/60 mb-3">Supports a limited set of Fares V2: rider categories, fare media, and fare products. More tables coming soon.</p>

Discovery: GTFS_ENUMS is auto-derived from spec enumValues via deriveGTFSEnums in adapter.ts, so fare_media_type and is_default_fare_container are automatically in the enum registry and render as selects without any manual registration.


Phase 5: Fares Table Column Header Tooltips

The three panel tables in fares-modal.ts use plain hardcoded <th> text with no tooltips, presence marks, or spec links. The column headers should use the same renderFieldLabelContent(config) from src/utils/field-component.ts that the rest of the app uses for labeled fields, so hovering a column header shows the field description, presence level, and spec link.

Relevant context:

  • renderFieldLabelContent(config: FieldConfig) in src/utils/field-component.ts — renders label + presence mark wrapped in a tooltip; already exported
  • generateFieldConfigsFromSchema(schema, {}, tableName) — call with empty data {} to get configs for all fields (no value needed for headers)
  • GTFSSchemas[GTFS_TABLES.RIDER_CATEGORIES | GTFS_TABLES.FARE_MEDIA | GTFS_TABLES.FARE_PRODUCTS] — schemas already imported in fares-modal.ts
  • The action column (<th></th>) stays as-is (no field)

Steps:

  • Add a helper renderColumnHeader(fieldName: string, configs: FieldConfig[]): string in fares-modal.ts that finds the matching config by field and returns <th>${renderFieldLabelContent(config)}</th>, falling back to <th>${fieldName}</th> if not found
  • In renderRiderCategoriesPanel: call generateFieldConfigsFromSchema with the rider_categories schema and {} at the top of the function to build a configs array; replace the hardcoded <th>ID</th><th>Name</th><th>Default</th> with renderColumnHeader calls for rider_category_id, rider_category_name, is_default_fare_container
  • In renderFareMediaPanel: same pattern for fare_media_id, fare_media_name, fare_media_type
  • In renderFareProductsPanel: same pattern for fare_product_id, fare_product_name, rider_category_id, fare_media_id, amount, currency
  • Import renderFieldLabelContent from field-component.js (already imports generateFieldConfigsFromSchema, renderFormFields, FieldConfig — add renderFieldLabelContent to that import)

Gotcha: renderRiderCategoriesPanel, renderFareMediaPanel, and renderFareProductsPanel are currently pure functions with no schema imports — keep them pure by computing configs inline at the top of each function, not via a shared module-level variable.


Phase 6: Fares Navbar Button

Remove the "Fares" section from the home page (page-content-renderer.ts) and replace it with a ticket icon button in the navbar (src/index.html), wired up in src/index.ts.

Relevant context:

  • Navbar is in src/index.html lines 66–end of <div class="navbar-end">. The Files button (id="files-btn") is the pattern to follow: btn btn-ghost btn-sm btn-square hidden md:flex with a title attribute and an SVG icon
  • The fares button is currently rendered as .fares-btn in renderHome() in src/modules/page-content-renderer.ts and wired in addEventListeners() in the same file
  • showFaresModal is imported and called in page-content-renderer.ts; after this change it moves to src/index.ts
  • A ticket/fare SVG: use a simple ticket outline icon — Heroicons has a ticket outline icon (24×24 viewBox) that fits the existing btn-square pattern
  • src/index.ts is where all module wiring happens; GTFSEditor class owns the top-level event listeners

Steps:

  • In src/index.html: add a new <button id="fares-btn" class="btn btn-ghost btn-sm btn-square" title="Fares (V2)"> with a ticket SVG icon, placed before the Files button inside <div class="navbar-end">
  • In src/modules/page-content-renderer.ts: remove the entire Fares <section> block from renderHome() and remove the .fares-btn click listener from addEventListeners(); remove the showFaresModal import
  • In src/index.ts: import showFaresModal from ./modules/fares-modal.js; in the post-construction wiring section, add a click listener on document.getElementById('fares-btn') that calls showFaresModal with the appropriate gtfsDatabase and patchManager deps (same deps already passed to page-content-renderer — look at how patchManager is accessed there)

Gotcha: The navbar button should always be visible (not gated on a loaded feed), consistent with how the Files button works. If no feed is loaded the modal just shows empty tables with "No X yet" messages, which is fine.


Phase 7: Fix ID Field Editable on Add

The ID fields (rider_category_id, fare_media_id, fare_product_id) are rendered as disabled in both Add and Edit mode because generateFieldConfigsFromSchema unconditionally sets readonly: isPrimaryKey (see src/utils/field-component.ts line 618). The correct behavior is: readonly during Edit (can't change a primary key in place), but writable during Add (the user must supply the new ID).

Relevant context:

  • generateFieldConfigsFromSchema in src/utils/field-component.ts — always marks the primary key field with readonly: true; this is correct for edit but wrong for add
  • showAddEditRiderCategoryModal, showAddEditFareMediaModal, showAddEditFareProductModal in src/modules/fares-modal.ts — each calls generateFieldConfigsFromSchema then maps the result to add recordId; the post-processing step is where readonly must be fixed
  • For fare_products, rider_category_id and fare_media_id are already post-processed as custom selects with readonly: isEdit; only fare_product_id needs the fix here

Steps:

  • In showAddEditRiderCategoryModal: after the .map((c) => ({ ...c, recordId })) call, chain another .map that sets readonly: isEdit for the rider_category_id field (i.e., c.field === 'rider_category_id' ? { ...c, readonly: isEdit } : c)
  • In showAddEditFareMediaModal: same pattern for fare_media_id
  • In showAddEditFareProductModal: same pattern for fare_product_id (the rider_category_id and fare_media_id overrides that follow already set readonly: isEdit correctly)

Gotcha: readFormValues reads by data-field attribute; renderFormFields renders disabled inputs with the disabled HTML attribute (not readonly). A disabled input's value is not included in form submissions but IS readable via el.value in JS — so readFormValues will still pick up the value. No changes needed to readFormValues. Verify the ID value is still read correctly after the fix by checking that vals.rider_category_id is non-empty on save for the add path.


Original Issue

The files associated with GTFS-Fares V2 are:

  • fare_media.txt
    • independent
  • fare_products.txt
    • connects categories and media with prices, can be on some independent fares page
  • rider_categories.txt
    • independent

P2 (later):

  • fare_leg_rules.txt
    • complex as hell
  • fare_leg_join_rules.txt
    • also complex
  • fare_transfer_rules.txt
    • complex
  • timeframes.txt
    • required fk to service_id, so add these from service schedule page?
  • networks.txt
    • just names networks
  • route_networks.txt
    • required fk to routes, maybe show with routes?
  • areas.txt
    • just names areas
  • stop_areas.txt
    • required fk to stop, maybe show with stop?
    • some fancy stuff with stop parent station can be done here

Lets start by adding a simple modal that allows adding products, categories, and media.

## Summary Add GTFS Fares V2 support for the three "simple" tables: `fare_media.txt`, `fare_products.txt`, and `rider_categories.txt`. The GTFS spec files and TypeScript entity types are already defined in the codebase; what was missing was the database schema registration and all UI. The approach is a modal-based CRUD interface accessible from a new "Fares" section on the Browse home page. The modal supports add/edit/delete for all three tables with schema-driven field rendering (tooltips, presence asterisks, spec links) matching the rest of the app. ## Relevant Context **Files modified on this branch:** - `src/modules/gtfs-database.ts` — added three new IDB store registrations and indexes (schema v9) - `src/modules/fares-modal.ts` — new file (~800 lines), full CRUD modal for all three V2 tables - `src/modules/page-content-renderer.ts` — added Fares section to home page, wired to `showFaresModal` - `src/modules/modal-utils.ts` — added optional `boxClassName` to `showModal` for wider modals **Key utilities used:** - `generateFieldConfigsFromSchema(schema, data, tableName)` + `renderFormFields(configs)` in `src/utils/field-component.ts` — drives all form field rendering with tooltips/asterisks/spec links - `generateCompositeKeyFromRecord('fare_products', record)` in `src/utils/gtfs-primary-keys.ts` — builds the IDB key string for the composite-key `fare_products` table - `GTFS_ENUMS` in `src/types/gtfs-enums.ts` — auto-derived from spec `enumValues`, so `fare_media_type` and `is_default_fare_container` automatically render as selects **Architecture note:** The fares modal does NOT use the form-patch-bridge. Fields are rendered with `data-field` attributes (for visual consistency), but values are read manually in each Save handler and routed through `patchManager.record*()` directly. --- ## Phase 1: Database Schema Registration Register the three new tables in the IndexedDB schema. - [x] Bump `dbVersion` from `8` to `9` in `src/modules/gtfs-database.ts` - [x] Add `| 'rider_categories' | 'fare_media' | 'fare_products'` to `GTFSStoreName` union - [x] Add three entries to `GTFSDBSchema` interface - [x] Import `RiderCategories`, `FareMedia`, `FareProducts` from `gtfs-entities` - [x] Add index cases in `addIndexesForTable()` for FK lookups on `fare_products` and name indexes on `fare_media`/`rider_categories` **Discovery:** Version bump does a clean-slate wipe of all IDB stores (intentional per project's no-migration policy). --- ## Phase 2: Fares Modal UI Create `src/modules/fares-modal.ts` with a single exported `showFaresModal(deps)` function. Three DaisyUI tab panels, one per table. Each tab shows a list of existing records with add/edit/delete. - [x] Define `FaresModalDeps` interface - [x] Implement `renderRiderCategoriesPanel`, `renderFareMediaPanel`, `renderFareProductsPanel` — table of rows with edit/delete buttons per row and "Add" button at top - [x] Implement `showAddEditRiderCategoryModal`, `showAddEditFareMediaModal`, `showAddEditFareProductModal` — nested `showModal()` with form HTML, validation, and `patchManager.record*()` calls - [x] Implement `showDeleteConfirmModal` — nested confirmation dialog - [x] Implement `showFaresModal` — tabs + event delegation + `refreshPanel()` helper **Discovery:** `fare_products` composite key fields (fare_product_id, rider_category_id, fare_media_id) are disabled during edit since changing them would create key mismatch in IDB. Users must delete and re-add to change key fields. `noImplicitReturns` tsconfig rule required explicit `return;` at end of each async `onClick` handler. --- ## Phase 3: Entry Point on Home Page - [x] Add Fares section to `renderHome()` in `page-content-renderer.ts` with a "Manage Fares (V2)" button - [x] Wire `.fares-btn` click to `showFaresModal` in `addEventListeners()` - [x] Import `showFaresModal` in `page-content-renderer.ts` --- ## Phase 4: Polish — Abstracted Field Rendering, Bigger Modal, V2 Note Bring the fares forms in line with how all other browse panels render GTFS fields. The rest of the app uses `generateFieldConfigsFromSchema` + `renderFormFields` from `src/utils/field-component.ts`, which auto-generates labels with tooltips, colored presence asterisks (*), and spec links. The original modal used hardcoded HTML form fields with no tooltips or asterisks. - [x] Add `boxClassName?: string` to `showModal` options in `src/modules/modal-utils.ts`; append it to the `modal-box` div class - [x] In `fares-modal.ts`: update `readFormValues` to query by `[data-field="${field}"]` instead of `[name="${field}"]` (required because `renderFormField` outputs `data-field`, not `name`) - [x] Add imports: `generateFieldConfigsFromSchema`, `renderFormFields`, `FieldConfig` from `field-component.js`; `GTFSSchemas`, `GTFS_TABLES` from `gtfs.js`; `type { z }` from `zod` - [x] Refactor `showAddEditRiderCategoryModal` to use `generateFieldConfigsFromSchema(GTFSSchemas[GTFS_TABLES.RIDER_CATEGORIES], existing ?? {}, GTFS_TABLES.RIDER_CATEGORIES)` + `renderFormFields` - [x] Refactor `showAddEditFareMediaModal` similarly with `GTFS_TABLES.FARE_MEDIA` — `fare_media_type` auto-renders as a select via `GTFS_ENUMS` - [x] Refactor `showAddEditFareProductModal`: generate base configs from `GTFS_TABLES.FARE_PRODUCTS`, then post-process `rider_category_id` and `fare_media_id` to custom selects (populated from loaded rows); set `readonly: isEdit` on those two FK fields in edit mode (they're part of the composite key but not marked `isPrimaryKey` in the spec) - [x] Pass `boxClassName: 'max-w-3xl'` to the outer `showFaresModal` call for wider modal - [x] Add "limited Fares V2" note above the tabs: `<p class="text-xs text-base-content/60 mb-3">Supports a limited set of Fares V2: rider categories, fare media, and fare products. More tables coming soon.</p>` **Discovery:** `GTFS_ENUMS` is auto-derived from spec `enumValues` via `deriveGTFSEnums` in `adapter.ts`, so `fare_media_type` and `is_default_fare_container` are automatically in the enum registry and render as selects without any manual registration. --- ## Phase 5: Fares Table Column Header Tooltips The three panel tables in `fares-modal.ts` use plain hardcoded `<th>` text with no tooltips, presence marks, or spec links. The column headers should use the same `renderFieldLabelContent(config)` from `src/utils/field-component.ts` that the rest of the app uses for labeled fields, so hovering a column header shows the field description, presence level, and spec link. **Relevant context:** - `renderFieldLabelContent(config: FieldConfig)` in `src/utils/field-component.ts` — renders `label + presence mark` wrapped in a tooltip; already exported - `generateFieldConfigsFromSchema(schema, {}, tableName)` — call with empty data `{}` to get configs for all fields (no value needed for headers) - `GTFSSchemas[GTFS_TABLES.RIDER_CATEGORIES | GTFS_TABLES.FARE_MEDIA | GTFS_TABLES.FARE_PRODUCTS]` — schemas already imported in `fares-modal.ts` - The action column (`<th></th>`) stays as-is (no field) **Steps:** - [x] Add a helper `renderColumnHeader(fieldName: string, configs: FieldConfig[]): string` in `fares-modal.ts` that finds the matching config by `field` and returns `<th>${renderFieldLabelContent(config)}</th>`, falling back to `<th>${fieldName}</th>` if not found - [x] In `renderRiderCategoriesPanel`: call `generateFieldConfigsFromSchema` with the `rider_categories` schema and `{}` at the top of the function to build a `configs` array; replace the hardcoded `<th>ID</th><th>Name</th><th>Default</th>` with `renderColumnHeader` calls for `rider_category_id`, `rider_category_name`, `is_default_fare_container` - [x] In `renderFareMediaPanel`: same pattern for `fare_media_id`, `fare_media_name`, `fare_media_type` - [x] In `renderFareProductsPanel`: same pattern for `fare_product_id`, `fare_product_name`, `rider_category_id`, `fare_media_id`, `amount`, `currency` - [x] Import `renderFieldLabelContent` from `field-component.js` (already imports `generateFieldConfigsFromSchema`, `renderFormFields`, `FieldConfig` — add `renderFieldLabelContent` to that import) **Gotcha:** `renderRiderCategoriesPanel`, `renderFareMediaPanel`, and `renderFareProductsPanel` are currently pure functions with no schema imports — keep them pure by computing configs inline at the top of each function, not via a shared module-level variable. --- ## Phase 6: Fares Navbar Button Remove the "Fares" section from the home page (`page-content-renderer.ts`) and replace it with a ticket icon button in the navbar (`src/index.html`), wired up in `src/index.ts`. **Relevant context:** - Navbar is in `src/index.html` lines 66–end of `<div class="navbar-end">`. The Files button (`id="files-btn"`) is the pattern to follow: `btn btn-ghost btn-sm btn-square hidden md:flex` with a `title` attribute and an SVG icon - The fares button is currently rendered as `.fares-btn` in `renderHome()` in `src/modules/page-content-renderer.ts` and wired in `addEventListeners()` in the same file - `showFaresModal` is imported and called in `page-content-renderer.ts`; after this change it moves to `src/index.ts` - A ticket/fare SVG: use a simple ticket outline icon — Heroicons has a `ticket` outline icon (24×24 viewBox) that fits the existing btn-square pattern - `src/index.ts` is where all module wiring happens; `GTFSEditor` class owns the top-level event listeners **Steps:** - [x] In `src/index.html`: add a new `<button id="fares-btn" class="btn btn-ghost btn-sm btn-square" title="Fares (V2)">` with a ticket SVG icon, placed before the Files button inside `<div class="navbar-end">` - [x] In `src/modules/page-content-renderer.ts`: remove the entire Fares `<section>` block from `renderHome()` and remove the `.fares-btn` click listener from `addEventListeners()`; remove the `showFaresModal` import - [x] In `src/index.ts`: import `showFaresModal` from `./modules/fares-modal.js`; in the post-construction wiring section, add a click listener on `document.getElementById('fares-btn')` that calls `showFaresModal` with the appropriate `gtfsDatabase` and `patchManager` deps (same deps already passed to `page-content-renderer` — look at how `patchManager` is accessed there) **Gotcha:** The navbar button should always be visible (not gated on a loaded feed), consistent with how the Files button works. If no feed is loaded the modal just shows empty tables with "No X yet" messages, which is fine. --- ## Phase 7: Fix ID Field Editable on Add The ID fields (`rider_category_id`, `fare_media_id`, `fare_product_id`) are rendered as disabled in both Add and Edit mode because `generateFieldConfigsFromSchema` unconditionally sets `readonly: isPrimaryKey` (see `src/utils/field-component.ts` line 618). The correct behavior is: readonly during Edit (can't change a primary key in place), but writable during Add (the user must supply the new ID). **Relevant context:** - `generateFieldConfigsFromSchema` in `src/utils/field-component.ts` — always marks the primary key field with `readonly: true`; this is correct for edit but wrong for add - `showAddEditRiderCategoryModal`, `showAddEditFareMediaModal`, `showAddEditFareProductModal` in `src/modules/fares-modal.ts` — each calls `generateFieldConfigsFromSchema` then maps the result to add `recordId`; the post-processing step is where `readonly` must be fixed - For `fare_products`, `rider_category_id` and `fare_media_id` are already post-processed as custom selects with `readonly: isEdit`; only `fare_product_id` needs the fix here **Steps:** - [x] In `showAddEditRiderCategoryModal`: after the `.map((c) => ({ ...c, recordId }))` call, chain another `.map` that sets `readonly: isEdit` for the `rider_category_id` field (i.e., `c.field === 'rider_category_id' ? { ...c, readonly: isEdit } : c`) - [x] In `showAddEditFareMediaModal`: same pattern for `fare_media_id` - [x] In `showAddEditFareProductModal`: same pattern for `fare_product_id` (the `rider_category_id` and `fare_media_id` overrides that follow already set `readonly: isEdit` correctly) **Gotcha:** `readFormValues` reads by `data-field` attribute; `renderFormFields` renders disabled inputs with the `disabled` HTML attribute (not `readonly`). A disabled input's value is not included in form submissions but IS readable via `el.value` in JS — so `readFormValues` will still pick up the value. No changes needed to `readFormValues`. Verify the ID value is still read correctly after the fix by checking that `vals.rider_category_id` is non-empty on save for the add path. --- ## Original Issue The files associated with GTFS-Fares V2 are: - fare_media.txt - independent - fare_products.txt - connects categories and media with prices, can be on some independent fares page - rider_categories.txt - independent P2 (later): - fare_leg_rules.txt - complex as hell - fare_leg_join_rules.txt - also complex - fare_transfer_rules.txt - complex - timeframes.txt - required fk to service_id, so add these from service schedule page? - networks.txt - just names networks - route_networks.txt - required fk to routes, maybe show with routes? - areas.txt - just names areas - stop_areas.txt - required fk to stop, maybe show with stop? - some fancy stuff with stop parent station can be done here Lets start by adding a simple modal that allows adding products, categories, and media.
maxtkc self-assigned this 2026-04-21 23:35:34 +00:00
Author
Owner

almost perfect, double check fields and make sure we show everything (including age in tables). Maybe better way to show an overview?

almost perfect, double check fields and make sure we show everything (including age in tables). Maybe better way to show an overview?
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gtfs.zone/coloring-book#95
No description provided.