learning_ai_common_plat/dashboards/admin-web/src/lib/docs.ts
saravanakumardb1 2d54795c30 feat(dashboards): migrate admin + tracker dashboards to common-plat as product-agnostic
- Copy admin-dashboard-web → dashboards/admin-web
- Copy tracker-dashboard-web → dashboards/tracker-web
- Update pnpm-workspace.yaml to include dashboards/*
- Replace file: refs with workspace:* for @bytelyst/* packages
- Replace all hardcoded LysnrAI/lysnn.com branding with generic platform refs
- Make telemetry use NEXT_PUBLIC_PRODUCT_ID / PRODUCT_ID env vars
- Update mock credentials, seed data, invitation codes, placeholders
- Update READMEs, e2e tests, unit tests for product-agnostic content
- Both dashboards pass tsc --noEmit clean
2026-02-28 02:17:35 -08:00

182 lines
5.5 KiB
TypeScript

/**
* Server-side utilities for reading project documentation files.
*
* Scans the repo root for .md files in docs/, plus root-level READMEs etc.
* New docs added to the repo auto-appear — no code changes needed.
*/
import fs from 'fs';
import path from 'path';
/** Resolve the repo root (two levels up from admin-dashboard-web/src/lib/) */
function getRepoRoot(): string {
// In dev: process.cwd() is admin-dashboard-web/
// DOCS_DIR env override for Docker/custom setups
if (process.env.DOCS_DIR) return path.resolve(process.env.DOCS_DIR, '..');
return path.resolve(process.cwd(), '..');
}
export interface DocFile {
slug: string; // e.g. "docs/STRIPE_SETUP_GUIDE" or "README"
title: string; // Human-readable title derived from filename
path: string; // Relative path from repo root, e.g. "docs/STRIPE_SETUP_GUIDE.md"
category: string; // e.g. "docs", "docs/research", "root"
sizeBytes: number;
modifiedAt: string; // ISO date
}
/** Derive a human-readable title from a filename */
function titleFromFilename(filename: string): string {
return filename
.replace(/\.md$/i, '')
.replace(/[_-]/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
}
/** List all .md files in the repo that should be shown in the admin portal */
export function listDocs(): DocFile[] {
const root = getRepoRoot();
const results: DocFile[] = [];
// 1. Root-level .md files
const rootFiles = fs.readdirSync(root).filter(f => f.endsWith('.md'));
for (const f of rootFiles) {
const full = path.join(root, f);
const stat = fs.statSync(full);
results.push({
slug: f.replace(/\.md$/i, ''),
title: titleFromFilename(f),
path: f,
category: 'root',
sizeBytes: stat.size,
modifiedAt: stat.mtime.toISOString(),
});
}
// 2. docs/ directory (recursive)
const docsDir = path.join(root, 'docs');
if (fs.existsSync(docsDir)) {
walkDir(docsDir, root, results);
}
// 3. Service READMEs
const serviceDirs = [
'backend',
'admin-dashboard-web',
'user-dashboard-web',
'mobile_app',
'tracker-dashboard-web',
];
for (const dir of serviceDirs) {
const readme = path.join(root, dir, 'README.md');
if (fs.existsSync(readme)) {
const stat = fs.statSync(readme);
results.push({
slug: `${dir}/README`,
title: `${dir} — README`,
path: `${dir}/README.md`,
category: 'services',
sizeBytes: stat.size,
modifiedAt: stat.mtime.toISOString(),
});
}
}
return results.sort((a, b) => a.path.localeCompare(b.path));
}
function walkDir(dir: string, repoRoot: string, results: DocFile[]): void {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(full, repoRoot, results);
} else if (entry.name.endsWith('.md')) {
const rel = path.relative(repoRoot, full);
const stat = fs.statSync(full);
const category = path.dirname(rel);
results.push({
slug: rel.replace(/\.md$/i, ''),
title: titleFromFilename(entry.name),
path: rel,
category,
sizeBytes: stat.size,
modifiedAt: stat.mtime.toISOString(),
});
}
}
}
/** Read a single doc by its slug (e.g. "docs/STRIPE_SETUP_GUIDE") */
export function readDoc(slug: string): { content: string; meta: DocFile } | null {
const root = getRepoRoot();
const filePath = path.join(root, slug + '.md');
// Security: prevent path traversal
const resolved = path.resolve(filePath);
if (!resolved.startsWith(path.resolve(root))) return null;
if (!fs.existsSync(resolved)) return null;
const stat = fs.statSync(resolved);
const content = fs.readFileSync(resolved, 'utf-8');
const rel = path.relative(root, resolved);
return {
content,
meta: {
slug,
title: titleFromFilename(path.basename(rel)),
path: rel,
category: path.dirname(rel) === '.' ? 'root' : path.dirname(rel),
sizeBytes: stat.size,
modifiedAt: stat.mtime.toISOString(),
},
};
}
/** Full-text search across all docs (case-insensitive) */
export function searchDocs(query: string): Array<DocFile & { snippet: string }> {
const docs = listDocs();
const root = getRepoRoot();
const results: Array<DocFile & { snippet: string }> = [];
const lowerQuery = query.toLowerCase();
for (const doc of docs) {
const filePath = path.join(root, doc.path);
if (!fs.existsSync(filePath)) continue;
const content = fs.readFileSync(filePath, 'utf-8');
const lowerContent = content.toLowerCase();
const idx = lowerContent.indexOf(lowerQuery);
if (idx !== -1) {
const start = Math.max(0, idx - 80);
const end = Math.min(content.length, idx + query.length + 80);
const snippet =
(start > 0 ? '...' : '') +
content.slice(start, end).replace(/\n/g, ' ') +
(end < content.length ? '...' : '');
results.push({ ...doc, snippet });
}
}
return results;
}
/** Gather all docs content for RAG context (truncated to stay within token limits) */
export function getAllDocsContent(maxCharsPerDoc = 4000): string {
const docs = listDocs();
const root = getRepoRoot();
const parts: string[] = [];
for (const doc of docs) {
const filePath = path.join(root, doc.path);
if (!fs.existsSync(filePath)) continue;
let content = fs.readFileSync(filePath, 'utf-8');
if (content.length > maxCharsPerDoc) {
content = content.slice(0, maxCharsPerDoc) + '\n...(truncated)';
}
parts.push(`=== ${doc.path} ===\n${content}`);
}
return parts.join('\n\n');
}