feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
This commit is contained in:
47
web/components/support-availability/public-status.tsx
Normal file
47
web/components/support-availability/public-status.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { RefreshCwIcon } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useAppLocale, useI18n } from "@/i18n/provider"
|
||||
import { fetchPublicSupportStatus, type PublicSupportStatus } from "@/lib/api/support-availability"
|
||||
import { formatAvailabilityTime } from "@/lib/support-availability"
|
||||
|
||||
// Public server aggregates only. This never reads employee identities or private absence data.
|
||||
// A successful HTTP check is not a socket connection and is not itself human presence.
|
||||
export function PublicSupportAvailability({ identityScope }: { identityScope: string }) {
|
||||
return <PublicSupportAvailabilityContent key={identityScope} />
|
||||
}
|
||||
function PublicSupportAvailabilityContent() {
|
||||
const t = useI18n()
|
||||
const { locale } = useAppLocale()
|
||||
const [status, setStatus] = useState<PublicSupportStatus | null>(null)
|
||||
const [failed, setFailed] = useState(false)
|
||||
const [retry, setRetry] = useState(0)
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
let inFlight = false
|
||||
let controller: AbortController | null = null
|
||||
async function check() {
|
||||
if (inFlight) return
|
||||
inFlight = true
|
||||
controller = new AbortController()
|
||||
try {
|
||||
const result = await fetchPublicSupportStatus(controller.signal)
|
||||
if (active) { setStatus(result); setFailed(false) }
|
||||
} catch { if (active) { setStatus(null); setFailed(true) } }
|
||||
finally { inFlight = false }
|
||||
}
|
||||
void check()
|
||||
const refresh = () => { if (document.visibilityState === "visible") void check() }
|
||||
window.addEventListener("focus", refresh)
|
||||
document.addEventListener("visibilitychange", refresh)
|
||||
const timer = window.setInterval(refresh, 30000)
|
||||
return () => { active = false; controller?.abort(); window.clearInterval(timer); window.removeEventListener("focus", refresh); document.removeEventListener("visibilitychange", refresh) }
|
||||
}, [retry])
|
||||
const reason = status?.available ? "available" : status && ["outside_hours", "temporarily_absent", "capacity"].includes(status.reason) ? status.reason : "unknown"
|
||||
return <div className="mt-2 flex items-start justify-between gap-2 border-t border-border/50 pt-2 text-xs text-muted-foreground" aria-live="polite">
|
||||
<div className="min-w-0 space-y-1"><p>{t(!status && !failed ? "supportAvailability.publicChecking" : `supportAvailability.public.${reason}`)}</p>{status?.nextOpeningAt ? <p>{t("supportAvailability.publicNextOpening", { time: formatAvailabilityTime(status.nextOpeningAt, status.timezone, locale) })}</p> : null}</div>
|
||||
{failed ? <Button type="button" variant="ghost" size="icon" className="size-6 shrink-0" aria-label={t("supportAvailability.retry")} onClick={() => setRetry(value => value + 1)}><RefreshCwIcon className="size-3" /></Button> : null}
|
||||
</div>
|
||||
}
|
||||
Reference in New Issue
Block a user