@burdenoff/microfe-technospam 2026.1102.3 → 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/global-types.d.ts +307 -41
- package/dist/generated/global-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +2025 -11
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.js +32 -28
- package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
- package/dist/technospam/api/assessmentApi.d.ts +183 -0
- package/dist/technospam/api/assessmentApi.d.ts.map +1 -0
- package/dist/technospam/api/assessmentApi.js +278 -0
- package/dist/technospam/api/assessmentApi.js.map +1 -0
- 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/CourseAssessments.d.ts +9 -0
- package/dist/technospam/components/CourseAssessments.d.ts.map +1 -0
- package/dist/technospam/components/CourseAssessments.js +66 -0
- package/dist/technospam/components/CourseAssessments.js.map +1 -0
- package/dist/technospam/components/StatusBadge.d.ts.map +1 -1
- package/dist/technospam/components/StatusBadge.js +12 -1
- package/dist/technospam/components/StatusBadge.js.map +1 -1
- package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts +7 -0
- package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts.map +1 -0
- package/dist/technospam/components/admin/AssessmentsAdminTab.js +421 -0
- package/dist/technospam/components/admin/AssessmentsAdminTab.js.map +1 -0
- 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/components/lesson/LessonKindFields.d.ts +3 -1
- package/dist/technospam/components/lesson/LessonKindFields.d.ts.map +1 -1
- package/dist/technospam/components/lesson/LessonKindFields.js +2 -2
- package/dist/technospam/components/lesson/LessonKindFields.js.map +1 -1
- package/dist/technospam/hooks/useAssessmentIntegrity.d.ts +29 -0
- package/dist/technospam/hooks/useAssessmentIntegrity.d.ts.map +1 -0
- package/dist/technospam/hooks/useAssessmentIntegrity.js +57 -0
- package/dist/technospam/hooks/useAssessmentIntegrity.js.map +1 -0
- 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/dist/technospam/pages/AdminAdminDashboardPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js +364 -359
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts +9 -0
- package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts.map +1 -0
- package/dist/technospam/pages/CoursesAssessmentPagePage.js +464 -0
- package/dist/technospam/pages/CoursesAssessmentPagePage.js.map +1 -0
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +210 -208
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/utils/assessment.d.ts +64 -0
- package/dist/technospam/utils/assessment.d.ts.map +1 -0
- package/dist/technospam/utils/assessment.js +122 -0
- package/dist/technospam/utils/assessment.js.map +1 -0
- 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":";;;;;;;;;;;AAoDA,IAAM,IAAmB,8DACnB,IAAc;AAEpB,SAAS,EAAa,EACpB,MAAM,GACN,UACA,kBAKC;AACD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,QAAD;GAAM,WAAU;aACd,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;GACb,CAAA,EACP,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,MAAD;IAAI,WAAU;cAA6C;IAAW,CAAA,EACtE,kBAAC,KAAD;IAAG,WAAU;cAAsC;IAAgB,CAAA,CAC/D;KACF;;;AAIV,SAAS,IAA8B;CACrC,IAAM,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAW,KAAgB,EAAmB,EAAE,CAAC,EAClD,EAAE,SAAM,YAAS,aAAU,EAA2B,EAAU,EAChE,IAAS,EAAa,EAAM,EAC5B,IAAU,EAAO,SAAA,IACjB,IAAU,GAAM,yBAAyB,EAAE;AAEjD,QACE,kBAAC,GAAD;EAAW,WAAU;EAAM,eAAY;YAAvC;GACE,kBAAC,GAAD;IACE,MAAM;IACN,OAAM;IACN,aAAY;IACZ,CAAA;GACF,kBAAC,SAAD;IACE,SAAQ;IACR,WAAU;cAFZ;KAGC;;KAC4D;KACrD;;GACR,kBAAC,GAAD;IACE,IAAG;IACH,OAAO;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;IACzC,aAAY;IACZ,MAAM;IACN,WAAU;IACV,CAAA;GACF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,MAAK;KACL,eAAe,EAAa,EAAO;KACnC,UAAU,EAAO,WAAW,KAAK,KAAW;eAH9C,CAKG,IAAU,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA,EAAC,SAE1E;QACT,kBAAC,QAAD;KAAM,WAAU;eACb,IACG,qBAAqB,EAAO,OAAO,mBACnC,GAAG,EAAO,OAAO,SAAS,EAAO,WAAW,IAAI,KAAK;KACpD,CAAA,CACH;;GAEL,IACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAW,OAAM;KAAyB;KAAS,CAAA;IAC/C,CAAA,GACJ,EAAQ,SAAS,IACnB,kBAAC,MAAD;IAAI,WAAU;IAAiB,cAAW;cACvC,EAAQ,KAAK,MACZ,kBAAC,MAAD;KAEE,WAAU;eAFZ,CAIE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAgD,EAAE;OAAa,CAAA,EAC9E,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAE,WAAW,cACV,+CACA,GAAG,EAAE,eAAe,SAAS,WAAW,EAAE,SAAS,EAAE,YAAY,EAAW,EAAE,SAAS,GACrF,EAAE,YAAY,cAAc,EAAW,EAAE,UAAU,KAAK;OAE1D,CAAA,CACF;SACN,kBAAC,GAAD,EAAa,QAAQ,EAAE,QAAU,CAAA,CAC9B;OAdE,EAAE,OAcJ,CACL;IACC,CAAA,GACH;GAEJ,kBAAC,WAAD;IAAS,WAAU;cAAnB;KACE,kBAAC,WAAD;MAAS,WAAU;gBAAuD;MAEhE,CAAA;KACV,kBAAC,KAAD;MAAG,WAAU;gBAAb;OAAgD;OACP,kBAAC,QAAD,EAAA,UAAM,yCAA4C,CAAA;OAAC;OAAI;;OACjE;OAC3B;;KACJ,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAuB,EAAiB;MACrC,CAAA;KACE;;GACA;;;AAIhB,SAAS,EAAa,GAAkB,GAAkB;CACxD,IAAM,IAAO,IAAI,KAAK,CAAC,KAAK,UAAU,GAAS,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,oBAAoB,CAAC,EACjF,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAS,SAAS,cAAc,IAAI;AAM1C,CALA,EAAO,OAAO,GACd,EAAO,WAAW,GAClB,SAAS,KAAK,YAAY,EAAO,EACjC,EAAO,OAAO,EACd,EAAO,QAAQ,EACf,IAAI,gBAAgB,EAAI;;AAG1B,SAAS,IAAiB;CACxB,IAAM,EAAE,MAAM,MAAgB,EAAgB,EAAE,CAAC,EAC3C,IAAU,GAAa,gBAAgB,SAAS,EAAE,EAClD,CAAC,GAAU,KAAe,EAAS,EAAY,EAC/C,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAI,KAAS,EAAS,GAAG,EAC1B,CAAC,GAAW,KAAgB,EAAwB,KAAK,EACzD,EAAE,QAAK,YAAS,aAAU,GAAe;AAwB/C,QACE,kBAAC,GAAD;EAAW,WAAU;EAAM,eAAY;YAAvC;GACE,kBAAC,GAAD;IACE,MAAM;IACN,OAAM;IACN,aAAY;IACZ,CAAA;GACF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,SAAD;OACE,WAAU;OACV,SAAQ;iBACT;OAEO,CAAA,EACR,kBAAC,GAAD;OAAQ,OAAO;OAAU,eAAe;iBAAxC,CACE,kBAAC,GAAD;QAAe,IAAG;QAAkB,WAAU;kBAC5C,kBAAC,GAAD,EAAa,aAAY,eAAgB,CAAA;QAC3B,CAAA,EAChB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;QAAY,OAAO;kBAAa;QAAwB,CAAA,EACvD,EAAQ,KAAK,MACZ,kBAAC,GAAD;QAAuB,OAAO,EAAE;kBAC7B,EAAE;QACQ,EAFI,EAAE,GAEN,CACb,CACY,EAAA,CAAA,CACT;SACL;;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,WAAU;MAAiD,SAAQ;gBAAgB;MAElF,CAAA,EACR,kBAAC,GAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,KAAK,KAAM,KAAA;MACX,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;MACxC,WAAU;MACV,CAAA,CACE,EAAA,CAAA;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,WAAU;MAAiD,SAAQ;gBAAc;MAEhF,CAAA,EACR,kBAAC,GAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,KAAK,KAAQ,KAAA;MACb,WAAW,MAAM,EAAM,EAAE,OAAO,MAAM;MACtC,WAAU;MACV,CAAA,CACE,EAAA,CAAA;KACN,kBAAC,GAAD;MAAQ,MAAK;MAAK,SA7EH,YAAY;OAC/B,IAAM,IAAS,EAAQ,MAAM,MAAM,EAAE,OAAO,EAAS,EAC/C,IAAS,MAAM,EAAI;QACvB,UAAU,GAAQ;QAClB,OAAO,EAAY,EAAK;QACxB,OAAO,EAAgB,EAAG;QAC3B,CAAC;AACG,cAEL;YADA,EAAa,EAAO,WAAW,OAAO,EAClC,EAAO,WAAW,WAAW,GAAG;AAClC,WAAO,KAAK,qBAAqB,qCAAqC;AACtE;;AAGF,QADA,EAAa,kBAAmB,IAAI,MAAM,EAAE,GAAQ,KAAK,EAAE,EAAO,WAAW,EAC7E,EAAO,QACL,0BACA,GAAG,EAAO,WAAW,OAAO,YAAY,EAAO,WAAW,WAAW,IAAI,KAAK,MAC5E,EAAO,YAAY,wDAAwD,GAC5E,GACF;;;MA0D4C,UAAU;gBAAnD,CACG,IAAU,kBAAC,GAAD;OAAS,MAAM;OAAI,WAAU;OAAiB,CAAA,GAAG,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA,EAAC,oBAE5E;;KACL;;GACL,MAAc,QAAQ,CAAC,IACtB,kBAAC,KAAD;IAAG,WAAU;IAA+B,MAAK;cAAjD;KAA0D;KAC1C;KAAU;KAAW,MAAc,IAAI,KAAK;KAAI;KAC5D;QACF;GACH,IACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAW,OAAM;KAA0C;KAAS,CAAA;IAChE,CAAA,GACJ;GACM;;;AAKhB,IAAM,IAAqB,KAErB,IAAmB,KACnB,IAAgC;CAAE,UAAU;CAAI,KAAK;CAAI,QAAQ;CAAI;AAE3E,SAAS,EAAe,GAA0C;AAChE,KAAI,CAAC,EAAO,QAAO;CACnB,IAAM,IAAO,IAAI,KAAK,EAAM;AAC5B,QAAO,OAAO,MAAM,EAAK,SAAS,CAAC,GAAG,UAAU,EAAK,gBAAgB;;AAGvE,SAAS,EAAS,EAChB,OACA,UACA,UACA,eAMC;AACD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,SAAD;IAAO,SAAS;IAAI,WAAU;cAC3B;IACK,CAAA;GACP;GACA,IACC,kBAAC,KAAD;IAAG,IAAI,GAAG,EAAG;IAAS,WAAU;IAAsC,MAAK;cACxE;IACC,CAAA,GACF;GACA;;;AAIV,SAAS,EAAe,EACtB,SACA,gBAIC;CACD,IAAM,CAAC,GAAQ,KAAa,EAAwB,EAAe,EAC7D,CAAC,GAAQ,KAAa,EAAwB,EAAE,CAAC,EAEjD,KAAO,OAAgC,MAC3C,GAAW,OAAO;EAAE,GAAG;GAAI,IAAQ,EAAE,OAAO;EAAO,EAAE;AAWvD,QACE,kBAAC,QAAD;EAAM,UAVa,OAAO,MAAiB;AAC3C,KAAE,gBAAgB;GAClB,IAAM,IAAQ,EAAgB,EAAO;AACrC,KAAU,EAAM,EACZ,SAAO,KAAK,EAAM,CAAC,SAAS,MACrB,MAAM,EAAU,EAAO,IAC1B,EAAU,EAAe;;EAIH,YAAA;EAAW,cAAW;YAApD,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KAAU,IAAG;KAAmB,OAAM;KAAe,OAAO,EAAO;eACjE,kBAAC,GAAD;MACE,IAAG;MACH,MAAK;MACL,WAAU;MACV,cAAa;MACb,aAAY;MACZ,OAAO,EAAO;MACd,UAAU,EAAI,WAAW;MACzB,gBAAc,EAAQ,EAAO;MAC7B,oBAAkB,EAAO,WAAW,2BAA2B,KAAA;MAC/D,CAAA;KACO,CAAA;IACX,kBAAC,GAAD;KAAU,IAAG;KAAc,OAAM;KAAM,OAAO,EAAO;eACnD,kBAAC,GAAD;MACE,IAAG;MACH,cAAa;MACb,OAAO,EAAO;MACd,UAAU,EAAI,MAAM;MACpB,gBAAc,EAAQ,EAAO;MAC7B,oBAAkB,EAAO,MAAM,sBAAsB,KAAA;MACrD,CAAA;KACO,CAAA;IACX,kBAAC,GAAD;KAAU,IAAG;KAAiB,OAAM;KAAS,OAAO,EAAO;eACzD,kBAAC,GAAD;MACE,IAAG;MACH,MAAK;MACL,cAAa;MACb,OAAO,EAAO;MACd,UAAU,EAAI,SAAS;MACvB,gBAAc,EAAQ,EAAO;MAC7B,oBAAkB,EAAO,SAAS,yBAAyB,KAAA;MAC3D,CAAA;KACO,CAAA;IACP;MACN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAQ,MAAK;IAAK,MAAK;IAAS,UAAU;cAA1C,CACG,IAAO,kBAAC,GAAD;KAAS,MAAM;KAAI,WAAU;KAAiB,CAAA,GAAG,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EAAC,cAErE;OACT,kBAAC,QAAD;IAAM,WAAU;cAA0B;IAGnC,CAAA,CACH;KACD;;;AAIX,SAAS,EAAQ,EAAE,UAAO,YAA8C;AACtE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,MAAD;GAAI,WAAU;aAA2B;GAAW,CAAA,EACpD,kBAAC,MAAD;GAAI,WAAU;aAA6C;GAAW,CAAA,CAClE;;;AAIV,SAAS,KAAc;CACrB,IAAM,EAAE,SAAM,YAAS,UAAO,iBAAc,mBAAgB,GAAqB,EAC3E,IAAO,GAAM,kBAAkB,MAC/B,EAAE,MAAM,MAAmB,EAAsB,GAAM,aAAa,EACpE,EAAE,YAAS,eAAY,YAAS,YAAS,GAAmB,EAC5D,IAAa,GAAgB,iBAAiB,MAC9C,IACJ,OAAO,GAAY,QAAS,WAAe,WAAW,EAAW,OAAO,UAAa,MAIjF,IAAU,IAEZ,EAAK,UAAU,aAAa,EAAK,UAAU,cACzC,IACA,IAHF;AAqBJ,QAjBA,QAAgB;AACV,YAAW,KAEf,QADA,EAAa,EAAO,QACP,GAAa;IACzB;EAAC;EAAQ;EAAc;EAAY,CAAC,EAcrC,kBAAC,GAAD;EAAW,WAAU;EAAM,eAAY;YAAvC,CACE,kBAAC,GAAD;GACE,MAAM;GACN,OAAM;GACN,aAAY;GACZ,CAAA,EACD,KAAW,CAAC,IACX,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAS,MAAM;IAAI,WAAU;IAAiB,CAAA,EAAA,wBAC1C;OACJ,IACF,kBAAC,GAAD;GAAW,OAAM;GAAoC;GAAS,CAAA,GAC5D,IACF,kBAAC,OAAD,EAAA,UAAA;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAa,QAAQ,EAAK,OAAS,CAAA,EACnC,kBAAC,QAAD;KAAM,WAAU;KAA8B,MAAK;eAChD,EAAe,EAAK,OAAO,EAAK,oBAAoB;KAChD,CAAA,CACH;;GACN,kBAAC,MAAD;IAAI,WAAU;cAAd;KACE,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,kBAAC,QAAD;OAAM,WAAU;iBAAa,KAAY,GAAY,QAAQ;OAAW,CAAA;MAC/E,CAAA;KACF,kBAAC,GAAD;MAAS,OAAM;MAAY,OAAO,EAAe,EAAK,WAAW;MAAI,CAAA;KACrE,kBAAC,GAAD;MAAS,OAAM;MAAe,OAAO,EAAe,EAAK,cAAc;MAAI,CAAA;KAC3E,kBAAC,GAAD;MAAS,OAAM;MAAkB,OAAO,EAAK,iBAAiB,gBAAgB;MAAI,CAAA;KAClF,kBAAC,GAAD;MAAS,OAAM;MAAY,OAAO,EAAe,EAAK,cAAc;MAAI,CAAA;KACxE,kBAAC,GAAD;MAAS,OAAM;MAAW,OAAO,SAAS,EAAK,gBAAgB;MAAS,CAAA;KACrE;;GACJ,EAAK,YACJ,kBAAC,OAAD;IACE,WAAU;IACV,eAAY;cAFd,CAIE,kBAAC,OAAD;KAAK,WAAU;eAAf,CAA4D,iBAC5C,EAAe,EAAK,YAAY,CAC1C;QACN,kBAAC,OAAD;KAAK,WAAU;eAAoB,EAAK;KAAgB,CAAA,CACpD;QACJ;GACJ,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,MAAK;KAAK,eAAe,KAAK,GAAS;KAAE,UAAU,MAAS;eAApE,CACG,MAAS,SACR,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAE9C,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA,EACvB,WAEK;QACT,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAe,KAAK,EAAW,EAAK,aAAa;KACjD,UAAU,MAAS;eAJrB,CAMG,MAAS,eACR,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAE9C,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA,EACpB,aAEK;OACL;;GACF,EAAA,CAAA,GAEN,kBAAC,GAAD;GAAgB,MAAM,MAAS;GAAW,WA/E1B,OAAO,MAA0B;IACrD,IAAM,IAAa,EAAqB,EAAO,SAAS;AAOxD,WAAO,EANQ,MAAM,EAAQ;KAC3B,MAAM,EAAkB,EAAW;KACnC,UAAU;KACV,KAAK,EAAO,IAAI,MAAM;KACtB,QAAQ,EAAO;KAChB,CAAC;;GAwEwE,CAAA,CAE9D;;;AAIhB,SAAS,KAAe;AACtB,QACE,kBAAC,GAAD;EAAW,WAAU;YACnB,kBAAC,GAAD;GACE,MAAM;GACN,OAAM;GACN,aAAY;GACZ,CAAA;EACQ,CAAA;;AAShB,SAAgB,KAA4B;AAC1C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD,EAA+B,CAAA;GAC/B,kBAAC,GAAD,EAAkB,CAAA;GAClB,kBAAC,IAAD,EAAe,CAAA;GACf,kBAAC,IAAD,EAAgB,CAAA;GACZ"}
|
|
@@ -7,9 +7,11 @@ import { LessonKindDraft } from './lessonAuthoring';
|
|
|
7
7
|
* expected output) and the multiple-choice questions of a QUIZ lesson with
|
|
8
8
|
* their answer key. Renders nothing for other kinds.
|
|
9
9
|
*/
|
|
10
|
-
export declare function LessonKindFields({ kind, draft, onChange, }: {
|
|
10
|
+
export declare function LessonKindFields({ kind, draft, onChange, heading, }: {
|
|
11
11
|
kind: TecLessonKind;
|
|
12
12
|
draft: LessonKindDraft;
|
|
13
13
|
onChange: (next: LessonKindDraft) => void;
|
|
14
|
+
/** Overrides the QUIZ section heading (assessments carry their own pass mark; BOFF-4427). */
|
|
15
|
+
heading?: string;
|
|
14
16
|
}): import("react").JSX.Element | null;
|
|
15
17
|
//# sourceMappingURL=LessonKindFields.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonKindFields.d.ts","sourceRoot":"","sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,KAAK,EACL,QAAQ,
|
|
1
|
+
{"version":3,"file":"LessonKindFields.d.ts","sourceRoot":"","sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,OAAO,GACR,EAAE;IACD,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IAC1C,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,sCA6HA"}
|
|
@@ -4,7 +4,7 @@ import { Plus as i, Trash2 as a } from "lucide-react";
|
|
|
4
4
|
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
5
5
|
import { Button as c, Input as l, Select as u, SelectContent as d, SelectItem as f, SelectTrigger as p, SelectValue as m, Textarea as h } from "@burdenoff/fe-libs/ui";
|
|
6
6
|
//#region src/technospam/components/lesson/LessonKindFields.tsx
|
|
7
|
-
function g({ kind: g, draft: _, onChange: v }) {
|
|
7
|
+
function g({ kind: g, draft: _, onChange: v, heading: y }) {
|
|
8
8
|
if (g === "INTERACTIVE") return /* @__PURE__ */ s("div", {
|
|
9
9
|
className: "mt-2 space-y-2 rounded-lg border border-border-seam p-3",
|
|
10
10
|
children: [
|
|
@@ -76,7 +76,7 @@ function g({ kind: g, draft: _, onChange: v }) {
|
|
|
76
76
|
children: [
|
|
77
77
|
/* @__PURE__ */ o("p", {
|
|
78
78
|
className: "text-xs font-semibold uppercase tracking-wide text-text-muted",
|
|
79
|
-
children: "Quiz questions (learners need 70% to pass)"
|
|
79
|
+
children: y ?? "Quiz questions (learners need 70% to pass)"
|
|
80
80
|
}),
|
|
81
81
|
_.quiz.map((t, n) => {
|
|
82
82
|
let i = r(t.options);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonKindFields.js","names":[],"sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"sourcesContent":["import { Plus, Trash2 } from 'lucide-react';\nimport {\n Button,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport type { TecLessonKind } from '../../api/technospamApi';\nimport { humanizeEnum } from '../../utils/format';\nimport {\n LESSON_LANGUAGES,\n emptyQuestion,\n splitOptions,\n type LessonKindDraft,\n type QuizQuestionDraft,\n} from './lessonAuthoring';\n\n/**\n * Kind-specific authoring fields for a lesson (BOFF-4107): the runnable\n * workspace of an INTERACTIVE lesson (language, starter code, sample input,\n * expected output) and the multiple-choice questions of a QUIZ lesson with\n * their answer key. Renders nothing for other kinds.\n */\nexport function LessonKindFields({\n kind,\n draft,\n onChange,\n}: {\n kind: TecLessonKind;\n draft: LessonKindDraft;\n onChange: (next: LessonKindDraft) => void;\n}) {\n if (kind === 'INTERACTIVE') {\n return (\n <div className=\"mt-2 space-y-2 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Runnable workspace\n </p>\n <Select value={draft.language} onValueChange={(v) => onChange({ ...draft, language: v })}>\n <SelectTrigger aria-label=\"Workspace language\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {LESSON_LANGUAGES.map((l) => (\n <SelectItem key={l} value={l}>\n {humanizeEnum(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Textarea\n aria-label=\"Starter code\"\n placeholder=\"Starter code shown in the editor\"\n className=\"font-mono text-xs\"\n value={draft.starterCode}\n onChange={(e) => onChange({ ...draft, starterCode: e.target.value })}\n spellCheck={false}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Textarea\n aria-label=\"Sample input\"\n placeholder=\"Sample input (stdin, optional)\"\n className=\"font-mono text-xs\"\n value={draft.sampleInput}\n onChange={(e) => onChange({ ...draft, sampleInput: e.target.value })}\n spellCheck={false}\n />\n <Textarea\n aria-label=\"Expected output\"\n placeholder=\"Expected output (optional; a matching run completes the lesson)\"\n className=\"font-mono text-xs\"\n value={draft.expectedOutput}\n onChange={(e) => onChange({ ...draft, expectedOutput: e.target.value })}\n spellCheck={false}\n />\n </div>\n </div>\n );\n }\n\n if (kind === 'QUIZ') {\n const update = (index: number, patch: Partial<QuizQuestionDraft>) =>\n onChange({\n ...draft,\n quiz: draft.quiz.map((q, i) => (i === index ? { ...q, ...patch } : q)),\n });\n return (\n <div className=\"mt-2 space-y-3 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Quiz questions (learners need 70% to pass)\n </p>\n {draft.quiz.map((q, i) => {\n const options = splitOptions(q.options);\n return (\n <div key={i} className=\"space-y-2 rounded-lg bg-bg-sunken p-3\">\n <div className=\"flex items-center gap-2\">\n <Input\n aria-label={`Question ${i + 1} prompt`}\n placeholder={`Question ${i + 1}`}\n value={q.prompt}\n onChange={(e) => update(i, { prompt: e.target.value })}\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove question ${i + 1}`}\n onClick={() => onChange({ ...draft, quiz: draft.quiz.filter((_, j) => j !== i) })}\n >\n <Trash2 size={14} />\n </Button>\n </div>\n <Textarea\n aria-label={`Question ${i + 1} options`}\n placeholder=\"Answer options, one per line\"\n value={q.options}\n onChange={(e) => update(i, { options: e.target.value })}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Select\n value={String(q.correctOption)}\n onValueChange={(v) => update(i, { correctOption: Number(v) })}\n disabled={options.length === 0}\n >\n <SelectTrigger aria-label={`Question ${i + 1} correct option`}>\n <SelectValue placeholder=\"Correct option\" />\n </SelectTrigger>\n <SelectContent>\n {options.map((o, oi) => (\n <SelectItem key={oi} value={String(oi)}>\n Correct: {o}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Input\n aria-label={`Question ${i + 1} explanation`}\n placeholder=\"Explanation shown after grading (optional)\"\n value={q.explanation}\n onChange={(e) => update(i, { explanation: e.target.value })}\n />\n </div>\n </div>\n );\n })}\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onChange({ ...draft, quiz: [...draft.quiz, emptyQuestion()] })}\n >\n <Plus size={14} /> Add question\n </Button>\n </div>\n );\n }\n\n return null;\n}\n"],"mappings":";;;;;;AA2BA,SAAgB,EAAiB,EAC/B,SACA,UACA,
|
|
1
|
+
{"version":3,"file":"LessonKindFields.js","names":[],"sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"sourcesContent":["import { Plus, Trash2 } from 'lucide-react';\nimport {\n Button,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport type { TecLessonKind } from '../../api/technospamApi';\nimport { humanizeEnum } from '../../utils/format';\nimport {\n LESSON_LANGUAGES,\n emptyQuestion,\n splitOptions,\n type LessonKindDraft,\n type QuizQuestionDraft,\n} from './lessonAuthoring';\n\n/**\n * Kind-specific authoring fields for a lesson (BOFF-4107): the runnable\n * workspace of an INTERACTIVE lesson (language, starter code, sample input,\n * expected output) and the multiple-choice questions of a QUIZ lesson with\n * their answer key. Renders nothing for other kinds.\n */\nexport function LessonKindFields({\n kind,\n draft,\n onChange,\n heading,\n}: {\n kind: TecLessonKind;\n draft: LessonKindDraft;\n onChange: (next: LessonKindDraft) => void;\n /** Overrides the QUIZ section heading (assessments carry their own pass mark; BOFF-4427). */\n heading?: string;\n}) {\n if (kind === 'INTERACTIVE') {\n return (\n <div className=\"mt-2 space-y-2 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Runnable workspace\n </p>\n <Select value={draft.language} onValueChange={(v) => onChange({ ...draft, language: v })}>\n <SelectTrigger aria-label=\"Workspace language\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {LESSON_LANGUAGES.map((l) => (\n <SelectItem key={l} value={l}>\n {humanizeEnum(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Textarea\n aria-label=\"Starter code\"\n placeholder=\"Starter code shown in the editor\"\n className=\"font-mono text-xs\"\n value={draft.starterCode}\n onChange={(e) => onChange({ ...draft, starterCode: e.target.value })}\n spellCheck={false}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Textarea\n aria-label=\"Sample input\"\n placeholder=\"Sample input (stdin, optional)\"\n className=\"font-mono text-xs\"\n value={draft.sampleInput}\n onChange={(e) => onChange({ ...draft, sampleInput: e.target.value })}\n spellCheck={false}\n />\n <Textarea\n aria-label=\"Expected output\"\n placeholder=\"Expected output (optional; a matching run completes the lesson)\"\n className=\"font-mono text-xs\"\n value={draft.expectedOutput}\n onChange={(e) => onChange({ ...draft, expectedOutput: e.target.value })}\n spellCheck={false}\n />\n </div>\n </div>\n );\n }\n\n if (kind === 'QUIZ') {\n const update = (index: number, patch: Partial<QuizQuestionDraft>) =>\n onChange({\n ...draft,\n quiz: draft.quiz.map((q, i) => (i === index ? { ...q, ...patch } : q)),\n });\n return (\n <div className=\"mt-2 space-y-3 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n {heading ?? 'Quiz questions (learners need 70% to pass)'}\n </p>\n {draft.quiz.map((q, i) => {\n const options = splitOptions(q.options);\n return (\n <div key={i} className=\"space-y-2 rounded-lg bg-bg-sunken p-3\">\n <div className=\"flex items-center gap-2\">\n <Input\n aria-label={`Question ${i + 1} prompt`}\n placeholder={`Question ${i + 1}`}\n value={q.prompt}\n onChange={(e) => update(i, { prompt: e.target.value })}\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove question ${i + 1}`}\n onClick={() => onChange({ ...draft, quiz: draft.quiz.filter((_, j) => j !== i) })}\n >\n <Trash2 size={14} />\n </Button>\n </div>\n <Textarea\n aria-label={`Question ${i + 1} options`}\n placeholder=\"Answer options, one per line\"\n value={q.options}\n onChange={(e) => update(i, { options: e.target.value })}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Select\n value={String(q.correctOption)}\n onValueChange={(v) => update(i, { correctOption: Number(v) })}\n disabled={options.length === 0}\n >\n <SelectTrigger aria-label={`Question ${i + 1} correct option`}>\n <SelectValue placeholder=\"Correct option\" />\n </SelectTrigger>\n <SelectContent>\n {options.map((o, oi) => (\n <SelectItem key={oi} value={String(oi)}>\n Correct: {o}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Input\n aria-label={`Question ${i + 1} explanation`}\n placeholder=\"Explanation shown after grading (optional)\"\n value={q.explanation}\n onChange={(e) => update(i, { explanation: e.target.value })}\n />\n </div>\n </div>\n );\n })}\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onChange({ ...draft, quiz: [...draft.quiz, emptyQuestion()] })}\n >\n <Plus size={14} /> Add question\n </Button>\n </div>\n );\n }\n\n return null;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TecAssessmentSignal } from '../api/assessmentApi';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser-side integrity hooks for a running assessment attempt (BOFF-4427).
|
|
5
|
+
*
|
|
6
|
+
* While `active`, it:
|
|
7
|
+
* - reports leaving the tab (`visibilitychange`) and the window (`blur`);
|
|
8
|
+
* a blur that the tab switch already explains is not reported twice;
|
|
9
|
+
* - tracks fullscreen and reports leaving it when the attempt requires it;
|
|
10
|
+
* - blocks copy / cut / paste and the context menu when the attempt says so,
|
|
11
|
+
* and reports each try.
|
|
12
|
+
*
|
|
13
|
+
* These are deterrents and evidence, not a lockdown: the server decides what
|
|
14
|
+
* counts as a violation and what happens to the attempt.
|
|
15
|
+
*/
|
|
16
|
+
export interface IntegrityOptions {
|
|
17
|
+
active: boolean;
|
|
18
|
+
requireFullscreen: boolean;
|
|
19
|
+
blockClipboard: boolean;
|
|
20
|
+
onSignal: (signal: TecAssessmentSignal, detail?: Record<string, string | number | boolean>) => void;
|
|
21
|
+
}
|
|
22
|
+
export interface IntegrityState {
|
|
23
|
+
/** Fullscreen is supported by this browser (and not blocked by the embedding page). */
|
|
24
|
+
fullscreenSupported: boolean;
|
|
25
|
+
isFullscreen: boolean;
|
|
26
|
+
enterFullscreen: () => Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
export declare function useAssessmentIntegrity({ active, requireFullscreen, blockClipboard, onSignal, }: IntegrityOptions): IntegrityState;
|
|
29
|
+
//# sourceMappingURL=useAssessmentIntegrity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAssessmentIntegrity.d.ts","sourceRoot":"","sources":["../../../src/technospam/hooks/useAssessmentIntegrity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,CACR,MAAM,EAAE,mBAAmB,EAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,KAC/C,IAAI,CAAC;CACX;AAED,MAAM,WAAW,cAAc;IAC7B,uFAAuF;IACvF,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACzC;AASD,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,QAAQ,GACT,EAAE,gBAAgB,GAAG,cAAc,CA8GnC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback as e, useEffect as t, useRef as n, useState as r } from "react";
|
|
2
|
+
//#region src/technospam/hooks/useAssessmentIntegrity.ts
|
|
3
|
+
var i = 800;
|
|
4
|
+
function a() {
|
|
5
|
+
return typeof document > "u" ? null : document.fullscreenElement;
|
|
6
|
+
}
|
|
7
|
+
function o({ active: o, requireFullscreen: s, blockClipboard: c, onSignal: l }) {
|
|
8
|
+
let u = typeof document < "u" && document.fullscreenEnabled === !0 && typeof document.documentElement.requestFullscreen == "function", [d, f] = r(() => a() !== null), p = n(l);
|
|
9
|
+
t(() => {
|
|
10
|
+
p.current = l;
|
|
11
|
+
}, [l]);
|
|
12
|
+
let m = n(0), h = n(!1), g = e(async () => {
|
|
13
|
+
if (!u) return !1;
|
|
14
|
+
try {
|
|
15
|
+
return await document.documentElement.requestFullscreen(), h.current = !0, !0;
|
|
16
|
+
} catch {
|
|
17
|
+
return !1;
|
|
18
|
+
}
|
|
19
|
+
}, [u]);
|
|
20
|
+
return t(() => {
|
|
21
|
+
if (!o) return;
|
|
22
|
+
let e = (e, t) => p.current(e, t), t, n = () => {
|
|
23
|
+
t !== void 0 && window.clearTimeout(t), t = void 0;
|
|
24
|
+
}, r = () => {
|
|
25
|
+
document.visibilityState === "hidden" && (m.current = Date.now(), n(), e("VISIBILITY_HIDDEN"));
|
|
26
|
+
}, l = () => {
|
|
27
|
+
Date.now() - m.current < i || document.visibilityState !== "hidden" && (n(), t = window.setTimeout(() => {
|
|
28
|
+
t = void 0, document.visibilityState !== "hidden" && (Date.now() - m.current < i || e("WINDOW_BLUR"));
|
|
29
|
+
}, i));
|
|
30
|
+
}, u = () => {
|
|
31
|
+
let t = a() !== null;
|
|
32
|
+
f(t), s && e(t ? "FULLSCREEN_ENTER" : "FULLSCREEN_EXIT");
|
|
33
|
+
}, d = (t) => (n) => {
|
|
34
|
+
c && (n.preventDefault(), e(t));
|
|
35
|
+
}, h = d("COPY_ATTEMPT"), g = d("CUT_ATTEMPT"), _ = d("PASTE_ATTEMPT"), v = d("CONTEXT_MENU");
|
|
36
|
+
return document.addEventListener("visibilitychange", r), window.addEventListener("blur", l), document.addEventListener("fullscreenchange", u), document.addEventListener("copy", h), document.addEventListener("cut", g), document.addEventListener("paste", _), document.addEventListener("contextmenu", v), () => {
|
|
37
|
+
n(), document.removeEventListener("visibilitychange", r), window.removeEventListener("blur", l), document.removeEventListener("fullscreenchange", u), document.removeEventListener("copy", h), document.removeEventListener("cut", g), document.removeEventListener("paste", _), document.removeEventListener("contextmenu", v);
|
|
38
|
+
};
|
|
39
|
+
}, [
|
|
40
|
+
o,
|
|
41
|
+
s,
|
|
42
|
+
c
|
|
43
|
+
]), t(() => {
|
|
44
|
+
let e = () => {
|
|
45
|
+
h.current && (h.current = !1, a() !== null && typeof document.exitFullscreen == "function" && document.exitFullscreen().catch(() => void 0));
|
|
46
|
+
};
|
|
47
|
+
return o || e(), e;
|
|
48
|
+
}, [o]), {
|
|
49
|
+
fullscreenSupported: u,
|
|
50
|
+
isFullscreen: d,
|
|
51
|
+
enterFullscreen: g
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { o as useAssessmentIntegrity };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=useAssessmentIntegrity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAssessmentIntegrity.js","names":[],"sources":["../../../src/technospam/hooks/useAssessmentIntegrity.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { TecAssessmentSignal } from '../api/assessmentApi';\n\n/**\n * Browser-side integrity hooks for a running assessment attempt (BOFF-4427).\n *\n * While `active`, it:\n * - reports leaving the tab (`visibilitychange`) and the window (`blur`);\n * a blur that the tab switch already explains is not reported twice;\n * - tracks fullscreen and reports leaving it when the attempt requires it;\n * - blocks copy / cut / paste and the context menu when the attempt says so,\n * and reports each try.\n *\n * These are deterrents and evidence, not a lockdown: the server decides what\n * counts as a violation and what happens to the attempt.\n */\nexport interface IntegrityOptions {\n active: boolean;\n requireFullscreen: boolean;\n blockClipboard: boolean;\n onSignal: (\n signal: TecAssessmentSignal,\n detail?: Record<string, string | number | boolean>\n ) => void;\n}\n\nexport interface IntegrityState {\n /** Fullscreen is supported by this browser (and not blocked by the embedding page). */\n fullscreenSupported: boolean;\n isFullscreen: boolean;\n enterFullscreen: () => Promise<boolean>;\n}\n\n/** Window-level blur within this window of a visibility change is the same event. */\nconst BLUR_DEDUP_MS = 800;\n\nfunction fullscreenElement(): Element | null {\n return typeof document === 'undefined' ? null : document.fullscreenElement;\n}\n\nexport function useAssessmentIntegrity({\n active,\n requireFullscreen,\n blockClipboard,\n onSignal,\n}: IntegrityOptions): IntegrityState {\n const fullscreenSupported =\n typeof document !== 'undefined' &&\n document.fullscreenEnabled === true &&\n typeof document.documentElement.requestFullscreen === 'function';\n const [isFullscreen, setIsFullscreen] = useState<boolean>(() => fullscreenElement() !== null);\n const signalRef = useRef(onSignal);\n useEffect(() => {\n signalRef.current = onSignal;\n }, [onSignal]);\n const lastHiddenAt = useRef(0);\n // Whether this hook put the page into fullscreen, so it only exits what it entered.\n const acquiredFullscreen = useRef(false);\n\n const enterFullscreen = useCallback(async (): Promise<boolean> => {\n if (!fullscreenSupported) return false;\n try {\n await document.documentElement.requestFullscreen();\n acquiredFullscreen.current = true;\n return true;\n } catch {\n return false;\n }\n }, [fullscreenSupported]);\n\n useEffect(() => {\n if (!active) return undefined;\n const report = (\n signal: TecAssessmentSignal,\n detail?: Record<string, string | number | boolean>\n ): void => signalRef.current(signal, detail);\n\n // A tab switch fires both blur and visibilitychange, in either order.\n // Blur is reported only after a short wait, and dropped if the tab was\n // hidden around it, so one switch counts once.\n let pendingBlur: number | undefined;\n const cancelBlur = (): void => {\n if (pendingBlur !== undefined) window.clearTimeout(pendingBlur);\n pendingBlur = undefined;\n };\n const onVisibility = (): void => {\n if (document.visibilityState === 'hidden') {\n lastHiddenAt.current = Date.now();\n cancelBlur();\n report('VISIBILITY_HIDDEN');\n }\n };\n const onBlur = (): void => {\n if (Date.now() - lastHiddenAt.current < BLUR_DEDUP_MS) return;\n if (document.visibilityState === 'hidden') return;\n cancelBlur();\n pendingBlur = window.setTimeout(() => {\n pendingBlur = undefined;\n if (document.visibilityState === 'hidden') return;\n if (Date.now() - lastHiddenAt.current < BLUR_DEDUP_MS) return;\n report('WINDOW_BLUR');\n }, BLUR_DEDUP_MS);\n };\n const onFullscreen = (): void => {\n const now = fullscreenElement() !== null;\n setIsFullscreen(now);\n if (!requireFullscreen) return;\n report(now ? 'FULLSCREEN_ENTER' : 'FULLSCREEN_EXIT');\n };\n const clipboard =\n (signal: TecAssessmentSignal) =>\n (event: Event): void => {\n if (!blockClipboard) return;\n event.preventDefault();\n report(signal);\n };\n const onCopy = clipboard('COPY_ATTEMPT');\n const onCut = clipboard('CUT_ATTEMPT');\n const onPaste = clipboard('PASTE_ATTEMPT');\n const onContextMenu = clipboard('CONTEXT_MENU');\n\n document.addEventListener('visibilitychange', onVisibility);\n window.addEventListener('blur', onBlur);\n document.addEventListener('fullscreenchange', onFullscreen);\n document.addEventListener('copy', onCopy);\n document.addEventListener('cut', onCut);\n document.addEventListener('paste', onPaste);\n document.addEventListener('contextmenu', onContextMenu);\n return () => {\n cancelBlur();\n document.removeEventListener('visibilitychange', onVisibility);\n window.removeEventListener('blur', onBlur);\n document.removeEventListener('fullscreenchange', onFullscreen);\n document.removeEventListener('copy', onCopy);\n document.removeEventListener('cut', onCut);\n document.removeEventListener('paste', onPaste);\n document.removeEventListener('contextmenu', onContextMenu);\n };\n }, [active, requireFullscreen, blockClipboard]);\n\n // Leave fullscreen when the attempt ends (monitoring turned off, or the exam\n // unmounts after submit) so the learner gets their window back.\n useEffect(() => {\n const release = (): void => {\n if (!acquiredFullscreen.current) return;\n acquiredFullscreen.current = false;\n if (fullscreenElement() !== null && typeof document.exitFullscreen === 'function') {\n void document.exitFullscreen().catch(() => undefined);\n }\n };\n if (!active) release();\n return release;\n }, [active]);\n\n return { fullscreenSupported, isFullscreen, enterFullscreen };\n}\n"],"mappings":";;AAkCA,IAAM,IAAgB;AAEtB,SAAS,IAAoC;AAC3C,QAAO,OAAO,WAAa,MAAc,OAAO,SAAS;;AAG3D,SAAgB,EAAuB,EACrC,WACA,sBACA,mBACA,eACmC;CACnC,IAAM,IACJ,OAAO,WAAa,OACpB,SAAS,sBAAsB,MAC/B,OAAO,SAAS,gBAAgB,qBAAsB,YAClD,CAAC,GAAc,KAAmB,QAAwB,GAAmB,KAAK,KAAK,EACvF,IAAY,EAAO,EAAS;AAClC,SAAgB;AACd,IAAU,UAAU;IACnB,CAAC,EAAS,CAAC;CACd,IAAM,IAAe,EAAO,EAAE,EAExB,IAAqB,EAAO,GAAM,EAElC,IAAkB,EAAY,YAA8B;AAChE,MAAI,CAAC,EAAqB,QAAO;AACjC,MAAI;AAGF,UAFA,MAAM,SAAS,gBAAgB,mBAAmB,EAClD,EAAmB,UAAU,IACtB;UACD;AACN,UAAO;;IAER,CAAC,EAAoB,CAAC;AAsFzB,QApFA,QAAgB;AACd,MAAI,CAAC,EAAQ;EACb,IAAM,KACJ,GACA,MACS,EAAU,QAAQ,GAAQ,EAAO,EAKxC,GACE,UAAyB;AAE7B,GADI,MAAgB,KAAA,KAAW,OAAO,aAAa,EAAY,EAC/D,IAAc,KAAA;KAEV,UAA2B;AAC/B,GAAI,SAAS,oBAAoB,aAC/B,EAAa,UAAU,KAAK,KAAK,EACjC,GAAY,EACZ,EAAO,oBAAoB;KAGzB,UAAqB;AACrB,QAAK,KAAK,GAAG,EAAa,UAAU,KACpC,SAAS,oBAAoB,aACjC,GAAY,EACZ,IAAc,OAAO,iBAAiB;AACpC,QAAc,KAAA,GACV,SAAS,oBAAoB,aAC7B,KAAK,KAAK,GAAG,EAAa,UAAU,KACxC,EAAO,cAAc;MACpB,EAAc;KAEb,UAA2B;GAC/B,IAAM,IAAM,GAAmB,KAAK;AACpC,KAAgB,EAAI,EACf,KACL,EAAO,IAAM,qBAAqB,kBAAkB;KAEhD,KACH,OACA,MAAuB;AACjB,SACL,EAAM,gBAAgB,EACtB,EAAO,EAAO;KAEZ,IAAS,EAAU,eAAe,EAClC,IAAQ,EAAU,cAAc,EAChC,IAAU,EAAU,gBAAgB,EACpC,IAAgB,EAAU,eAAe;AAS/C,SAPA,SAAS,iBAAiB,oBAAoB,EAAa,EAC3D,OAAO,iBAAiB,QAAQ,EAAO,EACvC,SAAS,iBAAiB,oBAAoB,EAAa,EAC3D,SAAS,iBAAiB,QAAQ,EAAO,EACzC,SAAS,iBAAiB,OAAO,EAAM,EACvC,SAAS,iBAAiB,SAAS,EAAQ,EAC3C,SAAS,iBAAiB,eAAe,EAAc,QAC1C;AAQX,GAPA,GAAY,EACZ,SAAS,oBAAoB,oBAAoB,EAAa,EAC9D,OAAO,oBAAoB,QAAQ,EAAO,EAC1C,SAAS,oBAAoB,oBAAoB,EAAa,EAC9D,SAAS,oBAAoB,QAAQ,EAAO,EAC5C,SAAS,oBAAoB,OAAO,EAAM,EAC1C,SAAS,oBAAoB,SAAS,EAAQ,EAC9C,SAAS,oBAAoB,eAAe,EAAc;;IAE3D;EAAC;EAAQ;EAAmB;EAAe,CAAC,EAI/C,QAAgB;EACd,IAAM,UAAsB;AACrB,KAAmB,YACxB,EAAmB,UAAU,IACzB,GAAmB,KAAK,QAAQ,OAAO,SAAS,kBAAmB,cAChE,SAAS,gBAAgB,CAAC,YAAY,KAAA,EAAU;;AAIzD,SADK,KAAQ,GAAS,EACf;IACN,CAAC,EAAO,CAAC,EAEL;EAAE;EAAqB;EAAc;EAAiB"}
|
|
@@ -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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminAdminDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/AdminAdminDashboardPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminAdminDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/AdminAdminDashboardPagePage.tsx"],"names":[],"mappings":"AAioDA,wBAAgB,2BAA2B,gCAoE1C"}
|