Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function index(Request $request): JsonResponse
$status = is_string($request->query('status')) ? $request->query('status') : 'pending';

$query = AiAgentKeyRequest::query()
->with(['agent:id,uuid,name,type'])
->with(['agent:id,uuid,name,type', 'application:uuid,name'])
->where('team_id', $teamId)
->orderByDesc('created_at')
->limit(50);
Expand Down Expand Up @@ -190,6 +190,12 @@ private function present(AiAgentKeyRequest $request): array
'agent_uuid' => $request->agent?->uuid,
'agent_name' => $request->agent?->name,
'agent_type' => $request->agent?->type,
'application' => $request->application ? [
'uuid' => $request->application->uuid,
'name' => $request->application->name,
] : null,
'application_uuid' => $request->application?->uuid,
'application_name' => $request->application?->name,
'created_at' => $request->created_at?->toISOString(),
'resolved_at' => $request->resolved_at?->toISOString(),
];
Expand Down
84 changes: 49 additions & 35 deletions backend/app/Http/Controllers/DevForge/DeploymentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,55 @@ class DeploymentController extends Controller
{
public function index(Request $request, CurrentTeamContext $teamContext, DeploymentData $deploymentData): JsonResponse
{
$validated = $request->validate([
'application_uuid' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', Rule::enum(ApplicationDeploymentStatus::class)],
'active' => ['nullable', 'boolean'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
]);
$team = $teamContext->resolve($request->user());
$activeOnly = $request->boolean('active');
$deployments = $deploymentData->paginate(
$team,
(int) ($validated['page'] ?? 1),
(int) ($validated['per_page'] ?? 25),
$validated['application_uuid'] ?? null,
$activeOnly ? null : ($validated['status'] ?? null),
$activeOnly
? [
ApplicationDeploymentStatus::QUEUED->value,
ApplicationDeploymentStatus::IN_PROGRESS->value,
]
: null,
);

return response()->json([
'data' => $deployments->getCollection()
->map(fn ($deployment): array => $deploymentData->deployment($deployment))
->values()
->all(),
'meta' => [
'current_page' => $deployments->currentPage(),
'per_page' => $deployments->perPage(),
'total' => $deployments->total(),
'last_page' => $deployments->lastPage(),
],
]);
try {
$validated = $request->validate([
'application_uuid' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', Rule::enum(ApplicationDeploymentStatus::class)],
'active' => ['nullable', 'boolean'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
]);
$team = $teamContext->resolve($request->user());
$activeOnly = $request->boolean('active');
$deployments = $deploymentData->paginate(
$team,
(int) ($validated['page'] ?? 1),
(int) ($validated['per_page'] ?? 25),
$validated['application_uuid'] ?? null,
$activeOnly ? null : ($validated['status'] ?? null),
$activeOnly
? [
ApplicationDeploymentStatus::QUEUED->value,
ApplicationDeploymentStatus::IN_PROGRESS->value,
]
: null,
);

return response()->json([
'data' => $deployments->getCollection()
->map(fn ($deployment): array => $deploymentData->deployment($deployment))
->values()
->all(),
'meta' => [
'current_page' => $deployments->currentPage(),
'per_page' => $deployments->perPage(),
'total' => $deployments->total(),
'last_page' => $deployments->lastPage(),
],
]);
} catch (Throwable $e) {
return response()->json([
'error' => 'Impossible de charger les déploiements.',
'message' => $e->getMessage(),
'data' => [],
'meta' => [
'current_page' => 1,
'per_page' => 25,
'total' => 0,
'last_page' => 1,
],
], 200);
}
}

public function show(
Expand Down
5 changes: 5 additions & 0 deletions backend/app/Models/AiAgentKeyRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ public function run(): BelongsTo
{
return $this->belongsTo(AiAgentRun::class, 'run_id');
}

public function application(): BelongsTo
{
return $this->belongsTo(Application::class, 'resource_uuid', 'uuid');
}
}
4 changes: 3 additions & 1 deletion frontend/src/lib/application-logo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export function resolveApplicationLogoUrl(configuration: Record<string, unknown>
for (const domain of domains) {
const hostname = hostnameFromCandidate(domain);
if (hostname && !isGeneratedApplicationHostname(hostname)) {
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(hostname)}&sz=64`;
// Try app's own favicon first, fallback to Google if it fails
const withScheme = hostname.includes('://') ? hostname : `https://${hostname}`;
return `${withScheme}/favicon.ico`;
}
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/routing/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const appRoutes: AppRoute[] = [

{ path: '/databases', label: 'Bases de données', description: 'Instances, sauvegardes et métriques.', icon: Database, page: 'databases' },

{ path: '/services', label: 'Services', description: 'Stacks et services gérés.', icon: Wrench, page: 'services' },
{ path: '/services', label: 'Connexions', description: 'Intégrations, tokens API, GitHub, MCP et services.', icon: Wrench, page: 'connexions' },

{ path: '/deployments', label: 'Déploiements', description: 'Pipeline apps, GitHub, déploiements et agents.', icon: Rocket, page: 'deployments' },

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/routing/sidebar-nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ export const sidebarNav: SidebarNavEntry[] = [
},
{
id: 'services',
label: 'Services',
label: 'Connexions',
path: '/services',
pages: ['services'],
pages: ['connexions', 'services'],
icon: Wrench,
},
],
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/pages/docker/_DockerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function DockerPage() {

const containers = containersQuery.data?.data || [];
const serverMeta = containersQuery.data?.meta?.server;
const serverError = (containersQuery.data as any)?.error;
const imagesData = imagesQuery.data?.data;
const apps = imagesData?.applications || [];
const services = imagesData?.services || [];
Expand Down Expand Up @@ -213,6 +214,17 @@ export function DockerPage() {
<span>{bannerMessage.text}</span>
</div>
)}

{serverError && !bannerMessage && (
<div class="alert alert-warning text-xs shadow-sm">
<span>⚠️ Serveur Docker inaccessible : {serverError}</span>
{serverMeta && (
<span class="text-[11px] opacity-70">
(Serveur : {serverMeta.name || serverMeta.ip || 'localhost'})
</span>
)}
</div>
)}

{/* Statistiques clés */}
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
Expand Down
45 changes: 32 additions & 13 deletions frontend/src/pages/sources/_SourcesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ export function ConnexionsPage({ permissions }: ConnexionsPageProps) {
return (
<div class="grid gap-3 sm:gap-4 md:gap-5">
<PageHeader
title="Tokens & Clés API"
description="Gestion centralisée de vos tokens d'accès, clés API pour vos agents et identifiants de déploiement."
title="Connexions & Intégrations"
description="Hub central pour GitHub, tokens API, clés d'agents, MCP et tous vos services connectés."
/>
{(feedback || error) && (
<p class={`text-sm ${error ? 'text-error' : 'text-success'}`}>{error ?? feedback}</p>
Expand Down Expand Up @@ -157,31 +157,47 @@ export function ConnexionsPage({ permissions }: ConnexionsPageProps) {
<div class="grid gap-2.5 sm:gap-3 md:gap-4">
{agentRequests.data.data.map((req) => (
<div key={req.uuid} class="flex flex-col gap-2 rounded-lg border border-base-300 bg-base-100 p-4">
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 flex-wrap">
<Bot class="size-3.5 sm:size-4 text-primary" aria-hidden />
<span class="font-semibold">{req.agent?.name ?? 'Agent'}</span>
<span class="text-xs text-base-content/60">a besoin de</span>
<code class="font-mono text-sm">{req.key_name}</code>
</div>
{req.application_name && (
<div class="flex items-center gap-2 text-xs text-base-content/70">
<span>Pour l'application :</span>
<a
href={`/application/${req.application_uuid}`}
class="font-medium hover:text-primary underline"
>
{req.application_name}
</a>
</div>
)}
{req.reason && <p class="text-xs text-base-content/70">{req.reason}</p>}
<div class="mt-2 flex gap-2">
<form
class="mt-2 flex gap-2"
onSubmit={(e) => {
e.preventDefault();
void fulfillAgentRequest(req.uuid);
}}
>
<input
class="input input-sm input-bordered flex-1 font-mono"
type="password"
placeholder={`Valeur pour ${req.key_name}`}
value={requestDrafts[req.uuid] ?? ''}
onInput={(e) => setRequestDrafts(cur => ({ ...cur, [req.uuid]: (e.target as HTMLInputElement).value }))}
onKeyDown={(e) => e.key === 'Enter' && fulfillAgentRequest(req.uuid)}
/>
<button
class="btn btn-primary btn-sm"
type="submit"
disabled={savingUuid === req.uuid || !(requestDrafts[req.uuid]?.trim())}
onClick={() => void fulfillAgentRequest(req.uuid)}
>
<KeyRound class="size-3.5" aria-hidden />
Fournir
</button>
</div>
</form>
</div>
))}
</div>
Expand Down Expand Up @@ -229,7 +245,13 @@ export function ConnexionsPage({ permissions }: ConnexionsPageProps) {
<DevForgeMcpTokenCard />

<Modal title="Token GitHub (Packages)" open={!!editingGithubApp} onClose={() => setEditingGithubApp(null)}>
<div class="p-6">
<form
class="p-6"
onSubmit={(e) => {
e.preventDefault();
if (editingGithubApp) void savePackagesToken(editingGithubApp);
}}
>
<h3 class="text-lg font-bold">Token GitHub (Packages)</h3>
<p class="mt-2 text-sm text-base-content/70">
Saisissez le Personal Access Token (PAT) avec la permission <code class="font-mono text-xs">read:packages</code> pour le compte {editingGithubApp ? accountLabel(editingGithubApp) : ''}.
Expand All @@ -252,17 +274,14 @@ export function ConnexionsPage({ permissions }: ConnexionsPageProps) {
<button class="btn btn-ghost" type="button" onClick={() => setEditingGithubApp(null)}>Annuler</button>
<button
class="btn btn-primary"
type="button"
type="submit"
disabled={!editingGithubApp || savingUuid === editingGithubApp?.uuid}
onClick={() => {
if (editingGithubApp) void savePackagesToken(editingGithubApp);
}}
>
<KeyRound class="size-3.5" aria-hidden />
Enregistrer
</button>
</div>
</div>
</form>
</Modal>


Expand Down