Fix : No log in footer
This commit is contained in:
@@ -179,6 +179,12 @@ export function App() {
|
|||||||
Streamers
|
Streamers
|
||||||
{liveTargets > 0 && <span className="pill">{liveTargets}</span>}
|
{liveTargets > 0 && <span className="pill">{liveTargets}</span>}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={route === 'logs' ? 'tab active' : 'tab'}
|
||||||
|
onClick={() => setRoute('logs')}
|
||||||
|
>
|
||||||
|
Journal
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="topbar-actions">
|
<div className="topbar-actions">
|
||||||
@@ -220,7 +226,9 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{route === 'streamers' ? (
|
{route === 'logs' ? (
|
||||||
|
<LogPanel logs={logs} />
|
||||||
|
) : route === 'streamers' ? (
|
||||||
<StreamersPage
|
<StreamersPage
|
||||||
targets={targets}
|
targets={targets}
|
||||||
agents={agents}
|
agents={agents}
|
||||||
@@ -253,8 +261,6 @@ export function App() {
|
|||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<LogPanel logs={logs} />
|
|
||||||
|
|
||||||
{openAgent && (
|
{openAgent && (
|
||||||
<AgentSettings
|
<AgentSettings
|
||||||
agent={openAgent}
|
agent={openAgent}
|
||||||
|
|||||||
@@ -1,27 +1,38 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { LogEntry } from '@stream-control/shared';
|
import type { LogEntry } from '@stream-control/shared';
|
||||||
import { formatTime } from '../format';
|
import { formatTime } from '../format';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Journal global, toutes machines confondues. Il reste la seule vue des
|
||||||
|
* évènements sans agent rattaché — veille, suppression d'un agent, tentative de
|
||||||
|
* connexion refusée — que l'historique d'une VM ne peut pas montrer.
|
||||||
|
*
|
||||||
|
* Plus récent en tête, comme l'historique par VM : en pleine page, ramener le
|
||||||
|
* défilement vers le bas à chaque nouvelle entrée arracherait la lecture.
|
||||||
|
*/
|
||||||
export function LogPanel({ logs }: { logs: LogEntry[] }) {
|
export function LogPanel({ logs }: { logs: LogEntry[] }) {
|
||||||
const endRef = useRef<HTMLDivElement>(null);
|
const newestFirst = useMemo(() => [...logs].reverse(), [logs]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
endRef.current?.scrollIntoView({ block: 'end' });
|
|
||||||
}, [logs.length]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="logs">
|
<section className="logs">
|
||||||
<h3>Journal</h3>
|
<header className="logs-head">
|
||||||
|
<h3>Journal</h3>
|
||||||
|
<span className="muted small">
|
||||||
|
{logs.length} entrée(s) — l'historique d'une machine s'ouvre depuis sa fiche (🕘)
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div className="log-list">
|
<div className="log-list">
|
||||||
{logs.length === 0 && <p className="muted small">Aucun évènement pour le moment.</p>}
|
{newestFirst.length === 0 && (
|
||||||
{logs.map((entry) => (
|
<p className="muted small">Aucun évènement pour le moment.</p>
|
||||||
|
)}
|
||||||
|
{newestFirst.map((entry) => (
|
||||||
<div key={entry.id} className={`log-line level-${entry.level}`}>
|
<div key={entry.id} className={`log-line level-${entry.level}`}>
|
||||||
<span className="mono small muted">{formatTime(entry.ts)}</span>
|
<span className="mono small muted">{formatTime(entry.ts)}</span>
|
||||||
<span className="log-agent">{entry.agentName ?? 'serveur'}</span>
|
<span className="log-agent">{entry.agentName ?? 'serveur'}</span>
|
||||||
<span className="log-message">{entry.message}</span>
|
<span className="log-message">{entry.message}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div ref={endRef} />
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -671,15 +671,22 @@ fieldset.group > legend {
|
|||||||
|
|
||||||
/* --- Journal --- */
|
/* --- Journal --- */
|
||||||
|
|
||||||
|
/* Vue pleine page depuis l'onglet Journal. Elle défile avec la page, comme la
|
||||||
|
grille d'agents et la liste de streamers — la barre du haut est collante. */
|
||||||
.logs {
|
.logs {
|
||||||
border-top: 1px solid var(--border);
|
flex: 1;
|
||||||
background: var(--panel);
|
display: flex;
|
||||||
padding: 12px 18px;
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
.logs-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.log-list {
|
.log-list {
|
||||||
max-height: 220px;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-top: 8px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const ROUTES = ['agents', 'streamers'] as const;
|
export const ROUTES = ['agents', 'streamers', 'logs'] as const;
|
||||||
export type Route = (typeof ROUTES)[number];
|
export type Route = (typeof ROUTES)[number];
|
||||||
|
|
||||||
function currentRoute(): Route {
|
function currentRoute(): Route {
|
||||||
@@ -9,8 +9,8 @@ function currentRoute(): Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routage minimal par ancre — l'application n'a que deux vues, et l'ancre suffit
|
* Routage minimal par ancre — l'application n'a qu'une poignée de vues, et
|
||||||
* à les rendre partageables et à survivre à un rechargement.
|
* l'ancre suffit à les rendre partageables et à survivre à un rechargement.
|
||||||
*/
|
*/
|
||||||
export function useHashRoute(): [Route, (route: Route) => void] {
|
export function useHashRoute(): [Route, (route: Route) => void] {
|
||||||
const [route, setRoute] = useState<Route>(currentRoute);
|
const [route, setRoute] = useState<Route>(currentRoute);
|
||||||
|
|||||||
Reference in New Issue
Block a user