db connection pooling and rag limit transparency
Some checks failed
Deploy Nyla Frontend to Integration / build-and-deploy (push) Failing after 17s

This commit is contained in:
ValueOn AG 2026-05-17 20:38:40 +02:00
parent bb441f5268
commit f37774ff36
3 changed files with 79 additions and 5 deletions

View file

@ -348,6 +348,9 @@ export interface RagDataSourceDto {
ragIndexEnabled: boolean;
neutralize: boolean;
lastIndexed: number | null;
/** Distinct files indexed for this DataSource (one row per source document). */
fileCount: number;
/** Embedding-sized text fragments (one per ContentChunk row, ~400 tokens each). */
chunkCount: number;
}
@ -358,6 +361,7 @@ export interface RagConnectionDto {
knowledgeIngestionEnabled: boolean;
preferences: KnowledgePreferences;
dataSources: RagDataSourceDto[];
totalFiles: number;
totalChunks: number;
runningJobs: { jobId: string; progress: number; progressMessage: string }[];
lastError?: { jobId: string; errorMessage: string; finishedAt: number | null } | null;
@ -369,12 +373,17 @@ export interface RagConnectionDto {
skippedPolicy: number;
failed: number;
durationMs: number;
/** Name of the first budget that bit (e.g. "maxBytes", "maxItems", "maxTasks"); null if walk completed naturally. */
stoppedAtLimit?: string | null;
/** Effective limits used by the walker, for showing the value next to the limit name. */
limits?: Record<string, number>;
bytesProcessed?: number;
} | null;
}
export interface RagInventoryDto {
connections: RagConnectionDto[];
totals: { chunks: number; bytes?: number };
totals: { files: number; chunks: number; bytes?: number };
}
export interface RagActiveJobDto {

View file

@ -220,6 +220,22 @@
opacity: 0.85;
}
/* Sync finished, but a hard limit (maxBytes/maxItems/...) cut the walk short.
Amber, not red the data we DID index is valid; the user just needs to
know more would have been indexed without the limit. */
.partialBanner {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #fffbeb;
border: 1px solid #fcd34d;
border-radius: 6px;
margin-bottom: 8px;
font-size: 0.8125rem;
color: #92400e;
}
.reindexBtn {
display: flex;
align-items: center;

View file

@ -139,6 +139,21 @@ export const RagInventoryPage: React.FC = () => {
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
}, []);
/** Render the budget value next to its name. Bytes get MB units so the user
* immediately recognises the 200 MB default; everything else stays raw. */
const _formatLimit = useCallback((name: string, budget: number | undefined, bytesProcessed: number | undefined): string => {
if (budget == null) return name;
if (name === 'maxBytes') {
const mb = Math.round(budget / 1024 / 1024);
const procMb = bytesProcessed != null ? ` (${(bytesProcessed / 1024 / 1024).toFixed(0)} MB ${t('verarbeitet')})` : '';
return `${name}=${mb} MB${procMb}`;
}
if (name === 'maxFileSize') {
return `${name}=${Math.round(budget / 1024 / 1024)} MB`;
}
return `${name}=${budget}`;
}, [t]);
const scopeOptions = useMemo(() => {
const opts: { value: string; label: string }[] = [
{ value: 'personal', label: t('Meine Verbindungen') },
@ -193,7 +208,9 @@ export const RagInventoryPage: React.FC = () => {
{inventory && (
<div className={styles.content}>
<div className={styles.totals}>
<span className={styles.totalLabel}>{t('Total Chunks')}:</span>
<span className={styles.totalLabel}>{t('Total Dateien')}:</span>
<strong className={styles.totalValue}>{inventory.totals?.files ?? 0}</strong>
<span className={styles.totalLabel} title={t('Embedding-Fragmente (~400 Tokens), die der RAG-Retrieval trifft')}>{t('Total Chunks')}:</span>
<strong className={styles.totalValue}>{inventory.totals?.chunks ?? 0}</strong>
{inventory.totals?.bytes != null && inventory.totals.bytes > 0 && (
<span className={styles.totalBytes}>{(inventory.totals.bytes / 1024 / 1024).toFixed(1)} MB</span>
@ -205,8 +222,13 @@ export const RagInventoryPage: React.FC = () => {
<div className={styles.connectionHeader}>
<span className={styles.authority}>{conn.authority}</span>
<span className={styles.email}>{conn.externalEmail}</span>
{conn.totalChunks > 0 && (
<span className={styles.connChunks}>{conn.totalChunks} chunks</span>
{(conn.totalFiles > 0 || conn.totalChunks > 0) && (
<span
className={styles.connChunks}
title={t('Embedding-Fragmente (~400 Tokens), die der RAG-Retrieval trifft')}
>
{t('{f} Dateien · {c} Chunks', { f: conn.totalFiles, c: conn.totalChunks })}
</span>
)}
<button
className={styles.consentToggle}
@ -261,6 +283,28 @@ export const RagInventoryPage: React.FC = () => {
s.skippedPolicy > 0 ? t('{n} übersprungen', { n: s.skippedPolicy }) : null,
s.failed > 0 ? t('{n} fehler', { n: s.failed }) : null,
].filter(Boolean).join(' · ');
const stop = s.stoppedAtLimit;
if (stop) {
const budget = s.limits?.[stop];
const limitText = _formatLimit(stop, budget, s.bytesProcessed);
return (
<div className={styles.partialBanner}>
<FaExclamationTriangle />
<span>
<strong>{t('Sync abgeschlossen, Korpus aber unvollständig')}</strong> ({_formatRelative(okAt)})
{' — '}
{t('Limit {l} erreicht', { l: limitText })}.
{stats && <> {stats}.</>}{' '}
{t('Weitere Dateien wurden NICHT indexiert. Limit erhöhen oder DataSource enger eingrenzen, dann erneut starten.')}
</span>
<button className={styles.reindexBtn} onClick={() => _handleReindex(conn.id)} title={t('Erneut indexieren')}>
<FaRedo size={12} /> {t('Erneut indexieren')}
</button>
</div>
);
}
return (
<div className={styles.successBanner}>
<FaCheckCircle />
@ -293,7 +337,12 @@ export const RagInventoryPage: React.FC = () => {
<div key={ds.id} className={`${styles.dsRow} ${ds.ragIndexEnabled ? styles.dsActive : ''}`}>
<span className={styles.dsLabel}>{ds.label || ds.path}</span>
<span className={styles.dsType}>{ds.sourceType}</span>
<span className={styles.dsChunks}>{ds.chunkCount} chunks</span>
<span
className={styles.dsChunks}
title={t('{f} indizierte Dateien · {c} Embedding-Chunks (~400 Tokens)', { f: ds.fileCount, c: ds.chunkCount })}
>
{ds.fileCount} {t('Dateien')} · {ds.chunkCount} {t('Chunks')}
</span>
<span className={styles.dsIndex}>{ds.ragIndexEnabled ? '\uD83E\uDDE0' : '\u2014'}</span>
</div>
))}