Fix NewBook write mechanism, bundle push extension, remove vestigial hotel_id

- setBookingCustomFields: use bookings_update's own custom_fields array
  (confirmed from the real legacy plugin code) instead of
  instance_custom_fields_set, which was only ever proven for a different,
  unrelated plugin
- online_checkin_complete is now a configurable per-hotel custom field
  (online_checkin_complete_custom_field_id setting), not a bogus top-level
  bookings_update field NewBook would silently ignore
- Fix field-name mismatches vs a real live NewBook booking response:
  booking_arrival/departure/length, guest address (street/city/state/
  postcode/country, not a single field), phone/mobile (in contact_details,
  not top-level), and pickPrimaryGuest now checks primary_client (was
  checking fields that don't exist in the real API)
- Bundle the standalone push extension into backend/extension-src/, add
  GET /api/extension/download (zips on request via archiver) and a
  download button on the Settings page
- Remove newbook_hotel_id — vestigial from the legacy system's multi-hotel
  routing model, never actually reached NewBook's API, this stack is
  single-hotel-per-deployment so there's nothing to route between

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-09-01 18:11:35 +00:00
parent 3b2e25df3f
commit 4d3774ef0d
25 changed files with 3557 additions and 15 deletions

View file

@ -0,0 +1,32 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
max-width: 480px;
margin: 32px auto;
padding: 0 16px;
color: #1e293b;
}
h1 { font-size: 18px; color: #1a1a2e; }
label { display: block; margin-top: 16px; font-weight: 600; font-size: 13px; }
input {
width: 100%;
box-sizing: border-box;
padding: 8px 10px;
margin-top: 4px;
border: 1px solid #e4e8ee;
border-radius: 8px;
font-size: 13px;
}
.hint { color: #64748b; font-size: 12px; margin: 4px 0 0; }
button {
margin-top: 20px;
margin-right: 8px;
padding: 8px 16px;
border: none;
border-radius: 8px;
background: #c9a84c;
color: #1a1a2e;
font-weight: 600;
cursor: pointer;
}
button#test { background: #f4f5f7; color: #1e293b; border: 1px solid #e4e8ee; }
#status { margin-top: 12px; font-size: 13px; }

View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Send to Check-In Tablet — Settings</title>
<link rel="stylesheet" href="options.css" />
</head>
<body>
<h1>Send to Check-In Tablet</h1>
<p class="hint">
Two things needed before this extension can push a booking to a tablet — both come from the
<strong>tablet-checkin</strong> app itself, not from here.
</p>
<label for="baseUrl">Tablet Check-In base URL</label>
<input id="baseUrl" type="text" placeholder="https://<host>/tablet-checkin — not confirmed yet, see TODO in background.js" />
<p class="hint">The app's own address once deployed. Include no trailing slash.</p>
<label for="apiKey">Terminal API key</label>
<input id="apiKey" type="password" placeholder="Paste the key shown once when you created this terminal" />
<p class="hint">
Create one from tablet-checkin's <strong>Terminals</strong> admin page (requires the <code>admin</code>
capability). The raw key is shown exactly once at creation — if you've lost it, revoke it there and
create a new one.
</p>
<button id="save">Save</button>
<button id="test">Test connection</button>
<p id="status"></p>
<script type="module" src="options.js"></script>
</body>
</html>

View file

@ -0,0 +1,48 @@
const baseUrlInput = document.getElementById('baseUrl');
const apiKeyInput = document.getElementById('apiKey');
const statusEl = document.getElementById('status');
async function load() {
const { baseUrl, apiKey } = await chrome.storage.local.get(['baseUrl', 'apiKey']);
if (baseUrl) baseUrlInput.value = baseUrl;
if (apiKey) apiKeyInput.value = apiKey; // deliberately shown back here (options page,
// not the popup) so the value can be corrected without regenerating the key.
}
async function save() {
const baseUrl = baseUrlInput.value.trim().replace(/\/$/, '');
const apiKey = apiKeyInput.value.trim();
await chrome.storage.local.set({ baseUrl, apiKey });
statusEl.textContent = 'Saved.';
statusEl.style.color = '#16a34a';
}
async function test() {
const { baseUrl, apiKey } = await chrome.storage.local.get(['baseUrl', 'apiKey']);
if (!baseUrl || !apiKey) {
statusEl.textContent = 'Set both fields and save first.';
statusEl.style.color = '#dc2626';
return;
}
statusEl.textContent = 'Testing…';
statusEl.style.color = '#64748b';
try {
const res = await fetch(`${baseUrl}/api/staff/devices`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `HTTP ${res.status}`);
}
const devices = await res.json();
statusEl.textContent = `Connected — found ${devices.length} tablet(s).`;
statusEl.style.color = '#16a34a';
} catch (err) {
statusEl.textContent = `Failed: ${err.message}`;
statusEl.style.color = '#dc2626';
}
}
document.getElementById('save').addEventListener('click', save);
document.getElementById('test').addEventListener('click', test);
load();