@burdenoff/microfe-technospam 2026.1102.4 → 2026.1102.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-types.d.ts +295 -3
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +62 -0
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +429 -297
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/StatusBadge.d.ts.map +1 -1
- package/dist/technospam/components/StatusBadge.js +4 -0
- package/dist/technospam/components/StatusBadge.js.map +1 -1
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.d.ts +2 -1
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.d.ts.map +1 -1
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.js +325 -95
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.js.map +1 -1
- package/dist/technospam/lib/enterprise.d.ts +20 -0
- package/dist/technospam/lib/enterprise.d.ts.map +1 -1
- package/dist/technospam/lib/enterprise.js +33 -1
- package/dist/technospam/lib/enterprise.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EnterpriseIntegrationsTab.js","names":[],"sources":["../../../../src/technospam/components/admin/EnterpriseIntegrationsTab.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { BadgeCheck, Download, FileJson, KeyRound, Loader2, Search } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n MAX_VERIFY_BATCH,\n useCoursesQuery,\n useVerifyCertificatesQuery,\n useXapiExport,\n} from '../../api/technospamApi';\nimport {\n atsVerificationSnippet,\n dayStartIso,\n nextDayStartIso,\n parseSerials,\n xapiExportFileName,\n} from '../../lib/enterprise';\nimport { notify } from '../../lib/toast';\nimport { formatDate } from '../../utils/format';\nimport { ErrorView } from '../StateViews';\nimport { StatusBadge } from '../StatusBadge';\n\n/** Public workspaces gateway ATS partners call (production). */\nconst PARTNER_ENDPOINT = 'https://graphqlworkspaces.burdenoff.com/workspaces/graphql';\nconst ALL_COURSES = '__all__';\n\nfunction SectionTitle({\n icon: Icon,\n title,\n description,\n}: {\n icon: typeof BadgeCheck;\n title: string;\n description: string;\n}) {\n return (\n <div className=\"mb-4 flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-accent-primary/10 text-accent-primary\">\n <Icon size={18} />\n </span>\n <div className=\"min-w-0\">\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">{description}</p>\n </div>\n </div>\n );\n}\n\nfunction CertificateVerificationCard() {\n const [input, setInput] = useState('');\n const [submitted, setSubmitted] = useState<string[]>([]);\n const { data, loading, error } = useVerifyCertificatesQuery(submitted);\n const parsed = parseSerials(input);\n const tooMany = parsed.length > MAX_VERIFY_BATCH;\n const results = data?.tecVerifyCertificates ?? [];\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-verify\">\n <SectionTitle\n icon={BadgeCheck}\n title=\"Certificate verification for ATS partners\"\n description=\"Recruiters and applicant-tracking systems confirm a candidate's TechnoSpam certificate by its serial. The check is public and returns only the course, score, dates and status — never the learner's identity.\"\n />\n <label\n htmlFor=\"tec-verify-serials\"\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n >\n Certificate serials (one per line, up to {MAX_VERIFY_BATCH})\n </label>\n <Textarea\n id=\"tec-verify-serials\"\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"TEC-MH1ABC2D-1A2B3C4D\"\n rows={3}\n className=\"font-mono text-sm\"\n />\n <div className=\"mt-3 flex flex-wrap items-center gap-3\">\n <Button\n size=\"sm\"\n onClick={() => setSubmitted(parsed)}\n disabled={parsed.length === 0 || tooMany || loading}\n >\n {loading ? <Loader2 size={15} className=\"animate-spin\" /> : <Search size={15} />}\n Verify\n </Button>\n <span className=\"text-xs text-text-muted\">\n {tooMany\n ? `Too many serials: ${parsed.length} of at most ${MAX_VERIFY_BATCH}.`\n : `${parsed.length} serial${parsed.length === 1 ? '' : 's'}`}\n </span>\n </div>\n\n {error ? (\n <div className=\"mt-4\">\n <ErrorView title=\"Couldn't verify\" error={error} />\n </div>\n ) : results.length > 0 ? (\n <ul className=\"mt-4 space-y-2\" aria-label=\"Verification results\">\n {results.map((r) => (\n <li\n key={r.serial}\n className=\"flex flex-col gap-1 rounded-lg border border-border-seam bg-bg-sunken p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate font-mono text-sm text-text-primary\">{r.serial}</div>\n <div className=\"mt-0.5 text-xs text-text-muted\">\n {r.status === 'NOT_FOUND'\n ? 'No TechnoSpam certificate has this serial.'\n : `${r.courseTitle ?? 'Course'} · score ${r.score ?? 0} · issued ${formatDate(r.issuedAt)}${\n r.revokedAt ? ` · revoked ${formatDate(r.revokedAt)}` : ''\n }`}\n </div>\n </div>\n <StatusBadge status={r.status} />\n </li>\n ))}\n </ul>\n ) : null}\n\n <details className=\"mt-5 rounded-lg border border-border-seam p-3\">\n <summary className=\"cursor-pointer text-sm font-medium text-text-primary\">\n Integration details for ATS partners\n </summary>\n <p className=\"mt-2 text-sm text-text-secondary\">\n No API key is needed. Batch checks use <code>tecVerifyCertificates(serials: [...])</code>{' '}\n with up to {MAX_VERIFY_BATCH} serials per request.\n </p>\n <pre className=\"mt-2 overflow-x-auto rounded-md bg-bg-sunken p-3 text-xs text-text-secondary\">\n {atsVerificationSnippet(PARTNER_ENDPOINT)}\n </pre>\n </details>\n </GlassCard>\n );\n}\n\nfunction downloadJson(fileName: string, payload: unknown) {\n const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = fileName;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n URL.revokeObjectURL(url);\n}\n\nfunction XapiExportCard() {\n const { data: coursesData } = useCoursesQuery({});\n const courses = coursesData?.tecListCourses?.items ?? [];\n const [courseId, setCourseId] = useState(ALL_COURSES);\n const [from, setFrom] = useState('');\n const [to, setTo] = useState('');\n const [lastCount, setLastCount] = useState<number | null>(null);\n const { run, running, error } = useXapiExport();\n\n const handleExport = async () => {\n const course = courses.find((c) => c.id === courseId);\n const result = await run({\n courseId: course?.id,\n since: dayStartIso(from),\n until: nextDayStartIso(to),\n });\n if (!result) return;\n setLastCount(result.statements.length);\n if (result.statements.length === 0) {\n notify.info('Nothing to export', 'No learner activity in this range.');\n return;\n }\n downloadJson(xapiExportFileName(new Date(), course?.slug), result.statements);\n notify.success(\n 'xAPI export downloaded',\n `${result.statements.length} statement${result.statements.length === 1 ? '' : 's'}${\n result.truncated ? ' (first 5,000 — narrow the date range for the rest)' : ''\n }.`\n );\n };\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-xapi\">\n <SectionTitle\n icon={FileJson}\n title=\"xAPI learning-record export (LMS / LRS / HRIS)\"\n description=\"Download learner activity as standard xAPI 1.0.3 statements — enrollments, lesson and course completions, certificates and revocations — ready to POST to any Learning Record Store. Statement ids are stable, so re-importing the same range is safe.\"\n />\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"min-w-48\">\n <label\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n htmlFor=\"tec-xapi-course\"\n >\n Course\n </label>\n <Select value={courseId} onValueChange={setCourseId}>\n <SelectTrigger id=\"tec-xapi-course\" className=\"w-56\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_COURSES}>All courses</SelectItem>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-xapi-from\">\n From\n </label>\n <Input\n id=\"tec-xapi-from\"\n type=\"date\"\n value={from}\n max={to || undefined}\n onChange={(e) => setFrom(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-xapi-to\">\n To\n </label>\n <Input\n id=\"tec-xapi-to\"\n type=\"date\"\n value={to}\n min={from || undefined}\n onChange={(e) => setTo(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <Button size=\"sm\" onClick={handleExport} disabled={running}>\n {running ? <Loader2 size={15} className=\"animate-spin\" /> : <Download size={15} />}\n Export statements\n </Button>\n </div>\n {lastCount !== null && !error ? (\n <p className=\"mt-3 text-xs text-text-muted\" role=\"status\">\n Last export: {lastCount} statement{lastCount === 1 ? '' : 's'}.\n </p>\n ) : null}\n {error ? (\n <div className=\"mt-4\">\n <ErrorView title=\"Couldn't export learning records\" error={error} />\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\nfunction IdentityCard() {\n return (\n <GlassCard className=\"p-5\">\n <SectionTitle\n icon={KeyRound}\n title=\"Single sign-on and user provisioning\"\n description=\"SAML single sign-on and SCIM 2.0 user provisioning come from the Burdenoff platform identity service that TechnoSpam signs in with, so learners provisioned by your identity provider or HRIS arrive with the same account id used in certificates and xAPI statements.\"\n />\n </GlassCard>\n );\n}\n\n/**\n * Enterprise connectors (BOFF-4430): ATS certificate verification, xAPI\n * export for LMS / LRS / HRIS, and a pointer to platform SSO/SCIM.\n */\nexport function EnterpriseIntegrationsTab() {\n return (\n <div className=\"space-y-4\">\n <CertificateVerificationCard />\n <XapiExportCard />\n <IdentityCard />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EnterpriseIntegrationsTab.js","names":[],"sources":["../../../../src/technospam/components/admin/EnterpriseIntegrationsTab.tsx"],"sourcesContent":["import { useEffect, useState, type ChangeEvent, type FormEvent, type ReactNode } from 'react';\nimport {\n BadgeCheck,\n Download,\n FileJson,\n KeyRound,\n Loader2,\n RefreshCw,\n Search,\n Send,\n Unplug,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n MAX_VERIFY_BATCH,\n useCoursesQuery,\n useLrsConnectionQuery,\n useVerifyCertificatesQuery,\n useXapiExport,\n useXapiLrsActions,\n useXapiLrsSyncQuery,\n} from '../../api/technospamApi';\nimport {\n type LrsFormErrors,\n type LrsFormValues,\n atsVerificationSnippet,\n dayStartIso,\n lrsConnectionName,\n lrsSyncSummary,\n nextDayStartIso,\n normalizeLrsEndpoint,\n parseSerials,\n validateLrsForm,\n xapiExportFileName,\n} from '../../lib/enterprise';\nimport { notify } from '../../lib/toast';\nimport { formatDate } from '../../utils/format';\nimport { ErrorView } from '../StateViews';\nimport { StatusBadge } from '../StatusBadge';\n\n/** Public workspaces gateway ATS partners call (production). */\nconst PARTNER_ENDPOINT = 'https://graphqlworkspaces.burdenoff.com/workspaces/graphql';\nconst ALL_COURSES = '__all__';\n\nfunction SectionTitle({\n icon: Icon,\n title,\n description,\n}: {\n icon: typeof BadgeCheck;\n title: string;\n description: string;\n}) {\n return (\n <div className=\"mb-4 flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-accent-primary/10 text-accent-primary\">\n <Icon size={18} />\n </span>\n <div className=\"min-w-0\">\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">{description}</p>\n </div>\n </div>\n );\n}\n\nfunction CertificateVerificationCard() {\n const [input, setInput] = useState('');\n const [submitted, setSubmitted] = useState<string[]>([]);\n const { data, loading, error } = useVerifyCertificatesQuery(submitted);\n const parsed = parseSerials(input);\n const tooMany = parsed.length > MAX_VERIFY_BATCH;\n const results = data?.tecVerifyCertificates ?? [];\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-verify\">\n <SectionTitle\n icon={BadgeCheck}\n title=\"Certificate verification for ATS partners\"\n description=\"Recruiters and applicant-tracking systems confirm a candidate's TechnoSpam certificate by its serial. The check is public and returns only the course, score, dates and status — never the learner's identity.\"\n />\n <label\n htmlFor=\"tec-verify-serials\"\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n >\n Certificate serials (one per line, up to {MAX_VERIFY_BATCH})\n </label>\n <Textarea\n id=\"tec-verify-serials\"\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"TEC-MH1ABC2D-1A2B3C4D\"\n rows={3}\n className=\"font-mono text-sm\"\n />\n <div className=\"mt-3 flex flex-wrap items-center gap-3\">\n <Button\n size=\"sm\"\n onClick={() => setSubmitted(parsed)}\n disabled={parsed.length === 0 || tooMany || loading}\n >\n {loading ? <Loader2 size={15} className=\"animate-spin\" /> : <Search size={15} />}\n Verify\n </Button>\n <span className=\"text-xs text-text-muted\">\n {tooMany\n ? `Too many serials: ${parsed.length} of at most ${MAX_VERIFY_BATCH}.`\n : `${parsed.length} serial${parsed.length === 1 ? '' : 's'}`}\n </span>\n </div>\n\n {error ? (\n <div className=\"mt-4\">\n <ErrorView title=\"Couldn't verify\" error={error} />\n </div>\n ) : results.length > 0 ? (\n <ul className=\"mt-4 space-y-2\" aria-label=\"Verification results\">\n {results.map((r) => (\n <li\n key={r.serial}\n className=\"flex flex-col gap-1 rounded-lg border border-border-seam bg-bg-sunken p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate font-mono text-sm text-text-primary\">{r.serial}</div>\n <div className=\"mt-0.5 text-xs text-text-muted\">\n {r.status === 'NOT_FOUND'\n ? 'No TechnoSpam certificate has this serial.'\n : `${r.courseTitle ?? 'Course'} · score ${r.score ?? 0} · issued ${formatDate(r.issuedAt)}${\n r.revokedAt ? ` · revoked ${formatDate(r.revokedAt)}` : ''\n }`}\n </div>\n </div>\n <StatusBadge status={r.status} />\n </li>\n ))}\n </ul>\n ) : null}\n\n <details className=\"mt-5 rounded-lg border border-border-seam p-3\">\n <summary className=\"cursor-pointer text-sm font-medium text-text-primary\">\n Integration details for ATS partners\n </summary>\n <p className=\"mt-2 text-sm text-text-secondary\">\n No API key is needed. Batch checks use <code>tecVerifyCertificates(serials: [...])</code>{' '}\n with up to {MAX_VERIFY_BATCH} serials per request.\n </p>\n <pre className=\"mt-2 overflow-x-auto rounded-md bg-bg-sunken p-3 text-xs text-text-secondary\">\n {atsVerificationSnippet(PARTNER_ENDPOINT)}\n </pre>\n </details>\n </GlassCard>\n );\n}\n\nfunction downloadJson(fileName: string, payload: unknown) {\n const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = fileName;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n URL.revokeObjectURL(url);\n}\n\nfunction XapiExportCard() {\n const { data: coursesData } = useCoursesQuery({});\n const courses = coursesData?.tecListCourses?.items ?? [];\n const [courseId, setCourseId] = useState(ALL_COURSES);\n const [from, setFrom] = useState('');\n const [to, setTo] = useState('');\n const [lastCount, setLastCount] = useState<number | null>(null);\n const { run, running, error } = useXapiExport();\n\n const handleExport = async () => {\n const course = courses.find((c) => c.id === courseId);\n const result = await run({\n courseId: course?.id,\n since: dayStartIso(from),\n until: nextDayStartIso(to),\n });\n if (!result) return;\n setLastCount(result.statements.length);\n if (result.statements.length === 0) {\n notify.info('Nothing to export', 'No learner activity in this range.');\n return;\n }\n downloadJson(xapiExportFileName(new Date(), course?.slug), result.statements);\n notify.success(\n 'xAPI export downloaded',\n `${result.statements.length} statement${result.statements.length === 1 ? '' : 's'}${\n result.truncated ? ' (first 5,000 — narrow the date range for the rest)' : ''\n }.`\n );\n };\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-xapi\">\n <SectionTitle\n icon={FileJson}\n title=\"xAPI learning-record export (LMS / LRS / HRIS)\"\n description=\"Download learner activity as standard xAPI 1.0.3 statements — enrollments, lesson and course completions, certificates and revocations — ready to POST to any Learning Record Store. Statement ids are stable, so re-importing the same range is safe.\"\n />\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"min-w-48\">\n <label\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n htmlFor=\"tec-xapi-course\"\n >\n Course\n </label>\n <Select value={courseId} onValueChange={setCourseId}>\n <SelectTrigger id=\"tec-xapi-course\" className=\"w-56\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_COURSES}>All courses</SelectItem>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-xapi-from\">\n From\n </label>\n <Input\n id=\"tec-xapi-from\"\n type=\"date\"\n value={from}\n max={to || undefined}\n onChange={(e) => setFrom(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-xapi-to\">\n To\n </label>\n <Input\n id=\"tec-xapi-to\"\n type=\"date\"\n value={to}\n min={from || undefined}\n onChange={(e) => setTo(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <Button size=\"sm\" onClick={handleExport} disabled={running}>\n {running ? <Loader2 size={15} className=\"animate-spin\" /> : <Download size={15} />}\n Export statements\n </Button>\n </div>\n {lastCount !== null && !error ? (\n <p className=\"mt-3 text-xs text-text-muted\" role=\"status\">\n Last export: {lastCount} statement{lastCount === 1 ? '' : 's'}.\n </p>\n ) : null}\n {error ? (\n <div className=\"mt-4\">\n <ErrorView title=\"Couldn't export learning records\" error={error} />\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\n/** Refresh while a run is in progress or the first one is pending. */\nconst LRS_POLL_ACTIVE_MS = 5000;\n/** Refresh while connected otherwise, so scheduled runs show up without a reload. */\nconst LRS_POLL_IDLE_MS = 30000;\nconst EMPTY_LRS_FORM: LrsFormValues = { endpoint: '', key: '', secret: '' };\n\nfunction formatDateTime(value: string | null | undefined): string {\n if (!value) return 'Never';\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? 'Never' : date.toLocaleString();\n}\n\nfunction LrsField({\n id,\n label,\n error,\n children,\n}: {\n id: string;\n label: string;\n error?: string;\n children: ReactNode;\n}) {\n return (\n <div className=\"min-w-0\">\n <label htmlFor={id} className=\"mb-1 block text-xs font-medium text-text-muted\">\n {label}\n </label>\n {children}\n {error ? (\n <p id={`${id}-error`} className=\"mt-1 text-xs text-status-error-text\" role=\"alert\">\n {error}\n </p>\n ) : null}\n </div>\n );\n}\n\nfunction LrsConnectForm({\n busy,\n onConnect,\n}: {\n busy: boolean;\n onConnect: (values: LrsFormValues) => Promise<boolean>;\n}) {\n const [values, setValues] = useState<LrsFormValues>(EMPTY_LRS_FORM);\n const [errors, setErrors] = useState<LrsFormErrors>({});\n\n const set = (field: keyof LrsFormValues) => (e: ChangeEvent<HTMLInputElement>) =>\n setValues((v) => ({ ...v, [field]: e.target.value }));\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n const found = validateLrsForm(values);\n setErrors(found);\n if (Object.keys(found).length > 0) return;\n const ok = await onConnect(values);\n if (ok) setValues(EMPTY_LRS_FORM);\n };\n\n return (\n <form onSubmit={handleSubmit} noValidate aria-label=\"Connect a Learning Record Store\">\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <LrsField id=\"tec-lrs-endpoint\" label=\"LRS endpoint\" error={errors.endpoint}>\n <Input\n id=\"tec-lrs-endpoint\"\n type=\"url\"\n inputMode=\"url\"\n autoComplete=\"off\"\n placeholder=\"https://lrs.example.com/data/xAPI\"\n value={values.endpoint}\n onChange={set('endpoint')}\n aria-invalid={Boolean(errors.endpoint)}\n aria-describedby={errors.endpoint ? 'tec-lrs-endpoint-error' : undefined}\n />\n </LrsField>\n <LrsField id=\"tec-lrs-key\" label=\"Key\" error={errors.key}>\n <Input\n id=\"tec-lrs-key\"\n autoComplete=\"off\"\n value={values.key}\n onChange={set('key')}\n aria-invalid={Boolean(errors.key)}\n aria-describedby={errors.key ? 'tec-lrs-key-error' : undefined}\n />\n </LrsField>\n <LrsField id=\"tec-lrs-secret\" label=\"Secret\" error={errors.secret}>\n <Input\n id=\"tec-lrs-secret\"\n type=\"password\"\n autoComplete=\"new-password\"\n value={values.secret}\n onChange={set('secret')}\n aria-invalid={Boolean(errors.secret)}\n aria-describedby={errors.secret ? 'tec-lrs-secret-error' : undefined}\n />\n </LrsField>\n </div>\n <div className=\"mt-3 flex flex-wrap items-center gap-3\">\n <Button size=\"sm\" type=\"submit\" disabled={busy}>\n {busy ? <Loader2 size={15} className=\"animate-spin\" /> : <Send size={15} />}\n Connect LRS\n </Button>\n <span className=\"text-xs text-text-muted\">\n The secret is stored encrypted in your workspace integrations, never in the TechnoSpam\n database.\n </span>\n </div>\n </form>\n );\n}\n\nfunction LrsStat({ label, value }: { label: string; value: ReactNode }) {\n return (\n <div className=\"min-w-0\">\n <dt className=\"text-xs text-text-muted\">{label}</dt>\n <dd className=\"mt-0.5 truncate text-sm text-text-primary\">{value}</dd>\n </div>\n );\n}\n\nfunction LrsSyncCard() {\n const { data, loading, error, startPolling, stopPolling } = useXapiLrsSyncQuery();\n const sync = data?.tecXapiLrsSync ?? null;\n const { data: connectionData } = useLrsConnectionQuery(sync?.connectionId);\n const { connect, disconnect, syncNow, busy } = useXapiLrsActions();\n const connection = connectionData?.getConnection ?? null;\n const endpoint =\n typeof connection?.config?.['baseUrl'] === 'string' ? connection.config['baseUrl'] : null;\n\n // Keep the status current while an LRS is connected: quickly while a run is\n // in progress or the first one is pending, slowly between scheduled runs.\n const pollMs = !sync\n ? null\n : sync.state === 'RUNNING' || sync.state === 'NEVER_RUN'\n ? LRS_POLL_ACTIVE_MS\n : LRS_POLL_IDLE_MS;\n useEffect(() => {\n if (pollMs === null) return undefined;\n startPolling(pollMs);\n return () => stopPolling();\n }, [pollMs, startPolling, stopPolling]);\n\n const handleConnect = async (values: LrsFormValues) => {\n const normalized = normalizeLrsEndpoint(values.endpoint);\n const result = await connect({\n name: lrsConnectionName(normalized),\n endpoint: normalized,\n key: values.key.trim(),\n secret: values.secret,\n });\n return Boolean(result);\n };\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-lrs\">\n <SectionTitle\n icon={RefreshCw}\n title=\"Automatic sync to your LRS / HRIS\"\n description=\"Connect a Learning Record Store (or an LMS or HRIS that accepts xAPI) once, and new learner activity is sent to it as xAPI 1.0.3 statements every few minutes. The first sync sends the full history.\"\n />\n {loading && !data ? (\n <div className=\"flex items-center gap-2 text-sm text-text-muted\">\n <Loader2 size={15} className=\"animate-spin\" /> Loading sync status…\n </div>\n ) : error ? (\n <ErrorView title=\"Couldn't load the LRS sync\" error={error} />\n ) : sync ? (\n <div>\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={sync.state} />\n <span className=\"text-sm text-text-secondary\" role=\"status\">\n {lrsSyncSummary(sync.state, sync.consecutiveFailures)}\n </span>\n </div>\n <dl className=\"mt-4 grid gap-3 sm:grid-cols-3\">\n <LrsStat\n label=\"Endpoint\"\n value={<span className=\"font-mono\">{endpoint ?? connection?.name ?? '—'}</span>}\n />\n <LrsStat label=\"Last sync\" value={formatDateTime(sync.lastSyncAt)} />\n <LrsStat label=\"Last success\" value={formatDateTime(sync.lastSuccessAt)} />\n <LrsStat label=\"Statements sent\" value={sync.statementsPushed.toLocaleString()} />\n <LrsStat label=\"Next sync\" value={formatDateTime(sync.nextAttemptAt)} />\n <LrsStat label=\"Interval\" value={`Every ${sync.intervalMinutes} min`} />\n </dl>\n {sync.lastError ? (\n <div\n className=\"mt-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-3 text-sm text-text-primary\"\n data-testid=\"tec-lrs-last-error\"\n >\n <div className=\"text-xs font-medium text-status-error-text\">\n Last error · {formatDateTime(sync.lastErrorAt)}\n </div>\n <div className=\"mt-1 break-words\">{sync.lastError}</div>\n </div>\n ) : null}\n <div className=\"mt-4 flex flex-wrap gap-2\">\n <Button size=\"sm\" onClick={() => void syncNow()} disabled={busy !== null}>\n {busy === 'sync' ? (\n <Loader2 size={15} className=\"animate-spin\" />\n ) : (\n <RefreshCw size={15} />\n )}\n Sync now\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void disconnect(sync.connectionId)}\n disabled={busy !== null}\n >\n {busy === 'disconnect' ? (\n <Loader2 size={15} className=\"animate-spin\" />\n ) : (\n <Unplug size={15} />\n )}\n Disconnect\n </Button>\n </div>\n </div>\n ) : (\n <LrsConnectForm busy={busy === 'connect'} onConnect={handleConnect} />\n )}\n </GlassCard>\n );\n}\n\nfunction IdentityCard() {\n return (\n <GlassCard className=\"p-5\">\n <SectionTitle\n icon={KeyRound}\n title=\"Single sign-on and user provisioning\"\n description=\"SAML single sign-on and SCIM 2.0 user provisioning come from the Burdenoff platform identity service that TechnoSpam signs in with, so learners provisioned by your identity provider or HRIS arrive with the same account id used in certificates and xAPI statements.\"\n />\n </GlassCard>\n );\n}\n\n/**\n * Enterprise connectors (BOFF-4430): ATS certificate verification, xAPI\n * export for LMS / LRS / HRIS, the scheduled push to a connected LRS\n * (BOFF-10575), and a pointer to platform SSO/SCIM.\n */\nexport function EnterpriseIntegrationsTab() {\n return (\n <div className=\"space-y-4\">\n <CertificateVerificationCard />\n <XapiExportCard />\n <LrsSyncCard />\n <IdentityCard />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -18,4 +18,24 @@ export declare function nextDayStartIso(date: string): string | undefined;
|
|
|
18
18
|
export declare function xapiExportFileName(now?: Date, courseSlug?: string): string;
|
|
19
19
|
/** The GraphQL request an ATS partner sends to verify a serial (shown in the UI). */
|
|
20
20
|
export declare function atsVerificationSnippet(endpoint: string): string;
|
|
21
|
+
export interface LrsFormValues {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
key: string;
|
|
24
|
+
secret: string;
|
|
25
|
+
}
|
|
26
|
+
export type LrsFormErrors = Partial<Record<keyof LrsFormValues, string>>;
|
|
27
|
+
/**
|
|
28
|
+
* Check the connect form before anything is sent. The service applies the
|
|
29
|
+
* authoritative checks (public address, the xAPI /about answer); this only
|
|
30
|
+
* catches what can be told from the text: an https URL with no query,
|
|
31
|
+
* fragment or embedded credentials, and a key and secret.
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateLrsForm(values: LrsFormValues): LrsFormErrors;
|
|
34
|
+
/** The endpoint as the service will call it: trimmed, no trailing slash, no `/statements`. */
|
|
35
|
+
export declare function normalizeLrsEndpoint(endpoint: string): string;
|
|
36
|
+
/** Name of the integration connection created for the LRS, e.g. `TechnoSpam LRS (lrs.example.com)`. */
|
|
37
|
+
export declare function lrsConnectionName(endpoint: string): string;
|
|
38
|
+
export type TecXapiSyncState = 'NEVER_RUN' | 'OK' | 'FAILING' | 'RUNNING';
|
|
39
|
+
/** One-line summary of the sync state for the status row. */
|
|
40
|
+
export declare function lrsSyncSummary(state: TecXapiSyncState, consecutiveFailures: number): string;
|
|
21
41
|
//# sourceMappingURL=enterprise.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enterprise.d.ts","sourceRoot":"","sources":["../../../src/technospam/lib/enterprise.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAOpD;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMhE;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,IAAiB,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAStF;AAED,qFAAqF;AACrF,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAe/D"}
|
|
1
|
+
{"version":3,"file":"enterprise.d.ts","sourceRoot":"","sources":["../../../src/technospam/lib/enterprise.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAOpD;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMhE;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,IAAiB,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAStF;AAED,qFAAqF;AACrF,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAe/D;AAID,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;AAEzE;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAuBpE;AAED,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,uGAAuG;AACvG,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,CAAC;AAE1E,6DAA6D;AAC7D,wBAAgB,cAAc,CAAC,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAa3F"}
|
|
@@ -30,7 +30,39 @@ function i(e) {
|
|
|
30
30
|
}, null, 2)
|
|
31
31
|
].join("\n");
|
|
32
32
|
}
|
|
33
|
+
function a(e) {
|
|
34
|
+
let t = {}, n = e.endpoint.trim();
|
|
35
|
+
if (!n) t.endpoint = "Enter the LRS endpoint.";
|
|
36
|
+
else {
|
|
37
|
+
let e = null;
|
|
38
|
+
try {
|
|
39
|
+
e = new URL(n);
|
|
40
|
+
} catch {
|
|
41
|
+
t.endpoint = "Enter a full URL, for example https://lrs.example.com/data/xAPI.";
|
|
42
|
+
}
|
|
43
|
+
e && (e.protocol === "https:" ? e.username || e.password ? t.endpoint = "Put the key and secret in their own fields, not in the URL." : (e.search || e.hash) && (t.endpoint = "Leave out the query string; enter the xAPI base URL only.") : t.endpoint = "The endpoint must start with https://.");
|
|
44
|
+
}
|
|
45
|
+
return e.key.trim() || (t.key = "Enter the LRS key."), e.secret || (t.secret = "Enter the LRS secret."), t;
|
|
46
|
+
}
|
|
47
|
+
function o(e) {
|
|
48
|
+
return e.trim().replace(/\/+$/, "").replace(/\/statements$/i, "");
|
|
49
|
+
}
|
|
50
|
+
function s(e) {
|
|
51
|
+
try {
|
|
52
|
+
return `TechnoSpam LRS (${new URL(e).host})`;
|
|
53
|
+
} catch {
|
|
54
|
+
return "TechnoSpam LRS";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function c(e, t) {
|
|
58
|
+
switch (e) {
|
|
59
|
+
case "RUNNING": return "Sending learner activity now.";
|
|
60
|
+
case "OK": return "Up to date. New activity is sent automatically.";
|
|
61
|
+
case "FAILING": return t > 1 ? `The last ${t} attempts failed. Retrying automatically with a growing delay.` : "The last attempt failed. Retrying automatically.";
|
|
62
|
+
default: return "Connected. The first sync starts within a minute.";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
33
65
|
//#endregion
|
|
34
|
-
export { i as atsVerificationSnippet, t as dayStartIso, n as nextDayStartIso, e as parseSerials, r as xapiExportFileName };
|
|
66
|
+
export { i as atsVerificationSnippet, t as dayStartIso, s as lrsConnectionName, c as lrsSyncSummary, n as nextDayStartIso, o as normalizeLrsEndpoint, e as parseSerials, a as validateLrsForm, r as xapiExportFileName };
|
|
35
67
|
|
|
36
68
|
//# sourceMappingURL=enterprise.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enterprise.js","names":[],"sources":["../../../src/technospam/lib/enterprise.ts"],"sourcesContent":["/**\n * Pure helpers for the enterprise connectors admin tab (BOFF-4430).\n */\n\n/**\n * Split pasted serials (newlines, commas, semicolons or whitespace),\n * normalize them the way the service does (trim + upper-case) and drop\n * duplicates while keeping the first-seen order.\n */\nexport function parseSerials(input: string): string[] {\n const seen = new Set<string>();\n for (const raw of input.split(/[\\s,;]+/)) {\n const serial = raw.trim().toUpperCase();\n if (serial) seen.add(serial);\n }\n return [...seen];\n}\n\n/** Inclusive ISO start of a `<input type=\"date\">` day (UTC). */\nexport function dayStartIso(date: string): string | undefined {\n return date ? `${date}T00:00:00.000Z` : undefined;\n}\n\n/**\n * Exclusive ISO end for a `<input type=\"date\">` day: the start of the next\n * UTC day, matching the export's `until` (exclusive) contract.\n */\nexport function nextDayStartIso(date: string): string | undefined {\n if (!date) return undefined;\n const next = new Date(`${date}T00:00:00.000Z`);\n if (Number.isNaN(next.getTime())) return undefined;\n next.setUTCDate(next.getUTCDate() + 1);\n return next.toISOString();\n}\n\n/** File name for a downloaded xAPI export, e.g. `technospam-xapi-2026-10-31.json`. */\nexport function xapiExportFileName(now: Date = new Date(), courseSlug?: string): string {\n const day = now.toISOString().slice(0, 10);\n const safeSlug = courseSlug\n ? courseSlug\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/^-+|-+$/g, '')\n : '';\n return safeSlug ? `technospam-xapi-${safeSlug}-${day}.json` : `technospam-xapi-${day}.json`;\n}\n\n/** The GraphQL request an ATS partner sends to verify a serial (shown in the UI). */\nexport function atsVerificationSnippet(endpoint: string): string {\n return [\n `POST ${endpoint}`,\n 'Content-Type: application/json',\n '',\n JSON.stringify(\n {\n query:\n 'query Verify($serial: String!) { tecVerifyCertificate(serial: $serial) { serial status valid courseTitle score issuedAt revokedAt } }',\n variables: { serial: 'TEC-XXXXXXXX-XXXXXXXX' },\n },\n null,\n 2\n ),\n ].join('\\n');\n}\n"],"mappings":";AASA,SAAgB,EAAa,GAAyB;CACpD,IAAM,oBAAO,IAAI,KAAa;AAC9B,MAAK,IAAM,KAAO,EAAM,MAAM,UAAU,EAAE;EACxC,IAAM,IAAS,EAAI,MAAM,CAAC,aAAa;AACvC,EAAI,KAAQ,EAAK,IAAI,EAAO;;AAE9B,QAAO,CAAC,GAAG,EAAK;;AAIlB,SAAgB,EAAY,GAAkC;AAC5D,QAAO,IAAO,GAAG,EAAK,kBAAkB,KAAA;;AAO1C,SAAgB,EAAgB,GAAkC;AAChE,KAAI,CAAC,EAAM;CACX,IAAM,oBAAO,IAAI,KAAK,GAAG,EAAK,gBAAgB;AAC1C,aAAO,MAAM,EAAK,SAAS,CAAC,CAEhC,QADA,EAAK,WAAW,EAAK,YAAY,GAAG,EAAE,EAC/B,EAAK,aAAa;;AAI3B,SAAgB,EAAmB,oBAAY,IAAI,MAAM,EAAE,GAA6B;CACtF,IAAM,IAAM,EAAI,aAAa,CAAC,MAAM,GAAG,GAAG,EACpC,IAAW,IACb,EACG,aAAa,CACb,QAAQ,gBAAgB,IAAI,CAC5B,QAAQ,YAAY,GAAG,GAC1B;AACJ,QAAO,IAAW,mBAAmB,EAAS,GAAG,EAAI,SAAS,mBAAmB,EAAI;;AAIvF,SAAgB,EAAuB,GAA0B;AAC/D,QAAO;EACL,QAAQ;EACR;EACA;EACA,KAAK,UACH;GACE,OACE;GACF,WAAW,EAAE,QAAQ,yBAAyB;GAC/C,EACD,MACA,EACD;EACF,CAAC,KAAK,KAAK"}
|
|
1
|
+
{"version":3,"file":"enterprise.js","names":[],"sources":["../../../src/technospam/lib/enterprise.ts"],"sourcesContent":["/**\n * Pure helpers for the enterprise connectors admin tab (BOFF-4430).\n */\n\n/**\n * Split pasted serials (newlines, commas, semicolons or whitespace),\n * normalize them the way the service does (trim + upper-case) and drop\n * duplicates while keeping the first-seen order.\n */\nexport function parseSerials(input: string): string[] {\n const seen = new Set<string>();\n for (const raw of input.split(/[\\s,;]+/)) {\n const serial = raw.trim().toUpperCase();\n if (serial) seen.add(serial);\n }\n return [...seen];\n}\n\n/** Inclusive ISO start of a `<input type=\"date\">` day (UTC). */\nexport function dayStartIso(date: string): string | undefined {\n return date ? `${date}T00:00:00.000Z` : undefined;\n}\n\n/**\n * Exclusive ISO end for a `<input type=\"date\">` day: the start of the next\n * UTC day, matching the export's `until` (exclusive) contract.\n */\nexport function nextDayStartIso(date: string): string | undefined {\n if (!date) return undefined;\n const next = new Date(`${date}T00:00:00.000Z`);\n if (Number.isNaN(next.getTime())) return undefined;\n next.setUTCDate(next.getUTCDate() + 1);\n return next.toISOString();\n}\n\n/** File name for a downloaded xAPI export, e.g. `technospam-xapi-2026-10-31.json`. */\nexport function xapiExportFileName(now: Date = new Date(), courseSlug?: string): string {\n const day = now.toISOString().slice(0, 10);\n const safeSlug = courseSlug\n ? courseSlug\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/^-+|-+$/g, '')\n : '';\n return safeSlug ? `technospam-xapi-${safeSlug}-${day}.json` : `technospam-xapi-${day}.json`;\n}\n\n/** The GraphQL request an ATS partner sends to verify a serial (shown in the UI). */\nexport function atsVerificationSnippet(endpoint: string): string {\n return [\n `POST ${endpoint}`,\n 'Content-Type: application/json',\n '',\n JSON.stringify(\n {\n query:\n 'query Verify($serial: String!) { tecVerifyCertificate(serial: $serial) { serial status valid courseTitle score issuedAt revokedAt } }',\n variables: { serial: 'TEC-XXXXXXXX-XXXXXXXX' },\n },\n null,\n 2\n ),\n ].join('\\n');\n}\n\n/* ---------------------------------------------- LRS sync (BOFF-10575) */\n\nexport interface LrsFormValues {\n endpoint: string;\n key: string;\n secret: string;\n}\n\nexport type LrsFormErrors = Partial<Record<keyof LrsFormValues, string>>;\n\n/**\n * Check the connect form before anything is sent. The service applies the\n * authoritative checks (public address, the xAPI /about answer); this only\n * catches what can be told from the text: an https URL with no query,\n * fragment or embedded credentials, and a key and secret.\n */\nexport function validateLrsForm(values: LrsFormValues): LrsFormErrors {\n const errors: LrsFormErrors = {};\n const endpoint = values.endpoint.trim();\n if (!endpoint) {\n errors.endpoint = 'Enter the LRS endpoint.';\n } else {\n let url: URL | null = null;\n try {\n url = new URL(endpoint);\n } catch {\n errors.endpoint = 'Enter a full URL, for example https://lrs.example.com/data/xAPI.';\n }\n if (url) {\n if (url.protocol !== 'https:') errors.endpoint = 'The endpoint must start with https://.';\n else if (url.username || url.password)\n errors.endpoint = 'Put the key and secret in their own fields, not in the URL.';\n else if (url.search || url.hash)\n errors.endpoint = 'Leave out the query string; enter the xAPI base URL only.';\n }\n }\n if (!values.key.trim()) errors.key = 'Enter the LRS key.';\n if (!values.secret) errors.secret = 'Enter the LRS secret.';\n return errors;\n}\n\n/** The endpoint as the service will call it: trimmed, no trailing slash, no `/statements`. */\nexport function normalizeLrsEndpoint(endpoint: string): string {\n return endpoint\n .trim()\n .replace(/\\/+$/, '')\n .replace(/\\/statements$/i, '');\n}\n\n/** Name of the integration connection created for the LRS, e.g. `TechnoSpam LRS (lrs.example.com)`. */\nexport function lrsConnectionName(endpoint: string): string {\n try {\n return `TechnoSpam LRS (${new URL(endpoint).host})`;\n } catch {\n return 'TechnoSpam LRS';\n }\n}\n\nexport type TecXapiSyncState = 'NEVER_RUN' | 'OK' | 'FAILING' | 'RUNNING';\n\n/** One-line summary of the sync state for the status row. */\nexport function lrsSyncSummary(state: TecXapiSyncState, consecutiveFailures: number): string {\n switch (state) {\n case 'RUNNING':\n return 'Sending learner activity now.';\n case 'OK':\n return 'Up to date. New activity is sent automatically.';\n case 'FAILING':\n return consecutiveFailures > 1\n ? `The last ${consecutiveFailures} attempts failed. Retrying automatically with a growing delay.`\n : 'The last attempt failed. Retrying automatically.';\n default:\n return 'Connected. The first sync starts within a minute.';\n }\n}\n"],"mappings":";AASA,SAAgB,EAAa,GAAyB;CACpD,IAAM,oBAAO,IAAI,KAAa;AAC9B,MAAK,IAAM,KAAO,EAAM,MAAM,UAAU,EAAE;EACxC,IAAM,IAAS,EAAI,MAAM,CAAC,aAAa;AACvC,EAAI,KAAQ,EAAK,IAAI,EAAO;;AAE9B,QAAO,CAAC,GAAG,EAAK;;AAIlB,SAAgB,EAAY,GAAkC;AAC5D,QAAO,IAAO,GAAG,EAAK,kBAAkB,KAAA;;AAO1C,SAAgB,EAAgB,GAAkC;AAChE,KAAI,CAAC,EAAM;CACX,IAAM,oBAAO,IAAI,KAAK,GAAG,EAAK,gBAAgB;AAC1C,aAAO,MAAM,EAAK,SAAS,CAAC,CAEhC,QADA,EAAK,WAAW,EAAK,YAAY,GAAG,EAAE,EAC/B,EAAK,aAAa;;AAI3B,SAAgB,EAAmB,oBAAY,IAAI,MAAM,EAAE,GAA6B;CACtF,IAAM,IAAM,EAAI,aAAa,CAAC,MAAM,GAAG,GAAG,EACpC,IAAW,IACb,EACG,aAAa,CACb,QAAQ,gBAAgB,IAAI,CAC5B,QAAQ,YAAY,GAAG,GAC1B;AACJ,QAAO,IAAW,mBAAmB,EAAS,GAAG,EAAI,SAAS,mBAAmB,EAAI;;AAIvF,SAAgB,EAAuB,GAA0B;AAC/D,QAAO;EACL,QAAQ;EACR;EACA;EACA,KAAK,UACH;GACE,OACE;GACF,WAAW,EAAE,QAAQ,yBAAyB;GAC/C,EACD,MACA,EACD;EACF,CAAC,KAAK,KAAK;;AAmBd,SAAgB,EAAgB,GAAsC;CACpE,IAAM,IAAwB,EAAE,EAC1B,IAAW,EAAO,SAAS,MAAM;AACvC,KAAI,CAAC,EACH,GAAO,WAAW;MACb;EACL,IAAI,IAAkB;AACtB,MAAI;AACF,OAAM,IAAI,IAAI,EAAS;UACjB;AACN,KAAO,WAAW;;AAEpB,EAAI,MACE,EAAI,aAAa,WACZ,EAAI,YAAY,EAAI,WAC3B,EAAO,WAAW,iEACX,EAAI,UAAU,EAAI,UACzB,EAAO,WAAW,+DAJW,EAAO,WAAW;;AASrD,QAFK,EAAO,IAAI,MAAM,KAAE,EAAO,MAAM,uBAChC,EAAO,WAAQ,EAAO,SAAS,0BAC7B;;AAIT,SAAgB,EAAqB,GAA0B;AAC7D,QAAO,EACJ,MAAM,CACN,QAAQ,QAAQ,GAAG,CACnB,QAAQ,kBAAkB,GAAG;;AAIlC,SAAgB,EAAkB,GAA0B;AAC1D,KAAI;AACF,SAAO,mBAAmB,IAAI,IAAI,EAAS,CAAC,KAAK;SAC3C;AACN,SAAO;;;AAOX,SAAgB,EAAe,GAAyB,GAAqC;AAC3F,SAAQ,GAAR;EACE,KAAK,UACH,QAAO;EACT,KAAK,KACH,QAAO;EACT,KAAK,UACH,QAAO,IAAsB,IACzB,YAAY,EAAoB,kEAChC;EACN,QACE,QAAO"}
|