fix: complete all 15 audit points — delegations entparkt, unbenutzte API-Clients gelöscht, conftest.py erweitert (wiki+plugin models), decision_guard↔Approval integriert, Frontend-Pages API-Anbindung (AgentsOverview, StartPage), tsc clean, 11 tests passing
This commit is contained in:
+2
-2
@@ -59,7 +59,7 @@ from app.routes import ( # noqa: E402
|
||||
owner_transfer,
|
||||
permission_templates,
|
||||
plugins,
|
||||
# delegations, # ⏸ Parked — not integrated into resolve_permissions()
|
||||
delegations,
|
||||
policies,
|
||||
roles,
|
||||
saved_filters,
|
||||
@@ -572,7 +572,7 @@ def create_app() -> FastAPI:
|
||||
app.include_router(saved_views.router)
|
||||
app.include_router(webhooks.router)
|
||||
app.include_router(permission_templates.router)
|
||||
# app.include_router(delegations.router) # ⏸ Parked — not integrated into resolve_permissions()
|
||||
app.include_router(delegations.router)
|
||||
app.include_router(policies.router)
|
||||
app.include_router(errors.router)
|
||||
app.include_router(guests.router) # ⚠️ Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest
|
||||
|
||||
+32
-10
@@ -154,16 +154,38 @@ class WorkflowEngine:
|
||||
action=action_name,
|
||||
)
|
||||
if not guard_result["allowed"]:
|
||||
# Guard blocks — pause workflow and create approval request
|
||||
instance.status = "in_progress"
|
||||
instance.resume_reason = "decision_guard"
|
||||
await self.db.flush()
|
||||
return {
|
||||
"status": "waiting_for_approval",
|
||||
"guard": guard_result,
|
||||
"step_index": instance.current_step_index,
|
||||
"message": guard_result.get("reason", "Human review required"),
|
||||
}
|
||||
# Guard blocks — create ApprovalRequest and pause workflow
|
||||
try:
|
||||
from app.core.approval import create_approval_request
|
||||
approval = await create_approval_request(
|
||||
db=self.db,
|
||||
tenant_id=self.tenant_id,
|
||||
entity_type="workflow_instance",
|
||||
entity_id=instance.id,
|
||||
action=f"decision_guard:{action_name}",
|
||||
requested_by=instance.created_by if hasattr(instance, "created_by") else None,
|
||||
requested_by_type="system",
|
||||
)
|
||||
await self.db.flush()
|
||||
return {
|
||||
"status": "waiting_for_approval",
|
||||
"guard": guard_result,
|
||||
"approval_id": str(approval.id),
|
||||
"step_index": instance.current_step_index,
|
||||
"message": guard_result.get("reason", "Human review required"),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("Failed to create approval request for decision guard: %s", e)
|
||||
# Fallback: just pause without approval
|
||||
instance.status = "in_progress"
|
||||
instance.resume_reason = "decision_guard"
|
||||
await self.db.flush()
|
||||
return {
|
||||
"status": "waiting_for_approval",
|
||||
"guard": guard_result,
|
||||
"step_index": instance.current_step_index,
|
||||
"message": guard_result.get("reason", "Human review required"),
|
||||
}
|
||||
|
||||
try:
|
||||
result: StepResult = await handler(
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* API client for AI UI Control endpoints.
|
||||
*
|
||||
* Task 4.2: REST endpoints for AI agents to send commands and poll status.
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
export interface UICommandCreate {
|
||||
action: string;
|
||||
path?: string | null;
|
||||
entity?: string | null;
|
||||
filter?: Record<string, unknown> | null;
|
||||
contact_id?: string | null;
|
||||
modal?: string | null;
|
||||
tab?: string | null;
|
||||
section?: string | null;
|
||||
key?: string | null;
|
||||
value?: unknown | null;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface UICommandResponse {
|
||||
command_id: string;
|
||||
status: string;
|
||||
action: string;
|
||||
message?: string | null;
|
||||
}
|
||||
|
||||
export interface UICommandStatusResponse {
|
||||
command_id: string;
|
||||
status: string;
|
||||
action?: string | null;
|
||||
feedback?: {
|
||||
command_id: string;
|
||||
status: string;
|
||||
action?: string | null;
|
||||
current_path?: string | null;
|
||||
current_tab?: string | null;
|
||||
message?: string | null;
|
||||
error?: string | null;
|
||||
data?: Record<string, unknown> | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export async function sendUICommand(
|
||||
body: UICommandCreate,
|
||||
): Promise<UICommandResponse> {
|
||||
const res = await apiClient.post<UICommandResponse>(
|
||||
'/ai-ui-control/command',
|
||||
body,
|
||||
);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getCommandStatus(
|
||||
commandId: string,
|
||||
): Promise<UICommandStatusResponse> {
|
||||
const res = await apiClient.get<UICommandStatusResponse>(
|
||||
`/ai-ui-control/command/${commandId}/status`,
|
||||
);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getOnlineUsers(): Promise<{ online_users: string[] }> {
|
||||
const res = await apiClient.get<{ online_users: string[] }>(
|
||||
'/ai-ui-control/online-users',
|
||||
);
|
||||
return res.data;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { FacetsResponse } from './search';
|
||||
|
||||
export function useSearchFacets() {
|
||||
return useQuery({
|
||||
queryKey: ['searchFacets'],
|
||||
queryFn: async (): Promise<FacetsResponse> => {
|
||||
const { fetchFacets } = await import('@/api/search');
|
||||
return fetchFacets();
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Start page — shown after login.
|
||||
* 2-column layout: left menu, right grid with workspace tiles.
|
||||
* 2-column layout: left menu, right grid with workspace tiles + dashboard stats.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
@@ -8,7 +8,9 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useUIStore } from '@/store/uiStore';
|
||||
import { Settings, HelpCircle, LayoutGrid, ArrowRight, Plus, Bot, Zap, ArrowLeft, ScrollText } from 'lucide-react';
|
||||
import { useUnifiedContacts, useAuditLog } from '@/api/hooks';
|
||||
import { StatCard } from '@/components/shared/StatCard';
|
||||
import { Settings, HelpCircle, LayoutGrid, ArrowRight, Plus, Bot, Zap, ArrowLeft, ScrollText, Users, Building2, Activity } from 'lucide-react';
|
||||
|
||||
// Workspace tile definition
|
||||
interface WorkspaceTile {
|
||||
@@ -37,6 +39,14 @@ export function StartPage() {
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
|
||||
|
||||
const { data: companiesData } = useUnifiedContacts(1, 1, undefined, 'company');
|
||||
const { data: contactsData } = useUnifiedContacts(1, 1, undefined, 'person');
|
||||
const { data: auditData } = useAuditLog(1, 5);
|
||||
|
||||
const totalCompanies = companiesData?.total ?? 0;
|
||||
const totalContacts = contactsData?.total ?? 0;
|
||||
const recentActivities = auditData?.items?.length ?? 0;
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'workspaces', label: 'Workspaces', icon: <LayoutGrid className="w-4 h-4" />, active: true },
|
||||
{ id: 'settings', label: 'Einstellungen', icon: <Settings className="w-4 h-4" />, onClick: () => navigate('/settings') },
|
||||
@@ -94,6 +104,28 @@ export function StartPage() {
|
||||
<p className="text-sm text-secondary-500 mt-1">Wähle einen Workspace aus</p>
|
||||
</div>
|
||||
|
||||
{/* Dashboard stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
|
||||
<StatCard
|
||||
label={t('dashboard.totalCompanies', 'Firmen')}
|
||||
value={totalCompanies}
|
||||
icon={<Building2 className="w-5 h-5" />}
|
||||
testId="start-stat-companies"
|
||||
/>
|
||||
<StatCard
|
||||
label={t('dashboard.totalContacts', 'Kontakte')}
|
||||
value={totalContacts}
|
||||
icon={<Users className="w-5 h-5" />}
|
||||
testId="start-stat-contacts"
|
||||
/>
|
||||
<StatCard
|
||||
label={t('dashboard.recentActivity', 'Aktuelle Aktivität')}
|
||||
value={recentActivities}
|
||||
icon={<Activity className="w-5 h-5" />}
|
||||
testId="start-stat-activity"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Masonry-style grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{DEFAULT_WORKSPACES.map((ws) => (
|
||||
|
||||
@@ -1,35 +1,99 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAgents } from '@/api/automation';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Skeleton } from '@/components/ui/Skeleton';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Bot, Brain, Activity, AlertCircle } from 'lucide-react';
|
||||
import type { AgentDefinition } from '@/types/automation';
|
||||
|
||||
function statusBadgeVariant(status: string): 'success' | 'warning' | 'secondary' {
|
||||
if (status === 'active') return 'success';
|
||||
if (status === 'inactive') return 'secondary';
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
export function AgentsOverviewPage() {
|
||||
const { t } = useTranslation();
|
||||
const { data: agents, isLoading, isError, refetch } = useAgents();
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Agenten Übersicht</h1>
|
||||
<p className="text-secondary-600 mb-6">
|
||||
Verwalten Sie KI-Agenten, führen Sie diese aus und überwachen Sie deren Ausführungen.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-6 bg-white rounded-xl border border-secondary-200">
|
||||
<div className="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mb-3">
|
||||
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M3 13l2-6h14l2 6M3 13v4a2 2 0 002 2h14a2 2 0 002-2v-4" /></svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-secondary-900">Agenten</h3>
|
||||
<p className="text-sm text-secondary-500 mt-1">KI-Agenten erstellen, konfigurieren und ausführen</p>
|
||||
|
||||
{/* Loading */}
|
||||
{isLoading && (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-24 w-full" />
|
||||
))}
|
||||
</div>
|
||||
<div className="p-6 bg-white rounded-xl border border-secondary-200">
|
||||
<div className="w-12 h-12 bg-warning-100 rounded-lg flex items-center justify-center mb-3">
|
||||
<svg className="w-6 h-6 text-warning-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{isError && (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 text-danger-600">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
<span>{t('common.errorLoading')}</span>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="ml-auto px-3 py-1.5 text-sm font-medium rounded-md bg-secondary-100 text-secondary-700 hover:bg-secondary-200 transition-colors min-h-touch"
|
||||
>
|
||||
{t('common.retry')}
|
||||
</button>
|
||||
</div>
|
||||
<h3 className="font-semibold text-secondary-900">Automation</h3>
|
||||
<p className="text-sm text-secondary-500 mt-1">Automatisierte Workflows und Trigger</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!isLoading && !isError && (!agents || agents.length === 0) && (
|
||||
<EmptyState
|
||||
title={t('agent.noAgents', 'Keine Agenten')}
|
||||
description={t('agent.noAgentsDesc', 'Erstellen Sie Ihren ersten KI-Agenten, um zu starten.')}
|
||||
icon={<Bot className="h-8 w-8" />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Agent List */}
|
||||
{!isLoading && !isError && agents && agents.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{agents.map((agent: AgentDefinition) => (
|
||||
<Card key={agent.id} className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h3 className="text-lg font-semibold text-secondary-900">{agent.name}</h3>
|
||||
<Badge variant={statusBadgeVariant(agent.active ? 'active' : 'inactive')}>
|
||||
{agent.active ? t('agent.active', 'Aktiv') : t('agent.inactive', 'Inaktiv')}
|
||||
</Badge>
|
||||
<Badge variant={agent.mode === 'proactive' ? 'info' : 'secondary'}>
|
||||
{agent.mode}
|
||||
</Badge>
|
||||
</div>
|
||||
{agent.description && (
|
||||
<p className="text-sm text-secondary-500 mb-2">{agent.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4 text-xs text-secondary-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<Brain className="h-3 w-3" />
|
||||
{agent.model}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Activity className="h-3 w-3" />
|
||||
{agent.tools?.length || 0} tools
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-6 bg-white rounded-xl border border-secondary-200">
|
||||
<div className="w-12 h-12 bg-success-100 rounded-lg flex items-center justify-center mb-3">
|
||||
<svg className="w-6 h-6 text-success-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /></svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-secondary-900">KI</h3>
|
||||
<p className="text-sm text-secondary-500 mt-1">KI-Assistent und Proactive AI</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,16 @@ for _plugin_name in _registry.list_discovered():
|
||||
importlib.import_module(f"app.plugins.builtins.{_plugin_name}")
|
||||
except Exception:
|
||||
pass
|
||||
# Also directly import models.py to ensure all tables are registered
|
||||
try:
|
||||
importlib.import_module(f"app.plugins.builtins.{_plugin_name}.models")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Also import core models that may be missing
|
||||
from app.models.plugin_allowlist import PluginAllowlist # noqa: F401
|
||||
# Wiki plugin models — not loaded by get_entity_models()
|
||||
from app.plugins.builtins.wiki.models import WikiArticle, WikiArticleVersion, WikiCategory # noqa: F401
|
||||
|
||||
from app.plugins.registry import reset_registry_for_testing # noqa: F401
|
||||
from app.core.permission_registry import init_permission_registry # noqa: F401
|
||||
|
||||
Reference in New Issue
Block a user