mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-17 20:32:11 +00:00
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:
@@ -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';
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -170,27 +170,31 @@ model role_permissions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model settings {
|
model settings {
|
||||||
id String @id
|
id String @id
|
||||||
server_url String @default("http://localhost:3001")
|
server_url String @default("http://localhost:3001")
|
||||||
server_protocol String @default("http")
|
server_protocol String @default("http")
|
||||||
server_host String @default("localhost")
|
server_host String @default("localhost")
|
||||||
server_port Int @default(3001)
|
server_port Int @default(3001)
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime
|
updated_at DateTime
|
||||||
update_interval Int @default(60)
|
update_interval Int @default(60)
|
||||||
auto_update Boolean @default(false)
|
auto_update Boolean @default(false)
|
||||||
github_repo_url String @default("https://github.com/PatchMon/PatchMon.git")
|
github_repo_url String @default("https://github.com/PatchMon/PatchMon.git")
|
||||||
ssh_key_path String?
|
ssh_key_path String?
|
||||||
repository_type String @default("public")
|
repository_type String @default("public")
|
||||||
last_update_check DateTime?
|
last_update_check DateTime?
|
||||||
latest_version String?
|
latest_version String?
|
||||||
update_available Boolean @default(false)
|
update_available Boolean @default(false)
|
||||||
signup_enabled Boolean @default(false)
|
signup_enabled Boolean @default(false)
|
||||||
default_user_role String @default("user")
|
default_user_role String @default("user")
|
||||||
ignore_ssl_self_signed Boolean @default(false)
|
ignore_ssl_self_signed Boolean @default(false)
|
||||||
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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
148
backend/src/routes/metricsRoutes.js
Normal file
148
backend/src/routes/metricsRoutes.js
Normal 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;
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
175
backend/src/services/automation/metricsReporting.js
Normal file
175
backend/src/services/automation/metricsReporting.js
Normal 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;
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,13 +428,15 @@ function AppRoutes() {
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<ColorThemeProvider>
|
||||||
<UpdateNotificationProvider>
|
<AuthProvider>
|
||||||
<LogoProvider>
|
<UpdateNotificationProvider>
|
||||||
<AppRoutes />
|
<LogoProvider>
|
||||||
</LogoProvider>
|
<AppRoutes />
|
||||||
</UpdateNotificationProvider>
|
</LogoProvider>
|
||||||
</AuthProvider>
|
</UpdateNotificationProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</ColorThemeProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,17 +127,110 @@ const BrandingTab = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<div className="flex items-center mb-6">
|
{/* Header */}
|
||||||
<Image className="h-6 w-6 text-primary-600 mr-3" />
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-secondary-900 dark:text-white">
|
<div className="flex items-center mb-6">
|
||||||
Logo & Branding
|
<Image className="h-6 w-6 text-primary-600 mr-3" />
|
||||||
</h2>
|
<h2 className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||||
|
Logo & Branding
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-secondary-500 dark:text-secondary-300 mb-6">
|
||||||
|
Customize your PatchMon installation with custom logos, favicon, and
|
||||||
|
color themes. These will be displayed throughout the application.
|
||||||
|
</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>
|
||||||
<p className="text-sm text-secondary-500 dark:text-secondary-300 mb-6">
|
|
||||||
Customize your PatchMon installation with custom logos and favicon.
|
|
||||||
These will be displayed throughout the application.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<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 */}
|
||||||
|
|||||||
194
frontend/src/contexts/ColorThemeContext.jsx
Normal file
194
frontend/src/contexts/ColorThemeContext.jsx
Normal 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;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,312 +379,532 @@ 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 */}
|
||||||
<div>
|
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||||
<div className="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-primary-100">
|
<div className="absolute inset-0 bg-gradient-to-br from-black/40 to-black/60" />
|
||||||
<Lock size={24} color="#2563eb" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-secondary-900">
|
|
||||||
{isSignupMode ? "Create PatchMon Account" : "Sign in to PatchMon"}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-center text-sm text-secondary-600">
|
|
||||||
Monitor and manage your Linux package updates
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!requiresTfa ? (
|
{/* Left side - Info Panel (hidden on mobile) */}
|
||||||
<form
|
<div className="hidden lg:flex lg:w-1/2 xl:w-3/5 relative z-10">
|
||||||
className="mt-8 space-y-6"
|
<div className="flex flex-col justify-between text-white p-12 h-full w-full">
|
||||||
onSubmit={isSignupMode ? handleSignupSubmit : handleSubmit}
|
<div className="flex-1 flex flex-col justify-center items-start max-w-xl mx-auto">
|
||||||
>
|
<div className="space-y-6">
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<img
|
||||||
htmlFor={usernameId}
|
src="/assets/logo_dark.png"
|
||||||
className="block text-sm font-medium text-secondary-700"
|
alt="PatchMon"
|
||||||
|
className="h-16 mb-4"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-blue-200 font-medium tracking-wide uppercase">
|
||||||
|
Linux Patch Monitoring
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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"
|
||||||
>
|
>
|
||||||
{isSignupMode ? "Username" : "Username or Email"}
|
<Github className="h-5 w-5 text-white" />
|
||||||
</label>
|
{githubStars !== null && (
|
||||||
<div className="mt-1 relative">
|
<div className="flex items-center gap-1">
|
||||||
<input
|
<Star className="h-3.5 w-3.5 fill-current text-yellow-400" />
|
||||||
id={usernameId}
|
<span className="text-sm font-medium text-white">
|
||||||
name="username"
|
{githubStars}
|
||||||
type="text"
|
</span>
|
||||||
required
|
</div>
|
||||||
value={formData.username}
|
)}
|
||||||
onChange={handleInputChange}
|
</a>
|
||||||
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
|
||||||
placeholder={
|
{/* Roadmap */}
|
||||||
isSignupMode
|
<a
|
||||||
? "Enter your username"
|
href="https://github.com/orgs/PatchMon/projects/2/views/1"
|
||||||
: "Enter your username or email"
|
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"
|
||||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-20 flex items-center">
|
title="Roadmap"
|
||||||
<User size={20} color="#64748b" strokeWidth={2} />
|
>
|
||||||
|
<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"}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-center text-sm text-secondary-600 dark:text-secondary-400">
|
||||||
|
Monitor and manage your Linux package updates
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!requiresTfa ? (
|
||||||
|
<form
|
||||||
|
className="mt-8 space-y-6"
|
||||||
|
onSubmit={isSignupMode ? handleSignupSubmit : handleSubmit}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor={usernameId}
|
||||||
|
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
|
||||||
|
>
|
||||||
|
{isSignupMode ? "Username" : "Username or Email"}
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative">
|
||||||
|
<input
|
||||||
|
id={usernameId}
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder={
|
||||||
|
isSignupMode
|
||||||
|
? "Enter your username"
|
||||||
|
: "Enter your username or email"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-20 flex items-center">
|
||||||
|
<User size={20} color="#64748b" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isSignupMode && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor={firstNameId}
|
||||||
|
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
|
||||||
|
>
|
||||||
|
First Name
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<User className="h-5 w-5 text-secondary-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id={firstNameId}
|
||||||
|
name="firstName"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Enter your first name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor={lastNameId}
|
||||||
|
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
|
||||||
|
>
|
||||||
|
Last Name
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<User className="h-5 w-5 text-secondary-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id={lastNameId}
|
||||||
|
name="lastName"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.lastName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Enter your last name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor={emailId}
|
||||||
|
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative">
|
||||||
|
<input
|
||||||
|
id={emailId}
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-20 flex items-center">
|
||||||
|
<Mail size={20} color="#64748b" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor={passwordId}
|
||||||
|
className="block text-sm font-medium text-secondary-900 dark:text-secondary-100"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative">
|
||||||
|
<input
|
||||||
|
id={passwordId}
|
||||||
|
name="password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
required
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="appearance-none rounded-md relative block w-full pl-10 pr-10 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-20 flex items-center">
|
||||||
|
<Lock size={20} color="#64748b" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 z-20 flex items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="bg-transparent border-none cursor-pointer p-1 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff size={20} color="#64748b" strokeWidth={2} />
|
||||||
|
) : (
|
||||||
|
<Eye size={20} color="#64748b" strokeWidth={2} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSignupMode && (
|
{error && (
|
||||||
<>
|
<div className="bg-danger-50 border border-danger-200 rounded-md p-3">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="flex">
|
||||||
<div>
|
<AlertCircle size={20} color="#dc2626" strokeWidth={2} />
|
||||||
<label
|
<div className="ml-3">
|
||||||
htmlFor={firstNameId}
|
<p className="text-sm text-danger-700">{error}</p>
|
||||||
className="block text-sm font-medium text-secondary-700"
|
|
||||||
>
|
|
||||||
First Name
|
|
||||||
</label>
|
|
||||||
<div className="mt-1 relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<User className="h-5 w-5 text-secondary-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id={firstNameId}
|
|
||||||
name="firstName"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.firstName}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
|
||||||
placeholder="Enter your first name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor={lastNameId}
|
|
||||||
className="block text-sm font-medium text-secondary-700"
|
|
||||||
>
|
|
||||||
Last Name
|
|
||||||
</label>
|
|
||||||
<div className="mt-1 relative">
|
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<User className="h-5 w-5 text-secondary-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id={lastNameId}
|
|
||||||
name="lastName"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.lastName}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
|
||||||
placeholder="Enter your last name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<label
|
|
||||||
htmlFor={emailId}
|
|
||||||
className="block text-sm font-medium text-secondary-700"
|
|
||||||
>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<div className="mt-1 relative">
|
|
||||||
<input
|
|
||||||
id={emailId}
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
|
||||||
placeholder="Enter your email"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-20 flex items-center">
|
|
||||||
<Mail size={20} color="#64748b" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<button
|
||||||
htmlFor={passwordId}
|
type="submit"
|
||||||
className="block text-sm font-medium text-secondary-700"
|
disabled={isLoading}
|
||||||
|
className="group relative w-full flex justify-center py-2 px-4 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"
|
||||||
>
|
>
|
||||||
Password
|
{isLoading ? (
|
||||||
</label>
|
<div className="flex items-center">
|
||||||
<div className="mt-1 relative">
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
<input
|
{isSignupMode ? "Creating account..." : "Signing in..."}
|
||||||
id={passwordId}
|
</div>
|
||||||
name="password"
|
) : isSignupMode ? (
|
||||||
type={showPassword ? "text" : "password"}
|
"Create Account"
|
||||||
required
|
) : (
|
||||||
value={formData.password}
|
"Sign in"
|
||||||
onChange={handleInputChange}
|
)}
|
||||||
className="appearance-none rounded-md relative block w-full pl-10 pr-10 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
</button>
|
||||||
placeholder="Enter your password"
|
</div>
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none z-20 flex items-center">
|
{signupEnabled && (
|
||||||
<Lock size={20} color="#64748b" strokeWidth={2} />
|
<div className="text-center">
|
||||||
</div>
|
<p className="text-sm text-secondary-700 dark:text-secondary-300">
|
||||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 z-20 flex items-center">
|
{isSignupMode
|
||||||
|
? "Already have an account?"
|
||||||
|
: "Don't have an account?"}{" "}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={toggleMode}
|
||||||
className="bg-transparent border-none cursor-pointer p-1 flex items-center justify-center"
|
className="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300 focus:outline-none focus:underline"
|
||||||
>
|
>
|
||||||
{showPassword ? (
|
{isSignupMode ? "Sign in" : "Sign up"}
|
||||||
<EyeOff size={20} color="#64748b" strokeWidth={2} />
|
|
||||||
) : (
|
|
||||||
<Eye size={20} color="#64748b" strokeWidth={2} />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</form>
|
||||||
|
) : (
|
||||||
{error && (
|
<form className="mt-8 space-y-6" onSubmit={handleTfaSubmit}>
|
||||||
<div className="bg-danger-50 border border-danger-200 rounded-md p-3">
|
|
||||||
<div className="flex">
|
|
||||||
<AlertCircle size={20} color="#dc2626" strokeWidth={2} />
|
|
||||||
<div className="ml-3">
|
|
||||||
<p className="text-sm text-danger-700">{error}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="group relative w-full flex justify-center py-2 px-4 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"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
||||||
{isSignupMode ? "Creating account..." : "Signing in..."}
|
|
||||||
</div>
|
|
||||||
) : isSignupMode ? (
|
|
||||||
"Create Account"
|
|
||||||
) : (
|
|
||||||
"Sign in"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{signupEnabled && (
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-secondary-600">
|
<div className="mx-auto h-16 w-16 flex items-center justify-center">
|
||||||
{isSignupMode
|
<img
|
||||||
? "Already have an account?"
|
src="/assets/favicon.svg"
|
||||||
: "Don't have an account?"}{" "}
|
alt="PatchMon Logo"
|
||||||
<button
|
className="h-16 w-16"
|
||||||
type="button"
|
/>
|
||||||
onClick={toggleMode}
|
</div>
|
||||||
className="font-medium text-primary-600 hover:text-primary-500 focus:outline-none focus:underline"
|
<h3 className="mt-4 text-lg font-medium text-secondary-900 dark:text-secondary-100">
|
||||||
>
|
Two-Factor Authentication
|
||||||
{isSignupMode ? "Sign in" : "Sign up"}
|
</h3>
|
||||||
</button>
|
<p className="mt-2 text-sm text-secondary-600 dark:text-secondary-400">
|
||||||
|
Enter the 6-digit code from your authenticator app
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleTfaSubmit}>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-blue-100">
|
|
||||||
<Smartphone size={24} color="#2563eb" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
<h3 className="mt-4 text-lg font-medium text-secondary-900">
|
|
||||||
Two-Factor Authentication
|
|
||||||
</h3>
|
|
||||||
<p className="mt-2 text-sm text-secondary-600">
|
|
||||||
Enter the 6-digit code from your authenticator app
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<input
|
||||||
id={tokenId}
|
id={tokenId}
|
||||||
name="token"
|
name="token"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
value={tfaData.token}
|
value={tfaData.token}
|
||||||
onChange={handleTfaInputChange}
|
onChange={handleTfaInputChange}
|
||||||
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm text-center text-lg font-mono tracking-widest"
|
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm text-center text-lg font-mono tracking-widest"
|
||||||
placeholder="000000"
|
placeholder="000000"
|
||||||
maxLength="6"
|
maxLength="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
id={rememberMeId}
|
|
||||||
name="remember_me"
|
|
||||||
type="checkbox"
|
|
||||||
checked={tfaData.remember_me}
|
|
||||||
onChange={handleTfaInputChange}
|
|
||||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor={rememberMeId}
|
|
||||||
className="ml-2 block text-sm text-secondary-700"
|
|
||||||
>
|
|
||||||
Remember me on this computer (skip TFA for 30 days)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="bg-danger-50 border border-danger-200 rounded-md p-3">
|
|
||||||
<div className="flex">
|
|
||||||
<AlertCircle size={20} color="#dc2626" strokeWidth={2} />
|
|
||||||
<div className="ml-3">
|
|
||||||
<p className="text-sm text-danger-700">{error}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="flex items-center">
|
||||||
<button
|
<input
|
||||||
type="submit"
|
id={rememberMeId}
|
||||||
disabled={isLoading || tfaData.token.length !== 6}
|
name="remember_me"
|
||||||
className="group relative w-full flex justify-center py-2 px-4 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"
|
type="checkbox"
|
||||||
>
|
checked={tfaData.remember_me}
|
||||||
{isLoading ? (
|
onChange={handleTfaInputChange}
|
||||||
<div className="flex items-center">
|
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded"
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
/>
|
||||||
Verifying...
|
<label
|
||||||
|
htmlFor={rememberMeId}
|
||||||
|
className="ml-2 block text-sm text-secondary-900 dark:text-secondary-200"
|
||||||
|
>
|
||||||
|
Remember me on this computer (skip TFA for 30 days)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-danger-50 border border-danger-200 rounded-md p-3">
|
||||||
|
<div className="flex">
|
||||||
|
<AlertCircle size={20} color="#dc2626" strokeWidth={2} />
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm text-danger-700">{error}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
"Verify Code"
|
)}
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<div className="space-y-3">
|
||||||
type="button"
|
<button
|
||||||
onClick={handleBackToLogin}
|
type="submit"
|
||||||
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"
|
disabled={isLoading || tfaData.token.length !== 6}
|
||||||
>
|
className="group relative w-full flex justify-center py-2 px-4 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"
|
||||||
<ArrowLeft size={16} color="#475569" strokeWidth={2} />
|
>
|
||||||
Back to Login
|
{isLoading ? (
|
||||||
</button>
|
<div className="flex items-center">
|
||||||
</div>
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
Verifying...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Verify Code"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="text-center">
|
<button
|
||||||
<p className="text-sm text-secondary-600">
|
type="button"
|
||||||
Don't have access to your authenticator? Use a backup code.
|
onClick={handleBackToLogin}
|
||||||
</p>
|
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"
|
||||||
</div>
|
>
|
||||||
</form>
|
<ArrowLeft
|
||||||
)}
|
size={16}
|
||||||
|
className="text-secondary-700 dark:text-secondary-200"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
Back to Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-secondary-600 dark:text-secondary-400">
|
||||||
|
Don't have access to your authenticator? Use a backup code.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
399
frontend/src/pages/settings/SettingsMetrics.jsx
Normal file
399
frontend/src/pages/settings/SettingsMetrics.jsx
Normal 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;
|
||||||
@@ -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
530
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user