Compare commits
2 Commits
2acf54e1a9
...
0c8723ef84
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c8723ef84 | |||
| 377bb1ce38 |
@@ -68,7 +68,8 @@ public class KioskController : Controller
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Idle branded screen displayed on the front-desk tablet.
|
/// Idle branded screen displayed on the front-desk tablet.
|
||||||
/// Validates the KioskDevice cookie; returns 403 if missing or token mismatch.
|
/// Validates the KioskDevice cookie; returns 403 if missing or token mismatch.
|
||||||
/// The view connects to KioskHub and listens for StartIntake events.
|
/// The view polls /Kiosk/PollSession every 3 seconds and navigates when staff
|
||||||
|
/// triggers a session via the Dashboard "Start Intake" button.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> Welcome()
|
public async Task<IActionResult> Welcome()
|
||||||
@@ -86,6 +87,35 @@ public class KioskController : Controller
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lightweight polling endpoint called every 3 seconds by the kiosk Welcome screen.
|
||||||
|
/// Returns the most recent InPerson KioskSession created in the last 60 seconds so
|
||||||
|
/// the tablet can navigate without relying on SignalR (which Azure App Service blocks
|
||||||
|
/// for anonymous WebSocket/SSE connections through its ingress proxy).
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous, HttpGet]
|
||||||
|
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
||||||
|
public async Task<IActionResult> PollSession()
|
||||||
|
{
|
||||||
|
var cookie = ReadKioskCookie();
|
||||||
|
if (cookie == null) return Json(new { hasSession = false });
|
||||||
|
|
||||||
|
var company = await _unitOfWork.Companies.GetByIdAsync(cookie.Value.companyId, ignoreQueryFilters: true);
|
||||||
|
if (company == null || company.KioskActivationToken != cookie.Value.token)
|
||||||
|
return Json(new { hasSession = false });
|
||||||
|
|
||||||
|
var window = DateTime.UtcNow.AddSeconds(-60);
|
||||||
|
var session = await _unitOfWork.KioskSessions.FirstOrDefaultAsync(
|
||||||
|
s => s.CompanyId == cookie.Value.companyId
|
||||||
|
&& s.SessionType == KioskSessionType.InPerson
|
||||||
|
&& s.Status == KioskSessionStatus.Active
|
||||||
|
&& s.CreatedAt >= window,
|
||||||
|
ignoreQueryFilters: true);
|
||||||
|
|
||||||
|
if (session == null) return Json(new { hasSession = false });
|
||||||
|
return Json(new { hasSession = true, sessionToken = session.SessionToken });
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serves the company logo for anonymous kiosk pages. Resolves the company from the
|
/// Serves the company logo for anonymous kiosk pages. Resolves the company from the
|
||||||
/// KioskDevice cookie so no tenant context is needed on the anonymous request.
|
/// KioskDevice cookie so no tenant context is needed on the anonymous request.
|
||||||
|
|||||||
@@ -85,7 +85,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script>
|
|
||||||
@await RenderSectionAsync("Scripts", required: false)
|
@await RenderSectionAsync("Scripts", required: false)
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// Polls /Kiosk/PollSession every 3 seconds and navigates when staff triggers an intake.
|
||||||
|
// SignalR was replaced with polling because Azure App Service's ingress proxy cancels
|
||||||
|
// anonymous WebSocket and SSE handshakes before the SignalR protocol exchange completes.
|
||||||
(function () {
|
(function () {
|
||||||
const el = document.getElementById("kiosk-welcome-root");
|
const el = document.getElementById("kiosk-welcome-root");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
const companyId = el.dataset.companyId;
|
|
||||||
const dot = document.getElementById("kiosk-conn-dot");
|
const dot = document.getElementById("kiosk-conn-dot");
|
||||||
const label = document.getElementById("kiosk-conn-label");
|
const label = document.getElementById("kiosk-conn-label");
|
||||||
|
|
||||||
@@ -13,51 +15,28 @@
|
|||||||
if (label) label.textContent = text;
|
if (label) label.textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!companyId) {
|
|
||||||
setStatus("#ef4444", "Not configured (no company ID)");
|
|
||||||
console.error("KioskHub: data-company-id is empty — kiosk activation may be invalid.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus("#94a3b8", "Connecting…");
|
setStatus("#94a3b8", "Connecting…");
|
||||||
|
|
||||||
// Skip WebSocket — anonymous WebSocket upgrades are blocked by the Azure App Service
|
let active = true;
|
||||||
// ingress proxy before the SignalR handshake completes. Server-Sent Events and
|
|
||||||
// long polling work fine for the low-frequency "StartIntake" push this hub needs.
|
|
||||||
const connection = new signalR.HubConnectionBuilder()
|
|
||||||
.withUrl(`/hubs/kiosk?companyId=${companyId}`, {
|
|
||||||
transport: signalR.HttpTransportType.ServerSentEvents | signalR.HttpTransportType.LongPolling
|
|
||||||
})
|
|
||||||
.withAutomaticReconnect([2000, 5000, 10000, 30000])
|
|
||||||
.configureLogging(signalR.LogLevel.Information)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
connection.on("StartIntake", function (sessionToken) {
|
async function poll() {
|
||||||
setStatus("#2563eb", "Starting…");
|
if (!active) return;
|
||||||
window.location.href = `/Kiosk/Intake/${sessionToken}/Contact`;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function startConnection() {
|
|
||||||
try {
|
try {
|
||||||
await connection.start();
|
const res = await fetch("/Kiosk/PollSession", { cache: "no-store" });
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
const data = await res.json();
|
||||||
setStatus("#16a34a", "Ready");
|
setStatus("#16a34a", "Ready");
|
||||||
console.info("KioskHub connected, group kiosk-" + companyId);
|
if (data.hasSession && data.sessionToken) {
|
||||||
} catch (err) {
|
active = false;
|
||||||
setStatus("#ef4444", "Connection failed — retrying…");
|
setStatus("#2563eb", "Starting…");
|
||||||
console.warn("KioskHub connect failed, retrying in 10s…", err);
|
window.location.href = `/Kiosk/Intake/${data.sessionToken}/Contact`;
|
||||||
setTimeout(startConnection, 10000);
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setStatus("#f59e0b", "Connection issue — retrying…");
|
||||||
}
|
}
|
||||||
|
if (active) setTimeout(poll, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
startConnection();
|
setTimeout(poll, 500); // first poll quickly; subsequent every 3s
|
||||||
|
|
||||||
connection.onreconnecting(() => setStatus("#f59e0b", "Reconnecting…"));
|
|
||||||
connection.onreconnected(() => {
|
|
||||||
setStatus("#16a34a", "Ready");
|
|
||||||
console.info("KioskHub reconnected");
|
|
||||||
});
|
|
||||||
connection.onclose(() => {
|
|
||||||
setStatus("#ef4444", "Disconnected — retrying…");
|
|
||||||
setTimeout(startConnection, 10000);
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
// Minimal service worker — required for PWA installability.
|
// Minimal service worker — required for PWA installability.
|
||||||
// No caching: all requests pass through to the network normally.
|
// No caching: all requests pass through to the network.
|
||||||
// This exists solely so browsers recognize the site as installable,
|
// Exists solely so browsers recognize the site as installable
|
||||||
// which causes iOS/Android to persist camera permissions after "Add to Home Screen."
|
// (iOS/Android persist camera permissions after "Add to Home Screen").
|
||||||
|
//
|
||||||
|
// IMPORTANT: /hubs/ (SignalR) requests are excluded from interception entirely.
|
||||||
|
// Service worker fetch() wraps SSE/WebSocket responses in a buffered Response,
|
||||||
|
// which prevents real-time streaming — SignalR handshakes time out as a result.
|
||||||
|
|
||||||
|
const SKIP_PREFIXES = ['/hubs/', '/Kiosk/PollSession'];
|
||||||
|
|
||||||
self.addEventListener('install', () => self.skipWaiting());
|
self.addEventListener('install', () => self.skipWaiting());
|
||||||
self.addEventListener('activate', e => e.waitUntil(self.clients.claim()));
|
self.addEventListener('activate', e => e.waitUntil(self.clients.claim()));
|
||||||
|
|
||||||
self.addEventListener('fetch', e => {
|
self.addEventListener('fetch', e => {
|
||||||
if (new URL(e.request.url).origin !== self.location.origin) return;
|
const url = new URL(e.request.url);
|
||||||
|
|
||||||
|
// Always skip cross-origin requests
|
||||||
|
if (url.origin !== self.location.origin) return;
|
||||||
|
|
||||||
|
// Skip SignalR hubs and kiosk polling — let the browser handle these directly
|
||||||
|
if (SKIP_PREFIXES.some(p => url.pathname.startsWith(p))) return;
|
||||||
|
|
||||||
|
// Passthrough: no caching, no modification
|
||||||
e.respondWith(fetch(e.request));
|
e.respondWith(fetch(e.request));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user