Ownware
Home›Fleetora›Test run
Fleetora · Test run · run 2026-09-25

Fleetora Test run, as shipped in the download

373 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-09-25 with php tests/run.php. Run it yourself after you download.

== 1. dates: a date is a real calendar date or it is nothing ==
  PASS  '2026-02-28' is a date (got '2026-02-28', want '2026-02-28')
  PASS  '2026-02-29' is not (2026 is not a leap year) (got NULL, want NULL)
  PASS  '2024-02-29' is (2024 is) (got '2024-02-29', want '2024-02-29')
  PASS  '0000-00-00' is refused (got NULL, want NULL)
  PASS  '2026-13-01' is refused (got NULL, want NULL)
  PASS  junk is refused (got NULL, want NULL)
  PASS  a datetime is truncated to its date (got '2026-06-15', want '2026-06-15')
  PASS  daysUntil forward (got 10, want 10)
  PASS  daysUntil backward (got -10, want -10)
  PASS  daysUntil today (got 0, want 0)
  PASS  daysUntil across a month end (got 29, want 29)
  PASS  daysUntil on a bad date is null (got NULL, want NULL)
  PASS  addDays crosses a year end (got '2027-01-09', want '2027-01-09')
  PASS  addDays backwards (got '2025-12-26', want '2025-12-26')
  PASS  addDays on a bad date is null (got NULL, want NULL)

== 2. expiryState: the four bands, and the fifth answer that is not a band ==
  PASS  yesterday is expired (got 'expired', want 'expired')
  PASS  today is critical, not expired (got 'critical', want 'critical')
  PASS  inside 7 days is critical (got 'critical', want 'critical')
  PASS  the 8th day is only soon (got 'soon', want 'soon')
  PASS  inside 30 days is soon (got 'soon', want 'soon')
  PASS  the 31st day is ok (got 'ok', want 'ok')
  PASS  no date recorded is "none", never "ok" (got 'none', want 'none')
  PASS  the day count comes back with the state (got 10, want 10)
  PASS  a 60-day warning window moves the boundary (got 'soon', want 'soon')
  PASS  a 0-day critical window still flags the day itself (got 'critical', want 'critical')

== 3. ⭐ serviceDue: whichever comes FIRST — the rule the product is sold on ==
  PASS  A: due date is computed from the date interval (got '2027-01-01', want '2027-01-01')
  PASS  A: due odometer is computed from the distance interval (got 50000, want 50000)
  PASS  A: days until (got 200, want 200)
  PASS  A: distance until (got 400, want 400)
  PASS  A: the DISTANCE leg is the one that bites (got 'distance', want 'distance')
  PASS  A: state is soon (got 'soon', want 'soon')
  PASS  B: the DATE leg is the one that bites (got 'date', want 'date')
  PASS  B: days until (got 10, want 10)
  PASS  B: distance until (got 9000, want 9000)
  PASS  B: state is soon (got 'soon', want 'soon')
  PASS  B2: two legs in the same band are not ranked against each other (got 'both', want 'both')
  PASS  B2: and the state is that shared band (got 'ok', want 'ok')
  PASS  C: overdue (got 'overdue', want 'overdue')
  PASS  C: both legs are past, so due_by says both (got 'both', want 'both')
  PASS  C: the date overrun is reported as a negative (got -135, want -135)
  PASS  C: the distance overrun is reported as a negative (got -8600, want -8600)
  PASS  D: state is unknown, never a guess (got 'unknown', want 'unknown')
  PASS  D: no due date is invented (got NULL, want NULL)
  PASS  D: no day count is invented (got NULL, want NULL)
  PASS  D: no distance-remaining is invented (got NULL, want NULL)
  PASS  D: but the due odometer IS known, because it is arithmetic (got 20000, want 20000)
  PASS  E: a missing reading is not hidden behind a healthy date leg (got 'unknown', want 'unknown')
  PASS  E: the date leg is still reported (got 351, want 351)
  PASS  F: no interval of either kind is "none" (got 'none', want 'none')
  PASS  F: due_by is null when nothing is tracked (got NULL, want NULL)
  PASS  G: due exactly today is critical (got 'critical', want 'critical')
  PASS  G: zero days until (got 0, want 0)
  PASS  H: 100 miles to go is critical (got 'critical', want 'critical')
  PASS  H: due_by is distance (got 'distance', want 'distance')
  PASS  H2: 300 miles to go is only soon (got 'soon', want 'soon')
  PASS  I: a 5,000 warning window makes 3,000 to go "soon" (got 'soon', want 'soon')
  PASS  J: after completion the date clock is full (got 365, want 365)
  PASS  J: after completion the distance clock is full (got 10000, want 10000)
  PASS  J: and it is no longer due (got 'ok', want 'ok')

== 4. urgencyRank: the order the whole product is sorted by ==
  PASS  expired and overdue rank together at the top
  PASS  the bands descend: overdue < critical < soon
  PASS  UNKNOWN sorts after everything actionable but BEFORE ok — it is a gap to close, not a pass
  PASS  not-tracked sorts last of the known states
  PASS  an unrecognised state sorts last of all rather than first

== 5. readingOk: an odometer does not run backwards ==
  PASS  a higher reading is accepted
  PASS  the same reading is accepted
  PASS  a lower reading is refused
  PASS  the refusal names the last reading, so the operator can see the typo
  PASS  the first reading on a vehicle is always allowed
  PASS  zero is a legitimate first reading
  PASS  a negative reading is refused
  PASS  an absurd reading is refused
  PASS  every refusal carries a sentence, never a bare false

== 6. dayPhrase: three different truths never collapse into one ==
  PASS  today (got 'today', want 'today')
  PASS  tomorrow (got 'tomorrow', want 'tomorrow')
  PASS  yesterday (got 'yesterday', want 'yesterday')
  PASS  forward (got 'in 12 days', want 'in 12 days')
  PASS  backward (got '12 days ago', want '12 days ago')
  PASS  unknown says WHY it is unknown (got 'no reading on file', want 'no reading on file')
  PASS  not tracked is not the same as unknown (got 'not tracked', want 'not tracked')
  PASS  a distance-due row says so rather than "no date" (got 'by distance', want 'by distance')

== 7. defects: the flow is a matrix, and the screen reads the same matrix ==
  PASS  open can be triaged, fixed or dismissed
  PASS  a fixed defect can only be reopened — the record of the fix stands
  PASS  a dismissed defect can only be reopened
  PASS  a status cannot transition to itself
  PASS  an unknown status has no moves at all
  PASS  the dropdown is generated from the SAME matrix the writer enforces
  PASS  severity is ordered worst-first, and immobilise is the worst

== 8. distance: formatted in the vehicle's OWN unit, never converted ==
  PASS  miles (got '49,600 mi', want '49,600 mi')
  PASS  kilometres (got '49,600 km', want '49,600 km')
  PASS  an unknown unit falls back to mi rather than inventing one (got '10 mi', want '10 mi')
  PASS  no conversion factor exists anywhere in the engine

== 9. csvCell: numeric-aware formula-injection guard ==
  PASS  a leading = is neutralised (got '\'=1+1', want '\'=1+1')
  PASS  a leading + is neutralised (got '\'+cmd', want '\'+cmd')
  PASS  a leading @ is neutralised (got '\'@SUM', want '\'@SUM')
  PASS  a leading tab is neutralised (got '\'	=1', want '\'	=1')
  PASS  a NEGATIVE NUMBER is left alone — it is data, not a formula (got '-1500', want '-1500')
  PASS  a negative decimal is left alone (got '-15.00', want '-15.00')
  PASS  a hyphenated word is still neutralised (got '\'-cmd', want '\'-cmd')
  PASS  ordinary text is untouched (got 'FL-1042', want 'FL-1042')

== 10. csrf_valid: the empty-token bypass (hash_equals('','') is TRUE) ==
  PASS  an unset session token can never match
  PASS  an unset session token rejects a supplied one
  PASS  a real token matches itself
  PASS  a real token rejects a wrong one

== 11. Pdf: produces a valid, non-trivial document ==
  PASS  starts with the PDF magic
  PASS  ends with the EOF marker
  PASS  is not a stub
  PASS  page placeholders are resolved, never printed literally

== PWA ==
  PASS  manifest.json is valid JSON
  PASS  manifest has required fields
  PASS  manifest URLs are RELATIVE (subpath-install safe)
  PASS  manifest icons exist on disk (192 + 512 + apple-touch)
  PASS  sw.js never caches non-GET
  PASS  sw.js never caches page navigations (auth HTML can not enter cache storage)
  PASS  sw.js cache name carries the version (old caches purged on activate)
  PASS  offline fallback page ships
  PASS  every app layout links the manifest + registers the SW

-- Own It 2.0 --
  PASS  v2: tables
  PASS  v2: totp_cols
  PASS  v2: smtp_cols
  PASS  v2: notify_defaults_off
  PASS  v2: warning_windows_seeded
  PASS  v2: fleet_tables
  PASS  v2: reg_is_unique
  PASS  v2: mint
  PASS  v2: revoke
  PASS  v2: totp
  PASS  v2: recovery
  PASS  v2: delivery
  PASS  v2: rate
  PASS  v2: csv_dry
  PASS  v2: backup_tables
  PASS  v2: upgrade_from_1x
  PASS  v2: serializer_matches_core
  PASS  v2: index.php calls Database::migrate() at boot — the ONLY upgrade path for an existing install
  PASS  v2: routes registered (api, 2fa, security, backup, import, healthz)
  PASS  v2: 2FA intercepts login + login is rate-limited
  PASS  v2: the API reuses the tested core (never re-derives a state or a guard)
  PASS  v2: no ternary key re-reads (PLAN item 10)
  PASS  v2: dark palette + zero hardcoded white backgrounds
  PASS  v2: the kit ships and the layout loads it + has a toggle
  PASS  v3: API.md ships + sw cache is v1.0.4
  PASS  v2: cli-server base guard present (PLAN item 9)
  PASS  v3: v3_tables
  PASS  v3: v3_columns
  PASS  v3: upgrade_restores_v3_tables
  PASS  v3: update_on_a_missing_settings_row_silently_does_nothing
  PASS  v3: migrate_ensures_the_settings_singleton
  PASS  v3: due_by_distance_when_distance_bites_first
  PASS  v3: due_by_date_when_date_bites_first
  PASS  v3: overdue_on_both_legs_is_overdue
  PASS  v3: distance_service_with_no_reading_is_unknown_never_guessed
  PASS  v3: no_interval_is_not_tracked_not_overdue
  PASS  v3: there_is_exactly_ONE_service_completion_writer
  PASS  v3: completing_a_service_resets_BOTH_clocks
  PASS  v3: a_completed_service_is_no_longer_due
  PASS  v3: a_reading_may_go_up
  PASS  v3: a_reading_may_not_go_down
  PASS  v3: a_first_reading_is_always_allowed
  PASS  v3: a_negative_reading_is_refused
  PASS  v3: every_door_uses_the_same_reading_guard
  PASS  v3: refused_reading_wrote_nothing
  PASS  v3: expiry_bands
  PASS  v3: a_zero_date_is_not_a_date
  PASS  v3: hero_merges_documents_and_services
  PASS  v3: hero_is_ranked_worst_first
  PASS  v3: hero_puts_unknown_after_actionable_but_before_ok
  PASS  v3: hero_counts_are_a_count_of_the_hero_rows
  PASS  v3: every_state_key_is_present_even_at_zero
  PASS  v3: expired_and_overdue_are_counted_separately
  PASS  v3: mcp_expiring_matches_the_engine
  PASS  v3: mcp_record_matches_the_engine
  PASS  v3: mcp_refuses_an_unknown_registration
  PASS  v3: every_read_tool_is_annotated_readOnly
  PASS  v3: write_tools_are_not_annotated_readOnly
  PASS  v3: a_read_only_key_still_sees_the_read_tools
  PASS  v3: mcp_write_hits_the_same_refusal
  PASS  v3: mcp_write_actually_writes
  PASS  v3: unknown_role_gets_the_most_restrictive
  PASS  v3: empty_role_is_treated_as_legacy_admin
  PASS  v3: a_driver_may_report_and_log
  PASS  v3: a_driver_may_not_edit_the_fleet
  PASS  v3: a_member_may_not_dispose
  PASS  v3: only_an_admin_owns_the_doc_types
  PASS  v3: a_viewer_writes_nothing
  PASS  v3: last_admin_cannot_be_demoted
  PASS  v3: a_driver_sees_report_and_log_but_the_register_stays_read_only
  PASS  v3: the_role_ladder_has_one_definition
  PASS  v3: every_role_survives_being_set
  PASS  v3: a_normalize_without_the_product_list_would_lose_the_driver
  PASS  v3: no_caller_normalizes_without_the_product_list
  PASS  v3: the_team_screen_offers_every_rung
  PASS  v3: defect_flow_allows_the_real_moves
  PASS  v3: defect_flow_refuses_a_jump_that_skips_reality
  PASS  v3: the_screen_offers_exactly_what_the_writer_accepts
  PASS  v3: mail_is_off_until_the_operator_arms_it
  PASS  v3: a_disarmed_notify_sends_nothing_and_says_so
  PASS  v3: the_same_notice_is_never_sent_twice
  PASS  v3: a_failed_notice_is_still_retryable
  PASS  v3: a_bad_address_is_refused_before_a_connection_is_opened
  PASS  v3: driver_mail_carries_no_cost_and_no_other_driver
  PASS  v3: the_installer_requires_only_modules_that_exist
  PASS  v3: the_installer_migrates_before_it_writes_a_row
  PASS  v3: the_installer_keeps_what_the_operator_typed
  PASS  v3: the_installer_seeds_neutral_document_types
  PASS  v3: healthz_answers_before_the_installer_redirect
  PASS  v3: a_dry_run_writes_nothing
  PASS  v3: the_report_and_the_import_agree_exactly
  PASS  v3: the_report_names_every_reason_a_row_is_skipped
  PASS  v3: the_import_wrote_exactly_what_it_promised
  PASS  v3: vehicle_disposed_is_declared
  PASS  v3: vehicle_disposed_actually_fires
  PASS  v3: disposal_closes_the_open_assignment
  PASS  v3: a_disposed_vehicle_leaves_the_hero_list
  PASS  v3: a_disposed_vehicle_is_still_in_the_full_list
  PASS  v3: fleet_pdf_engine_renders
  PASS  v3: feed_token_verifies
  PASS  v3: revoking_the_feed_locks_it
  PASS  v3: feed_renders_a_calendar
  PASS  v3: feed_omits_the_undateable_rather_than_guessing
  PASS  v3: brand_css_brands_the_light_theme
  PASS  v3: brand_css_brands_the_dark_theme_too
  PASS  v3: brand_dark_accent_is_legible_on_the_dark_canvas
  PASS  v3: unbranded_install_emits_no_brand_css
  PASS  v3: scheduled_backup_writes_a_dated_file
  PASS  v3: scheduled_backup_redacts_credential_material
  PASS  v3: scheduled_backup_prunes_to_the_retention_count
  PASS  v3: a_vehicle_row_holds_no_personal_data
  PASS  v3: the_personal_data_is_where_the_product_says_it_is
  PASS  v3: audit_records_the_writes
  PASS  v3: audit_detail_carries_no_secret
  PASS  v3: db_files_cleaned
  PASS  v3: the layout INJECTS the brand CSS (a helper nothing calls brands nothing)
  PASS  v3: the branding form exists and posts to the branding route
  PASS  v3: the logo is rendered, not merely stored
  PASS  v3: the calendar feed and the scheduled backup are reachable (helpers with a route)
  PASS  v3: the feed can be REVOKED from the screen, not only created
  PASS  v3: GET /mcp answers the transport hint BEFORE the key check
  PASS  v3: ONE completion writer — every door goes through _fl_service_complete_apply()
  PASS  v3: the hero endpoint does NOT own a second copy of the completion rule
  PASS  v3: the PDFs read Fleet/Registry rather than doing their own date arithmetic
  PASS  v3: no view posts to a route that does not exist
  PASS  v3: no POST route shares a path with a real directory
  PASS  v3: every route has a function to answer it
  PASS  v3: every function the controllers CALL actually exists
  PASS  v3: every view the controllers render actually exists on disk
  PASS  v3: the deploy kit is filled and ships inside app/ (where package.sh will find it)
  PASS  v3: the nine cores are byte-identical to the reference copy (or skipped when unavailable)
  PASS  v3: README documents the release and API.md gains the MCP section
  PASS  v3: the audit viewer reads the column the core actually writes
  PASS  v3: MCP domain refusals THROW McpRefusal, never return an error array
  PASS  v3: UI mutations + api-settings are role-gated, not merely logged in
  PASS  v3: a screen that renders a credential is gated on READ, not only on write
  PASS  v3: every sidebar link a role can SEE is a page that role can OPEN
  PASS  v3: no ungated write route beyond the by-design baseline (router-derived)
  PASS  nav: every sidebar link is highlighted by the page it opens
  PASS  nav: no page highlights a sidebar item that does not exist

== client_ip ==
  PASS  ip: a direct visitor's forged headers are ignored
  PASS  ip: behind a trusted proxy the forwarded visitor is used
  PASS  ip: CF-Connecting-IP wins over the X-Forwarded-For chain
  PASS  ip: the chain is read right-to-left, past trusted hops
  PASS  ip: a Cloudflare edge is not a trusted proxy by default
  PASS  ip: CIDR maths (v4 boundaries + v6, no cross-family match)

== UI kit ==
  PASS  kit: layout loads the kit css+js, groups the nav (>=3 groups), keeps the theme toggle and a who-line
  PASS  kit: no theme.js; style.css is an app layer with a dark accent; pre-kit scratch file removed
  PASS  kit: App::asset() is versioned by mtime and the service worker revalidates code
  PASS  kit: no font or script is fetched from another host (fonts bundled)
  PASS  kit: every structured empty state uses an icon the kit actually masks
  PASS  kit: no shipped asset is dead weight — every js/css file is actually loaded
  PASS  kit: every table uses the kit's WRAPPER contract, not class="tbl" on the table itself
  PASS  kit: no view leans on a CSS class the app layer never defines
  PASS  hero: the dashboard IS the ranked list, computed in the controller from the engine
  PASS  hero: the one-click action posts CSRF as a HEADER, because the body is JSON
  PASS  hero: the row is re-rendered from the SERVER answer, not from what the button hoped
  PASS  hero: a 422 asking for a fact is asked, never filled in with a plausible number
  PASS  dashboard: the first-run checklist is computed from the DATABASE, never a stored flag
  PASS  record: the vehicle page shows documents, servicing, defects, readings and who holds it
  PASS  honesty: the product says out loud what it cannot know
  PASS  country-neutrality: no country's compliance regime is hard-coded anywhere in the product
  PASS  country-neutrality: document types are ROWS the operator owns, with a screen to own them

== API-key scope ==
  PASS  scope: api_keys gains a scope column defaulting to full — an upgrade never loosens
  PASS  scope: requireKey reads the key scope and refuses a write on a read-only key
  PASS  scope: mintKey persists the chosen scope, and anything but read is full access
  PASS  scope: /mcp hands the key scope to the core, which refuses write tools per tool
  PASS  scope: every MCP tool declares whether it reads or writes, so a read-only key can see something
  PASS  scope: the key form lets an administrator mint a read-only key
  PASS  scope: the key-create route passes the chosen scope through to mintKey
  PASS  licence: every font ships the SIL OFL text for its own family beside it
  PASS  licence: LICENSE.txt is present and carries the IF WE STOP (CONTINUITY) clause

== fix wave: backups are admin-only ==
  PASS  backup: a viewer is refused the JSON backup (403, no tables)
  PASS  backup: a viewer is refused the database file (403, no SQLite bytes)
  PASS  backup: the Security page shows a viewer no backup buttons
  PASS  backup: a driver is refused the JSON backup (403, no tables)
  PASS  backup: a driver is refused the database file (403, no SQLite bytes)
  PASS  backup: the Security page shows a driver no backup buttons
  PASS  backup: a member is refused the JSON backup (403, no tables)
  PASS  backup: a member is refused the database file (403, no SQLite bytes)
  PASS  backup: the Security page shows a member no backup buttons
  PASS  backup: an admin still downloads the JSON backup
  PASS  backup: an admin still downloads the database file
  PASS  backup: the Security page still offers an admin both downloads

== fix wave: expiry and service-due reminders actually go out ==
  PASS  reminders: a fresh install writes to nobody
  PASS  reminders: switched on without smtp sends nothing and says so
  PASS  reminders: dry run names who would be written to and sends nothing
  PASS  reminders: each driver and the office get one message
  PASS  reminders: a driver hears only about their own vehicle
  PASS  reminders: a service notice says the service is overdue
  PASS  reminders: the office digest lists every item in window and the unknowns
  PASS  reminders: the same day run twice writes to nobody twice
  PASS  reminders: crossing into expired tells the driver once more
  PASS  reminders: a renewed document leaves the run
  PASS  reminders: with the driver switch off no driver is written to
  PASS  reminders: every reference fits the 60 character column
  PASS  reminders: the run is audited
  PASS  reminders: both saved settings are read by the sender (the defect: saved, read by nothing)
  PASS  reminders: cron/expiry.php exists and calls the one run
  PASS  reminders: cron/expiry.php refuses anything but the command line
  PASS  reminders: the web server refuses /cron/ (router and .htaccess)
  PASS  reminders: the button route exists and is admin-only
  PASS  reminders: an admin presses the button and is told what happened (here: mail is off)
  PASS  reminders: Settings shows the admin the schedule line and the button
  PASS  reminders: a viewer is refused the send-now button (403)
  PASS  reminders: a driver is refused the send-now button (403)
  PASS  reminders: a member is refused the send-now button (403)

== fix wave: every REST write checks the key holder's role ==
  PASS  api: a viewer's key is refused POST /api/vehicles (403, needs fleet.write)
  PASS  api: a driver's key is refused POST /api/vehicles (403, needs fleet.write)
  PASS  api: a member's key passes the role check on POST /api/vehicles
  PASS  api: a admin's key passes the role check on POST /api/vehicles
  PASS  api: a viewer's key is refused POST /api/vehicles/999/mileage (403, needs mileage.log)
  PASS  api: a driver's key passes the role check on POST /api/vehicles/999/mileage
  PASS  api: a member's key passes the role check on POST /api/vehicles/999/mileage
  PASS  api: a viewer's key is refused POST /api/vehicles/999/defects (403, needs defect.report)
  PASS  api: a driver's key passes the role check on POST /api/vehicles/999/defects
  PASS  api: a admin's key passes the role check on POST /api/vehicles/999/defects
  PASS  api: every /api/* write route checks the key holder's role (no key-only writes)

== fix wave: every backup and restore route is administrator-only ==
  PASS  backup: a signed-in viewer gets 403 on GET /backup.json
  PASS  backup: a signed-in viewer gets 403 on GET /backup.sqlite
  PASS  backup: a signed-in viewer gets 403 on GET /restore
  PASS  backup: a signed-in viewer gets 403 on POST /restore
  PASS  backup: a signed-in viewer gets 403 on POST /restore/commit
  PASS  backup: a signed-in viewer gets 403 on POST /restore/backup-token
  PASS  backup: a signed-in viewer without the token gets nothing from GET /backup/scheduled
  PASS  backup: a signed-in driver gets 403 on GET /backup.json
  PASS  backup: a signed-in driver gets 403 on GET /backup.sqlite
  PASS  backup: a signed-in driver gets 403 on GET /restore
  PASS  backup: a signed-in driver gets 403 on POST /restore
  PASS  backup: a signed-in driver gets 403 on POST /restore/commit
  PASS  backup: a signed-in driver gets 403 on POST /restore/backup-token
  PASS  backup: a signed-in driver without the token gets nothing from GET /backup/scheduled
  PASS  backup: a signed-in member gets 403 on GET /backup.json
  PASS  backup: a signed-in member gets 403 on GET /backup.sqlite
  PASS  backup: a signed-in member gets 403 on GET /restore
  PASS  backup: a signed-in member gets 403 on POST /restore
  PASS  backup: a signed-in member gets 403 on POST /restore/commit
  PASS  backup: a signed-in member gets 403 on POST /restore/backup-token
  PASS  backup: a signed-in member without the token gets nothing from GET /backup/scheduled
  PASS  backup: an admin still opens the restore page

== fix wave: the SMTP password is never written into the page ==
  PASS  smtp: a typed password is saved
  PASS  smtp: the settings page does not contain the stored password
  PASS  smtp: saving with the password field left blank keeps the stored password

== fix wave: one server name on every door ==
  PASS  mcp: the spec and the probe both name the server "fleetora"
  PASS  mcp: the probe answers with the same name
  PASS  ui: the sidebar brand cannot be shrunk to nothing (flex-shrink:0)

== fix wave: the webhook buttons work ==
  PASS  webhooks: Add creates the endpoint (no 404)
  PASS  webhooks: Pause switches it off (no 404)
  PASS  webhooks: pressing it again switches it back on
  PASS  webhooks: the page offers Pause and Delete for the endpoint, at those same addresses
  PASS  webhooks: Delete removes it (no 404)

-- F2b: the demo ribbon's wording and size; the JSON backup's secrets --
  PASS  demo ribbon: Fleetora has no public page, so a page served without sign-in keeps the admin wording too
  PASS  demo ribbon: an administrator's page keeps the admin wording
  PASS  demo ribbon: below 600px it starts as the small "Own it" pill; a tap opens it and the choice holds for the session
  PASS  demo ribbon: registered only behind the .demo-mode marker, and non-HTML output passes through untouched
  PASS  backup: the JSON download carries none of the 10 credentials this schema holds (settings.oidc_client_secret, settings.backup_token_hash, settings.feed_token_hash, settings.smtp_pass, users.password_hash, users.totp_secret, users.totp_recovery, api_keys.token_hash, webhooks.secret, invites.token_hash)

————————————————————————————————————————————————————
ALL 373 TESTS PASSED (0 failed)

← Back to Fleetora · Manual · Quickstart · API

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →