chore: update admin docs, role config, and kb management

This commit is contained in:
Ashwin Kumar 2026-04-06 01:46:58 +02:00
parent ab658d826b
commit 9bfaa2279c
11 changed files with 177 additions and 73 deletions

View file

@ -1,9 +1,11 @@
> Policy Update (2026-04-05): External onboarding management is deprecated and removed. After sign-up/login, users are routed directly by role key to role-specific dashboard/runtime modules. Any legacy references to /onboarding, /choose-role onboarding flows, onboarding schema management, or fallback data strategies should be treated as obsolete. Runtime config and canonical backend APIs are the only source of truth.
# Nxtgauge End-to-End Testing Blueprint
## 1. Scope
- Validate full lifecycle for external users from sign-up to role approval and role-specific module insertion.
- Validate admin-side verification and approval pipelines and state propagation into management modules.
- Validate runtime-config-driven onboarding/dashboard behavior and guard/redirect behavior.
- Validate runtime-config-driven role-key dashboard routing and guard/redirect behavior.
- Validate notification and marketplace flows (customer requirement posting, professional discovery).
- Validate UI parity and visual regressions for management modules using Playwright + pixelmatch + Storybook + VisBug.
- Validate frontend (`vitest`, Playwright) and backend (`cargo test --workspace`) quality gates.
@ -16,7 +18,7 @@
- role-specific table insertion,
- notification event.
- Multi-role users are supported and role visibility must remain isolated by role/module.
- Runtime config must not silently mask missing onboarding/dashboard config.
- Runtime config must not silently mask missing role/dashboard config.
## 3. Environments and Preconditions
- Frontend admin app: `/Users/ashwin/workspace/nxtgauge-admin-solid`
@ -27,13 +29,13 @@
- Preconditions:
- `npm install` completed in admin repo.
- Rust toolchain + dependencies available for backend repo.
- Seed users/roles configured in backend DB (or fallback data strategy used in UI where applicable).
- Seed users/roles configured in backend DB (no UI fallback data strategy).
- Admin preview mode supported with `?_preview=1` for deterministic UI checks.
## 4. Test Data Matrix
- External identities:
- User without role.
- User with pending role onboarding.
- User with assigned role key and pending approval.
- User with verified/pending approval role.
- User with approved active role.
- Multi-role user (2+ roles).
@ -54,10 +56,10 @@
- external identity blocked from admin login.
- internal identity allowed and redirected to admin.
- external-resolved session redirected to `/login?from=...`.
2. Onboarding:
- role-aware onboarding route loads via runtime config.
- missing config renders explicit error state.
- onboarding submissions persist and move to verification state.
2. Role Runtime:
- role-key based dashboard route resolves via runtime config.
- missing runtime config renders explicit error state.
- role state persists and moves to verification/approval state.
3. Dashboard:
- role-specific sidebar and tabs render correctly.
- guarded routes reject wrong portal/session.
@ -106,12 +108,12 @@
- Assertions:
- lifecycle transitions emit expected payload shape/state.
- role key propagation remains consistent across services.
- no silent fallback on missing config.
- no silent fallback on missing runtime config.
## 8. Database Validation Plan
- Validate tables/events after each phase:
- user record creation at sign-up.
- onboarding row/state creation.
- role state creation/update.
- verification record creation/update.
- approval decision and role activation state.
- role-specific module table insertion on approval only.
@ -196,4 +198,3 @@
- Runtime config partial failures causing invisible fallback behavior.
- Backend endpoint shape drift (`users`, `external-users`, `dashboard-config`) causing UI empty states.
- Visual regressions in shared table controls/icons across management modules.

View file

@ -20,8 +20,6 @@ const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [
{ prefix: '/admin/roles', label: 'Internal Role Management' },
{ prefix: '/admin/employees', label: 'Employee Management' },
{ prefix: '/admin/external-roles', label: 'External Role Management' },
{ prefix: '/admin/onboarding-management', label: 'Onboarding Management' },
{ prefix: '/admin/onboarding-schemas', label: 'Onboarding Management' },
{ prefix: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management' },
{ prefix: '/admin/external-dashboard-management', label: 'External Dashboard Management' },
{ prefix: '/admin/role-ui-configs', label: 'External Dashboard Management' },
@ -73,8 +71,6 @@ const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [
{ prefix: '/admin/roles', keys: ['INTERNAL_ROLE_MANAGEMENT', 'ROLES'] },
{ prefix: '/admin/employees', keys: ['EMPLOYEE_MANAGEMENT', 'EMPLOYEES'] },
{ prefix: '/admin/external-roles', keys: ['EXTERNAL_ROLE_MANAGEMENT', 'EXTERNAL_ROLES'] },
{ prefix: '/admin/onboarding-management', keys: ['EXTERNAL_ONBOARDING_MANAGEMENT', 'ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] },
{ prefix: '/admin/onboarding-schemas', keys: ['EXTERNAL_ONBOARDING_MANAGEMENT', 'ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] },
{ prefix: '/admin/internal-dashboard-management', keys: ['INTERNAL_DASHBOARD_MANAGEMENT', 'INTERNAL_DASHBOARDS', 'INTERNAL_DASHBOARD_CONFIG'] },
{ prefix: '/admin/external-dashboard-management', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'EXTERNAL_DASHBOARD_CONFIG', 'RUNTIME_ROLES'] },
{ prefix: '/admin/role-ui-configs', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'EXTERNAL_DASHBOARD_CONFIG', 'RUNTIME_ROLES'] },

View file

@ -30,7 +30,6 @@ const GROUPS: NavItem[][] = [
],
[
{ href: '/admin/external-roles', label: 'External Role Management', icon: ShieldCheck, moduleKeys: ['EXTERNAL_ROLE_MANAGEMENT', 'EXTERNAL_ROLES'] },
{ href: '/admin/onboarding-management', label: 'External Onboarding Management', icon: FileText, aliasPrefix: '/admin/onboarding-schemas', moduleKeys: ['EXTERNAL_ONBOARDING_MANAGEMENT', 'ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] },
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: LayoutDashboard, moduleKeys: ['INTERNAL_DASHBOARD_MANAGEMENT', 'INTERNAL_DASHBOARDS', 'INTERNAL_DASHBOARD_CONFIG'] },
{ href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: LayoutDashboard, aliasPrefix: '/admin/role-ui-configs', moduleKeys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'EXTERNAL_DASHBOARD_CONFIG', 'RUNTIME_ROLES'] },
],

View file

@ -22,7 +22,7 @@ function resolveLegacyPath(modulePath: string): string {
case 'approvals':
return '/approval';
case 'onboarding-management':
return '/onboarding-management';
return '/';
case 'internal-dashboard-management':
return '/internal-dashboard-management';
case 'external-dashboard-management':

View file

@ -364,42 +364,17 @@ export default function ExternalDashboardManagementPage() {
fetch(`${API}/api/admin/roles?audience=EXTERNAL&per_page=200`, { headers: authHeaders(), credentials: 'include' }),
]);
const dashData = dashRes.ok ? await dashRes.json().catch(() => []) : [];
const roleData = rolesRes.ok ? await rolesRes.json().catch(() => []) : [];
if (!dashRes.ok) throw new Error(`Failed to load dashboard config (${dashRes.status})`);
if (!rolesRes.ok) throw new Error(`Failed to load external roles (${rolesRes.status})`);
const dashNeedsFallback = !dashRes.ok || looksLikeGateway404(dashData);
const rolesNeedFallback = !rolesRes.ok || looksLikeGateway404(roleData);
let finalDashData: any = dashData;
let finalRoleData: any = roleData;
if (dashNeedsFallback) {
const fallbackDashRes = await fetch(`${API}/api/config/dashboard`, {
headers: authHeaders(),
credentials: 'include',
});
finalDashData = fallbackDashRes.ok ? await fallbackDashRes.json().catch(() => []) : [];
const dashData = await dashRes.json().catch(() => []);
const roleData = await rolesRes.json().catch(() => []);
if (looksLikeGateway404(dashData) || looksLikeGateway404(roleData)) {
throw new Error('Required admin runtime endpoints are unavailable.');
}
if (rolesNeedFallback) {
// Gateway no longer routes /api/admin/roles in some environments.
// Build role options from dashboard configs so external dashboard table remains usable.
const rawRows = Array.isArray(finalDashData) ? finalDashData : (finalDashData?.items || finalDashData?.configs || []);
const roleMap = new Map<string, RoleOption>();
for (const row of rawRows) {
const audience = String(row?.audience || '').toUpperCase();
if (audience !== 'EXTERNAL') continue;
const roleId = String(row?.role_id || row?.roleId || '').trim();
const roleKey = String(row?.role_key || row?.config_json?.role_key || '').toUpperCase().trim();
if (!roleId) continue;
const name = normalizeRoleNameFromKey(roleKey || 'EXTERNAL_ROLE');
roleMap.set(roleId, { id: roleId, key: roleKey || 'EXTERNAL_ROLE', name });
}
finalRoleData = Array.from(roleMap.values());
}
const dashRows = Array.isArray(finalDashData) ? finalDashData : (finalDashData?.items || finalDashData?.configs || []);
const roleRows = Array.isArray(finalRoleData) ? finalRoleData : (finalRoleData?.roles || finalRoleData?.items || []);
const dashRows = Array.isArray(dashData) ? dashData : (dashData?.items || dashData?.configs || []);
const roleRows = Array.isArray(roleData) ? roleData : (roleData?.roles || roleData?.items || []);
setRoles(roleRows
.filter((r: any) => {

View file

@ -93,6 +93,16 @@ const PERMISSION_ACTIONS = [
{ key: 'approve', label: 'Approve' }
];
function StatusBadge(props: { status: string }) {
const active = () => props.status === 'ACTIVE';
return (
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
{active() ? 'Active' : 'Inactive'}
</span>
);
}
const FALLBACK_LOGS = [
{ id: 'l1', user: 'Admin Ashwin', action: 'Updated Permissions', target: 'Verified Company', date: '2026-03-27 10:30' },
{ id: 'l2', user: 'Admin Ashwin', action: 'Changed Status', target: 'Professional Photographer', date: '2026-03-26 14:15' },
@ -105,16 +115,6 @@ const FALLBACK_ASSIGNED_USERS = [
{ id: 'u3', name: 'Alice Wong', email: 'alice@photo.me', joined: '2026-03-12' },
];
function StatusBadge(props: { status: string }) {
const active = () => props.status === 'ACTIVE';
return (
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
{active() ? 'Active' : 'Inactive'}
</span>
);
}
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
return (
<label style="display:block">

View file

@ -22,6 +22,61 @@ type KbArticle = {
updated_at?: string;
};
type SeedArticle = {
title: string;
slug: string;
categorySlug: string;
content: string;
};
const KB_SEED_CATEGORIES: Array<{ name: string; slug: string; description: string }> = [
{ name: 'Getting Started', slug: 'getting-started', description: 'Account setup, role selection, and first actions.' },
{ name: 'Account & Login', slug: 'account-login', description: 'Login, password, account access, and security.' },
{ name: 'Profile & Verification', slug: 'profile-verification', description: 'Profile setup and verification workflow help.' },
{ name: 'Platform Basics', slug: 'platform-basics', description: 'Simple guides to understand core platform workflows.' },
{ name: 'Jobs & Applications', slug: 'jobs-applications', description: 'Posting jobs, applying, and status tracking.' },
{ name: 'Leads & Requests', slug: 'leads-requests', description: 'Handling incoming leads and request responses.' },
{ name: 'TraceCoins & Billing', slug: 'tracecoins-billing', description: 'Credits, payments, invoices, and refunds.' },
{ name: 'Privacy & Safety', slug: 'privacy-safety', description: 'Policy, reporting, trust and account safety.' },
{ name: 'Troubleshooting', slug: 'troubleshooting', description: 'Common issues and quick resolution guides.' },
];
const KB_SEED_ARTICLES: SeedArticle[] = [
{ title: 'Create your Nxtgauge account in 3 steps', slug: 'create-account-3-steps', categorySlug: 'getting-started', content: 'Step 1: Sign up with your email or mobile number.\n\nStep 2: Verify OTP and complete basic account details.\n\nStep 3: Go to role setup and add clear profile information.\n\nTip: Keep your name, location, and contact details accurate so approval and communication are smooth.' },
{ title: 'Choose your role after signup', slug: 'choose-role-after-signup', categorySlug: 'getting-started', content: 'Nxtgauge uses one account with role-based journeys. You can choose your role after signup.\n\nIf you are a company, use the company flow for job posting.\nIf you are a professional, use the service profile flow.\nIf you are a job seeker, complete candidate details.\n\nYou can update your role path later when needed.' },
{ title: 'Understand account statuses (Draft, Review, Approved)', slug: 'understand-account-statuses', categorySlug: 'getting-started', content: 'Draft: Your profile is incomplete and not ready for full visibility.\n\nReview: Your submission is under verification checks.\n\nApproved: Your profile or listing passed checks and is visible in discovery.\n\nIf your status stays in review for too long, check if any required data is missing.' },
{ title: 'How the Nxtgauge platform works', slug: 'how-nxtgauge-platform-works', categorySlug: 'platform-basics', content: 'Nxtgauge is built on a trust-first workflow.\n\n1. User submits profile, requirement, or job.\n2. Platform runs verification checks.\n3. Approved submissions become discoverable.\n4. Users track status and responses in one flow.\n\nThis helps reduce spam and improves first-contact quality.' },
{ title: 'One account, multiple journeys explained', slug: 'one-account-multiple-journeys', categorySlug: 'platform-basics', content: 'You do not need separate accounts for each use case.\n\nWith one account, you can move across role journeys based on your need.\nExamples:\n- Company posting jobs\n- Professional offering services\n- Job seeker applying to roles\n\nKeep each role profile complete for better matching.' },
{ title: 'How review and approval improve trust', slug: 'review-approval-improve-trust', categorySlug: 'platform-basics', content: 'Review and approval help ensure better quality listings and profiles.\n\nWhy it matters:\n- Less fake activity\n- Better discovery relevance\n- Higher confidence before first contact\n\nMost checks complete in 24-48 hours for standard submissions.' },
{ title: 'How to track progress without confusion', slug: 'track-progress-without-confusion', categorySlug: 'platform-basics', content: 'Use status labels and timeline views in your dashboard.\n\nBest practice:\n- Check updates daily\n- Respond quickly to requests\n- Keep your profile current\n\nWhen status changes, follow the next action shown in your panel.' },
{ title: 'Common platform terms in simple words', slug: 'platform-terms-simple-words', categorySlug: 'platform-basics', content: 'Draft: Not complete yet.\nReview: Being checked by the platform.\nApproved: Passed checks and visible.\nRequirement: Customer request.\nLead: Potential opportunity.\n\nKnowing these terms helps you move faster through workflows.' },
{ title: 'How to get better platform visibility', slug: 'get-better-platform-visibility', categorySlug: 'platform-basics', content: 'To improve visibility:\n- Complete profile fields fully\n- Add clear scope and category fit\n- Keep media and proof updated\n- Respond fast to valid requests\n\nProfiles with clear information usually get better conversion quality.' },
{ title: 'How to reset your password', slug: 'reset-password', categorySlug: 'account-login', content: 'Use Forgot Password on login, verify via OTP, and set a new secure password to restore account access.' },
{ title: 'Why your login OTP might fail', slug: 'otp-failure-reasons', categorySlug: 'account-login', content: 'OTP may fail due to expiry, wrong input, network delay, or blocked SMS routes. Request a fresh OTP and retry.' },
{ title: 'Update email or phone on your account', slug: 'update-email-phone', categorySlug: 'account-login', content: 'Go to account settings and submit an update request. Certain changes may require re-verification for security.' },
{ title: 'How to complete a high-conversion profile', slug: 'high-conversion-profile', categorySlug: 'profile-verification', content: 'Add role clarity, service scope, location, portfolio proof, pricing structure, and response timelines for better conversion.' },
{ title: 'Verification checklist before submission', slug: 'verification-checklist-before-submit', categorySlug: 'profile-verification', content: 'Check identity fields, profile completeness, media proofs, and category fit before submitting for review.' },
{ title: 'Expected verification timeline (24-48 hours)', slug: 'verification-timeline-24-48-hours', categorySlug: 'profile-verification', content: 'Most profile reviews are completed within 24-48 hours. Complex or incomplete submissions may take longer.' },
{ title: 'Why verification was sent back for correction', slug: 'verification-sent-back-correction', categorySlug: 'profile-verification', content: 'Correction requests happen when details are incomplete, unclear, mismatched, or missing required proof.' },
{ title: 'How companies can post jobs correctly', slug: 'companies-post-jobs-correctly', categorySlug: 'jobs-applications', content: 'Use clear job titles, responsibilities, compensation, location, and hiring timeline for better candidate fit.' },
{ title: 'How job seekers can track application status', slug: 'track-application-status', categorySlug: 'jobs-applications', content: 'Open your applications panel to check submitted, viewed, shortlisted, or closed statuses in one place.' },
{ title: 'How to improve job application quality', slug: 'improve-application-quality', categorySlug: 'jobs-applications', content: 'Tailor profile details to each role, keep availability updated, and include relevant proof of work where possible.' },
{ title: 'Handling duplicate or irrelevant leads', slug: 'handle-duplicate-irrelevant-leads', categorySlug: 'leads-requests', content: 'Use request filters, quick decline actions, and profile targeting updates to reduce low-fit lead volume.' },
{ title: 'Best response window for higher lead conversion', slug: 'best-response-window-lead-conversion', categorySlug: 'leads-requests', content: 'Respond early with clear scope and timeline. Fast, structured replies generally improve first-contact conversion.' },
{ title: 'How to organize incoming customer requests', slug: 'organize-customer-requests', categorySlug: 'leads-requests', content: 'Tag requests by urgency, scope, and budget range to prioritize responses and avoid missed opportunities.' },
{ title: 'What are TraceCoins and where they are used', slug: 'what-are-tracecoins', categorySlug: 'tracecoins-billing', content: 'TraceCoins are platform credits used for selected actions such as premium interactions and visibility utilities.' },
{ title: 'How to view invoices and payment history', slug: 'view-invoices-payment-history', categorySlug: 'tracecoins-billing', content: 'Open billing settings to view purchase records, invoice references, and transaction status history.' },
{ title: 'Refund and failed payment guidance', slug: 'refund-failed-payment-guidance', categorySlug: 'tracecoins-billing', content: 'If payment fails or is disputed, check transaction status first, then raise a support request with payment reference.' },
{ title: 'How to report suspicious activity', slug: 'report-suspicious-activity', categorySlug: 'privacy-safety', content: 'Use report actions on profiles or listings, include evidence, and avoid direct engagement with suspicious accounts.' },
{ title: 'Managing privacy settings for your account', slug: 'manage-privacy-settings', categorySlug: 'privacy-safety', content: 'Control profile visibility, notification preferences, and contact access from account privacy controls.' },
{ title: 'Account safety best practices', slug: 'account-safety-best-practices', categorySlug: 'privacy-safety', content: 'Use strong passwords, rotate credentials, enable secure recovery methods, and avoid sharing verification codes.' },
{ title: 'Fix profile image or upload issues', slug: 'fix-profile-image-upload-issues', categorySlug: 'troubleshooting', content: 'Try compressed files, supported formats, and stable network. Re-upload after clearing cached session state if needed.' },
{ title: 'What to do if dashboard data does not refresh', slug: 'dashboard-data-not-refreshing', categorySlug: 'troubleshooting', content: 'Refresh the session, confirm role context, and retry after a short delay. Contact support if stale state persists.' },
{ title: 'Why your listing may not appear in search', slug: 'listing-not-appearing-in-search', categorySlug: 'platform-basics', content: 'Listings may not appear when status is Draft/Review, category mapping is weak, or profile details are incomplete.\n\nCheck status first, then update role/category details and retry.' },
{ title: 'Platform checklist before submitting anything', slug: 'platform-checklist-before-submitting', categorySlug: 'platform-basics', content: 'Before submit:\n- Verify title and category\n- Add clear scope and location\n- Add supporting details\n- Check contact correctness\n\nThis reduces correction loops and helps faster approval.' },
{ title: 'How to use Help Center effectively', slug: 'use-help-center-effectively', categorySlug: 'platform-basics', content: 'Search by simple keywords like login, verification, leads, billing, status, and approval.\n\nOpen related articles to understand connected steps.\n\nIf still blocked, share your exact issue with screenshots when contacting support.' },
];
async function loadCategories(): Promise<KbCategory[]> {
try {
const res = await fetch(`${API}/api/admin/kb/categories`);
@ -75,6 +130,8 @@ export default function KbPage(props: KbPageProps = {}) {
const [articleSearch, setArticleSearch] = createSignal('');
const [deletingArticleId, setDeletingArticleId] = createSignal('');
const [articleActionError, setArticleActionError] = createSignal('');
const [seedingArticles, setSeedingArticles] = createSignal(false);
const [seedMessage, setSeedMessage] = createSignal('');
// --- Create Article tab state ---
const [artTitle, setArtTitle] = createSignal('');
@ -179,6 +236,77 @@ export default function KbPage(props: KbPageProps = {}) {
}
};
const seedKnowledgeBaseArticles = async () => {
try {
setSeedingArticles(true);
setArticleActionError('');
setSeedMessage('');
const categoriesRes = await fetch(`${API}/api/admin/kb/categories`);
if (!categoriesRes.ok) throw new Error('Failed to load categories for seeding');
const categoriesJson = await categoriesRes.json();
const existingCategories: KbCategory[] = Array.isArray(categoriesJson) ? categoriesJson : (categoriesJson.categories || []);
const categoryBySlug = new Map(existingCategories.map((c) => [c.slug, c]));
for (const cat of KB_SEED_CATEGORIES) {
if (categoryBySlug.has(cat.slug)) continue;
const createRes = await fetch(`${API}/api/admin/kb/categories`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cat),
});
if (!createRes.ok) throw new Error(`Failed to create category: ${cat.name}`);
}
const freshCategoriesRes = await fetch(`${API}/api/admin/kb/categories`);
if (!freshCategoriesRes.ok) throw new Error('Failed to refresh categories after seeding');
const freshCategoriesJson = await freshCategoriesRes.json();
const freshCategories: KbCategory[] = Array.isArray(freshCategoriesJson) ? freshCategoriesJson : (freshCategoriesJson.categories || []);
const freshCategoryBySlug = new Map(freshCategories.map((c) => [c.slug, c]));
const articlesRes = await fetch(`${API}/api/admin/kb/articles`);
if (!articlesRes.ok) throw new Error('Failed to load articles for duplicate checks');
const articlesJson = await articlesRes.json();
const existingArticles: KbArticle[] = Array.isArray(articlesJson) ? articlesJson : (articlesJson.articles || []);
const existingSlugs = new Set(existingArticles.map((a) => (a.slug || '').trim()).filter(Boolean));
let created = 0;
let skipped = 0;
for (const article of KB_SEED_ARTICLES) {
if (existingSlugs.has(article.slug)) {
skipped += 1;
continue;
}
const category = freshCategoryBySlug.get(article.categorySlug);
if (!category) throw new Error(`Missing category for seed article: ${article.title}`);
const createArticleRes = await fetch(`${API}/api/admin/kb/articles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: article.title,
slug: article.slug,
content: article.content,
status: 'PUBLISHED',
category_id: category.id,
}),
});
if (!createArticleRes.ok) throw new Error(`Failed to create article: ${article.title}`);
existingSlugs.add(article.slug);
created += 1;
}
refetchCategories();
refetchArticles();
setSeedMessage(`KB seeding complete: ${created} created, ${skipped} skipped (already existed).`);
} catch (err: any) {
setArticleActionError(err?.message || 'Failed to seed KB articles');
} finally {
setSeedingArticles(false);
}
};
// Create article
const handleCreateArticle = async (e: Event) => {
e.preventDefault();
@ -400,7 +528,11 @@ export default function KbPage(props: KbPageProps = {}) {
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{articleActionError()}</div>
</Show>
<div style="margin-bottom:16px">
<Show when={seedMessage()}>
<div class="mb-4 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700" style="margin-bottom:12px">{seedMessage()}</div>
</Show>
<div style="margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">
<input
type="text"
placeholder="Search articles by title..."
@ -408,6 +540,9 @@ export default function KbPage(props: KbPageProps = {}) {
onInput={(e) => setArticleSearch(e.currentTarget.value)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:280px"
/>
<button class="btn-primary" disabled={seedingArticles()} onClick={seedKnowledgeBaseArticles}>
{seedingArticles() ? 'Seeding KB…' : 'Seed KB Articles'}
</button>
</div>
<div class="table-card">

View file

@ -1,8 +1,5 @@
import { Navigate, useParams } from '@solidjs/router';
import { Navigate } from '@solidjs/router';
export default function OnboardingManagementDetailAliasPage() {
const params = useParams();
const schemaId = String(params.schemaId || '').trim();
if (!schemaId) return <Navigate href="/admin/onboarding-schemas" />;
return <Navigate href={`/admin/onboarding-schemas/${encodeURIComponent(schemaId)}`} />;
return <Navigate href="/admin" />;
}

View file

@ -1,5 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function OnboardingManagementAliasPage() {
return <Navigate href="/admin/onboarding-schemas" />;
return <Navigate href="/admin" />;
}

View file

@ -1,5 +1,5 @@
import { Navigate } from '@solidjs/router';
export default function OnboardingManagementCreateAliasPage() {
return <Navigate href="/admin/onboarding-schemas/new" />;
return <Navigate href="/admin" />;
}

View file

@ -339,7 +339,7 @@ export default function OnboardingManagementPage() {
setRows(list);
} catch (e) {
console.error('Onboarding load error:', e);
setRows(FALLBACK_SCHEMAS);
setRows([]);
}
};
@ -630,7 +630,7 @@ export default function OnboardingManagementPage() {
};
const submissionsFiltered = createMemo(() => {
let r = FALLBACK_SUBMISSIONS;
let r: OnboardingSubmission[] = [];
const q = subSearch().trim().toLowerCase();
if (q) r = r.filter(s => s.id.toLowerCase().includes(q) || s.userName.toLowerCase().includes(q) || s.userId.toLowerCase().includes(q));
if (subStatusFilter() !== 'all') r = r.filter(s => s.status === subStatusFilter());
@ -646,8 +646,9 @@ export default function OnboardingManagementPage() {
const auditFiltered = createMemo(() => {
const q = auditSearch().trim().toLowerCase();
if (!q) return FALLBACK_AUDIT;
return FALLBACK_AUDIT.filter(e => e.event.toLowerCase().includes(q) || e.actor.toLowerCase().includes(q) || e.target.toLowerCase().includes(q));
const logs: AuditEvent[] = [];
if (!q) return logs;
return logs.filter(e => e.event.toLowerCase().includes(q) || e.actor.toLowerCase().includes(q) || e.target.toLowerCase().includes(q));
});
const normalizeLabel = (s: string) => s.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' ');
@ -874,7 +875,7 @@ export default function OnboardingManagementPage() {
</tr>
</thead>
<tbody>
<For each={FALLBACK_SUBMISSIONS}>
<For each={submissionsFiltered()}>
{s => (
<tr style="border-top:1px solid #E5E7EB">
<td style="padding:12px 16px;font-size:12px;font-family:monospace;color:#6B7280">{s.id}</td>