AI insights now fetches the Claude API key from the central settings
service instead of storing its own encrypted copy, so wages and other
apps can share the same key. Also wires up the update-available banner
using the existing version-check hook pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live-tested again: 800 tokens still wasn't enough for Sonnet's bullet density
(6 dense bullets, cut off mid-sentence again). Capped bullet count to 4-5,
added a hard 35-word single-sentence limit per bullet, told it not to add a
closing summary, and raised the cap to 1200 for genuine headroom.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live-tested on the dev stack: with the more verbose Sonnet model, the old
550-token cap cut the last bullet off mid-sentence. Bumped to 800 and told
the model to keep each bullet to 1-2 sentences so it isn't relying on the
cap alone to stay in bounds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Fix gather_competitor_data() tier filter: it queried tier IN ('primary','secondary'),
values that never exist (real values are 'own'/'competitor'/'market'), so the
cheapest-competitor comparison has always silently returned nothing.
- Feed the previous insight back into the prompt so the model can note what's
changed/resolved instead of repeating itself.
- Extend forecast horizon from 14 to 30 days; add a per-day revenue table
alongside the existing occupancy table.
- Annotate the occupancy table with UK (England) bank holidays.
- Add a same-channel market-movement section (B.com vs B.com, rack vs rack)
diffing rates against the last insight's snapshot, threshold £3.
- Add a parsed headline field + insight history list on the Dashboard,
collapsed to headline/age and expandable to full content.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same fix as kitchen/kds: AuthGate unconditionally hard-navigated
window.top to the central /login on session expiry, even when not
embedded in the portal iframe — dropping an installed/directly-opened
forecasting session into the portal's framed browser view instead of
staying in its own window.
Now only bounces to central login when actually embedded (passing
?from= so it returns here afterwards); standalone or directly-opened
tabs get an in-app login form and never navigate away. Also wired up
the inactivity auto-logout timer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend returns { keys: [...] } but frontend was treating the whole
object as the array, so apiKeys.length was always falsy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows callers to switch between 364-day DOW-aligned prior year
and same calendar-date prior year comparison.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers model returns data (covers * spend) but if resos spend settings are
absent the result is zero, which previously bypassed the DOW fallback. Now
fall back to DOW when the covers-based dry or wet is zero.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ON CONFLICT clause was missing arrival_date, departure_date, nights, and
room_number — so early checkouts (where NewBook updates booking_departure
to the actual checkout date) never propagated to the DB columns. Reverts
the COUNT(DISTINCT) workaround since the root cause is now fixed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multiple bookings per room (early-checkout Departed + new Arrived, or
NewBook double-bookings) caused COUNT(*) to exceed total room capacity.
Switch to COUNT(DISTINCT COALESCE(room_number, newbook_id)) so each
physical room counts once regardless of how many booking rows span it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guests who check out early keep their original departure_date in NewBook,
so Departed status with departure_date > stay_date were being counted as
live OTB — inflating room count above physical capacity. Unconfirmed
bookings are also provisional so excluded from live OTB.
Prior-year queries retain all statuses since historical bookings will
legitimately be in Departed state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When forecast_rooms_for_date throws inside covers_model, asyncpg aborts the
whole transaction. The exception was being caught with a warning but no rollback,
so every subsequent query in the same request failed. Added db.rollback() in
all three exception handlers (breakfast pickupv2, dinner pickupv2, per-day loop)
and moved public.py's rollback into a finally block so it always fires even
when covers_model returns normally with a corrupted transaction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When forecast_covers_range or forecast_revenue_for_date throw DB exceptions,
the swallowed exception leaves the async session in an aborted transaction state.
Subsequent queries (DOW averages, per-date accom) then fail with
InFailedSQLTransactionError. Adding db.rollback() in the except blocks
resets the session so the fallback path can continue cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The table is newbook_occupancy_report_data (per-category rows). Aggregate
SUM(available - maintenance) WHERE category_id != '5' to get the correct
available room count excluding overflow and maintenance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace static system_config.total_rooms (80, includes overflow) with
per-day available_rooms from daily_occupancy table. This ensures
occupancy % is calculated against the correct base (non-overflow rooms)
consistent with how the rest of the app reports occupancy. Falls back
to system_config.total_rooms when daily_occupancy has no data for a date.
Also adds available_rooms per-day to the response payload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
registerSW.js was 404ing in the browser — the PWA plugin was a leftover
from the original port and this is an internal hotel app with no PWA requirement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fetches daily ERA5 weather from Open-Meteo (no API key).
Configurable location, timezone and sync time via Settings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidates to single sync path: resos_bookings_sync.py →
resos_bookings_data table. Removes sync_resos_data(), duplicate
load_resos_custom_field_mappings(), resos_api_key and
sync_resos_enabled seed rows, and the dead resos_sync scheduler job.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Yield to event loop after each booking DB commit so FastAPI can
respond to Docker health checks during long syncs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
resos_custom_field_mapping, resos_opening_hours_mapping,
resos_manual_breakfast_periods, resos_bookings_data,
resos_bookings_stats, resos_booking_pace were never added to the
schema — they only existed in the original standalone Postgres instance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The management monitor checks http://{host}:{port}/health (no slug prefix).
Adds a location = /health alias alongside the existing /forecasting/health
so both the slug URL (browser/NPM) and the bare path (monitor) work.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the removed API key form with a status card showing whether the
Resos key is configured in the central Settings app, plus a Test Connection
button. Adds back the GET /settings/resos and POST /settings/resos/test
endpoints (now reading from central_settings rather than system_config).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the standalone resos_api_key from the forecasting app's own
system_config table. All credential fetches now go through
central_settings.get_resos_credentials() / get_resos_credentials_sync()
which pull from the Settings app (LXC 116) via the internal integration
endpoint — the same pattern already used for NewBook. The Resos API
Config section is removed from the forecasting Settings page; users
manage the key in the central Settings app instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The renderContent() function used dangerouslySetInnerHTML without
first sanitizing the AI-generated text, allowing any HTML in the
model response to execute in the browser. Added escHtml() helper
and applied it before the bold-substitution regex.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the JWT expired, AuthGate redirected window.location to /auth/login
which loaded inside the portal iframe. The portal detected window.self !==
window.top and showed the EmbeddedFallback ("This app isn't available yet")
instead of the login page.
Redirect window.top instead so the portal itself navigates to /login.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deletes bookability.py, competitor_rates.py, Bookability.tsx,
CompetitorRates.tsx. Removes their imports, router mounts, and nav items.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
forecast_runs was missing, causing all manual/scheduled forecast jobs to
fail on startup. pickup_snapshots and pickup_curves are referenced by the
pickup model but were absent from schema.sql.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After a container restart any in-progress jobs leave orphaned 'running' rows
with no completed_at. Added startup cleanup to mark them failed, so the UI
doesn't show a permanently stuck status.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend now awaits the refresh in a thread executor instead of queuing a
background task, so the response returns only after data is written. Frontend
drops the 3s setTimeout and invalidates the cache immediately on success.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend adds valid_from to DateRateInfo and computes date_last_updated dict
(max valid_from across categories per date). Frontend renders it as a small
timestamp below the date in each column header — time only if today, date+time
if older. Full ISO string in title tooltip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add booking_com_hotels and booking_com_rates (competitor rate scraper tables)
- Fix forecast_snapshots: was using old pivoted schema (snapshot_date,
prophet_value etc.); code expects normalised rows (perception_date, model,
forecast_value, actual_value). Add DO block migration to drop+recreate if
old schema is detected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These two endpoints were missed in the central settings migration. They now try
the Settings service first and fall back to app-local system_config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>