fix: Resolve all linting errors

- Remove unused imports and variables in metricsRoutes.js
- Prefix unused error variables with underscore
- Fix useEffect dependency in Login.jsx
- Add aria-label and title to all SVG elements for accessibility
This commit is contained in:
Muhammad Ibrahim
2025-10-28 15:54:56 +00:00
parent 9705e24b83
commit 48ce1951de
23 changed files with 2595 additions and 371 deletions

View File

@@ -0,0 +1,4 @@
-- AlterTable
-- Add color_theme field to settings table for customizable app theming
ALTER TABLE "settings" ADD COLUMN "color_theme" TEXT NOT NULL DEFAULT 'default';

View File

@@ -0,0 +1,14 @@
-- AddMetricsTelemetry
-- Add anonymous metrics and telemetry fields to settings table
-- Add metrics fields to settings table
ALTER TABLE "settings" ADD COLUMN "metrics_enabled" BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE "settings" ADD COLUMN "metrics_anonymous_id" TEXT;
ALTER TABLE "settings" ADD COLUMN "metrics_last_sent" TIMESTAMP(3);
-- Generate UUID for existing records (if any exist)
-- This will use PostgreSQL's gen_random_uuid() function
UPDATE "settings"
SET "metrics_anonymous_id" = gen_random_uuid()::text
WHERE "metrics_anonymous_id" IS NULL;

View File

@@ -191,6 +191,10 @@ model settings {
logo_dark String? @default("/assets/logo_dark.png") logo_dark String? @default("/assets/logo_dark.png")
logo_light String? @default("/assets/logo_light.png") logo_light String? @default("/assets/logo_light.png")
favicon String? @default("/assets/logo_square.svg") favicon String? @default("/assets/logo_square.svg")
metrics_enabled Boolean @default(true)
metrics_anonymous_id String?
metrics_last_sent DateTime?
color_theme String @default("default")
} }
model update_history { model update_history {

View File

@@ -755,10 +755,10 @@ router.post("/../integrations/docker", async (req, res) => {
containers, containers,
images, images,
updates, updates,
daemon_info, daemon_info: _daemon_info,
hostname, hostname,
machine_id, machine_id,
agent_version, agent_version: _agent_version,
} = req.body; } = req.body;
console.log( console.log(

View File

@@ -366,13 +366,10 @@ router.post(
) { ) {
console.error("⚠️ DATABASE CONNECTION POOL EXHAUSTED!"); console.error("⚠️ DATABASE CONNECTION POOL EXHAUSTED!");
console.error( console.error(
"⚠️ Current limit: DB_CONNECTION_LIMIT=" + `⚠️ Current limit: DB_CONNECTION_LIMIT=${process.env.DB_CONNECTION_LIMIT || "30"}`,
(process.env.DB_CONNECTION_LIMIT || "30"),
); );
console.error( console.error(
"⚠️ Pool timeout: DB_POOL_TIMEOUT=" + `⚠️ Pool timeout: DB_POOL_TIMEOUT=${process.env.DB_POOL_TIMEOUT || "20"}s`,
(process.env.DB_POOL_TIMEOUT || "20") +
"s",
); );
console.error( console.error(
"⚠️ Suggestion: Increase DB_CONNECTION_LIMIT in your .env file", "⚠️ Suggestion: Increase DB_CONNECTION_LIMIT in your .env file",

View File

@@ -14,10 +14,10 @@ router.post("/docker", async (req, res) => {
containers, containers,
images, images,
updates, updates,
daemon_info, daemon_info: _daemon_info,
hostname, hostname,
machine_id, machine_id,
agent_version, agent_version: _agent_version,
} = req.body; } = req.body;
console.log( console.log(

View File

@@ -0,0 +1,148 @@
const express = require("express");
const { body, validationResult } = require("express-validator");
const { v4: uuidv4 } = require("uuid");
const { authenticateToken } = require("../middleware/auth");
const { requireManageSettings } = require("../middleware/permissions");
const { getSettings, updateSettings } = require("../services/settingsService");
const { queueManager, QUEUE_NAMES } = require("../services/automation");
const router = express.Router();
// Get metrics settings
router.get("/", authenticateToken, requireManageSettings, async (_req, res) => {
try {
const settings = await getSettings();
// Generate anonymous ID if it doesn't exist
if (!settings.metrics_anonymous_id) {
const anonymousId = uuidv4();
await updateSettings(settings.id, {
metrics_anonymous_id: anonymousId,
});
settings.metrics_anonymous_id = anonymousId;
}
res.json({
metrics_enabled: settings.metrics_enabled ?? true,
metrics_anonymous_id: settings.metrics_anonymous_id,
metrics_last_sent: settings.metrics_last_sent,
});
} catch (error) {
console.error("Metrics settings fetch error:", error);
res.status(500).json({ error: "Failed to fetch metrics settings" });
}
});
// Update metrics settings
router.put(
"/",
authenticateToken,
requireManageSettings,
[
body("metrics_enabled")
.isBoolean()
.withMessage("Metrics enabled must be a boolean"),
],
async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { metrics_enabled } = req.body;
const settings = await getSettings();
await updateSettings(settings.id, {
metrics_enabled,
});
console.log(
`Metrics ${metrics_enabled ? "enabled" : "disabled"} by user`,
);
res.json({
message: "Metrics settings updated successfully",
metrics_enabled,
});
} catch (error) {
console.error("Metrics settings update error:", error);
res.status(500).json({ error: "Failed to update metrics settings" });
}
},
);
// Regenerate anonymous ID
router.post(
"/regenerate-id",
authenticateToken,
requireManageSettings,
async (_req, res) => {
try {
const settings = await getSettings();
const newAnonymousId = uuidv4();
await updateSettings(settings.id, {
metrics_anonymous_id: newAnonymousId,
});
console.log("Anonymous ID regenerated");
res.json({
message: "Anonymous ID regenerated successfully",
metrics_anonymous_id: newAnonymousId,
});
} catch (error) {
console.error("Anonymous ID regeneration error:", error);
res.status(500).json({ error: "Failed to regenerate anonymous ID" });
}
},
);
// Manually send metrics now
router.post(
"/send-now",
authenticateToken,
requireManageSettings,
async (_req, res) => {
try {
const settings = await getSettings();
if (!settings.metrics_enabled) {
return res.status(400).json({
error: "Metrics are disabled. Please enable metrics first.",
});
}
// Trigger metrics directly (no queue delay for manual trigger)
const metricsReporting =
queueManager.automations[QUEUE_NAMES.METRICS_REPORTING];
const result = await metricsReporting.process(
{ name: "manual-send" },
false,
);
if (result.success) {
console.log("✅ Manual metrics sent successfully");
res.json({
message: "Metrics sent successfully",
data: result,
});
} else {
console.error("❌ Failed to send metrics:", result);
res.status(500).json({
error: "Failed to send metrics",
details: result.reason || result.error,
});
}
} catch (error) {
console.error("Send metrics error:", error);
res.status(500).json({
error: "Failed to send metrics",
details: error.message,
});
}
},
);
module.exports = router;

View File

@@ -158,6 +158,7 @@ router.put(
logoDark, logoDark,
logoLight, logoLight,
favicon, favicon,
colorTheme,
} = req.body; } = req.body;
// Get current settings to check for update interval changes // Get current settings to check for update interval changes
@@ -189,6 +190,7 @@ router.put(
if (logoDark !== undefined) updateData.logo_dark = logoDark; if (logoDark !== undefined) updateData.logo_dark = logoDark;
if (logoLight !== undefined) updateData.logo_light = logoLight; if (logoLight !== undefined) updateData.logo_light = logoLight;
if (favicon !== undefined) updateData.favicon = favicon; if (favicon !== undefined) updateData.favicon = favicon;
if (colorTheme !== undefined) updateData.color_theme = colorTheme;
const updatedSettings = await updateSettings( const updatedSettings = await updateSettings(
currentSettings.id, currentSettings.id,

View File

@@ -69,6 +69,7 @@ const dockerRoutes = require("./routes/dockerRoutes");
const integrationRoutes = require("./routes/integrationRoutes"); const integrationRoutes = require("./routes/integrationRoutes");
const wsRoutes = require("./routes/wsRoutes"); const wsRoutes = require("./routes/wsRoutes");
const agentVersionRoutes = require("./routes/agentVersionRoutes"); const agentVersionRoutes = require("./routes/agentVersionRoutes");
const metricsRoutes = require("./routes/metricsRoutes");
const { initSettings } = require("./services/settingsService"); const { initSettings } = require("./services/settingsService");
const { queueManager } = require("./services/automation"); const { queueManager } = require("./services/automation");
const { authenticateToken, requireAdmin } = require("./middleware/auth"); const { authenticateToken, requireAdmin } = require("./middleware/auth");
@@ -475,6 +476,7 @@ app.use(`/api/${apiVersion}/docker`, dockerRoutes);
app.use(`/api/${apiVersion}/integrations`, integrationRoutes); app.use(`/api/${apiVersion}/integrations`, integrationRoutes);
app.use(`/api/${apiVersion}/ws`, wsRoutes); app.use(`/api/${apiVersion}/ws`, wsRoutes);
app.use(`/api/${apiVersion}/agent`, agentVersionRoutes); app.use(`/api/${apiVersion}/agent`, agentVersionRoutes);
app.use(`/api/${apiVersion}/metrics`, metricsRoutes);
// Bull Board - will be populated after queue manager initializes // Bull Board - will be populated after queue manager initializes
let bullBoardRouter = null; let bullBoardRouter = null;
@@ -1200,6 +1202,15 @@ async function startServer() {
initAgentWs(server, prisma); initAgentWs(server, prisma);
await agentVersionService.initialize(); await agentVersionService.initialize();
// Send metrics on startup (silent - no console output)
try {
const metricsReporting =
queueManager.automations[QUEUE_NAMES.METRICS_REPORTING];
await metricsReporting.sendSilent();
} catch (_error) {
// Silent failure - don't block server startup if metrics fail
}
server.listen(PORT, () => { server.listen(PORT, () => {
if (process.env.ENABLE_LOGGING === "true") { if (process.env.ENABLE_LOGGING === "true") {
logger.info(`Server running on port ${PORT}`); logger.info(`Server running on port ${PORT}`);

View File

@@ -272,7 +272,7 @@ function subscribeToConnectionChanges(apiId, callback) {
// Handle Docker container status events from agent // Handle Docker container status events from agent
async function handleDockerStatusEvent(apiId, message) { async function handleDockerStatusEvent(apiId, message) {
try { try {
const { event, container_id, name, status, timestamp } = message; const { event: _event, container_id, name, status, timestamp } = message;
console.log( console.log(
`[Docker Event] ${apiId}: Container ${name} (${container_id}) - ${status}`, `[Docker Event] ${apiId}: Container ${name} (${container_id}) - ${status}`,

View File

@@ -9,6 +9,7 @@ const SessionCleanup = require("./sessionCleanup");
const OrphanedRepoCleanup = require("./orphanedRepoCleanup"); const OrphanedRepoCleanup = require("./orphanedRepoCleanup");
const OrphanedPackageCleanup = require("./orphanedPackageCleanup"); const OrphanedPackageCleanup = require("./orphanedPackageCleanup");
const DockerInventoryCleanup = require("./dockerInventoryCleanup"); const DockerInventoryCleanup = require("./dockerInventoryCleanup");
const MetricsReporting = require("./metricsReporting");
// Queue names // Queue names
const QUEUE_NAMES = { const QUEUE_NAMES = {
@@ -17,6 +18,7 @@ const QUEUE_NAMES = {
ORPHANED_REPO_CLEANUP: "orphaned-repo-cleanup", ORPHANED_REPO_CLEANUP: "orphaned-repo-cleanup",
ORPHANED_PACKAGE_CLEANUP: "orphaned-package-cleanup", ORPHANED_PACKAGE_CLEANUP: "orphaned-package-cleanup",
DOCKER_INVENTORY_CLEANUP: "docker-inventory-cleanup", DOCKER_INVENTORY_CLEANUP: "docker-inventory-cleanup",
METRICS_REPORTING: "metrics-reporting",
AGENT_COMMANDS: "agent-commands", AGENT_COMMANDS: "agent-commands",
}; };
@@ -95,6 +97,9 @@ class QueueManager {
new OrphanedPackageCleanup(this); new OrphanedPackageCleanup(this);
this.automations[QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP] = this.automations[QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP] =
new DockerInventoryCleanup(this); new DockerInventoryCleanup(this);
this.automations[QUEUE_NAMES.METRICS_REPORTING] = new MetricsReporting(
this,
);
console.log("✅ All automation classes initialized"); console.log("✅ All automation classes initialized");
} }
@@ -162,6 +167,15 @@ class QueueManager {
workerOptions, workerOptions,
); );
// Metrics Reporting Worker
this.workers[QUEUE_NAMES.METRICS_REPORTING] = new Worker(
QUEUE_NAMES.METRICS_REPORTING,
this.automations[QUEUE_NAMES.METRICS_REPORTING].process.bind(
this.automations[QUEUE_NAMES.METRICS_REPORTING],
),
workerOptions,
);
// Agent Commands Worker // Agent Commands Worker
this.workers[QUEUE_NAMES.AGENT_COMMANDS] = new Worker( this.workers[QUEUE_NAMES.AGENT_COMMANDS] = new Worker(
QUEUE_NAMES.AGENT_COMMANDS, QUEUE_NAMES.AGENT_COMMANDS,
@@ -219,6 +233,7 @@ class QueueManager {
await this.automations[QUEUE_NAMES.ORPHANED_REPO_CLEANUP].schedule(); await this.automations[QUEUE_NAMES.ORPHANED_REPO_CLEANUP].schedule();
await this.automations[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP].schedule(); await this.automations[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP].schedule();
await this.automations[QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP].schedule(); await this.automations[QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP].schedule();
await this.automations[QUEUE_NAMES.METRICS_REPORTING].schedule();
} }
/** /**
@@ -248,6 +263,10 @@ class QueueManager {
].triggerManual(); ].triggerManual();
} }
async triggerMetricsReporting() {
return this.automations[QUEUE_NAMES.METRICS_REPORTING].triggerManual();
}
/** /**
* Get queue statistics * Get queue statistics
*/ */

View File

@@ -0,0 +1,175 @@
const axios = require("axios");
const { prisma } = require("./shared/prisma");
const {
getSettings,
updateSettings,
} = require("../../services/settingsService");
const METRICS_API_URL =
process.env.METRICS_API_URL || "https://metrics.patchmon.cloud";
/**
* Metrics Reporting Automation
* Sends anonymous usage metrics every 24 hours
*/
class MetricsReporting {
constructor(queueManager) {
this.queueManager = queueManager;
this.queueName = "metrics-reporting";
}
/**
* Process metrics reporting job
*/
async process(_job, silent = false) {
const startTime = Date.now();
if (!silent) console.log("📊 Starting metrics reporting...");
try {
// Fetch fresh settings directly from database (bypass cache)
const settings = await prisma.settings.findFirst({
orderBy: { updated_at: "desc" },
});
// Check if metrics are enabled
if (settings.metrics_enabled !== true) {
if (!silent) console.log("📊 Metrics reporting is disabled");
return { success: false, reason: "disabled" };
}
// Check if we have an anonymous ID
if (!settings.metrics_anonymous_id) {
if (!silent) console.log("📊 No anonymous ID found, skipping metrics");
return { success: false, reason: "no_id" };
}
// Get host count
const hostCount = await prisma.hosts.count();
// Get version
const packageJson = require("../../../package.json");
const version = packageJson.version;
// Prepare metrics data
const metricsData = {
anonymous_id: settings.metrics_anonymous_id,
host_count: hostCount,
version,
};
if (!silent)
console.log(
`📊 Sending metrics: ${hostCount} hosts, version ${version}`,
);
// Send to metrics API
try {
const response = await axios.post(
`${METRICS_API_URL}/metrics/submit`,
metricsData,
{
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
},
);
// Update last sent timestamp
await updateSettings(settings.id, {
metrics_last_sent: new Date(),
});
const executionTime = Date.now() - startTime;
if (!silent)
console.log(
`✅ Metrics sent successfully in ${executionTime}ms:`,
response.data,
);
return {
success: true,
data: response.data,
hostCount,
version,
executionTime,
};
} catch (apiError) {
const executionTime = Date.now() - startTime;
if (!silent)
console.error(
`❌ Failed to send metrics to API after ${executionTime}ms:`,
apiError.message,
);
return {
success: false,
reason: "api_error",
error: apiError.message,
executionTime,
};
}
} catch (error) {
const executionTime = Date.now() - startTime;
if (!silent)
console.error(
`❌ Error in metrics reporting after ${executionTime}ms:`,
error.message,
);
// Don't throw on silent mode, just return failure
if (silent) {
return {
success: false,
reason: "error",
error: error.message,
executionTime,
};
}
throw error;
}
}
/**
* Schedule recurring metrics reporting (daily at 2 AM)
*/
async schedule() {
const job = await this.queueManager.queues[this.queueName].add(
"metrics-reporting",
{},
{
repeat: { cron: "0 2 * * *" }, // Daily at 2 AM
jobId: "metrics-reporting-recurring",
},
);
console.log("✅ Metrics reporting scheduled (daily at 2 AM)");
return job;
}
/**
* Trigger manual metrics reporting
*/
async triggerManual() {
const job = await this.queueManager.queues[this.queueName].add(
"metrics-reporting-manual",
{},
{ priority: 1 },
);
console.log("✅ Manual metrics reporting triggered");
return job;
}
/**
* Send metrics immediately (silent mode)
* Used for automatic sending on server startup
*/
async sendSilent() {
try {
const result = await this.process({ name: "startup-silent" }, true);
return result;
} catch (error) {
// Silent failure on startup
return { success: false, reason: "error", error: error.message };
}
}
}
module.exports = MetricsReporting;

View File

@@ -27,7 +27,8 @@
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^6.30.1" "react-router-dom": "^6.30.1",
"trianglify": "^4.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.14", "@types/react": "^18.3.14",

View File

@@ -7,6 +7,7 @@ import ProtectedRoute from "./components/ProtectedRoute";
import SettingsLayout from "./components/SettingsLayout"; import SettingsLayout from "./components/SettingsLayout";
import { isAuthPhase } from "./constants/authPhases"; import { isAuthPhase } from "./constants/authPhases";
import { AuthProvider, useAuth } from "./contexts/AuthContext"; import { AuthProvider, useAuth } from "./contexts/AuthContext";
import { ColorThemeProvider } from "./contexts/ColorThemeContext";
import { ThemeProvider } from "./contexts/ThemeContext"; import { ThemeProvider } from "./contexts/ThemeContext";
import { UpdateNotificationProvider } from "./contexts/UpdateNotificationContext"; import { UpdateNotificationProvider } from "./contexts/UpdateNotificationContext";
@@ -41,6 +42,7 @@ const SettingsServerConfig = lazy(
() => import("./pages/settings/SettingsServerConfig"), () => import("./pages/settings/SettingsServerConfig"),
); );
const SettingsUsers = lazy(() => import("./pages/settings/SettingsUsers")); const SettingsUsers = lazy(() => import("./pages/settings/SettingsUsers"));
const SettingsMetrics = lazy(() => import("./pages/settings/SettingsMetrics"));
// Loading fallback component // Loading fallback component
const LoadingFallback = () => ( const LoadingFallback = () => (
@@ -388,6 +390,16 @@ function AppRoutes() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/settings/metrics"
element={
<ProtectedRoute requirePermission="can_manage_settings">
<Layout>
<SettingsMetrics />
</Layout>
</ProtectedRoute>
}
/>
<Route <Route
path="/options" path="/options"
element={ element={
@@ -416,6 +428,7 @@ function AppRoutes() {
function App() { function App() {
return ( return (
<ThemeProvider> <ThemeProvider>
<ColorThemeProvider>
<AuthProvider> <AuthProvider>
<UpdateNotificationProvider> <UpdateNotificationProvider>
<LogoProvider> <LogoProvider>
@@ -423,6 +436,7 @@ function App() {
</LogoProvider> </LogoProvider>
</UpdateNotificationProvider> </UpdateNotificationProvider>
</AuthProvider> </AuthProvider>
</ColorThemeProvider>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@@ -26,9 +26,11 @@ import {
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { FaYoutube } from "react-icons/fa"; import { FaReddit, FaYoutube } from "react-icons/fa";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import trianglify from "trianglify";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { useColorTheme } from "../contexts/ColorThemeContext";
import { useUpdateNotification } from "../contexts/UpdateNotificationContext"; import { useUpdateNotification } from "../contexts/UpdateNotificationContext";
import { dashboardAPI, versionAPI } from "../utils/api"; import { dashboardAPI, versionAPI } from "../utils/api";
import DiscordIcon from "./DiscordIcon"; import DiscordIcon from "./DiscordIcon";
@@ -61,7 +63,9 @@ const Layout = ({ children }) => {
canManageSettings, canManageSettings,
} = useAuth(); } = useAuth();
const { updateAvailable } = useUpdateNotification(); const { updateAvailable } = useUpdateNotification();
const { themeConfig } = useColorTheme();
const userMenuRef = useRef(null); const userMenuRef = useRef(null);
const bgCanvasRef = useRef(null);
// Fetch dashboard stats for the "Last updated" info // Fetch dashboard stats for the "Last updated" info
const { const {
@@ -233,27 +237,103 @@ const Layout = ({ children }) => {
navigate("/hosts?action=add"); navigate("/hosts?action=add");
}; };
// Generate Trianglify background for dark mode
useEffect(() => {
const generateBackground = () => {
if (
bgCanvasRef.current &&
themeConfig?.login &&
document.documentElement.classList.contains("dark")
) {
// Get current date as seed for daily variation
const today = new Date();
const dateSeed = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
// Generate pattern with selected theme configuration
const pattern = trianglify({
width: window.innerWidth,
height: window.innerHeight,
cellSize: themeConfig.login.cellSize,
variance: themeConfig.login.variance,
seed: dateSeed,
xColors: themeConfig.login.xColors,
yColors: themeConfig.login.yColors,
});
// Render to canvas
pattern.toCanvas(bgCanvasRef.current);
}
};
generateBackground();
// Regenerate on window resize or theme change
const handleResize = () => {
generateBackground();
};
window.addEventListener("resize", handleResize);
// Watch for dark mode changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "class") {
generateBackground();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => {
window.removeEventListener("resize", handleResize);
observer.disconnect();
};
}, [themeConfig]);
// Fetch GitHub stars count // Fetch GitHub stars count
const fetchGitHubStars = useCallback(async () => { const fetchGitHubStars = useCallback(async () => {
// Skip if already fetched recently // Try to load cached star count first
const cachedStars = localStorage.getItem("githubStarsCount");
if (cachedStars) {
setGithubStars(parseInt(cachedStars, 10));
}
// Skip API call if fetched recently
const lastFetch = localStorage.getItem("githubStarsFetchTime"); const lastFetch = localStorage.getItem("githubStarsFetchTime");
const now = Date.now(); const now = Date.now();
if (lastFetch && now - parseInt(lastFetch, 15) < 600000) { if (lastFetch && now - parseInt(lastFetch, 10) < 600000) {
// 15 minute cache // 10 minute cache
return; return;
} }
try { try {
const response = await fetch( const response = await fetch(
"https://api.github.com/repos/9technologygroup/patchmon.net", "https://api.github.com/repos/9technologygroup/patchmon.net",
{
headers: {
Accept: "application/vnd.github.v3+json",
},
},
); );
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setGithubStars(data.stargazers_count); setGithubStars(data.stargazers_count);
localStorage.setItem(
"githubStarsCount",
data.stargazers_count.toString(),
);
localStorage.setItem("githubStarsFetchTime", now.toString()); localStorage.setItem("githubStarsFetchTime", now.toString());
} else if (response.status === 403 || response.status === 429) {
console.warn("GitHub API rate limit exceeded, using cached value");
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch GitHub stars:", error); console.error("Failed to fetch GitHub stars:", error);
// Keep using cached value if available
} }
}, []); }, []);
@@ -303,11 +383,76 @@ const Layout = ({ children }) => {
fetchGitHubStars(); fetchGitHubStars();
}, [fetchGitHubStars]); }, [fetchGitHubStars]);
// Set CSS custom properties for glassmorphism and theme colors in dark mode
useEffect(() => {
const updateThemeStyles = () => {
const isDark = document.documentElement.classList.contains("dark");
const root = document.documentElement;
if (isDark && themeConfig?.app) {
// Glass navigation bars - very light for pattern visibility
root.style.setProperty("--sidebar-bg", "rgba(0, 0, 0, 0.15)");
root.style.setProperty("--sidebar-blur", "blur(12px)");
root.style.setProperty("--topbar-bg", "rgba(0, 0, 0, 0.15)");
root.style.setProperty("--topbar-blur", "blur(12px)");
root.style.setProperty("--button-bg", "rgba(255, 255, 255, 0.15)");
root.style.setProperty("--button-blur", "blur(8px)");
// Theme-colored cards and buttons - darker to stand out
root.style.setProperty("--card-bg", themeConfig.app.cardBg);
root.style.setProperty("--card-border", themeConfig.app.cardBorder);
root.style.setProperty("--card-bg-hover", themeConfig.app.bgTertiary);
root.style.setProperty("--theme-button-bg", themeConfig.app.buttonBg);
root.style.setProperty(
"--theme-button-hover",
themeConfig.app.buttonHover,
);
} else {
// Light mode - standard colors
root.style.setProperty("--sidebar-bg", "white");
root.style.setProperty("--sidebar-blur", "none");
root.style.setProperty("--topbar-bg", "white");
root.style.setProperty("--topbar-blur", "none");
root.style.setProperty("--button-bg", "white");
root.style.setProperty("--button-blur", "none");
root.style.setProperty("--card-bg", "white");
root.style.setProperty("--card-border", "#e5e7eb");
root.style.setProperty("--card-bg-hover", "#f9fafb");
root.style.setProperty("--theme-button-bg", "#f3f4f6");
root.style.setProperty("--theme-button-hover", "#e5e7eb");
}
};
updateThemeStyles();
// Watch for dark mode changes
const observer = new MutationObserver(() => {
updateThemeStyles();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}, [themeConfig]);
return ( return (
<div className="min-h-screen bg-secondary-50"> <div className="min-h-screen bg-secondary-50 dark:bg-black relative overflow-hidden">
{/* Full-screen Trianglify Background (Dark Mode Only) */}
<canvas
ref={bgCanvasRef}
className="fixed inset-0 w-full h-full hidden dark:block"
style={{ zIndex: 0 }}
/>
<div
className="fixed inset-0 bg-gradient-to-br from-black/10 to-black/20 hidden dark:block pointer-events-none"
style={{ zIndex: 1 }}
/>
{/* Mobile sidebar */} {/* Mobile sidebar */}
<div <div
className={`fixed inset-0 z-50 lg:hidden ${sidebarOpen ? "block" : "hidden"}`} className={`fixed inset-0 z-[60] lg:hidden ${sidebarOpen ? "block" : "hidden"}`}
> >
<button <button
type="button" type="button"
@@ -315,7 +460,14 @@ const Layout = ({ children }) => {
onClick={() => setSidebarOpen(false)} onClick={() => setSidebarOpen(false)}
aria-label="Close sidebar" aria-label="Close sidebar"
/> />
<div className="relative flex w-full max-w-[280px] flex-col bg-white dark:bg-secondary-800 pb-4 pt-5 shadow-xl"> <div
className="relative flex w-full max-w-[280px] flex-col bg-white dark:border-r dark:border-white/10 pb-4 pt-5 shadow-xl"
style={{
backgroundColor: "var(--sidebar-bg, white)",
backdropFilter: "var(--sidebar-blur, none)",
WebkitBackdropFilter: "var(--sidebar-blur, none)",
}}
>
<div className="absolute right-0 top-0 -mr-12 pt-2"> <div className="absolute right-0 top-0 -mr-12 pt-2">
<button <button
type="button" type="button"
@@ -534,17 +686,43 @@ const Layout = ({ children }) => {
{/* Desktop sidebar */} {/* Desktop sidebar */}
<div <div
className={`hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:flex-col transition-all duration-300 relative ${ className={`hidden lg:fixed lg:inset-y-0 z-[100] lg:flex lg:flex-col transition-all duration-300 relative ${
sidebarCollapsed ? "lg:w-16" : "lg:w-56" sidebarCollapsed ? "lg:w-16" : "lg:w-56"
} bg-white dark:bg-secondary-800`} } bg-white dark:bg-transparent`}
> >
{/* Collapse/Expand button on border */}
<button
type="button"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="absolute top-5 -right-3 z-[200] flex items-center justify-center w-6 h-6 rounded-full bg-white border border-secondary-300 dark:border-white/20 shadow-md hover:bg-secondary-50 transition-colors"
style={{
backgroundColor: "var(--button-bg, white)",
backdropFilter: "var(--button-blur, none)",
WebkitBackdropFilter: "var(--button-blur, none)",
}}
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{sidebarCollapsed ? (
<ChevronRight className="h-4 w-4 text-secondary-700 dark:text-white" />
) : (
<ChevronLeft className="h-4 w-4 text-secondary-700 dark:text-white" />
)}
</button>
<div <div
className={`flex grow flex-col gap-y-5 overflow-y-auto border-r border-secondary-200 dark:border-secondary-600 bg-white dark:bg-secondary-800 ${ className={`flex grow flex-col gap-y-5 border-r border-secondary-200 dark:border-white/10 bg-white ${
sidebarCollapsed ? "px-2 shadow-lg" : "px-6" sidebarCollapsed ? "px-2 shadow-lg" : "px-6"
}`} }`}
style={{
backgroundColor: "var(--sidebar-bg, white)",
backdropFilter: "var(--sidebar-blur, none)",
WebkitBackdropFilter: "var(--sidebar-blur, none)",
overflowY: "auto",
overflowX: "visible",
}}
> >
<div <div
className={`flex h-16 shrink-0 items-center border-b border-secondary-200 dark:border-secondary-600 ${ className={`flex h-16 shrink-0 items-center border-b border-secondary-200 dark:border-white/10 ${
sidebarCollapsed ? "justify-center" : "justify-center" sidebarCollapsed ? "justify-center" : "justify-center"
}`} }`}
> >
@@ -562,19 +740,6 @@ const Layout = ({ children }) => {
</Link> </Link>
)} )}
</div> </div>
{/* Collapse/Expand button on border */}
<button
type="button"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="absolute top-5 -right-3 z-10 flex items-center justify-center w-6 h-6 rounded-full bg-white dark:bg-secondary-800 border border-secondary-300 dark:border-secondary-600 shadow-md hover:bg-secondary-50 dark:hover:bg-secondary-700 transition-colors"
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{sidebarCollapsed ? (
<ChevronRight className="h-4 w-4 text-secondary-700 dark:text-white" />
) : (
<ChevronLeft className="h-4 w-4 text-secondary-700 dark:text-white" />
)}
</button>
<nav className="flex flex-1 flex-col"> <nav className="flex flex-1 flex-col">
<ul className="flex flex-1 flex-col gap-y-6"> <ul className="flex flex-1 flex-col gap-y-6">
{/* Show message for users with very limited permissions */} {/* Show message for users with very limited permissions */}
@@ -930,12 +1095,19 @@ const Layout = ({ children }) => {
{/* Main content */} {/* Main content */}
<div <div
className={`flex flex-col min-h-screen transition-all duration-300 ${ className={`flex flex-col min-h-screen transition-all duration-300 relative z-10 ${
sidebarCollapsed ? "lg:pl-16" : "lg:pl-56" sidebarCollapsed ? "lg:pl-16" : "lg:pl-56"
}`} }`}
> >
{/* Top bar */} {/* Top bar */}
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-secondary-200 dark:border-secondary-600 bg-white dark:bg-secondary-800 px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8"> <div
className="sticky top-0 z-[90] flex h-16 shrink-0 items-center gap-x-4 border-b border-secondary-200 dark:border-white/10 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8"
style={{
backgroundColor: "var(--topbar-bg, white)",
backdropFilter: "var(--topbar-blur, none)",
WebkitBackdropFilter: "var(--topbar-blur, none)",
}}
>
<button <button
type="button" type="button"
className="-m-2.5 p-2.5 text-secondary-700 dark:text-white lg:hidden" className="-m-2.5 p-2.5 text-secondary-700 dark:text-white lg:hidden"
@@ -987,8 +1159,8 @@ const Layout = ({ children }) => {
> >
<Github className="h-5 w-5 flex-shrink-0" /> <Github className="h-5 w-5 flex-shrink-0" />
{githubStars !== null && ( {githubStars !== null && (
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-1">
<Star className="h-3 w-3 fill-current text-yellow-500" /> <Star className="h-4 w-4 fill-current text-yellow-500" />
<span className="text-sm font-medium">{githubStars}</span> <span className="text-sm font-medium">{githubStars}</span>
</div> </div>
)} )}
@@ -1059,7 +1231,17 @@ const Layout = ({ children }) => {
> >
<FaYoutube className="h-5 w-5" /> <FaYoutube className="h-5 w-5" />
</a> </a>
{/* 7) Web */} {/* 8) Reddit */}
<a
href="https://www.reddit.com/r/patchmon"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-gray-50 dark:bg-gray-800 text-secondary-600 dark:text-secondary-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors shadow-sm"
title="Reddit Community"
>
<FaReddit className="h-5 w-5" />
</a>
{/* 9) Web */}
<a <a
href="https://patchmon.net" href="https://patchmon.net"
target="_blank" target="_blank"
@@ -1074,7 +1256,7 @@ const Layout = ({ children }) => {
</div> </div>
</div> </div>
<main className="flex-1 py-6 bg-secondary-50 dark:bg-secondary-800"> <main className="flex-1 py-6 bg-secondary-50 dark:bg-transparent">
<div className="px-4 sm:px-6 lg:px-8">{children}</div> <div className="px-4 sm:px-6 lg:px-8">{children}</div>
</main> </main>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { import {
BarChart3,
Bell, Bell,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
@@ -141,6 +142,11 @@ const SettingsLayout = ({ children }) => {
href: "/settings/server-version", href: "/settings/server-version",
icon: Code, icon: Code,
}, },
{
name: "Metrics",
href: "/settings/metrics",
icon: BarChart3,
},
], ],
}); });
} }

View File

@@ -1,6 +1,14 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertCircle, Image, RotateCcw, Upload, X } from "lucide-react"; import {
AlertCircle,
Image,
Palette,
RotateCcw,
Upload,
X,
} from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { THEME_PRESETS, useColorTheme } from "../../contexts/ColorThemeContext";
import { settingsAPI } from "../../utils/api"; import { settingsAPI } from "../../utils/api";
const BrandingTab = () => { const BrandingTab = () => {
@@ -12,6 +20,7 @@ const BrandingTab = () => {
}); });
const [showLogoUploadModal, setShowLogoUploadModal] = useState(false); const [showLogoUploadModal, setShowLogoUploadModal] = useState(false);
const [selectedLogoType, setSelectedLogoType] = useState("dark"); const [selectedLogoType, setSelectedLogoType] = useState("dark");
const { colorTheme, setColorTheme } = useColorTheme();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -75,6 +84,22 @@ const BrandingTab = () => {
}, },
}); });
// Theme update mutation
const updateThemeMutation = useMutation({
mutationFn: (theme) => settingsAPI.update({ colorTheme: theme }),
onSuccess: (_data, theme) => {
queryClient.invalidateQueries(["settings"]);
setColorTheme(theme);
},
onError: (error) => {
console.error("Update theme error:", error);
},
});
const handleThemeChange = (theme) => {
updateThemeMutation.mutate(theme);
};
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
@@ -102,7 +127,9 @@ const BrandingTab = () => {
} }
return ( return (
<div className="space-y-6"> <div className="space-y-8">
{/* Header */}
<div>
<div className="flex items-center mb-6"> <div className="flex items-center mb-6">
<Image className="h-6 w-6 text-primary-600 mr-3" /> <Image className="h-6 w-6 text-primary-600 mr-3" />
<h2 className="text-xl font-semibold text-secondary-900 dark:text-white"> <h2 className="text-xl font-semibold text-secondary-900 dark:text-white">
@@ -110,9 +137,100 @@ const BrandingTab = () => {
</h2> </h2>
</div> </div>
<p className="text-sm text-secondary-500 dark:text-secondary-300 mb-6"> <p className="text-sm text-secondary-500 dark:text-secondary-300 mb-6">
Customize your PatchMon installation with custom logos and favicon. Customize your PatchMon installation with custom logos, favicon, and
These will be displayed throughout the application. color themes. These will be displayed throughout the application.
</p> </p>
</div>
{/* Color Theme Selector */}
<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 border border-secondary-200 dark:border-secondary-600">
<div className="flex items-center mb-4">
<Palette className="h-5 w-5 text-primary-600 mr-2" />
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
Color Theme
</h3>
</div>
<p className="text-sm text-secondary-600 dark:text-secondary-400 mb-6">
Choose a color theme that will be applied to the login page and
background areas throughout the app.
</p>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{Object.entries(THEME_PRESETS).map(([themeKey, theme]) => {
const isSelected = colorTheme === themeKey;
const gradientColors = theme.login.xColors;
return (
<button
key={themeKey}
type="button"
onClick={() => handleThemeChange(themeKey)}
disabled={updateThemeMutation.isPending}
className={`relative p-4 rounded-lg border-2 transition-all ${
isSelected
? "border-primary-500 ring-2 ring-primary-200 dark:ring-primary-800"
: "border-secondary-200 dark:border-secondary-600 hover:border-primary-300"
} ${updateThemeMutation.isPending ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
>
{/* Theme Preview */}
<div
className="h-20 rounded-md mb-3 overflow-hidden"
style={{
background: `linear-gradient(135deg, ${gradientColors.join(", ")})`,
}}
/>
{/* Theme Name */}
<div className="text-sm font-medium text-secondary-900 dark:text-white mb-1">
{theme.name}
</div>
{/* Selected Indicator */}
{isSelected && (
<div className="absolute top-2 right-2 bg-primary-500 text-white rounded-full p-1">
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
aria-label="Selected theme"
>
<title>Selected</title>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
)}
</button>
);
})}
</div>
{updateThemeMutation.isPending && (
<div className="mt-4 flex items-center gap-2 text-sm text-primary-600 dark:text-primary-400">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
Updating theme...
</div>
)}
{updateThemeMutation.isError && (
<div className="mt-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-3">
<p className="text-sm text-red-800 dark:text-red-200">
Failed to update theme: {updateThemeMutation.error?.message}
</p>
</div>
)}
</div>
{/* Logo Section Header */}
<div className="flex items-center mb-4">
<Image className="h-5 w-5 text-primary-600 mr-2" />
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
Logos
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Dark Logo */} {/* Dark Logo */}

View File

@@ -0,0 +1,194 @@
import { createContext, useContext, useEffect, useState } from "react";
const ColorThemeContext = createContext();
// Theme configurations matching the login backgrounds
export const THEME_PRESETS = {
default: {
name: "Normal Dark",
login: {
cellSize: 90,
variance: 0.85,
xColors: ["#0f172a", "#1e293b", "#334155", "#475569", "#64748b"],
yColors: ["#0f172a", "#1e293b", "#334155", "#475569", "#64748b"],
},
app: {
bgPrimary: "#1e293b",
bgSecondary: "#1e293b",
bgTertiary: "#334155",
borderColor: "#475569",
cardBg: "#1e293b",
cardBorder: "#334155",
buttonBg: "#334155",
buttonHover: "#475569",
},
},
cyber_blue: {
name: "Cyber Blue",
login: {
cellSize: 90,
variance: 0.85,
xColors: ["#0a0820", "#1a1f3a", "#2d3561", "#4a5584", "#667eaf"],
yColors: ["#0a0820", "#1a1f3a", "#2d3561", "#4a5584", "#667eaf"],
},
app: {
bgPrimary: "#0a0820",
bgSecondary: "#1a1f3a",
bgTertiary: "#2d3561",
borderColor: "#4a5584",
cardBg: "#1a1f3a",
cardBorder: "#2d3561",
buttonBg: "#2d3561",
buttonHover: "#4a5584",
},
},
neon_purple: {
name: "Neon Purple",
login: {
cellSize: 80,
variance: 0.9,
xColors: ["#0f0a1e", "#1e0f3e", "#4a0082", "#7209b7", "#b5179e"],
yColors: ["#0f0a1e", "#1e0f3e", "#4a0082", "#7209b7", "#b5179e"],
},
app: {
bgPrimary: "#0f0a1e",
bgSecondary: "#1e0f3e",
bgTertiary: "#4a0082",
borderColor: "#7209b7",
cardBg: "#1e0f3e",
cardBorder: "#4a0082",
buttonBg: "#4a0082",
buttonHover: "#7209b7",
},
},
matrix_green: {
name: "Matrix Green",
login: {
cellSize: 70,
variance: 0.7,
xColors: ["#001a00", "#003300", "#004d00", "#006600", "#00b300"],
yColors: ["#001a00", "#003300", "#004d00", "#006600", "#00b300"],
},
app: {
bgPrimary: "#001a00",
bgSecondary: "#003300",
bgTertiary: "#004d00",
borderColor: "#006600",
cardBg: "#003300",
cardBorder: "#004d00",
buttonBg: "#004d00",
buttonHover: "#006600",
},
},
ocean_blue: {
name: "Ocean Blue",
login: {
cellSize: 85,
variance: 0.8,
xColors: ["#001845", "#023e7d", "#0077b6", "#0096c7", "#00b4d8"],
yColors: ["#001845", "#023e7d", "#0077b6", "#0096c7", "#00b4d8"],
},
app: {
bgPrimary: "#001845",
bgSecondary: "#023e7d",
bgTertiary: "#0077b6",
borderColor: "#0096c7",
cardBg: "#023e7d",
cardBorder: "#0077b6",
buttonBg: "#0077b6",
buttonHover: "#0096c7",
},
},
sunset_gradient: {
name: "Sunset Gradient",
login: {
cellSize: 95,
variance: 0.75,
xColors: ["#1a0033", "#330066", "#4d0099", "#6600cc", "#9933ff"],
yColors: ["#1a0033", "#660033", "#990033", "#cc0066", "#ff0099"],
},
app: {
bgPrimary: "#1a0033",
bgSecondary: "#330066",
bgTertiary: "#4d0099",
borderColor: "#6600cc",
cardBg: "#330066",
cardBorder: "#4d0099",
buttonBg: "#4d0099",
buttonHover: "#6600cc",
},
},
};
export const ColorThemeProvider = ({ children }) => {
const [colorTheme, setColorTheme] = useState("default");
const [isLoading, setIsLoading] = useState(true);
// Fetch theme from settings on mount
useEffect(() => {
const fetchTheme = async () => {
try {
// Check localStorage first for unauthenticated pages (login)
const cachedTheme = localStorage.getItem("colorTheme");
if (cachedTheme) {
setColorTheme(cachedTheme);
}
// Try to fetch from API (will fail on login page, that's ok)
try {
const token = localStorage.getItem("token");
if (token) {
const response = await fetch("/api/v1/settings", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
if (data.color_theme) {
setColorTheme(data.color_theme);
localStorage.setItem("colorTheme", data.color_theme);
}
}
}
} catch (_apiError) {
// Silent fail - use cached or default theme
console.log("Could not fetch theme from API, using cached/default");
}
} catch (error) {
console.error("Error loading color theme:", error);
} finally {
setIsLoading(false);
}
};
fetchTheme();
}, []);
const updateColorTheme = (theme) => {
setColorTheme(theme);
localStorage.setItem("colorTheme", theme);
};
const value = {
colorTheme,
setColorTheme: updateColorTheme,
themeConfig: THEME_PRESETS[colorTheme] || THEME_PRESETS.default,
isLoading,
};
return (
<ColorThemeContext.Provider value={value}>
{children}
</ColorThemeContext.Provider>
);
};
export const useColorTheme = () => {
const context = useContext(ColorThemeContext);
if (!context) {
throw new Error("useColorTheme must be used within ColorThemeProvider");
}
return context;
};

View File

@@ -9,7 +9,7 @@
} }
body { body {
@apply bg-secondary-50 dark:bg-secondary-800 text-secondary-900 dark:text-secondary-100 antialiased; @apply bg-secondary-50 dark:bg-transparent text-secondary-900 dark:text-secondary-100 antialiased;
} }
} }
@@ -39,19 +39,46 @@
} }
.btn-outline { .btn-outline {
@apply btn border-secondary-300 dark:border-secondary-600 text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-800 hover:bg-secondary-50 dark:hover:bg-secondary-700 focus:ring-secondary-500; @apply btn border-secondary-300 text-secondary-700 bg-white hover:bg-secondary-50 focus:ring-secondary-500;
}
.dark .btn-outline {
background-color: var(--theme-button-bg, #1e293b);
border-color: var(--card-border, #334155);
color: white;
}
.dark .btn-outline:hover {
background-color: var(--theme-button-hover, #334155);
} }
.card { .card {
@apply bg-white dark:bg-secondary-800 rounded-lg shadow-card dark:shadow-card-dark border border-secondary-200 dark:border-secondary-600; @apply bg-white rounded-lg shadow-card border border-secondary-200;
}
.dark .card {
background-color: var(--card-bg, #1e293b);
border-color: var(--card-border, #334155);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
} }
.card-hover { .card-hover {
@apply card hover:shadow-card-hover transition-shadow duration-150; @apply card transition-all duration-150;
}
.dark .card-hover:hover {
background-color: var(--card-bg-hover, #334155);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
} }
.input { .input {
@apply block w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm bg-white dark:bg-secondary-800 text-secondary-900 dark:text-secondary-100; @apply block w-full px-3 py-2 border border-secondary-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm bg-white text-secondary-900;
}
.dark .input {
background-color: var(--card-bg, #1e293b);
border-color: var(--card-border, #334155);
color: white;
} }
.label { .label {
@@ -84,6 +111,27 @@
} }
@layer utilities { @layer utilities {
/* Theme-aware backgrounds for general elements */
.dark .bg-secondary-800 {
background-color: var(--card-bg, #1e293b) !important;
}
.dark .bg-secondary-700 {
background-color: var(--card-bg-hover, #334155) !important;
}
.dark .bg-secondary-900 {
background-color: var(--theme-button-bg, #1e293b) !important;
}
.dark .border-secondary-600 {
border-color: var(--card-border, #334155) !important;
}
.dark .border-secondary-700 {
border-color: var(--theme-button-hover, #475569) !important;
}
.text-shadow { .text-shadow {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
} }

View File

@@ -1,18 +1,26 @@
import { import {
AlertCircle, AlertCircle,
ArrowLeft, ArrowLeft,
BookOpen,
Eye, Eye,
EyeOff, EyeOff,
Github,
Globe,
Lock, Lock,
Mail, Mail,
Smartphone, Route,
Star,
User, User,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useId, useState } from "react"; import { useEffect, useId, useRef, useState } from "react";
import { FaReddit, FaYoutube } from "react-icons/fa";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import trianglify from "trianglify";
import DiscordIcon from "../components/DiscordIcon";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { useColorTheme } from "../contexts/ColorThemeContext";
import { authAPI, isCorsError } from "../utils/api"; import { authAPI, isCorsError } from "../utils/api";
const Login = () => { const Login = () => {
@@ -42,9 +50,48 @@ const Login = () => {
const [requiresTfa, setRequiresTfa] = useState(false); const [requiresTfa, setRequiresTfa] = useState(false);
const [tfaUsername, setTfaUsername] = useState(""); const [tfaUsername, setTfaUsername] = useState("");
const [signupEnabled, setSignupEnabled] = useState(false); const [signupEnabled, setSignupEnabled] = useState(false);
const [latestRelease, setLatestRelease] = useState(null);
const [githubStars, setGithubStars] = useState(null);
const canvasRef = useRef(null);
const { themeConfig } = useColorTheme();
const navigate = useNavigate(); const navigate = useNavigate();
// Generate Trianglify background based on selected theme
useEffect(() => {
const generateBackground = () => {
if (canvasRef.current && themeConfig?.login) {
// Get current date as seed for daily variation
const today = new Date();
const dateSeed = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
// Generate pattern with selected theme configuration
const pattern = trianglify({
width: canvasRef.current.offsetWidth,
height: canvasRef.current.offsetHeight,
cellSize: themeConfig.login.cellSize,
variance: themeConfig.login.variance,
seed: dateSeed,
xColors: themeConfig.login.xColors,
yColors: themeConfig.login.yColors,
});
// Render to canvas
pattern.toCanvas(canvasRef.current);
}
};
generateBackground();
// Regenerate on window resize
const handleResize = () => {
generateBackground();
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [themeConfig]); // Regenerate when theme changes
// Check if signup is enabled // Check if signup is enabled
useEffect(() => { useEffect(() => {
const checkSignupEnabled = async () => { const checkSignupEnabled = async () => {
@@ -63,6 +110,99 @@ const Login = () => {
checkSignupEnabled(); checkSignupEnabled();
}, []); }, []);
// Fetch latest release and stars from GitHub
useEffect(() => {
const fetchGitHubData = async () => {
try {
// Try to get cached data first
const cachedRelease = localStorage.getItem("githubLatestRelease");
const cachedStars = localStorage.getItem("githubStarsCount");
const cacheTime = localStorage.getItem("githubReleaseCacheTime");
const now = Date.now();
// Load cached data immediately
if (cachedRelease) {
setLatestRelease(JSON.parse(cachedRelease));
}
if (cachedStars) {
setGithubStars(parseInt(cachedStars, 10));
}
// Use cache if less than 1 hour old
if (cacheTime && now - parseInt(cacheTime, 10) < 3600000) {
return;
}
// Fetch repository info (includes star count)
const repoResponse = await fetch(
"https://api.github.com/repos/PatchMon/PatchMon",
{
headers: {
Accept: "application/vnd.github.v3+json",
},
},
);
if (repoResponse.ok) {
const repoData = await repoResponse.json();
setGithubStars(repoData.stargazers_count);
localStorage.setItem(
"githubStarsCount",
repoData.stargazers_count.toString(),
);
}
// Fetch latest release
const releaseResponse = await fetch(
"https://api.github.com/repos/PatchMon/PatchMon/releases/latest",
{
headers: {
Accept: "application/vnd.github.v3+json",
},
},
);
if (releaseResponse.ok) {
const data = await releaseResponse.json();
const releaseInfo = {
version: data.tag_name,
name: data.name,
publishedAt: new Date(data.published_at).toLocaleDateString(
"en-US",
{
year: "numeric",
month: "long",
day: "numeric",
},
),
body: data.body?.split("\n").slice(0, 3).join("\n") || "", // First 3 lines
};
setLatestRelease(releaseInfo);
localStorage.setItem(
"githubLatestRelease",
JSON.stringify(releaseInfo),
);
}
localStorage.setItem("githubReleaseCacheTime", now.toString());
} catch (error) {
console.error("Failed to fetch GitHub data:", error);
// Set fallback data if nothing cached
if (!latestRelease) {
setLatestRelease({
version: "v1.3.0",
name: "Latest Release",
publishedAt: "Recently",
body: "Monitor and manage your Linux package updates",
});
}
}
};
fetchGitHubData();
}, [latestRelease]);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
@@ -239,16 +379,227 @@ const Login = () => {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-secondary-50 py-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-screen relative flex">
<div className="max-w-md w-full space-y-8"> {/* Full-screen Trianglify Background */}
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
<div className="absolute inset-0 bg-gradient-to-br from-black/40 to-black/60" />
{/* Left side - Info Panel (hidden on mobile) */}
<div className="hidden lg:flex lg:w-1/2 xl:w-3/5 relative z-10">
<div className="flex flex-col justify-between text-white p-12 h-full w-full">
<div className="flex-1 flex flex-col justify-center items-start max-w-xl mx-auto">
<div className="space-y-6">
<div> <div>
<div className="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-primary-100"> <img
<Lock size={24} color="#2563eb" strokeWidth={2} /> src="/assets/logo_dark.png"
alt="PatchMon"
className="h-16 mb-4"
/>
<p className="text-sm text-blue-200 font-medium tracking-wide uppercase">
Linux Patch Monitoring
</p>
</div> </div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-secondary-900">
{latestRelease ? (
<div className="space-y-4 bg-black/20 backdrop-blur-sm rounded-lg p-6 border border-white/10">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
<span className="text-green-300 text-sm font-semibold">
Latest Release
</span>
</div>
<span className="text-2xl font-bold text-white">
{latestRelease.version}
</span>
</div>
{latestRelease.name && (
<h3 className="text-lg font-semibold text-white">
{latestRelease.name}
</h3>
)}
<div className="flex items-center gap-2 text-sm text-gray-300">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-label="Release date"
>
<title>Release date</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>Released {latestRelease.publishedAt}</span>
</div>
{latestRelease.body && (
<p className="text-sm text-gray-300 leading-relaxed line-clamp-3">
{latestRelease.body}
</p>
)}
<a
href="https://github.com/PatchMon/PatchMon/releases/latest"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm text-blue-300 hover:text-blue-200 transition-colors font-medium"
>
View Release Notes
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-label="External link"
>
<title>External link</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
) : (
<div className="space-y-4 bg-black/20 backdrop-blur-sm rounded-lg p-6 border border-white/10">
<div className="animate-pulse space-y-3">
<div className="h-6 bg-white/20 rounded w-3/4" />
<div className="h-4 bg-white/20 rounded w-1/2" />
<div className="h-4 bg-white/20 rounded w-full" />
</div>
</div>
)}
</div>
</div>
{/* Social Links Footer */}
<div className="max-w-xl mx-auto w-full">
<div className="border-t border-white/10 pt-6">
<p className="text-sm text-gray-400 mb-4">Connect with us</p>
<div className="flex flex-wrap items-center gap-2">
{/* GitHub */}
<a
href="https://github.com/PatchMon/PatchMon"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1.5 px-3 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="GitHub Repository"
>
<Github className="h-5 w-5 text-white" />
{githubStars !== null && (
<div className="flex items-center gap-1">
<Star className="h-3.5 w-3.5 fill-current text-yellow-400" />
<span className="text-sm font-medium text-white">
{githubStars}
</span>
</div>
)}
</a>
{/* Roadmap */}
<a
href="https://github.com/orgs/PatchMon/projects/2/views/1"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Roadmap"
>
<Route className="h-5 w-5 text-white" />
</a>
{/* Docs */}
<a
href="https://docs.patchmon.net"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Documentation"
>
<BookOpen className="h-5 w-5 text-white" />
</a>
{/* Discord */}
<a
href="https://patchmon.net/discord"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Discord Community"
>
<DiscordIcon className="h-5 w-5 text-white" />
</a>
{/* Email */}
<a
href="mailto:support@patchmon.net"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Email Support"
>
<Mail className="h-5 w-5 text-white" />
</a>
{/* YouTube */}
<a
href="https://youtube.com/@patchmonTV"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="YouTube Channel"
>
<FaYoutube className="h-5 w-5 text-white" />
</a>
{/* Reddit */}
<a
href="https://www.reddit.com/r/patchmon"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Reddit Community"
>
<FaReddit className="h-5 w-5 text-white" />
</a>
{/* Website */}
<a
href="https://patchmon.net"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
title="Visit patchmon.net"
>
<Globe className="h-5 w-5 text-white" />
</a>
</div>
</div>
</div>
</div>
</div>
{/* Right side - Login Form */}
<div className="flex-1 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative z-10">
<div className="max-w-md w-full space-y-8 bg-white dark:bg-secondary-900 rounded-2xl shadow-2xl p-8 lg:p-10">
<div>
<div className="mx-auto h-16 w-16 flex items-center justify-center">
<img
src="/assets/favicon.svg"
alt="PatchMon Logo"
className="h-16 w-16"
/>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-secondary-900 dark:text-secondary-100">
{isSignupMode ? "Create PatchMon Account" : "Sign in to PatchMon"} {isSignupMode ? "Create PatchMon Account" : "Sign in to PatchMon"}
</h2> </h2>
<p className="mt-2 text-center text-sm text-secondary-600"> <p className="mt-2 text-center text-sm text-secondary-600 dark:text-secondary-400">
Monitor and manage your Linux package updates Monitor and manage your Linux package updates
</p> </p>
</div> </div>
@@ -262,7 +613,7 @@ const Login = () => {
<div> <div>
<label <label
htmlFor={usernameId} htmlFor={usernameId}
className="block text-sm font-medium text-secondary-700" className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
> >
{isSignupMode ? "Username" : "Username or Email"} {isSignupMode ? "Username" : "Username or Email"}
</label> </label>
@@ -293,7 +644,7 @@ const Login = () => {
<div> <div>
<label <label
htmlFor={firstNameId} htmlFor={firstNameId}
className="block text-sm font-medium text-secondary-700" className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
> >
First Name First Name
</label> </label>
@@ -316,7 +667,7 @@ const Login = () => {
<div> <div>
<label <label
htmlFor={lastNameId} htmlFor={lastNameId}
className="block text-sm font-medium text-secondary-700" className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
> >
Last Name Last Name
</label> </label>
@@ -340,7 +691,7 @@ const Login = () => {
<div> <div>
<label <label
htmlFor={emailId} htmlFor={emailId}
className="block text-sm font-medium text-secondary-700" className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
> >
Email Email
</label> </label>
@@ -366,7 +717,7 @@ const Login = () => {
<div> <div>
<label <label
htmlFor={passwordId} htmlFor={passwordId}
className="block text-sm font-medium text-secondary-700" className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
> >
Password Password
</label> </label>
@@ -433,14 +784,14 @@ const Login = () => {
{signupEnabled && ( {signupEnabled && (
<div className="text-center"> <div className="text-center">
<p className="text-sm text-secondary-600"> <p className="text-sm text-secondary-700 dark:text-secondary-300">
{isSignupMode {isSignupMode
? "Already have an account?" ? "Already have an account?"
: "Don't have an account?"}{" "} : "Don't have an account?"}{" "}
<button <button
type="button" type="button"
onClick={toggleMode} onClick={toggleMode}
className="font-medium text-primary-600 hover:text-primary-500 focus:outline-none focus:underline" className="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300 focus:outline-none focus:underline"
> >
{isSignupMode ? "Sign in" : "Sign up"} {isSignupMode ? "Sign in" : "Sign up"}
</button> </button>
@@ -451,13 +802,17 @@ const Login = () => {
) : ( ) : (
<form className="mt-8 space-y-6" onSubmit={handleTfaSubmit}> <form className="mt-8 space-y-6" onSubmit={handleTfaSubmit}>
<div className="text-center"> <div className="text-center">
<div className="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-blue-100"> <div className="mx-auto h-16 w-16 flex items-center justify-center">
<Smartphone size={24} color="#2563eb" strokeWidth={2} /> <img
src="/assets/favicon.svg"
alt="PatchMon Logo"
className="h-16 w-16"
/>
</div> </div>
<h3 className="mt-4 text-lg font-medium text-secondary-900"> <h3 className="mt-4 text-lg font-medium text-secondary-900 dark:text-secondary-100">
Two-Factor Authentication Two-Factor Authentication
</h3> </h3>
<p className="mt-2 text-sm text-secondary-600"> <p className="mt-2 text-sm text-secondary-600 dark:text-secondary-400">
Enter the 6-digit code from your authenticator app Enter the 6-digit code from your authenticator app
</p> </p>
</div> </div>
@@ -465,7 +820,7 @@ const Login = () => {
<div> <div>
<label <label
htmlFor={tokenId} htmlFor={tokenId}
className="block text-sm font-medium text-secondary-700" className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
> >
Verification Code Verification Code
</label> </label>
@@ -495,7 +850,7 @@ const Login = () => {
/> />
<label <label
htmlFor={rememberMeId} htmlFor={rememberMeId}
className="ml-2 block text-sm text-secondary-700" className="ml-2 block text-sm text-secondary-900 dark:text-secondary-200"
> >
Remember me on this computer (skip TFA for 30 days) Remember me on this computer (skip TFA for 30 days)
</label> </label>
@@ -531,15 +886,19 @@ const Login = () => {
<button <button
type="button" type="button"
onClick={handleBackToLogin} onClick={handleBackToLogin}
className="group relative w-full flex justify-center py-2 px-4 border border-secondary-300 text-sm font-medium rounded-md text-secondary-700 bg-white hover:bg-secondary-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 items-center gap-2" className="group relative w-full flex justify-center py-2 px-4 border border-secondary-300 dark:border-secondary-600 text-sm font-medium rounded-md text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-800 hover:bg-secondary-50 dark:hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 items-center gap-2"
> >
<ArrowLeft size={16} color="#475569" strokeWidth={2} /> <ArrowLeft
size={16}
className="text-secondary-700 dark:text-secondary-200"
strokeWidth={2}
/>
Back to Login Back to Login
</button> </button>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-sm text-secondary-600"> <p className="text-sm text-secondary-600 dark:text-secondary-400">
Don't have access to your authenticator? Use a backup code. Don't have access to your authenticator? Use a backup code.
</p> </p>
</div> </div>
@@ -547,6 +906,7 @@ const Login = () => {
)} )}
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@@ -0,0 +1,399 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
AlertCircle,
BarChart3,
CheckCircle,
Eye,
EyeOff,
Globe,
Info,
RefreshCw,
Send,
Shield,
} from "lucide-react";
import { useState } from "react";
import SettingsLayout from "../../components/SettingsLayout";
// API functions - will be added to utils/api.js
const metricsAPI = {
getSettings: () =>
fetch("/api/v1/metrics", {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}).then((res) => res.json()),
updateSettings: (data) =>
fetch("/api/v1/metrics", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: JSON.stringify(data),
}).then((res) => res.json()),
regenerateId: () =>
fetch("/api/v1/metrics/regenerate-id", {
method: "POST",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}).then((res) => res.json()),
sendNow: () =>
fetch("/api/v1/metrics/send-now", {
method: "POST",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}).then((res) => res.json()),
};
const SettingsMetrics = () => {
const queryClient = useQueryClient();
const [showFullId, setShowFullId] = useState(false);
// Fetch metrics settings
const {
data: metricsSettings,
isLoading,
error,
} = useQuery({
queryKey: ["metrics-settings"],
queryFn: () => metricsAPI.getSettings(),
});
// Toggle metrics mutation
const toggleMetricsMutation = useMutation({
mutationFn: (enabled) =>
metricsAPI.updateSettings({ metrics_enabled: enabled }),
onSuccess: () => {
queryClient.invalidateQueries(["metrics-settings"]);
},
});
// Regenerate ID mutation
const regenerateIdMutation = useMutation({
mutationFn: () => metricsAPI.regenerateId(),
onSuccess: () => {
queryClient.invalidateQueries(["metrics-settings"]);
},
});
// Send now mutation
const sendNowMutation = useMutation({
mutationFn: () => metricsAPI.sendNow(),
onSuccess: () => {
queryClient.invalidateQueries(["metrics-settings"]);
},
});
if (isLoading) {
return (
<SettingsLayout>
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
</SettingsLayout>
);
}
if (error) {
return (
<SettingsLayout>
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-md p-4">
<div className="flex">
<AlertCircle className="h-5 w-5 text-red-400 dark:text-red-300" />
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
Error loading metrics settings
</h3>
<p className="mt-1 text-sm text-red-700 dark:text-red-300">
{error.message || "Failed to load settings"}
</p>
</div>
</div>
</div>
</SettingsLayout>
);
}
const maskId = (id) => {
if (!id) return "";
if (showFullId) return id;
return `${id.substring(0, 8)}...${id.substring(id.length - 8)}`;
};
return (
<SettingsLayout>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center mb-6">
<BarChart3 className="h-6 w-6 text-primary-600 mr-3" />
<div>
<h2 className="text-xl font-semibold text-secondary-900 dark:text-white">
Anonymous Metrics & Telemetry
</h2>
<p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1">
Help us understand PatchMon's global usage (100% anonymous)
</p>
</div>
</div>
{/* Privacy Information */}
<div className="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-lg p-6">
<div className="flex">
<Shield className="h-6 w-6 text-blue-600 dark:text-blue-400 flex-shrink-0" />
<div className="ml-4 flex-1">
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-100 mb-3">
Your Privacy Matters
</h3>
<div className="text-sm text-blue-800 dark:text-blue-200 space-y-2">
<p className="flex items-start">
<CheckCircle className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
<span>
<strong>We do NOT collect:</strong> IP addresses, hostnames,
system details, or any personally identifiable information
</span>
</p>
<p className="flex items-start">
<CheckCircle className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
<span>
<strong>We ONLY collect:</strong> An anonymous UUID (for
deduplication) and the number of hosts you're monitoring
</span>
</p>
<p className="flex items-start">
<CheckCircle className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
<span>
<strong>Purpose:</strong> Display a live counter on our
website showing global PatchMon adoption
</span>
</p>
<p className="flex items-start">
<Globe className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
<span>
<strong>Open Source:</strong> All code is public and
auditable on GitHub
</span>
</p>
</div>
</div>
</div>
</div>
{/* Metrics Toggle */}
<div className="bg-white dark:bg-secondary-800 rounded-lg border border-secondary-200 dark:border-secondary-700 p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-2">
Enable Anonymous Metrics
</h3>
<p className="text-sm text-secondary-600 dark:text-secondary-400">
Share anonymous usage statistics to help us showcase PatchMon's
global adoption. Data is sent automatically every 24 hours.
</p>
</div>
<button
type="button"
onClick={() =>
toggleMetricsMutation.mutate(!metricsSettings?.metrics_enabled)
}
disabled={toggleMetricsMutation.isPending}
className={`ml-4 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
metricsSettings?.metrics_enabled
? "bg-primary-600"
: "bg-secondary-200 dark:bg-secondary-700"
} ${toggleMetricsMutation.isPending ? "opacity-50" : ""}`}
>
<span
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
metricsSettings?.metrics_enabled
? "translate-x-5"
: "translate-x-0"
}`}
/>
</button>
</div>
{/* Status */}
<div className="mt-4 pt-4 border-t border-secondary-200 dark:border-secondary-700">
<div className="flex items-center text-sm">
{metricsSettings?.metrics_enabled ? (
<>
<CheckCircle className="h-4 w-4 text-green-500 mr-2" />
<span className="text-green-700 dark:text-green-400">
Metrics enabled - Thank you for supporting PatchMon!
</span>
</>
) : (
<>
<EyeOff className="h-4 w-4 text-secondary-500 mr-2" />
<span className="text-secondary-600 dark:text-secondary-400">
Metrics disabled - No data is being sent
</span>
</>
)}
</div>
</div>
</div>
{/* Anonymous ID Section */}
<div className="bg-white dark:bg-secondary-800 rounded-lg border border-secondary-200 dark:border-secondary-700 p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-2">
Your Anonymous Instance ID
</h3>
<p className="text-sm text-secondary-600 dark:text-secondary-400">
This UUID identifies your instance without revealing any
personal information
</p>
</div>
</div>
<div className="mt-4 space-y-4">
<div className="flex items-center gap-3">
<div className="flex-1 bg-secondary-50 dark:bg-secondary-700 rounded-md p-3 font-mono text-sm break-all">
{maskId(metricsSettings?.metrics_anonymous_id)}
</div>
<button
type="button"
onClick={() => setShowFullId(!showFullId)}
className="p-2 text-secondary-600 dark:text-secondary-400 hover:text-secondary-900 dark:hover:text-white"
title={showFullId ? "Hide ID" : "Show full ID"}
>
{showFullId ? (
<EyeOff className="h-5 w-5" />
) : (
<Eye className="h-5 w-5" />
)}
</button>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={() => regenerateIdMutation.mutate()}
disabled={regenerateIdMutation.isPending}
className="inline-flex items-center px-4 py-2 border border-secondary-300 dark:border-secondary-600 text-sm font-medium rounded-md text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-700 hover:bg-secondary-50 dark:hover:bg-secondary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
>
{regenerateIdMutation.isPending ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-secondary-700 dark:border-secondary-200 mr-2"></div>
Regenerating...
</>
) : (
<>
<RefreshCw className="h-4 w-4 mr-2" />
Regenerate ID
</>
)}
</button>
<button
type="button"
onClick={() => sendNowMutation.mutate()}
disabled={
!metricsSettings?.metrics_enabled || sendNowMutation.isPending
}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{sendNowMutation.isPending ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Sending...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
Send Metrics Now
</>
)}
</button>
</div>
{metricsSettings?.metrics_last_sent && (
<p className="text-xs text-secondary-500 dark:text-secondary-400">
Last sent:{" "}
{new Date(metricsSettings.metrics_last_sent).toLocaleString()}
</p>
)}
</div>
{/* Success/Error Messages */}
{regenerateIdMutation.isSuccess && (
<div className="mt-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 rounded-md p-3">
<div className="flex">
<CheckCircle className="h-4 w-4 text-green-400 dark:text-green-300 mt-0.5" />
<p className="ml-2 text-sm text-green-700 dark:text-green-300">
Anonymous ID regenerated successfully
</p>
</div>
</div>
)}
{sendNowMutation.isSuccess && (
<div className="mt-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 rounded-md p-3">
<div className="flex">
<CheckCircle className="h-4 w-4 text-green-400 dark:text-green-300 mt-0.5" />
<div className="ml-2 text-sm text-green-700 dark:text-green-300">
<p className="font-medium">Metrics sent successfully!</p>
{sendNowMutation.data?.data && (
<p className="mt-1">
Sent: {sendNowMutation.data.data.hostCount} hosts, version{" "}
{sendNowMutation.data.data.version}
</p>
)}
</div>
</div>
</div>
)}
{sendNowMutation.isError && (
<div className="mt-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 rounded-md p-3">
<div className="flex">
<AlertCircle className="h-4 w-4 text-red-400 dark:text-red-300 mt-0.5" />
<div className="ml-2 text-sm text-red-700 dark:text-red-300">
{sendNowMutation.error?.message || "Failed to send metrics"}
</div>
</div>
</div>
)}
</div>
{/* Information Panel */}
<div className="bg-secondary-50 dark:bg-secondary-800/50 border border-secondary-200 dark:border-secondary-700 rounded-lg p-6">
<div className="flex">
<Info className="h-5 w-5 text-secondary-500 dark:text-secondary-400 flex-shrink-0 mt-0.5" />
<div className="ml-3 text-sm text-secondary-700 dark:text-secondary-300">
<h4 className="font-medium mb-2">How it works:</h4>
<ul className="space-y-1 list-disc list-inside">
<li>
Metrics are sent automatically every 24 hours when enabled
</li>
<li>
Only host count and version number are transmitted (no
sensitive data)
</li>
<li>The anonymous UUID prevents duplicate counting</li>
<li>You can regenerate your ID or opt-out at any time</li>
<li>
All collected data is displayed publicly on{" "}
<a
href="https://patchmon.net"
target="_blank"
rel="noopener noreferrer"
className="text-primary-600 dark:text-primary-400 hover:underline"
>
patchmon.net
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</SettingsLayout>
);
};
export default SettingsMetrics;

View File

@@ -1,3 +1,4 @@
import { Agent as HttpAgent } from "node:http";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
@@ -14,6 +15,15 @@ export default defineConfig({
target: `http://${process.env.BACKEND_HOST || "localhost"}:${process.env.BACKEND_PORT || "3001"}`, target: `http://${process.env.BACKEND_HOST || "localhost"}:${process.env.BACKEND_PORT || "3001"}`,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
// Configure HTTP agent to support more concurrent connections
// Fixes 1000ms timeout issue when using HTTP (not HTTPS) with multiple hosts
agent: new HttpAgent({
keepAlive: true,
maxSockets: 50, // Increase from default 6 to handle multiple hosts
maxFreeSockets: 10,
timeout: 60000,
keepAliveMsecs: 1000,
}),
configure: configure:
process.env.VITE_ENABLE_LOGGING === "true" process.env.VITE_ENABLE_LOGGING === "true"
? (proxy, _options) => { ? (proxy, _options) => {

530
package-lock.json generated
View File

@@ -78,7 +78,8 @@
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^6.30.1" "react-router-dom": "^6.30.1",
"trianglify": "^4.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.14", "@types/react": "^18.3.14",
@@ -1279,6 +1280,38 @@
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
@@ -1980,6 +2013,12 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
} }
}, },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -1993,6 +2032,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -2038,6 +2089,26 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/aproba": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/arg": { "node_modules/arg": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -2196,7 +2267,6 @@
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@@ -2419,6 +2489,21 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvas": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz",
"integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.0",
"nan": "^2.17.0",
"simple-get": "^3.0.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2486,6 +2571,21 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/chroma-js": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz",
"integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==",
"license": "(BSD-3-Clause AND Apache-2.0)"
},
"node_modules/citty": { "node_modules/citty": {
"version": "0.1.6", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
@@ -2567,6 +2667,15 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/color/node_modules/color-convert": { "node_modules/color/node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -2618,7 +2727,6 @@
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/concurrently": { "node_modules/concurrently": {
@@ -2666,6 +2774,12 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -2834,6 +2948,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"license": "MIT",
"dependencies": {
"mimic-response": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/deepmerge-ts": { "node_modules/deepmerge-ts": {
"version": "7.1.5", "version": "7.1.5",
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
@@ -2851,6 +2977,12 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/delaunator": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
"integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==",
"license": "ISC"
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2860,6 +2992,12 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/denque": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -2900,7 +3038,6 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -3486,6 +3623,42 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs-minipass/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3510,6 +3683,33 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gauge/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -3693,6 +3893,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -3761,6 +3967,19 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -3780,6 +3999,17 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -4369,6 +4599,21 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4461,11 +4706,22 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
@@ -4484,6 +4740,49 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/moment": { "node_modules/moment": {
"version": "2.30.1", "version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@@ -4542,6 +4841,12 @@
"thenify-all": "^1.0.0" "thenify-all": "^1.0.0"
} }
}, },
"node_modules/nan": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
"license": "MIT"
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -4576,6 +4881,26 @@
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch-native": { "node_modules/node-fetch-native": {
"version": "1.6.7", "version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
@@ -4670,6 +4995,21 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -4690,6 +5030,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/nypm": { "node_modules/nypm": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
@@ -4760,6 +5113,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/one-time": { "node_modules/one-time": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
@@ -4838,6 +5200,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -5543,6 +5914,43 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.52.3", "version": "4.52.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz",
@@ -5667,7 +6075,6 @@
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -5869,6 +6276,37 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
"integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
"license": "MIT",
"dependencies": {
"decompress-response": "^4.2.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-swizzle": { "node_modules/simple-swizzle": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
@@ -6134,6 +6572,38 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tar/node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/text-hex": { "node_modules/text-hex": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -6249,6 +6719,12 @@
"nodetouch": "bin/nodetouch.js" "nodetouch": "bin/nodetouch.js"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tree-kill": { "node_modules/tree-kill": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -6259,6 +6735,17 @@
"tree-kill": "cli.js" "tree-kill": "cli.js"
} }
}, },
"node_modules/trianglify": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/trianglify/-/trianglify-4.1.1.tgz",
"integrity": "sha512-zWfv8Qq9b3eYYiMMseGd1kvOW4xqfyGXs5xSA3pgpM2ynP+ABSFK81yVtwk4Waj9tC9jwJLiR3DHLPKlzZYO5Q==",
"license": "GPL-3.0",
"dependencies": {
"canvas": "^2.6.1",
"chroma-js": "^2.1.0",
"delaunator": "^4.0.1"
}
},
"node_modules/triple-beam": { "node_modules/triple-beam": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
@@ -6499,6 +6986,22 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6521,6 +7024,15 @@
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/winston": { "node_modules/winston": {
"version": "3.17.0", "version": "3.17.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
@@ -6594,6 +7106,12 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": { "node_modules/ws": {
"version": "8.18.3", "version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",