Ir al contenido

CLOVERTRONIX SOUTHERN EUROPE SOCIEDAD LIMITADA.

Información sobre CLOVERTRONIX SOUTHERN EUROPE SOCIEDAD LIMITADA. instancia de Odoo, el ERP de código abierto.

Aplicaciones instalados

Ventas
De presupuestos a facturas
Facturación
Facturas, pagos, seguimientos y sincronización bancaria
CRM
Seguimiento de leads y oportunidades próximas
Sitio web
Creador de sitios web para empresas
Inventario
Gestione sus actividades de inventario y logística
Contabilidad
Gestione la contabilidad financiera y analítica
Proyecto
Organice y planifique sus proyectos
Comercio electrónico
Venda sus productos en línea
Marketing por correo electrónico
Diseñe, envíe y rastree correos electrónicos
Partes de horas
Seguimiento del tiempo de empleados en las tareas
Studio
Cree y personalice sus aplicaciones de Odoo
Documentos
Recolecta, organiza y comparte documentos
Ausencias
Asigne permisos y siga las solicitudes de permisos
Empleados
Centralice la información de los empleados
Battery Locker Management
Module for managing battery lockers, including locations and reservations.
Clover Admin Dashboard
2.20.4: T-406 — GET /api/v1/build-info (Bearer): deployed git sha + ir.module.module latest_version for named modules. Truth endpoint for the external stagingwatch LaunchAgent that alerts on silently-failed Odoo.sh staging builds — a failed build leaves the OLD code+DB serving, so nothing inside Odoo can report its own dead build (clover_customer_app sat at DB 18.0.2.17.0 for three days, 2026-07-27..30, while the branch reached 18.0.2.19.2). git sha is read straight off the checkout .git with stdlib file reads (no subprocess) and degrades to null rather than 500-ing; modules require an explicit sanitised ?names= allow-list (max 100) and report latest_version, i.e. what migrations actually ran. Controller-only: no model, no view, no migration. 2.20.3: T-359 tail — every per-row figure labelled MRR now shows the monthly-normalised rate (recurring_monthly) instead of the per-cycle order total, which overstated non-monthly plans: subscriptions column + sort, mobile card, sub-detail Monthly value, alert-row /mo figures, and the MRR CSV export column. Billing tab Upcoming/Overdue keep the per-cycle amount that will actually be collected and are now headed Amount rather than MRR. Impact today is nil (Clover bills monthly on every plan so the two fields are equal) - this closes a latent leak, not a number anyone was misreading. dist/views.js rebuilt. 2.20.2 (T-361): staff can no longer dun a paying customer, and the unpaid digest stops hiding real debt. Both fixed by routing every "unpaid" question through the ONE canonical predicate now living in clover_kpi (_clover_unpaid_invoice_domain / _clover_unpaid_invoices / _clover_amount_owed): genuinely unpaid iff posted out_invoice AND payment_state in (not_paid, partial) — `in_payment` MEANS PAID. (a) /staff/subscription/send-reminder counted in_payment as unpaid AND summed those invoices amount_residual. Prod has no Outstanding Receipts account on any account.payment.method.line, so every Stripe-paid invoice sits at in_payment carrying its FULL residual forever — one click emailed a fully-paid customer "we were unable to process your latest payment. Outstanding: EUR <whole invoice>". Now gated on the canonical filter and it REFUSES to send ("No genuinely unpaid invoices") rather than sending a dunning email with no debt behind it. (b) models/account_move.py daily unpaid digest was built on the opposite (also wrong) theory — that payment_state could not be trusted and a linked done payment.transaction was authoritative. payment.transaction.invoice_ids is a m2m accumulating the WHOLE subscription history, so the join answered "did this subscription ever pay", not "was THIS invoice paid": every genuinely-unpaid invoice on a sub that had ever paid was silently suppressed, which is precisely all 9 real delinquents T-361 found (EUR 709.98, one customer 5 invoices deep). A false-negative delinquency alarm reads as an all-clear, so this was worse than no digest. Detection now delegates to clover_kpi; the transaction_ids join is gone; digest EUR uses residual over the genuinely-unpaid set. (c) /staff/subscriptions unpaid_residual read_group was already state-correct; migrated onto the shared domain so one place decides. Python-only: no new field, no view change, no migration. NOTE the 2.15.11 entry below documents the superseded (WRONG) rule — kept for history, do not follow it. 2.20.1 (T-388 Phase 1b): Inventory tab. New sidebar entry between Mechanic and Billing; InventoryView renders the parts catalogue from clover_workshop /staff/inventory/* — on-hand vs minimum (below-min amber, NEGATIVE red: negative stock is allowed and loud by design), Solo/Duo/Both scope chips, handover + warranty badges, search across CLVP code / name / R&M ref / barcode. Edit modal writes min (orderpoint), warranty months, bike scope, mfr ref and barcode-bind (scan into the field; conflicts rejected). +/- adjust modal requires a reason. New-part modal mints CLVP codes server-side — branding panels and seatpost screws enter here without a deploy. Count mode: start session (one draft shared across phones), per-row counted qty saved on blur, submit applies small deltas and parks large ones on an admin approval strip (snapshot-delta math server-side). Smoke test switches through the new tab (8/8). 2.20.0: staff can PIN an alarm the customer cannot undo (T-389). The refusal already existed — clover.fleet.cron._set_alarm rejects a customer disarm while stock.lot.alarm_admin_locked is set, and every customer path (/my, /mobile/customer/alarm, the app) funnels through it — but NOTHING in /staff could SET that flag: it was reachable only from the customer's own "Report stolen" tap and from a buttonless JSON route in clover_customer_app. So a staff arm was, in practice, always undoable by the customer from the portal. NEW ENDPOINT /staff/fleet/admin-lock — the exact mirror of the existing /staff/fleet/admin-unlock: auth="user" + _is_staff() + _assert_can_mutate() (a read-only share link cannot pin a bike), degrades with feature_unavailable when clover_customer_app is absent rather than reporting a lock nothing enforces, idempotent when already locked, and logs who/why to the bike's chatter + Slack. DELIBERATELY SEPARATE from /staff/fleet/alarm rather than folded into its arm branch: a routine arm (testing a device, arming a workshop bike) must stay customer-undoable — pinning is the exception and gets its own click, with a reason field. It does NOT arm the bike either, so "lock" can never fire a siren by surprise. /staff/fleet/bike-detail now returns alarm.admin_locked / locked_by / locked_at / lockable so the Fleet slide-over can tell "armed, and the customer can undo that" apart from "armed, and it is pinned" — shown as a 🔒 locked pill, a CUSTOMER CONTROL row in words, and a Lock/Unlock CTA whose confirm states the customer-facing effect before you commit it. locked_by is redacted in share sessions. Controller + JSX only, no schema change (the fields are clover_customer_app's). 2.19.1: discount-code profanity guard — /staff/discounts/create and /update never checked code content. Both now reject a code whose alnum-only, upper-cased form contains a blocked word (DISCOUNT_CODE_BLOCKLIST, controllers/main.py) with a clean error instead of Odoo's duplicate-code exception path; DISCOUNT_CODE_ALLOWLIST is the escape hatch for a legitimate code that collides with a banned substring. Controller-only, no schema change. Prompted by a live "NAZI##" code, which turned out to be a clover_referral (18.0.1.45) auto-generated referral code, not a promo code from here — a 2026-07-29 sweep (tools/reroll_bad_codes.py) found zero existing discount codes on the blocklist, so no manual archiving needed on this side. 2.19.0: WhatsApp tab (T-140 Phase A) — /staff becomes a full alternative to running customer service out of the #whatsapp-support Slack threads. Conversation list (filtered by needs-reply / human / bot / closed) + transcript + reply box + Close / Take over / Open tickets / Open contact. FIVE JSON ENDPOINTS, all auth="user" + _is_staff(), all three mutating ones behind _assert_can_mutate() so a read-only share link cannot message a customer: /staff/whatsapp/threads, /transcript, /reply, /close, /takeover. NOTHING IS REIMPLEMENTED — /reply routes through clover_whatsapp's own bot.relay_agent_text_status(), so a dashboard reply passes the identical three outbound gates (production-only, Meta's 24h free-form window, configuration), lands the same clover.wa.message log row and posts the same in-Slack failure notice as a Slack reply; /close is `!close`; /takeover reuses the module's own escalation with trigger "staff:manual" so a staff-initiated handover is indistinguishable from a customer-triggered one (fresh Slack parent, ticket opened by the bridge). THE UI IS BUILT TO NOT LIE: the gate verdict is returned and shown in words ("Meta's 24h window is closed…", "Suppressed: this is not the production instance…"), a blocked or failed outbound is drawn dashed-and-red as NOT SENT rather than as a normal message, the composer is replaced by the reason whenever a send cannot succeed, and "needs reply" means the customer spoke last AND a human owns the conversation (a bot-driven chat is not waiting on us). Replying to a bot-driven conversation is refused with "take over first" — clover_whatsapp's first routing rule is that the bot and a human never drive at once. A successful reply is mirrored into the Slack thread ("💻 <staff>: …") so two operators cannot answer the same customer twice; only on success, since a mirrored message that never reached the customer would tell Slack the case was handled. Whole tab degrades to a "not enabled" card when clover_whatsapp is absent (prod today), and the ticket column is guarded separately on clover_whatsapp_tickets. Python + JSX only, no schema change, no migration; dist/ rebuilt (components, views, app) and put through tools/dashboard_smoke_test.mjs. 2.18.0 (T-376, SECURITY + SCALE): share-link privacy, the last blob-only fleet readers, and the synchronous snapshot bootstrap. PRIVACY. (F4/ORM, Fable review) models/res_partner.py _investor_session() gated on scope==investor while randomize_map defaults TRUE and scope defaults all — and the dashboard fetches its customer records via a DIRECT rpc(res.partner, read) from app.jsx that bypasses every /staff/* controller and lands in the read() override. So the DEFAULT share link still served real names, emails, phones, VAT and home addresses even after the controller sweep below. The ORM gate now matches _is_redacted_view() exactly (share token present AND (randomize_map OR investor scope)); investor stays a strict superset and non-share sessions are untouched. Both layers must agree or a viewer joins a pseudonym in one surface to a real name in another and unmasks the HMAC scheme. Pinned by 3 checks in tools/share_redaction_test.py (negative-controlled: reverting the gate fails exactly one). (F2) /staff/fleet/admin-locked — the STOLEN-BIKE triage screen — had no share redaction at all: it returned real GPS and real customer names to share-link viewers in every scope, investor included. It now gets the same tail /staff/fleet/bike-detail already had (serial-keyed coordinate scramble, customer pseudonym, locked-by identity dropped). (F4) The identity redactors were gated on scope=='investor', but clover.staff.share defaults randomize_map to TRUE on the default scope "all" — so the DEFAULT share link scrambled the map and then served real names, emails and home addresses on every other tab, breaking the promise in randomize_map's own help text. One gate, _is_redacted_view() = share viewer AND (randomize_map OR investor), now drives every one of them; investor scope is a strict superset, so nothing that was redacted before stops being redacted. Routes audited and brought onto the gate: fleet/impact, kpi/snapshot, fleet/offline-alerts, fleet/bike-detail, fleet/admin-locked, fleet/alarm-log, subs/needing-bike, subscriptions, orders, bikes/available, bikes/search, search, chatter/read, customers/search, customer/read (+ticket/detail via the shared payload), tickets, discounts/usage, km-alerts, baskets, quotations, payments/attempts, payments/live, payments/reconcile, marketing/leads. Two bugs found while auditing: the shared redactor never touched the key "customer" (so subs/needing-bike read as redacted while returning the real name — in investor scope too), and it never blanked city/zip or the partner_* address keys /staff/subscriptions flattens onto each row. Both fixed. The subtler half of F4: pseudonymizing a SEARCH RESULT while still matching on "name ilike q" leaves the lookup intact, so a redacted viewer could type any name they can guess, get one hit, and deanonymize that pseudonym plus the serial it rides — which defeats the HMAC pseudonym everywhere else. The redaction now reaches the DOMAIN: /staff/customers/search returns nothing for a redacted session (it only feeds the new-ticket form, a mutation share viewers cannot perform) and /staff/search drops the partner-PII and client_order_ref clauses, keeping serial / frame / order-number matching. /staff/fleet/test now refuses share sessions outright, joining the three CSV exports that already did — it echoes a real IMEI and Comodule's raw response body, which carries the bike's true coordinates and cannot be redacted after the fact. (F13) An IMEI is a permanent device identity for hardware parked on public streets and was never redacted anywhere. Share sessions now receive an opaque per-share handle (imh_<hmac>_<serial>, keyed on the share redact_secret + token) and /staff/fleet/bike-location resolves it server-side, so the map live-refresh button still works and the viewer never holds the real number; the handle is inert in any other share and cannot be forged for a bike the share never showed. Applied at the snapshot/delta chokepoint — the biggest emitter by far, the whole fleet re-sent to every open tab — plus fleet/bikes, fleet/lots, fleet/bike-detail, active- and all-assignments, bikes/available, bikes/search, the ticket bike cards and the km-summary segments. NOT redacted, deliberately: support-ticket subject/detail and chatter BODIES are customer free text with no honest field-by-field scrub — flagged, not silently half-fixed (chatter AUTHORS are pseudonymized). WAVE 3. fleet/offline-alerts, fleet/offline-escalate, fleet/bike-detail, fleet/admin-locked and the ticket bike cards read ONLY the legacy ir.config_parameter blob, which the push path never refreshes — up to 15 minutes stale beside a real-time map, sharpest on the stolen-bike screen. All five are now table-first (clover.bike.live, indexed single-row lookups where they want one bike) with the blob kept as the fallback for a mixed deploy. SCALE. (C2) /staff/fleet/snapshot ran the full Comodule fan-out SYNCHRONOUSLY in an HTTP worker on an empty cache — ~600 serial rounds at 3,000 bikes, plus two cr.commit() on the REQUEST cursor, which broke request atomicity and released every row lock the request held. It now fires the existing async cron trigger and returns empty, exactly as the stale-snapshot branch already did; a fresh install shows an empty fleet for one cron cycle. New gate tools/share_redaction_test.py (38 checks, behavioural + AST structural). 2.18.0 (T-376 F7b): the live-table coverage check could not detect the failure it exists to catch, so it gets a second opinion. _check_live_coverage measures clover.bike.live against the poll's OWN tracked-lot domain — correct for "is the upsert dropping rows", and structurally blind to the domain itself resolving narrower. If the bike-category heuristic ever shrinks (a renamed category, a recategorized product, the hardcoded categ_id=6 fallback landing on the wrong row of a restored DB) the poll tracks fewer bikes, prune_missing deletes the live rows it no longer sees, and numerator and denominator shrink TOGETHER: 0% drift, green panel, half the fleet missing from the map. New check live_imei_floor compares the live table against a count that shares no criteria with the poll at all — stock.lot rows carrying a non-empty x_studio_gps_imei, no category filter, no dependency on clover_fleet_tracking (so it keeps answering mid-upgrade, which is exactly when a restored DB is most likely to have resolved categories wrong). Shortfall only, since surplus rows are the coverage check's business and it already reds on them; yellow >10%, red >40%. The red line is deliberately loose: a handful of non-bike lots may legitimately carry an IMEI, and a row that sits permanently red teaches everyone to ignore the panel — the failure this is built for loses 60-100% of the fleet, not 12%. Costs two aggregates per pass (one search_count, one COUNT(*)), no recordsets materialised; the field is probed on stock.lot._fields first so a database without the Studio column skips instead of raising. Both checks kept — a monitor whose expectation is computed from the thing it monitors can only report that the thing agrees with itself. Python-only, no model/field/XML change, no migration. Gate: tools/fleet_health_test.py 69 -> 89 checks, including TestCoverageBlindSpot which pins the exact scenario (coverage green AND floor red on one fleet state) so the two are never merged back into one. 2.18.0: T-376 round 3, FRONTEND — the /staff fleet dashboard stops fighting itself. Four correctness fixes and two hot-path fixes, all in the browser; no models, no schema, no migration. (1) ONE WRITER FOR THE FLEET. loadData and refreshFleetImpact each ran their own /staff/fleet/snapshot outside the live feed's single-flight guard and resolved with a wholesale replace. A snapshot issued at t0, a delta that merged newer rows and advanced the cursor at t1, and the t0 response landing at t2 = the t1 rows silently REVERTED and never re-sent, because the cursor is past them; the stray fetch also stamped the truth-up clock, pushing the self-heal out another 5 minutes. Every full-snapshot read now goes through fleetFeedTick. loadMap seeds from the snapshot the feed already applied when it is fresh, so a cold 3,000-bike load downloads the fleet ONCE (1.33 MB) instead of twice (2.65 MB). (2) AN EMPTY FULL SNAPSHOT NO LONGER BLANKS THE MAP. The delta path has refused to trust an empty full reply since T-366; the snapshot path had no such guard and the server legitimately answers empty (cache bootstrapping, a blob fallback no longer written). The rule now lives in applyFleetRows, so it holds for every caller, and the truth-up clock is left alone so the NEXT tick retries instead of waiting 5 minutes with an empty map — worst on the stolen-bike screen. (3) TIMEOUTS. There was no AbortController anywhere in the tree and the feed's busy flag was cleared only in a finally, so a socket that HUNG rather than rejected (sleep, VPN flap, captive portal — the days-long-open-tab case) left the flag stuck true and the live feed dead for the rest of the session, silently and unrecoverably without a reload. /staff/fleet/delta is now aborted at 12 s (under the 15 s tick, headers AND body), /staff/fleet/snapshot at 45 s, and a 60 s watchdog hands the feed to the next tick if a tick is still holding the flag; a per-tick token stops the abandoned request from freeing its successor's lock when it eventually settles. (4) SEARCH AUTO-FLY FIRES ONCE PER QUERY, NOT PER TICK. The effect depended on the filtered-visible list, which gets a new identity on every delta carrying a row, so a search matching exactly one bike yanked the operator's map back and force-reopened the popup every 15 s forever while uncancelled retry chains piled up. Measured in a real browser: 3 flyTo calls over 2 delta ticks before, 1 after. Retry chains are now cancelled and generation-tokened. (5) THE MECHANIC REBUILD IS OFF THE 15 s PATH. window.MECH.setFleet — which re-applies telemetry to every work order, reconciles the maintenance dispatch set and re-clusters it — ran on EVERY fleet update on EVERY tab, Overview included, although MECH.* is read by one component that only mounts on its own tab. Now gated on the Mechanic tab, depending on the three mapState sub-objects it actually reads rather than the whole object, and debounced. The clustering inside it filtered and sorted the entire candidate array per anchor with a distance function called twice per comparison — quadratic, and invisible in review because it lives in workshop_preview/assets/data.js. It is now a 2.2 km spatial hash with a bounded nearest-5 selection; output is byte-identical (33 cases incl. maximum tie density and exact cell-boundary points), setFleet 11.7 ms -> 4.0 ms at 3,000 bikes and 0 ms off-tab. (6) MARKER ICONS ARE MEMOISED. setIcon ran unconditionally for every marker on every pass with a freshly allocated icon, though only six distinct icons exist; for painted markers Leaflet rewrites innerHTML, so every visible pin re-parsed its inline SVG each tick and every alarm pin's pulse restarted from frame zero. Six shared instances, and setIcon only when the status actually changed (same reasoning that made setLatLng conditional in round 2). Verified in a real browser: 400 markers held 400 distinct icon objects before, 4 after. Gated by tools/run_gates.sh (11 gates, incl. the real-browser smoke test and the stale-dist tripwire) plus a 12-tab browser walk with zero engine errors. dist/ rebuilt in the same commit. 2.17.2 (SECURITY): dropped two over-broad ACL rows on the share system. (1) clover.staff.share granted READ to base.group_public — that table stores the share BEARER TOKENS and redact_secret; the /staff/share/<token> route reads it via .sudo() so the grant was dead weight, but it made every token unauthenticated-readable the moment any auth=public path reached the ORM. (2) clover.staff.share.access.log granted read+write+create to group_clover_staff, which includes the share VIEWER itself — a viewer could read the IP/user-agent audit trail of every other share and forge entries. Both models are only ever accessed via .sudo(); verified no functional path changes. Found by the Fable deep-dive audit, 2026-07-27. 2.17.1: T-366 round 2 — two independent changes ship together: system health for the new fleet machinery, and the fleet map hot path. SYSTEM HEALTH FOR THE NEW FLEET MACHINERY. Round 1 replaced the snapshot blob with a live table and a delta feed; none of it was observable, so the ways it can fail are all silent. Three new checks on the /staff health panel: (1) FLEET POLL PASS DURATION — reads the stamps clover_fleet_tracking 1.8.1 writes every pass, green <300s, yellow 300-600s, red >600s or no pass completed in 45 min. On a mixed deploy (dashboard upgraded first) the keys do not exist yet and it reports green-informational rather than a false red — "the cron is dead" is already the snapshot check's job and alerting twice for one fault is how a panel gets ignored. (2) LIVE-TABLE COVERAGE — clover.bike.live row count vs the poll's own count_tracked_bikes() (its literal _step1 domain, not a reimplementation), >10% drift yellow, >30% red, counted in BOTH directions: fewer rows means the upsert is dropping bikes and the map shows a smaller fleet than exists, more rows means prune_missing is not reclaiming decommissioned ones. Skipped green when the model or the helper is absent. (3) DELTA-FEED ERRORS — /staff/fleet/delta fails SOFT (a live-read error is caught and answered with a full blob set, so the map keeps working and nobody notices the delta feed is broken); its except branch now bumps clover.fleet.delta.error_count + .last_error_at and the panel shows them, yellow while errors are still arriving. The counter bump is last in the branch and fully swallowed so bookkeeping can never cost us the fallback. NEW THREE-LEVEL CHECK CONTRACT: a check may return (ok, detail) or (ok, detail, level in green/yellow/red); run_check derives ok FROM the level so the two can never contradict, and yellow does not count as failing, does not flip status.ok and does not fire a Slack transition — the one red-line alert is the fleet cron's, debounced to once per 24h. The panel JSX still colours off `ok` alone, so until it learns `level` a yellow row renders green with a "⚠" prefixed to its detail (added server-side on purpose, so yellow is visible with no JSX rebuild). ALSO: system_health's own fleet reads move table-first (clover.bike.live.get_snapshot_dict, blob fallback) matching the fleet_snapshot controller — reading the blob made the panel report staleness the fleet did not have, since pushes land in the table in real time and only the 15-min cron shadow-writes the blob — and a snapshot with rows but no generated_at stamp (restored DB) now reports itself instead of crashing the check on `now - False`. Gated by tools/fleet_health_test.py (69 checks). Python-only, no schema change, no migration. ALSO IN 2.17.1 — MAP HOT PATH: the fleet map's marker-sync hot path stops doing per-bike work that only one bike could ever use. Two costs that ran once per bike per refresh (so 3,000x a pass at plan scale) are gone. (1) COLOUR: the sync resolved each pin's CSS custom property with its own getComputedStyle() call — 3,000 forced style recalcs a refresh. That value is consumed only by the popup card, so it is no longer resolved in the loop at all; the popup resolves it through the memoised cssTokenColor() helper introduced for cluster bubbles. The memo is per-pageload with no invalidation, which is correct here — tokens.css custom properties are static for the life of the document (no theme switcher, nothing rewrites a property at runtime). It now caches only SUCCESSFUL reads, so a miss during the first paint (stylesheet not yet applied) returns the fallback without freezing that placeholder in for the session — a latent bug for cluster bubbles too. (2) POPUP HTML: the sync rendered the full ~150-line popup card for EVERY bike on EVERY pass purely to diff it against the last render. Popups are now LAZY — bindPopup takes a content FUNCTION that Leaflet evaluates on open, building the card from marker._bike for the one bike a human actually clicked. Closed popups cost nothing and open fresher than before (rendered at open time, not at the last poll). A popup that is CURRENTLY OPEN still live-refreshes, and keeps round 1's changed-only guard: it re-renders only when its HTML really differs, so a poll can never replace the DOM under an in-progress click on Arm / Live Track / Start-maintenance, nor wipe the in-place alarm-section patches components.jsx writes into the open card. Behaviour preserved exactly: clustering + bubble repaint, flyToSerial/openPopup retry and zoomToShowLayer path, hover/select emphasis, stale-location pin fading, share-mode redaction. JS-only change (no model, no view, no migration); dist/*.js rebuilt via `node tools/build_dashboard.mjs` in the same commit. 2.17.0: T-366 — the /staff fleet dashboard scaled for 3,000 bikes. Three independent bottlenecks removed: whole-fleet polling, one Leaflet marker per bike, and runtime JSX compilation. DATA — the fleet map stops re-downloading the whole fleet on every poll. NEW POST /staff/fleet/delta returns only the bikes whose clover.bike.live row changed since the client's cursor ({server_time, full, bikes}), with the same _is_staff() gate and share/investor redaction as the snapshot route; it answers full:true when the cursor is missing, unparseable, from the future, older than 10 min, or when only the pre-1.8.0 blob can answer (no per-row change stamp = no honest delta). The query reaches 5s further back than asked because a write_date is stamped before its transaction commits, so a strict cursor can step over a row. /staff/fleet/snapshot now serves from the live table (fresher — pushes land in it in real time) and falls back to the blob when clover_fleet_tracking is below 1.8.0 or its table is not yet populated, so a mixed deploy keeps working; identical JSON either way. Redaction for both routes goes through one shared _redact_fleet_payload() so a future endpoint cannot forget it — and it now fires for every share viewer, letting _maybe_redact_bike_record self-gate, instead of pre-checking randomize_map and skipping investor-scope shares. FRONTEND (app.jsx + components.jsx): the dashboard now CONSUMES that endpoint. It used to re-download the entire fleet on two independent timers — Overview every 60 s and the Map every 2 min, so three full downloads a minute with the Map tab open — regardless of whether anything had moved. Both are replaced by ONE feed: a full snapshot on load and again as a truth-up every 5 min, and in between a delta every 15 s carrying the previous reply's server_time. Rows merge BY SERIAL and bikes nobody reported keep their object identity, so three moving bikes cost three updated markers rather than a rebuilt fleet; a quiet tick returns zero rows and performs no setState at all. generated_at stays owned by the snapshot route alone — a delta reply has no cron stamp, and letting one write that field would turn the Topbar sync pill and the "Refreshing GPS…" badge into permanent lies; between cron runs a delta that carries rows is the freshness signal instead. Degradation is silent and total: a 404, a non-JSON body or a NotFound envelope (mixed deploy, rollback) disables deltas for the page immediately, three consecutive other failures do the same, and the feed reverts to the pre-T-366 60 s snapshot cadence — deliberately NOT the 15 s delta rate, which would have been four times heavier than the behaviour being replaced. An empty full:true reply is never trusted to blank the fleet; it forces a real snapshot next tick instead. Share/investor viewers use the same path (the server redacts both routes identically). Gate: tools/fleet_delta_feed_test.js, 36 cases over the real sliced source. MAP — /staff Map drew one Leaflet divIcon per bike straight onto the map, which is already sluggish at ~200 markers on a phone and would freeze the tab at the planned 3,000. Bike pins now go into a leaflet.markercluster group (v1.5.3 from cdnjs, added in dashboard.html; maxClusterRadius 60, disableClusteringAtZoom 16, spiderfyOnMaxZoom, chunkedLoading, no hover coverage polygon) with a custom bubble: the child count, coloured by the WORST child status via BIKE_STATUS_META.rank + PIN_STYLE, plus the status glyph as a corner badge — an alarm can never hide inside a calm green bubble. Adds/removes are batched through addLayers/removeLayers, and markers that actually changed position are re-seated (markercluster grids on insert position) except while their popup is open. flyToSerial/focus waits for the target to un-cluster at zoom 16 and falls back to zoomToShowLayer(); hover emphasis, selection, popup-content reuse, stale-location opacity and stale-marker removal all preserved. window.cloverMap.clusterGroup — a slot components.jsx and app.jsx already read but nothing ever filled — is now published, so their unspiderfy-before-pan paths work. GUARDED end to end: if the CDN fails, window.L.markerClusterGroup is undefined and every marker falls back to the exact pre-T-366 direct addTo (unclustered, never blank), same contract as Leaflet-Geoman. mechanic-v2.jsx untouched (small filtered sets, out of scope). The map change is JS/HTML only; THIS module adds no model, field, XML or migration in 2.17.0 — the delta feed reads clover.bike.live, which is declared and migrated by clover_fleet_tracking 1.8.0. ASSETS — the /staff dashboard no longer ships a compiler. Babel-standalone was transpiling ~27k lines of JSX in-browser on every page load (the #1 mobile perf cost, ahead of the 3,000-bike scale-up); the JSX is now precompiled at build time instead of Babel-standalone transpiling ~27k lines in-browser on every page load (the #1 mobile perf cost, ahead of the 3,000-bike scale-up). New tools/build_dashboard.mjs (esbuild, pinned in tools/package.json) transpiles each of components.jsx, views.jsx, map.jsx, workshop_preview/assets/icons.jsx, mechanic-v2.jsx, analytics.jsx and app.jsx SEPARATELY — bundle:false, jsx transform, target es2018, whitespace-only minify, NO identifier minification — into committed static/src/js/dist/*.js, preserving the existing window.*-assignment cross-file contract exactly (verified: every window.X export/read matches the pre-change Babel output). dashboard.html drops the babel-standalone CDN script and points straight at dist/; icons.jsx no longer needs its Babel-via-MIME-workaround since dist/icons.js is served as a real .js file. The /staff controller's existing ?v=<mtime> cache-bust regex already matches js/dist/ paths unchanged (verified against the real filesystem — no controller edit needed). workshop_preview/index.html (the separate design-preview route, not /staff itself) is untouched and still Babel-compiles its own JSX at runtime. Rebuild rule: any .jsx source change must re-run `node tools/build_dashboard.mjs` and commit the regenerated dist/*.js in the same commit — see the header comment in that script. dist/*.js in this release is built from the FINAL merged JSX — i.e. it already contains the Track B clustering and the Track E delta feed above. Staff operations dashboard for Clover subscriptions and fleet. 2.16.0: T-359 one-MRR — Overview daily lines consume the canonical engine series (own recon + 2.15.9 anchor hack DELETED); tip==headline by construction with a visible drift chip if they ever diverge (no more silent pinning); all-time window; MRR delta = vs 30 days ago (was June-vs-May cash — confusing); headline shows — when live KPI missing (never a wrong number); /staff/subscriptions rows carry kpi_category + recurring_monthly; Bearer API categories now from engine categorise_subs (brain business.json bucket counts shift by design: ambassador≈3→19-style, paying+discounted mrr == canonical headline ex VAT); Customers tab uses server categories; /staff/kpi/debug consistency panel. Staff operations dashboard for Clover subscriptions and fleet. (2.15.11 [ITS UNPAID RULE WAS WRONG — SUPERSEDED BY 2.20.2, DO NOT FOLLOW THIS PARAGRAPH]: daily unpaid-invoice Slack digest to the ops channel (models/account_move.py + cron_clover_unpaid_invoice_digest, 08:15 daily) — customer invoices genuinely unpaid >48h. "Genuinely" = no completed payment.transaction: amount_residual and payment_state are deliberately NOT trusted, because every Stripe-paid invoice on prod sits at in_payment with a full residual forever (~EUR 13.2k phantom AR, the same trap that broke the T-314 sweep). One capped digest per day rather than per-invoice alerts, header always states the true total; silent when nothing is unpaid. 2.15.10: new public-safe GET /api/v1/investor endpoint (controllers/api.py) for the external, login-less investor portal (T-338). Sibling to /api/v1/impact: same Bearer-key auth (clover.kpi.api.key), auth=public, GET/OPTIONS, hand-curated public-safe aggregates only — net/potential MRR, retention (ever-subscribed distinct partners / active / %), fleet deployed+total, lifetime km, ARPU actual+list, courier and Solo/Duo plan mix (products resolved by name at request time — no env-specific ids), waitlist-without-bike, and the daily MRR/active history (clover.kpi.snapshot). Canonical figures come from clover.kpi.get_snapshot(); no partner PII, invoices, cash/runway or on-hand fleet exposed; missing sources degrade to 0, never 500. Compute-only — no models, fields, XML or migration.) (2.15.9: Overview daily MRR/potential lines — level-anchor to the live headline (kpi_daily_series). The 90-day reconstruction sums every active sub at its CURRENT forward price from day one, but the cash-categorised headline (clover.kpi.get_snapshot) excludes free-this-month promo subs until their first paid invoice, so with many promos live the recon line ran ~€1.3k above truth and the client pin yanked only the tip down — a phantom peak + 2-day slump. Both money series are now multiplicatively scaled so their last point equals the live headline (factor clamped to 0.5–2.0; an insane factor leaves the series un-anchored); the zero launch plateau and the curve shape are preserved. Compute-only — no stored fields, XML or migration; JSX untouched.) (2.15.8: mechanic-v2.jsx + mech-v2.css — design-system polish of the 1,000 km SOP checklist (T-330). The summary card, "Open checklist" modal, block headers and step rows moved off ad-hoc inline styles onto real .sopck-* classes scoped under .mech-v2: periwinkle progress bar + N-of-M / All-done state, a rounded peri-soft modal header band with serial + "1,000 km LITE" + live left/done chip, peri-accented block bands, ref-code badges, and torque/spec values rendered as monospace pills. Presentation only — no data flow, RPC, step-update payload, hard-gate or prop change. CSS/JS is mtime cache-busted; no module upgrade needed. clover_workshop unchanged.) (2.15.7: map -> SOP checklist round-trip. The fleet-map popup button was repurposed from the blind window.cloverMarkDone to window.cloverStartMaintenance (map.jsx): it POSTs /staff/workshop/begin, which creates + STARTS the next-due maintenance WO server-side so its SOP checklist is instantiated, then the dashboard jumps to the Mechanic tab and opens THAT work order in the gated WorkOrderPanelV2 (Mark-done blocked while any SOP step is outstanding). app.jsx exposes window.cloverStartMaintenance (POST begin + setTab + hand-off, polling up to 4s for the Mechanic bridge since MechanicViewV2 mounts only when its tab is active) and window.cloverFocusBikeOnMap (the return leg). mechanic-v2.jsx registers window.MECH.openWorkOrder (opens the panel by normalised WO id + refreshes MECH so the freshly-created WO resolves) and, on a SUCCESSFUL gated Mark-done ONLY, fires the new onDoneReturn prop -> window.cloverFocusBikeOnMap re-selects the same bike and flies to its pin. A checklist-gate rejection stays on the panel (the done RPC rejects, so onDoneReturn never fires). window.cloverMarkDone stays defined - RouteStopV2 still uses it. Frontend-only; pairs with clover_workshop 18.0.0.8.1.) (2.15.6: T-332 follow-up — display-honesty fix for the fleet-map "Last service" label. 2.15.5 made the stored maintenance value the snapped TIER (e.g. 400) so next-service math advances correctly, but the popup then showed "at 400 km" even when the mechanic physically serviced at 326 km. The tier keeps driving ALL calc (getNextInterval / maintColour / maintClass / urgency / bikeStatus / nextMilestone / Fleet-Attention) unchanged. Only the label now prefers the real reading: /staff/fleet/maintenance get_all additively returns records_odo (latest done maintenance clover.work.order.odometer_at_done per serial, one search + first-per-lot in Python, milestone-0 included); app.jsx carries it as mapState.maintenanceOdo; map.jsx wires b.lastServiceOdo through buildBikes and prefers it in lastSvcTxt, falling back to the tier for legacy bikes with no WO history; window.cloverMap.setMaintenance gained a 3rd optional `odo` arg (components.jsx markdone modal + views.jsx cluster onMarkDone both now pass the confirmed odometer) and is backward-tolerant when omitted. Read-only — no new writes.) (2.15.5: T-332 maintenance-tier advance — the two optimistic window.cloverMap.setMaintenance() call sites (components.jsx markdone modal, views.jsx cluster onMarkDone) now prefer the server-returned last_service (the snapped TIER, e.g. 400/1000/2000km) over the raw odometer, falling back to the odometer only when the response has no last_service or fires before one arrives. Pairs with the clover_workshop /staff/workshop/done server-side tier-snap fix. mechanic-v2.jsx onDone callback only drives the route-stop strike-through/toast — writes no maintenance value, so it is unchanged. Read-side threshold logic (MAINTENANCE_INTERVALS / getNextInterval / maintColour) untouched.) (2.15.4: T-331 customer-drawer ID fix — the drawer now reads the checkout identity field (identity_doc_number) as a read-only "from checkout" fallback when the canonical clover_identity_doc is blank, ranked ABOVE the legacy vat / order-ref fallbacks. Self-served customers whose ID only ever lived in the checkout field were showing blank in /staff; they now appear, and one Edit→Save migrates the value into the canonical field. Pure display — no write, no delete; pairs with website_sale_identity_compliance 18.0.1.0.27 which mirrors new checkout IDs into the canonical field going forward. Investor share-scope still blanks it.) (2.15.3: mechanic-v2.jsx — the WO panel SOP checklist is now a compact progress summary + "Open checklist" that launches a large centered modal (portal to body so the .so drawer transform doesn't trap the overlay; Esc / X / overlay-click close). The panel Mark-done button now hard-gates: disabled + "N steps left" while any SOP step is unsatisfied (outstanding count lifted from WoChecklistSection to WorkOrderPanelV2), and server rejections are toasted. Matches the clover_workshop 18.0.0.7.7 backend gate. Markdown-fallback checklist (no instantiated steps) stays inline.) (2.15.2: Overview MRR line — kill the phantom dip (T-314). kpi_daily_series used to RECONSTRUCT contracted MRR (Σ each active sub's forward_monthly × VAT) for days before the first clover.kpi.snapshot row, then SPLICE in the stored snapshot mrr_inc_vat from that day on. The two measures disagreed ~€2k at the seam (7 Jul recon €9,491 vs 8 Jul snapshot €7,409) so the Contracted-MRR line showed a cliff DOWN while active subs rose — a method-splice artifact, not real churn. Worse, the first snapshots (8-9 Jul) were captured DURING the T-314 billing outage, when the dead renewal cron left active subs looking uncounted, so their stored mrr is depressed. Fix: the MRR + Potential daily lines now use ONE consistent method (reconstruction) across the whole window; no snapshot splice. The card still pins today's point to the live headline. The snapshot table is unchanged and still serves point-in-time deltas elsewhere. Dashboard-only; no money-model change.) (2.15.1: T-314 billing-engine fix — SaleOrder._create_recurring_invoice now passes STRAIGHT THROUGH to super on an EMPTY recordset. The daily subscription cron calls it with no records and relies on stock searching due subs internally; filtering the empty set and returning early short-circuited the ENTIRE renewal engine on every cron run (the June-2026 billing outage — zero automated renewals since Jun-12). The pending-return exclusion is moved into a new _recurring_invoice_domain override so the cron's internal search enforces it too; the recordset filter stays for the manual "renew now" path. The negative-invoice floor guard still wraps cron-created renewals so Stripe can never get a negative charge.) (2.15.0: Overview KPI cards — daily line charts instead of the month-bucket bars. New LineMini SVG chart (hover readout with value + date, first/last date labels): Contracted MRR and Potential MRR plot 90 daily points from clover.kpi.snapshot where rows exist (since 3 Jul 2026) and a start/churn-date reconstruction before that; the Active subs · Bikes out card gets a dual line reconstructed exactly from sub and assignment dates. Served as a `daily` block on /staff/analytics/overview (clover.analytics.kpi_daily_series); cards fall back to the old bars while a cached pre-2.15 payload is live. The last point of every series is pinned to the live headline so the curve tip always equals the number above it.) (2.14.2: share-viewer sessions now get a 403 on the three PII CSV exports (subscriptions/export-detail, fleet/export-bikes-detail, marketing/export) — the res.partner redaction layer does not cover those sudo-getattr code paths; plus zones/delete hardening — model-presence guard + safe int cast instead of a 500.) (2.14.1: zone editor fix — existing-zone click-to-edit is suppressed while a Geoman draw is in progress; the city-wide operating-area circle was swallowing every vertex click and popping its Edit-zone form mid-draw.) (2.14.0: T-302 geofence zones on the Map tab — render zones coloured by category with a legend + per-category show/hide, and a Leaflet-Geoman "Edit zones" draw/edit/delete editor (circle+polygon) backed by /staff/fleet/zones CRUD endpoints. Re-landed on top of 2.13 after the fleet 1.7.0 migration fix; the 2.12 fleet-version workaround is obsolete.) (2.13.0: "Update now" button next to the Live pill — forces a genuine live pull (real Comodule GPS fan-out via force + force-refresh of every cached number), distinct from the lighter ↻ Refresh (cache re-read only). Shows a spinner while running and a brief "Update already running…" hint when the server debounces a concurrent click (in-flight lock). Frontend-only.) (2.12.0: T-305 review fixes — the shared cache now serializes date/datetime values (default=str) so the KPI snapshot actually caches instead of silently recomputing every call; manual Refresh buttons now pass force=true to bypass the cache (were showing up-to-5-min-old numbers); a 10-min full-page auto-refresh keeps the lists (subs/orders/billing) honest under the "Live" pill; the fleet-refresh lock is now a claim-by-token (closes a two-worker double-fan-out race); and a migration re-asserts the fleet snapshot cron to 15-min + active on upgrade — placed HERE, not in clover_fleet_tracking, so fleet stays <1.7.0 and the reverted-T-302 geofence migration can never re-run. Async catch-up now warns when the cron is disabled (a trigger on an inactive cron is a no-op).) (2.11.0: T-305 LIVE dashboard — the Overview now auto-polls the shared server cache (fleet snapshot 60s, KPI+impact 5min, analytics 15min, paused when the tab is hidden) so every viewer converges on the SAME numbers without a manual refresh; the sync pill shows the shared snapshot time ("Live · data HH:MM"), amber past 20min. Server-side: /staff/fleet/refresh is debounced (skips the Comodule fan-out if the snapshot is <5min fresh or a run is in-flight) and /staff/fleet/snapshot nudges the cron ASYNChronously when >20min stale instead of letting each browser fan out — killing the "N open dashboards = N x 205-bike Comodule storms" bug. KPI, fleet impact and both analytics endpoints are now TTL-cached in ir.config_parameter and shared across viewers; redacted (investor/share) views bypass the cache so PII never leaks or poisons it.) (2.9.0: Analytics tab V2 (T-301) — sub-tab layout (Overview/Growth/Retention/Money/Fleet), full-width charts with real axes + hover tooltips + Expand-to-fullscreen w/ data table + copy-CSV, server-side delta chips, and nine metric fixes (churn date chain, plan matcher + diagnostics, fleet on-hand = all bike lots, monotonic funnel, real ARPU/MRR history only, distinct-bike workshop counts, telemetry-stale guard). (2.8.0: NEW Analytics tab (T-301) — clover.analytics model + /staff/analytics2/* endpoints + analytics.jsx: pulse KPI cards, needs-attention strip, Growth/Churn/Money/Fleet sections, all server-computed (forward MRR, cohort flow, recovery funnel, unit economics). Adds clover_cancel_reason_code taxonomy on sale.order for the churn-reasons chart. (2.7.33: column-sort headers now show a resting ⇅ cue so it is obvious they are clickable (turns into ▲/▼ when active). NEW: Subscriptions CSV export with selectable columns + full detail (ref/status/plan/MRR/next-invoice/customer PII/bikes) via /staff/subscriptions/export-detail — mirrors the existing Fleet export; exports the current filtered+sorted list, staff-gated, read-only. (2.7.32: negative-invoice guard on discounts — fixed (euro) loyalty discounts are now capped at entry (€200 default, ICP clover.discount.fixed_ceiling) in /staff/discounts create + update (percent was already capped, euro was unbounded → a "€500 off" on a €69 plan made a negative invoice). Belt-and-suspenders: SaleOrder._create_recurring_invoice now floors any draft renewal invoice that still goes negative to €0 (scales the discount line) + logs + posts to chatter, so Stripe never gets a negative charge. Credit notes / refunds / free-month date-shift are untouched (different mechanisms). (2.7.31: T-286 click-to-sort on the Subscriptions + Fleet tables — tri-state per column (asc → desc → clear), numeric / locale-string / date-aware comparators, blanks always sink last, sort composes with existing filters + search; one shared comparator + header cell, no per-table forks. T-139 the bike-side "pick a subscription" assign modal now shows the full "Already assigned to <customer> — <sub #>" card (it previously dropped the customer/sub the backend already returns). (2.7.30: fleet search now matches the real FRAME NUMBER (R&M VIN-style, e.g. WRJ2U61F1PD301425). It lives in a Studio field on stock.lot, distinct from the serial — the Fleet + Map feeds never fetched it, and the Fleet tab's "frameNo" was wrongly wired to assignment.frame_no (a related to lot.name = the serial). Now /staff/fleet/bikes, /staff/fleet/lots and /staff/bikes/available all return the real frame_no (shared _lot_frame_no resolver), and the Fleet, Map + assign-picker searches match frame number + IMEI. (2.7.29: customer card shows both mobile + phone when both exist. rewrites telemetry on DONE work orders, so a closed WO's bike-history "Latest odometer" keeps the odometer captured at completion instead of showing today's live value (verified by a 6-surface audit swarm; only this + a low-risk bike-picker hint were flagged, rest confirmed fully synced). (2.7.27: Mechanic tab now shares the Map tab's fleet data — one source of truth. The Mechanic surface (MechanicViewV2) used to re-derive bike location / odometer / km-overdue / which-bikes from the /staff/workshop/* endpoints, so it drifted from the (accurate) Map tab. app.jsx now pushes the Map's canonical per-serial telemetry into the workshop data layer (MECH.setFleet); every WO/queue row's location + odometer + km-overdue is overridden from it, and the maintenance dispatch set is reconciled to the Map's own due-bike predicate (drop stale synthetics, mint missing due bikes). Backend maintenance-queue interval list also aligned [400..10000] to match the Map at source. (2.7.26: T-268 — bike CONDITION TAGS on the Fleet view. Each bike shows small coloured condition chips (Not rideable / Needs cleaning / Pending inspection / …); inline +tag control adds/removes/mints tags per bike; a Condition dropdown filters the list to bikes carrying a tag. Display-only — no park-guard / assignment / billing coupling. New /staff/fleet/tags, /staff/fleet/set-tags, /staff/fleet/create-tag endpoints. (2.7.25: UNIVERSAL search — the ⌘K palette now queries /staff/search live, matching ANY detail: bike serial / frame no / IMEI, customer name / email / phone / mobile / address / ID doc / referral, and subscription # / customer-ref. Results grouped (Customers/Bikes/Subscriptions) and open the right drawer. (2.7.24: Tickets tab fleshed out — staff can raise a ticket for a customer (customer typeahead + bike dropdown + type + priority + subject/notes), priority badges + bike + staff marker in the list, priority editable inline; /staff/ticket/create + /staff/customers/search. 2.7.23: Park-guard health line shows LOCKED when the portal (customer disarm) is absent. (2.7.22: ticket bike card gains inline Arm/Disarm + "Details & location" deep-link to the Fleet slide-over; ticket chatter fixed (clover.support.ticket added to the chatter allowlist — was "Model not allowed"); alarm-log endpoint now returns the durable arm/disarm history; staff arm/disarm writes to it. 2.7.21: Tickets tab detail — opening a ticket now shows who opened it, the customer card (contact + ID), their bike card(s) (serial/model/battery/lock), the chatter timeline, and an Open-full-customer deep link. /staff/ticket/detail. 2.7.20: system-health gains a "Park-guard sweep" line — shows active/inactive + per-pilot-bike reason a bike did/didn't arm (not_assigned / no_stationary_signal / waiting Nm / ARMED), so a park-guard test is debuggable on /staff. 2.7.19: new Tickets tab — manage customer "Report a problem" support tickets (filter by status, change status, internal note); /staff/tickets + /staff/ticket/update, gracefully disabled when the customer-app module is absent. (2.7.18: staff disarm restarts park-guard's 15-min timer (was leaving a parked bike to re-arm on the next sweep). 2.7.17: system-health gains a "Comodule push flowing" check — last-push age + count + last bike + whether iot_movement_state is arriving, so push setup is verifiable on /staff not the logs. 2.7.16: system-health Slack alerts are now prod-only — gated on web.base.url host (staging *.dev.odoo.com stays silent unless clover.alerts.force_send=1). 2.7.15: review fixes — new ID type/country fields added to investor PII redaction; partner-form ID fields wrapped in an explicit group; country-change chatter shows names not a recordset repr. 2.7.14: mobile Map tab gets a discoverable fleet-search — an always-visible floating magnifier (bottom-left) opens the list panel and focuses the search box, plus a 🔍 hint on the collapsed count-pill. Mobile-only; desktop unchanged. 2.7.13: Churned pill is neutral grey, not red — Critical payment status keeps its own red class. 2.7.12: churned/renewed subs now show their lifecycle status in the Customers list + drawer instead of a stale payment-health label like Critical/Active. 2.7.11: "Exclude from KPIs" now toggleable from the customer drawer — /staff/subscription/exclude-kpi, chatter-logged. 2.7.10: bike swap no longer shifts the billing cycle — suppresses the T-163 collection shift + clearer swap chatter. 2.7.9: customer ID gets a proper labelled field + two dropdowns — ID type (DNI/NIE/Passport/Other) and a type-to-filter issuing-country selector — on both the Odoo partner form and the /staff customer drawer. 2.7.8: customer edit returns the full read payload so the document-verified stamp + referral code survive a save. 2.7.7: fix dead cache-buster — controller overrides any baked-in ?v= token with the live file mtime so browsers stop running stale JS; dashboard.html URLs left query-free.)
Clover Investor Dashboard
1.0.6: T-359 tail — the MRR movement waterfall opened on billed invoice volume and closed on forward MRR, hiding the basis difference inside Expansion/Contraction. Both anchors now come from clover.kpi.daily_mrr_series() - one rule, two dates. If the engine cannot supply them the card shows More data needed instead of a proxied number, so the waterfall can never silently plug. JS cache-buster bumped to v=11 (investor.jsx is babel-standalone compiled in-browser, no dist bundle). 1.0.5: fleet snapshot read: live table first, blob fallback (T-366) — the thin-history fallback for total fleet km now reads clover.bike.live via a module-level _fleet_bikes() helper, falling back to the legacy clover.fleet.snapshot ICP blob when the model is absent or its table is empty. The model is resolved BY NAME at runtime because this module does not depend on clover_fleet_tracking. Python-only, no migration. 1.0.4: T-359 honesty pass — trend charts relabeled "Billed volume" (they chart posted-invoice revenue, not forward MRR; the silent rescale-to-headline is REMOVED, series served at true amount_total values); MoM/since-launch chips say "billed"; footnote discloses the two bases; dead solo_rec/duo_rec payload fields dropped; headline MRR/ARR stay canonical engine figures. Also: stale Duo hardware cost corrected €65→€69/mo in margin assumptions (T-223 supplier-invoice finding, confirmed 2026-06-16). Confidential investor report at /investors — growth, retention, unit economics, impact.
Mantenimiento
Segumiento a equipo y administración de solicitudes de mantenimiento
Odoo Facebook Pixel Integration
Add the Facebook Pixel event "PageView" to all website pages | Facebook Pixel Integration | Meta Pixel Integration | Website activity tracking
Odoo Facebook Conversions API | Meta Conversion API Integration
Meta Facebook Conversions API | Facebook Conversion API | Meta CAPI for Tracking Events | Facebook CAPI Integration | Facebook Pixel | Meta Pixel
Odoo Facebook Meta Pixel eCommerce Tracking
eCommerce Facebook Pixel | Meta Pixel | Track Events | Website events tracking | Facebook Pixel Integration | Website Tracking | Add eCommerce events to product and category website pages
Website | eCommerce Tracking Base
Track Customer Actions on Odoo Website and eCommerce
Mensajes de WhatsApp
Enviar mensaje de WhatsApp a contactos
Firma electrónica
Envíe documentos a firmar en línea, recibir y archivar las copias llenadas
Servicio de asistencia
Dé seguimiento, priorice y resuelva los tickets de clientes
Suscripciones
Generar facturas recurrentes y gestionar renovaciones
Planificación
Gestione el horario de sus empleados
Eventos
Publique eventos, venda entradas
Conversaciones
Chat, puerta de enlace al correo y canales privados
Contactos
Centralice su libreta de direcciones
Calendario
Programa las reuniones de empleados
Chat en vivo
Chatee con los visitantes de su sitio web
Citas
Permita que la gente reserve citas en su agenda
Reparaciones
Reparación de productos dañados
Código de barras
Lectores de código de barras para procesar operaciones logísticas
Actividades pendientes
Organice su trabajo con notas y listas de pendientes
VoIP
Relice y reciba llamadas desde Odoo