mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 01:37:05 +00:00
feat(design): animated design system foundation with animate-ui and motion
Install motion + animate-ui, overhaul design tokens with brand cyan accent, and replace CSS keyframe animations in Dialog, Tabs, Switch, and Tooltip with spring-physics and blur-fade transitions via animate-ui Radix primitives.
This commit is contained in:
+12
-11
@@ -32,8 +32,8 @@ const execAsync = promisify(exec);
|
||||
|
||||
// Suppress [DEP0060] DeprecationWarning emitted by http-proxy@1.18.1 which calls
|
||||
// util._extend internally. The warning fires at runtime when createProxyServer() is
|
||||
// first invoked (NOT at import time), so intercepting process.emitWarning here —
|
||||
// before the proxy instances are created below — fully prevents it.
|
||||
// first invoked (NOT at import time), so intercepting process.emitWarning here -
|
||||
// before the proxy instances are created below - fully prevents it.
|
||||
// http-proxy has no compatible update; this suppression is intentional and safe.
|
||||
const _origEmitWarning = process.emitWarning.bind(process);
|
||||
(process as any).emitWarning = (warning: any, ...args: any[]) => {
|
||||
@@ -359,7 +359,7 @@ const remoteNodeProxy = createProxyMiddleware<Request, Response>({
|
||||
proxyReq.setHeader('Authorization', `Bearer ${node.api_token}`);
|
||||
}
|
||||
// Strip the ?nodeId= query param so the remote's nodeContextMiddleware
|
||||
// doesn't reject the request with 404 ("Node X not found") — the remote
|
||||
// doesn't reject the request with 404 ("Node X not found") - the remote
|
||||
// has no record of the gateway's node IDs and should treat the request
|
||||
// as local. This affects endpoints like EventSource /api/containers/:id/logs
|
||||
// that pass nodeId as a query param rather than the x-node-id header.
|
||||
@@ -375,7 +375,7 @@ const remoteNodeProxy = createProxyMiddleware<Request, Response>({
|
||||
console.error('[Proxy] Remote node error:', (err as Error).message);
|
||||
// proxyRes can be either a ServerResponse (HTTP) or a raw Socket (WS/TCP errors).
|
||||
// Only attempt to send an HTTP 502 if it is a proper ServerResponse with a
|
||||
// headersSent flag — otherwise silently drop (the socket will be destroyed).
|
||||
// headersSent flag - otherwise silently drop (the socket will be destroyed).
|
||||
const res = proxyRes as any;
|
||||
if (typeof res?.headersSent === 'boolean' && !res.headersSent && typeof res.status === 'function') {
|
||||
res.status(502).json({
|
||||
@@ -418,9 +418,10 @@ const wss = new WebSocket.Server({ noServer: true });
|
||||
|
||||
let terminalWs: WebSocket | null = null;
|
||||
|
||||
// Notification push — set of authenticated browser clients subscribed to real-time alerts
|
||||
// Notification push - set of authenticated browser clients subscribed to real-time alerts
|
||||
const notificationSubscribers = new Set<WebSocket>();
|
||||
NotificationService.getInstance().setBroadcaster((notification) => {
|
||||
if (notificationSubscribers.size === 0) return;
|
||||
const msg = JSON.stringify({ type: 'notification', payload: notification });
|
||||
for (const ws of notificationSubscribers) {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
@@ -461,7 +462,7 @@ server.on('upgrade', async (req, socket, head) => {
|
||||
const parsedUrl = new URL(url, `http://${req.headers.host || 'localhost'}`);
|
||||
const pathname = parsedUrl.pathname;
|
||||
|
||||
// Notification push channel — always local, never proxied to remote nodes
|
||||
// Notification push channel - always local, never proxied to remote nodes
|
||||
if (pathname === '/ws/notifications') {
|
||||
const notifWss = new WebSocket.Server({ noServer: true });
|
||||
notifWss.handleUpgrade(req, socket, head, (ws) => {
|
||||
@@ -483,7 +484,7 @@ server.on('upgrade', async (req, socket, head) => {
|
||||
const wsTarget = node.api_url.replace(/\/$/, '').replace(/^https?/, (m) => m === 'https' ? 'wss' : 'ws');
|
||||
req.headers['authorization'] = `Bearer ${node.api_token}`;
|
||||
delete req.headers['x-node-id'];
|
||||
// Strip the browser's session cookie — it is signed by this instance's JWT secret and
|
||||
// Strip the browser's session cookie - it is signed by this instance's JWT secret and
|
||||
// would fail verification on the remote. Auth is handled exclusively via the Bearer token.
|
||||
delete req.headers['cookie'];
|
||||
// Strip nodeId from the forwarded URL so the remote treats the request as a local one.
|
||||
@@ -1096,7 +1097,7 @@ app.get('/api/logs/global', async (req: Request, res: Response) => {
|
||||
}));
|
||||
|
||||
// Sort globally by timestamp ascending (newest bottom).
|
||||
// Limit to 500 lines — the client renders at most 300 rows at once, so
|
||||
// Limit to 500 lines - the client renders at most 300 rows at once, so
|
||||
// sending 2000 lines was wasting bandwidth and inflating JSON parse time.
|
||||
allLogs.sort((a, b) => a.timestampMs - b.timestampMs);
|
||||
res.json(allLogs.slice(-500));
|
||||
@@ -1269,7 +1270,7 @@ app.post('/api/agents', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Keys that contain auth credentials — never exposed to the frontend or writable via settings API
|
||||
// Keys that contain auth credentials - never exposed to the frontend or writable via settings API
|
||||
const PRIVATE_SETTINGS_KEYS = new Set(['auth_username', 'auth_password_hash', 'auth_jwt_secret']);
|
||||
|
||||
// Strict allowlist of keys writable via the settings API (prevents overwriting auth credentials)
|
||||
@@ -1286,7 +1287,7 @@ const ALLOWED_SETTING_KEYS = new Set([
|
||||
'log_retention_days',
|
||||
]);
|
||||
|
||||
// Zod schema for bulk PATCH — all keys optional, present keys fully validated
|
||||
// Zod schema for bulk PATCH - all keys optional, present keys fully validated
|
||||
import { z } from 'zod';
|
||||
const SettingsPatchSchema = z.object({
|
||||
host_cpu_limit: z.coerce.number().int().min(1).max(100).transform(String),
|
||||
@@ -1304,7 +1305,7 @@ const SettingsPatchSchema = z.object({
|
||||
app.get('/api/settings', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const settings = DatabaseService.getInstance().getGlobalSettings();
|
||||
// Strip auth credentials — these are managed exclusively by /api/auth/* endpoints
|
||||
// Strip auth credentials - these are managed exclusively by /api/auth/* endpoints
|
||||
for (const key of PRIVATE_SETTINGS_KEYS) {
|
||||
delete settings[key];
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ export class DatabaseService {
|
||||
stmt.run(key, value);
|
||||
}
|
||||
|
||||
// --- System State (operational/runtime values — not user-defined config) ---
|
||||
// --- System State (operational/runtime values - not user-defined config) ---
|
||||
|
||||
public getSystemState(key: string): string | null {
|
||||
const row = this.db.prepare('SELECT value FROM system_state WHERE key = ?').get(key) as { value: string } | undefined;
|
||||
|
||||
@@ -109,7 +109,7 @@ async function getAuthToken(registry: string, repo: string): Promise<string | nu
|
||||
// ─── Remote digest lookup ─────────────────────────────────────────────────────
|
||||
|
||||
// Include manifest list types so we get the fat-manifest digest for multi-arch
|
||||
// images — this matches what Docker stores in local RepoDigests.
|
||||
// images - this matches what Docker stores in local RepoDigests.
|
||||
const MANIFEST_ACCEPT = [
|
||||
'application/vnd.docker.distribution.manifest.list.v2+json',
|
||||
'application/vnd.docker.distribution.manifest.v2+json',
|
||||
@@ -145,7 +145,7 @@ export class ImageUpdateService {
|
||||
private static readonly MANUAL_COOLDOWN_MS = 10 * 60 * 1000; // 10 min between manual triggers
|
||||
private static readonly INTER_IMAGE_DELAY_MS = 300; // be polite to registries
|
||||
|
||||
private constructor() {}
|
||||
private constructor() { }
|
||||
|
||||
public static getInstance(): ImageUpdateService {
|
||||
if (!ImageUpdateService.instance) {
|
||||
@@ -195,7 +195,7 @@ export class ImageUpdateService {
|
||||
|
||||
try {
|
||||
const db = DatabaseService.getInstance();
|
||||
// Only check local nodes — remote nodes run their own instance
|
||||
// Only check local nodes - remote nodes run their own instance
|
||||
for (const node of db.getNodes()) {
|
||||
if (node.type !== 'local' || !node.id) continue;
|
||||
try {
|
||||
@@ -282,7 +282,7 @@ export class ImageUpdateService {
|
||||
if (!localDigest) return false; // Locally built or never pulled with a digest
|
||||
|
||||
const remoteDigest = await getRemoteDigest(parsed.registry, parsed.repo, parsed.tag);
|
||||
if (!remoteDigest) return false; // Registry unreachable — no false positives
|
||||
if (!remoteDigest) return false; // Registry unreachable - no false positives
|
||||
|
||||
const hasUpdate = localDigest !== remoteDigest;
|
||||
console.log(
|
||||
|
||||
@@ -157,14 +157,14 @@ export class NodeRegistry {
|
||||
const headers = { Authorization: `Bearer ${node.api_token}` };
|
||||
|
||||
try {
|
||||
// Step 1: Verify auth. A 401 here means wrong token — surface that clearly.
|
||||
// Step 1: Verify auth. A 401 here means wrong token - surface that clearly.
|
||||
const authRes = await axios.get(`${baseUrl}/api/auth/check`, { headers, timeout: 8000 });
|
||||
if (authRes.status !== 200) throw new Error(`Unexpected status ${authRes.status}`);
|
||||
|
||||
db.updateNodeStatus(node.id, 'online');
|
||||
|
||||
// Step 2: Fetch Docker stats in parallel. Use allSettled so a slow or missing
|
||||
// endpoint doesn't fail the whole test — each field falls back to '-' gracefully.
|
||||
// endpoint doesn't fail the whole test - each field falls back to '-' gracefully.
|
||||
const [statsResult, sysResult, imagesResult] = await Promise.allSettled([
|
||||
axios.get(`${baseUrl}/api/stats`, { headers, timeout: 8000 }),
|
||||
axios.get(`${baseUrl}/api/system/stats`, { headers, timeout: 8000 }),
|
||||
|
||||
@@ -34,7 +34,7 @@ export class NotificationService {
|
||||
this.broadcaster(notification);
|
||||
}
|
||||
|
||||
// 2. Fetch enabled agents
|
||||
// 3. Fetch enabled agents
|
||||
const agents = this.dbService.getEnabledAgents();
|
||||
if (agents.length === 0) {
|
||||
console.log('No active notification agents found. Skipping external dispatch.');
|
||||
|
||||
@@ -248,7 +248,7 @@ export class TemplateService {
|
||||
});
|
||||
} else {
|
||||
// Legacy Portainer v2 Format (Fallback for custom registries)
|
||||
// The Portainer v2 spec includes a native `categories` field — pass it through.
|
||||
// The Portainer v2 spec includes a native `categories` field - pass it through.
|
||||
this.cachedTemplates = (response.data.templates || [])
|
||||
.filter((t: Template) => !!t.image && t.type === 1)
|
||||
.map((t: Template) => ({ ...t, source: 'custom' }));
|
||||
|
||||
Reference in New Issue
Block a user