diff --git a/.gitignore b/.gitignore index f68a83c..8a473e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .vscode/settings.json .env -logs.db +*.db data/ diff --git a/controllers/APIkeyController.js b/controllers/APIkeyController.js new file mode 100644 index 0000000..fb5bece --- /dev/null +++ b/controllers/APIkeyController.js @@ -0,0 +1,144 @@ +const { error, warn } = require("../utils/logger"); +const sqlite3 = require('sqlite3').verbose(); +const crypto = require("crypto"); +const path = require('path'); + +const VALIDITY_PERIOD_MS = 30 * 24 * 60 * 60 * 1000; // 30 days + +// Setup database for API keys +const dbPath = path.join(__dirname, '../APIkeys.db'); +const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error('Error opening database:', err); + } else { + console.log('Connected to API keys database'); + initializeDatabase(); + } +}); + +// Initialize database schema +function initializeDatabase() { + db.serialize(() => { + // Create table + db.run(` + CREATE TABLE IF NOT EXISTS api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + key TEXT NOT NULL, + created_at INTEGER DEFAULT (strftime('%s', 'now')), + last_used_at INTEGER, + expires_at INTEGER + ) + `, (err) => { + if (err) console.error('Error creating API keys table:', err); + else console.log('API keys table ready'); + }); + + // Create indexes + db.run('CREATE INDEX IF NOT EXISTS idx_user_id ON api_keys(user_id)'); + db.run('CREATE INDEX IF NOT EXISTS idx_key ON api_keys(key)'); + db.run('CREATE INDEX IF NOT EXISTS idx_created_at ON api_keys(created_at)'); + db.run('CREATE INDEX IF NOT EXISTS idx_expires_at ON api_keys(expires_at)'); + }); +} + +exports.getKeys = (req, res) => { + const sql = 'SELECT id, user_id, key, created_at, last_used_at, expires_at FROM api_keys'; + db.all(sql, [], (err, rows) => { + if (err) { + console.error('Error fetching API keys:', err); + res.status(500).json({ error: 'Internal Server Error' }); + } else { + const apiKeys = rows.map(row => ({ + id: row.id, + user_id: row.user_id, + key: row.key, + created_at: row.created_at, + last_used_at: row.last_used_at, + expires_at: row.expires_at + })); + res.json({ keys: apiKeys }); + } + }); +}; + +exports.getUserKey = (req, res) => { + const userId = req.params.id; + const sql = 'SELECT id, user_id, key, created_at, last_used_at, expires_at FROM api_keys WHERE user_id = ?'; + db.get(sql, [userId], (err, row) => { + if (err) { + console.error('Error fetching API key:', err); + res.status(500).json({ error: 'Internal Server Error' }); + } else { + res.json({ key: row }); + } + }); +}; + +exports.createKey = (req, res) => { + const userId = req.params.id; + const newKey = crypto.randomBytes(32).toString('hex'); + const sql = 'INSERT INTO api_keys (user_id, key) VALUES (?, ?)'; + db.run(sql, [userId, newKey], function (err) { + if (err) { + console.error('Error creating API key:', err); + res.status(500).json({ error: 'Internal Server Error' }); + } else { + res.status(201).json({ key: { id: this.lastID, user_id: userId, key: newKey } }); + } + }); +}; + +exports.renewKey = (req, res) => { + const userId = req.params.id; + const sql = 'UPDATE api_keys SET expires_at = ? WHERE user_id = ?'; + const expiresAt = Date.now() + VALIDITY_PERIOD_MS; + + db.run(sql, [expiresAt, userId], function (err) { + if (err) { + console.error('Error renewing API key:', err); + res.status(500).json({ error: 'Internal Server Error' }); + } else { + res.json({ key: { user_id: userId, expires_at: expiresAt } }); + } + }); +}; + +exports.deleteKey = (req, res) => { + const userId = req.params.id; + const sql = 'DELETE FROM api_keys WHERE user_id = ?'; + db.run(sql, [userId], function (err) { + if (err) { + console.error('Error deleting API key:', err); + res.status(500).json({ error: 'Internal Server Error' }); + } else { + res.json({ message: 'API key deleted successfully' }); + } + }); +}; + +// Cleanup old keys (older than 60 days) +exports.cleanupOldKeys = () => { + const sql = 'DELETE FROM api_keys WHERE expires_at < ?'; + const now = Date.now() - (2 * VALIDITY_PERIOD_MS); // 60 days ago + db.run(sql, [now], function (err) { + if (err) { + console.error('Error cleaning up old API keys:', err); + } else { + console.log(`Deleted ${this.changes} old API keys`); + } + }); +}; + +// Run cleanup periodically (every days) +setInterval(exports.cleanupOldKeys, 24 * 60 * 60 * 1000); // every days + + +// Graceful shutdown +process.on('SIGINT', () => { + db.close((err) => { + if (err) console.error('Error closing database:', err); + else console.log('Database connection closed'); + process.exit(0); + }); +}); \ No newline at end of file diff --git a/controllers/authController.js b/controllers/authController.js index 6f92405..94db5ee 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -11,7 +11,7 @@ exports.getLocalUser = async (req, res) => { const localUser = await redisService.getLocalUser(cid); res.json(localUser); } catch (err) { - error("Failed to get local user:", err); + error(`Failed to get local user: ${err}`, { category: "Auth" }); res.status(500).json({ error: "Failed to get local user" }); } }; @@ -24,7 +24,7 @@ exports.getAllLocalUsers = async (req, res) => { } res.json(users); } catch (err) { - error("Failed to get all local users:", err); + error(`Failed to get all local users: ${err}`, { category: "Auth" }); res.status(500).json({ error: "Failed to get users" }); } }; @@ -39,7 +39,7 @@ exports.updateLocalUser = async (req, res) => { } res.json(updated); } catch (err) { - error("Failed to update local user:", err); + error(`Failed to update local user: ${err}`, { category: "Auth" }); res.status(500).json({ error: "Failed to update local user" }); } }; @@ -67,7 +67,7 @@ exports.grantRole = async function (req, res) { } res.json({ ok: true, user }); } catch (err) { - error("Failed to grant role:", err); + error(`Failed to grant role: ${err}`, { category: "Auth" }); res.status(500).json({ error: "Failed to grant role" }); } }; @@ -90,12 +90,13 @@ exports.revokeRole = async function (req, res) { } res.json({ ok: true, user }); } catch (err) { - error("Failed to revoke role:", err); + error(`Failed to revoke role: ${err}`, { category: "Auth" }); res.status(500).json({ error: "Failed to revoke role" }); } }; exports.requireRoles = (roles) => { + const requiredRoles = Array.isArray(roles) ? roles : roles ? [roles] : []; return async (req, res, next) => { if (!req.user || !req.user.cid) { return res.status(401).json({ error: "Not authenticated" }); @@ -103,7 +104,7 @@ exports.requireRoles = (roles) => { try { const userRoles = req.user.roles || []; - const hasRequiredRole = roles.some((role) => userRoles.includes(role)); + const hasRequiredRole = requiredRoles.some((role) => userRoles.includes(role)); if (hasRequiredRole) { return next(); @@ -111,7 +112,7 @@ exports.requireRoles = (roles) => { return res.status(403).json({ error: "Insufficient permissions" }); } catch (err) { - error("Failed to check roles:", err); + error(`Failed to check roles: ${err}`, { category: "Auth" }); return res.status(500).json({ error: "Failed to check permissions" }); } }; @@ -120,7 +121,7 @@ exports.requireRoles = (roles) => { // Verifying token from plugins for manual stand assignement exports.verifyToken = (token, client) => { // Return true if token is valid, false otherwise - const secret = process.env.CORE_JWT_KEY; + const secret = process.env.AUTH_SECRET; if (!secret) { error("No secret found", { category: "Auth" }); @@ -151,7 +152,7 @@ exports.logout = async (req, res) => { try { deleteSession(res); const baseURL = process.env.BASE_URL; - return res.redirect(baseURL + "/rampagent/debug/"); + return res.redirect(baseURL + "/rampagent/"); } catch (err) { error("logout error: " + (err.message || err), { category: "Auth" }); return res.status(500).send("Error during logout"); @@ -202,7 +203,7 @@ exports.requireAuth = async (req, res, next) => { return next(); } catch (err) { - error("Auth error:", err); + error(`Auth error: ${err.message || err}`, { category: "Auth" }); return res.status(401).json({ error: "Not authenticated" }); } }; @@ -239,7 +240,9 @@ async function decryptToken(accessToken) { tokenContent: payload, }; } catch (err) { - error("Failed to verify session: " + err, { category: "Auth" }); + if (err.name !== "TokenExpiredError") { + error("Failed to verify session: " + err, { category: "Auth" }); + } return null; } } @@ -257,7 +260,7 @@ exports.getSession = async (req, res) => { if (!sessionData) return res.status(401).json({ error: "Invalid session" }); const coreUserUrl = - process.env.CORE_URL_INTERNAL + `/v1/user/${sessionData.tokenContent.cid}`; //FIXME: is correct ? + process.env.CORE_URL_INTERNAL + `/v1/user/${sessionData.tokenContent.cid}`; const coreRes = await fetch(coreUserUrl, { method: "GET", headers: { @@ -355,74 +358,9 @@ exports.loginCallback = async (req, res) => { // Redirect back to UI const baseURL = process.env.BASE_URL; - return res.redirect(baseURL + "/rampagent/debug/#dashboard"); + return res.redirect(baseURL + "/rampagent/#dashboard"); } catch (err) { error("loginCallback error: " + (err.message || err), { category: "Auth" }); return res.status(401).send("Authentication failed, check logs"); } -}; - -// API Key Management - -exports.getKeys = async (req, res) => { - try { - const keys = await redisService.getAllKeys(); - return res.json(keys); - } catch (err) { - error("Error fetching keys:", err); - return res.status(500).json({ error: "Internal Server Error" }); - } -}; - -exports.getUserKey = async (req, res) => { - try { - const keyId = req.params.id; - const key = await redisService.getKeyById(keyId); - if (key) { - return res.json(key); - } - return res.status(404).json({ error: "Key not found" }); - } catch (err) { - error("Error fetching key:", err); - return res.status(500).json({ error: "Internal Server Error" }); - } -}; - -exports.createKey = async (req, res) => { - try { - const id = req.params.id; - const newKey = await redisService.createKey(id); - return res.status(201).json(newKey); - } catch (err) { - error("Error creating key:", err); - return res.status(500).json({ error: "Internal Server Error" }); - } -}; - -exports.renewKey = async (req, res) => { - try { - const keyId = req.params.id; - const renewed = await redisService.renewKey(keyId); - if (renewed) { - return res.status(200).json({ message: "Key renewed successfully" }); - } - return res.status(404).json({ error: "Key not found" }); - } catch (err) { - error("Error renewing key:", err); - return res.status(500).json({ error: "Internal Server Error" }); - } -}; - -exports.deleteKey = async (req, res) => { - try { - const keyId = req.params.id; - const deleted = await redisService.deleteKey(keyId); - if (deleted) { - return res.status(200).json({ message: "Key deleted successfully" }); - } - return res.status(404).json({ error: "Key not found" }); - } catch (err) { - error("Error deleting key:", err); - return res.status(500).json({ error: "Internal Server Error" }); - } -}; +}; \ No newline at end of file diff --git a/controllers/occupancyController.js b/controllers/occupancyController.js index dab2fc9..b47ddd9 100644 --- a/controllers/occupancyController.js +++ b/controllers/occupancyController.js @@ -24,6 +24,8 @@ exports.getOccupied = (req, res) => { name: s.name, icao: s.icao, callsign: s.callsign || null, + remark: s.remark || null, + apronSize: s.apronSize || 0, })); res.json(occupied); } catch (err) { @@ -41,6 +43,8 @@ exports.getAssigned = (req, res) => { name: s.name, icao: s.icao, callsign: s.callsign || null, + remark: s.remark || null, + apronSize: s.apronSize || 0, })); res.json(assigned); } catch (err) { @@ -58,6 +62,8 @@ exports.getBlocked = (req, res) => { name: s.name, icao: s.icao, callsign: s.callsign || null, + remark: s.remark || null, + apronSize: s.apronSize || 0, })); res.json(blocked); } catch (err) { diff --git a/index.js b/index.js index 805c681..5c6cb24 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,7 @@ const redisService = require("./services/redisService"); const airportService = require("./services/airportService"); const healthRoutes = require("./routes/health"); const authRoutes = require("./routes/auth"); +const apiKeyRoutes = require("./routes/APIkey"); const app = express(); @@ -66,9 +67,10 @@ app.post('/api/config-webhook', async (req, res) => { app.use(express.json()); // Serve viewer -app.get("/debug", (req, res) => { +app.get("/", (req, res) => { res.sendFile(path.join(__dirname, "viewer", "viewer.html")); }); +app.use("/", express.static(path.join(__dirname, "viewer"))); // Authentication routes app.use("/api/auth", authRoutes); @@ -85,11 +87,13 @@ app.use("/api/airports", airportRoutes); // API endpoint to get stats (call service and return JSON) app.use("/api/stats", statRoutes); -// Register routes -app.use("/debug", express.static(path.join(__dirname, "viewer"))); +// API endpoint for Stands management app.use("/api/assign", assignRoutes); app.use("/api/occupancy", occupancyRoutes); +// API endpoint for API key management +app.use("/api/apikey", apiKeyRoutes); + // Connect to Redis redisService .connect() diff --git a/routes/APIkey.js b/routes/APIkey.js new file mode 100644 index 0000000..2c741d7 --- /dev/null +++ b/routes/APIkey.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); +const authController = require('../controllers/authController'); +const apiKeyController = require('../controllers/APIkeyController'); + +router.get('/', authController.requireAuth, authController.requireRoles('admin'), apiKeyController.getKeys); +router.get('/:id', authController.requireAuth, apiKeyController.getUserKey); +router.post('/:id', authController.requireAuth, apiKeyController.createKey); +router.post('/:id/renew', authController.requireAuth, apiKeyController.renewKey); +router.delete('/:id', authController.requireAuth, apiKeyController.deleteKey); +// router.post('/verify', authController.verify); //verify key middleware + +module.exports = router; \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js index 5c36c52..9872059 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -7,16 +7,9 @@ router.get('/logout', authController.logout); router.get('/callback', authController.loginCallback); router.get('/session', authController.requireAuth, authController.getSession); router.get('/internal/localusers', authController.requireAuth, authController.requireRoles('admin'), authController.getAllLocalUsers); -router.get('/internal/localuser/:cid', authController.getLocalUser); -router.post('/internal/localuser/:cid/update', express.json(), authController.updateLocalUser); +router.get('/internal/localuser/:cid', authController.requireAuth, authController.requireRoles('admin'), authController.getLocalUser); +router.post('/internal/localuser/:cid/update', express.json(), authController.requireAuth, authController.requireRoles('admin'), authController.updateLocalUser); router.post('/internal/localuser/:cid/roles', authController.requireAuth, authController.requireRoles('admin'), express.json(), authController.grantRole); router.delete('/internal/localuser/:cid/roles', authController.requireAuth, authController.requireRoles('admin'), express.json(), authController.revokeRole); -// router.post('/verify', authController.verify); -router.get('/keys', authController.requireAuth, authController.requireRoles('admin'), authController.getKeys); -router.get('/keys/:id', authController.requireAuth, authController.getUserKey); -router.post('/key/:id', authController.requireAuth, authController.createKey); -router.post('/keys/:id/renew', authController.requireAuth, authController.renewKey); -router.delete('/keys/:id', authController.requireAuth, authController.deleteKey); - module.exports = router; diff --git a/services/occupancyService.js b/services/occupancyService.js index 6481b25..c5e2764 100644 --- a/services/occupancyService.js +++ b/services/occupancyService.js @@ -37,24 +37,28 @@ function parseCoordinates(coordString, defaultRadius = 30) { } class Stand { - constructor(name, icao, callsign, remark = "") { + constructor(name, icao, callsign, remark = "", apronSize = 0) { this.name = name; this.icao = icao; this.callsign = callsign; this.remark = remark; + this.apronSize = apronSize; this.timestamp = Date.now(); } // Hash function for the Stand class key() { - return `${this.icao}:${this.name}`; + return this.apronSize > 0 + ? `${this.icao}:${this.name}:${this.callsign}` + : `${this.icao}:${this.name}`; } equals(other) { return ( this.icao === other.icao && this.name === other.name && - this.callsign === other.callsign + this.callsign === other.callsign && + this.apronSize === other.apronSize ); } @@ -64,6 +68,7 @@ class Stand { icao: this.icao, callsign: this.callsign, remark: this.remark, + apronSize: this.apronSize, timestamp: this.timestamp, }; } @@ -74,7 +79,6 @@ class StandRegistry { this.occupied = new Map(); // key -> Stand this.assigned = new Map(); // key -> Stand this.blocked = new Map(); // key -> Stand - this.apron = new Map(); // key -> Stand } addOccupied(stand) { @@ -101,20 +105,60 @@ class StandRegistry { this.blocked.delete(stand.key()); } - addApron(stand) { - this.apron.set(stand.key(), stand); - } - - removeApron(stand) { - this.apron.delete(stand.key()); + getApronOccupancyLevel(standName, icao) { + // Count how many pilot have this stand assigned or occupied + let count = 0; + for (const stand of this.occupied.values()) { + if (stand.name === standName && stand.icao === icao && stand.apronSize > 0) { + count++; + } + } + for (const stand of this.assigned.values()) { + if (stand.name === standName && stand.icao === icao && stand.apronSize > 0) { + count++; + } + } + return count; } isOccupied(icao, name) { - return this.occupied.has(`${icao}:${name}`); + // For non-apron stands, check simple key + const simpleOccupied = Array.from(this.occupied.values()).find( + s => s.icao === icao && s.name === name && s.apronSize === 0 + ); + if (simpleOccupied) return true; + + // For apron stands, check if capacity is reached + const apronStand = Array.from(this.occupied.values()).filter( + s => s.icao === icao && s.name === name && s.apronSize > 0 + ); + if (apronStand.length > 0) { + if(this.getApronOccupancyLevel(name, icao) >= apronStand[0].apronSize) { + return true; + } + } + + return false; } isAssigned(icao, name) { - return this.assigned.has(`${icao}:${name}`); + // For non-apron stands, check simple key + const simpleAssigned = Array.from(this.assigned.values()).find( + s => s.icao === icao && s.name === name && s.apronSize === 0 + ); + if (simpleAssigned) return true; + + // For apron stands, check if capacity is reached + const apronStand = Array.from(this.assigned.values()).filter( + s => s.icao === icao && s.name === name && s.apronSize > 0 + ); + if (apronStand.length > 0) { + if(this.getApronOccupancyLevel(name, icao) >= apronStand[0].apronSize) { + return true; + } + } + + return false; } isBlocked(icao, name) { @@ -133,16 +177,12 @@ class StandRegistry { return Array.from(this.blocked.values()); } - getAllApron() { - return Array.from(this.apron.values()); - } - clearExpired(predicateFn) { // e.g. remove old stands if needed for (const [key, stand] of this.occupied) { if (predicateFn(stand)) { this.occupied.delete(key); - warn( + info( `Clearing expired occupied stand ${stand.name} at ${stand.icao} for ${stand.callsign}`, { category: "Stand Management", @@ -155,7 +195,7 @@ class StandRegistry { for (const [key, stand] of this.assigned) { if (predicateFn(stand)) { this.assigned.delete(key); - warn( + info( `Clearing expired assigned stand ${stand.name} at ${stand.icao} for ${stand.callsign}`, { category: "Stand Management", @@ -168,7 +208,7 @@ class StandRegistry { for (const [key, stand] of this.blocked) { if (predicateFn(stand)) { this.blocked.delete(key); - warn( + info( `Clearing expired blocked stand ${stand.name} at ${stand.icao} for ${stand.callsign}`, { category: "Stand Management", @@ -178,19 +218,6 @@ class StandRegistry { ); } } - for (const [key, stand] of this.apron) { - if (predicateFn(stand)) { - this.apron.delete(key); - warn( - `Clearing expired Apron stand ${stand.name} at ${stand.icao} for ${stand.callsign}`, - { - category: "Stand Management", - callsign: stand.callsign, - icao: stand.icao, - } - ); - } - } } } @@ -278,8 +305,30 @@ const isAircraftOnStand = async ( coords.lon ); + if ( + standDef.Apron && + standDef.Apron.Coordinates && + Array.isArray(standDef.Apron.Coordinates) + ) { + // Check if aircraft is inside apron polygon + const apronCoords = standDef.Apron.Coordinates.map((coordString) => { + const coord = parseCoordinates(coordString); + return coord ? { lat: coord.lat, lon: coord.lon } : null; + }).filter((c) => c !== null); + + if ( + isPointInPolygon({ lat: ac.latitude, lon: ac.longitude }, apronCoords) + ) { + return standName; + } + } if (aircraftDist <= coords.radius) { - if (!ac.flight_plan || !ac.flight_plan.aircraft_short || ac.flight_plan.aircraft_short === "UNKNOWN" || ac.flight_plan.aircraft_short === "") { + if ( + !ac.flight_plan || + !ac.flight_plan.aircraft_short || + ac.flight_plan.aircraft_short === "UNKNOWN" || + ac.flight_plan.aircraft_short === "" + ) { if (ac.flight_plan) { warn( `Aircraft ${ac.callsign} on ground at ${ac.origin} has unknown type`, @@ -326,7 +375,10 @@ const isAircraftOnStand = async ( let bestPriority = Number.MAX_SAFE_INTEGER; for (const potentialStandName of potentialStands) { - const wingspan = getAircraftWingspan(config, ac.flight_plan.aircraft_short); + const wingspan = getAircraftWingspan( + config, + ac.flight_plan.aircraft_short + ); const aircraftCode = getAircraftCode(wingspan); const potentialStandDef = airportData.Stands[potentialStandName]; @@ -372,13 +424,33 @@ const blockStands = (standDef, icao, callsign) => { const blockedStand = new Stand( blockedStandName, icao || "UNKNOWN", - callsign + callsign, + "", + 0 ); registry.addBlocked(blockedStand); } } }; +function isPointInPolygon(point, polygon) { + // Ray casting algorithm for point-in-polygon + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].lon, + yi = polygon[i].lat; + const xj = polygon[j].lon, + yj = polygon[j].lat; + + const x = point.lon, + y = point.lat; + const intersect = + yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + return inside; +} + async function getAirportCoordinates(icao) { const airport = await airportService.getAirportConfig(icao); if (!airport || !airport.Coordinates) { @@ -393,7 +465,12 @@ async function getAirportCoordinates(icao) { } async function calculateRemainingDistance(ac) { - if (!ac.flight_plan || !ac.flight_plan.arrival || !ac.latitude || !ac.longitude) { + if ( + !ac.flight_plan || + !ac.flight_plan.arrival || + !ac.latitude || + !ac.longitude + ) { return Number.MAX_SAFE_INTEGER; } const destCoords = await getAirportCoordinates(ac.flight_plan.arrival); @@ -420,7 +497,8 @@ async function isConcernedArrival(ac, config, airportSet) { return false; } ac.remainingDistance = await calculateRemainingDistance(ac); - if (ac.remainingDistance * 0.00053996 > config.max_distance) { // convert to nautical miles + if (ac.remainingDistance * 0.00053996 > config.max_distance) { + // convert to nautical miles return false; } return true; @@ -542,21 +620,18 @@ function assignStand(airportConfig, config, ac) { const blockedStands = registry .getAllBlocked() .filter((s) => s.callsign === ac.callsign); - const apronStands = registry - .getAllApron() - .find((s) => s.callsign === ac.callsign); - if (assignedStand || apronStands) { - if (assignedStand && (registry.isOccupied(ac.destination, assignedStand.name) || registry.isBlocked(ac.destination, assignedStand.name))) { + if (assignedStand) { + if ( + assignedStand && + (registry.isOccupied(ac.destination, assignedStand.name) || + registry.isBlocked(ac.destination, assignedStand.name)) + ) { registry.removeAssigned(assignedStand); } else { - if (apronStands) { - apronStands.timestamp = Date.now(); - } else { assignedStand.timestamp = Date.now(); for (const s of blockedStands) { s.timestamp = Date.now(); } - } return; } } @@ -564,7 +639,11 @@ function assignStand(airportConfig, config, ac) { const schengen = isSchengen(ac.origin, ac.destination); const wingspan = getAircraftWingspan(config, ac.flight_plan.aircraft_short); const code = getAircraftCode(wingspan); - const use = getAircraftUse(config, ac.callsign, ac.flight_plan.aircraft_short); + const use = getAircraftUse( + config, + ac.callsign, + ac.flight_plan.aircraft_short + ); const originPrefix = ac.origin.substring(0, 2).toUpperCase(); const compagnyPrefix = ac.callsign.substring(0, 3).toUpperCase(); @@ -595,11 +674,21 @@ function assignStand(airportConfig, config, ac) { } } if (standDef.Callsigns && Array.isArray(standDef.Callsigns)) { - if (!standDef.Callsigns.includes(compagnyPrefix)) { + const cs = (ac.callsign || "").toUpperCase(); + let match = false; + // check prefixes from length 3 up to full callsign + for (let len = 3; len <= cs.length; len++) { + const prefix = cs.substring(0, len); + if (standDef.Callsigns.includes(prefix)) { + match = true; + break; + } + } + if (!match) { continue; } } - if (standDef.Apron === undefined || standDef.Apron === false) { + if (standDef.Apron === undefined) { if (registry.isOccupied(ac.destination, standName)) { continue; } @@ -609,6 +698,16 @@ function assignStand(airportConfig, config, ac) { if (registry.isBlocked(ac.destination, standName)) { continue; } + } else { + const apronSize = standDef.Apron.Size; + const currentApronOccupancy = registry.getApronOccupancyLevel( + standName, + airportConfig.ICAO + ); + if (currentApronOccupancy >= apronSize) { + // Apron is full + continue; + } } availableStandList.push(standDef); } @@ -649,18 +748,14 @@ function assignStand(airportConfig, config, ac) { const standName = Object.keys(airportConfig.Stands).find( (name) => airportConfig.Stands[name] === selectedStandDef ); - const stand = new Stand(standName, airportConfig.ICAO, ac.callsign); + const stand = new Stand(standName, airportConfig.ICAO, ac.callsign, "", selectedStandDef.Apron === undefined ? 0 : selectedStandDef.Apron.Size); info(`Assigning Stand ${standName} to ${ac.callsign}`, { category: "Assignation", callsign: ac.callsign, icao: airportConfig.ICAO, }); - if (selectedStandDef.apron === undefined || selectedStandDef.apron === false) { - registry.addAssigned(stand); - blockStands(selectedStandDef, ac.destination, ac.callsign); - } else { - registry.addApron(stand); - } + registry.addAssigned(stand); + blockStands(selectedStandDef, ac.destination, ac.callsign); return; } warn(`No available stands found for ${ac.callsign} at ${ac.destination}`, { @@ -751,43 +846,45 @@ processDatafeed = async (aircrafts) => { const standDef = airportJson && airportJson.Stands && airportJson.Stands[ac.stand]; + if (!standDef) { + warn( + `Stand definition for stand ${ac.stand} not found at airport ${ac.origin}, skipping occupancy`, + { category: "Assignation", callsign: ac.callsign, icao: ac.origin } + ); + continue; + } + let aircraftCode = "UNKNOWN"; if ( - standDef && - (standDef.Apron === undefined || standDef.Apron === false) + ac.flight_plan && + ac.flight_plan.aircraft_short && + ac.flight_plan.aircraft_short !== "UNKNOWN" && + ac.flight_plan.aircraft_short !== "" ) { - let aircraftCode = "UNKNOWN"; - if (ac.flight_plan && ac.flight_plan.aircraft_short && ac.flight_plan.aircraft_short !== "UNKNOWN" && ac.flight_plan.aircraft_short !== "") { - aircraftCode = getAircraftCode( - getAircraftWingspan(config, ac.flight_plan.aircraft_short) - ); - } - let remark = ""; - if (standDef.Remark && typeof standDef.Remark === "object") { - // Iterate through all keys in the Remark object - for (const [codeList, remarkText] of Object.entries( - standDef.Remark - )) { - // Check if the aircraft code is in this key - if (codeList.includes(aircraftCode)) { - remark = remarkText; - break; - } + aircraftCode = getAircraftCode( + getAircraftWingspan(config, ac.flight_plan.aircraft_short) + ); + } + let remark = ""; + if (standDef.Remark && typeof standDef.Remark === "object") { + // Iterate through all keys in the Remark object + for (const [codeList, remarkText] of Object.entries(standDef.Remark)) { + // Check if the aircraft code is in this key + if (codeList.includes(aircraftCode)) { + remark = remarkText; + break; } } - const stand = new Stand( - ac.stand, - ac.origin || "UNKNOWN", - ac.callsign, - remark - ); - // Remove preceeding entry if any - registry.removeOccupied(stand); - registry.addOccupied(stand); - - blockStands(standDef, ac.origin, ac.callsign); - } else { - registry.addApron(new Stand(ac.stand, ac.origin, ac.callsign)); } + const stand = new Stand( + ac.stand, + ac.origin || "UNKNOWN", + ac.callsign, + remark, + standDef.Apron === undefined ? 0 : standDef.Apron.Size + ); + + registry.addOccupied(stand); + blockStands(standDef, ac.origin, ac.callsign); } } @@ -799,10 +896,10 @@ processDatafeed = async (aircrafts) => { ac.origin = ac.flight_plan.departure; ac.destination = ac.flight_plan.arrival; // Check Assignement conditions - if (!await isConcernedArrival(ac, config, airportSet)) { + if (!(await isConcernedArrival(ac, config, airportSet))) { continue; } - + // Aircraft meets requirements for stand assignment // Use cached config let airportConfig = airportConfigCache.get(ac.destination); @@ -841,28 +938,22 @@ const getGlobalOccupied = () => { async function assignStandToPilot(standName, icao, callsign, client) { // Remove any existing assignment const existingStand = registry - .getAllAssigned() - .filter((s) => s.callsign === callsign); + .getAllAssigned() + .filter((s) => s.callsign === callsign); existingStand.forEach((existingStand) => { registry.removeAssigned(existingStand); }); const blockedStands = registry - .getAllBlocked() - .filter((s) => s.callsign === callsign); + .getAllBlocked() + .filter((s) => s.callsign === callsign); blockedStands.forEach((s) => { registry.removeBlocked(s); }); - const apronStands = registry - .getAllApron() - .filter((s) => s.callsign === callsign); - apronStands.forEach((s) => { - registry.removeApron(s); - }); if (standName === "None") { info(`Removed stand assignment for ${callsign}, Requester: ${client}`, { category: "Manual Assign", callsign: callsign, - icao: icao + icao: icao, }); return { action: "free", @@ -875,7 +966,11 @@ async function assignStandToPilot(standName, icao, callsign, client) { const standDef = await airportService .getAirportConfig(icao) .then((airportConfig) => { - if (airportConfig && airportConfig.Stands && airportConfig.Stands[standName]) { + if ( + airportConfig && + airportConfig.Stands && + airportConfig.Stands[standName] + ) { return airportConfig.Stands[standName]; } return null; @@ -885,67 +980,69 @@ async function assignStandToPilot(standName, icao, callsign, client) { warn(`Stand ${standName} not found at ${icao}, Requester: ${client}`, { category: "Manual Assign", callsign: callsign, - icao: icao + icao: icao, }); return { - action: "not_found", - stand: standName, - callsign: callsign, - icao: icao, - message: `Stand ${standName} does not exist at ${icao}`, - }; -} + action: "not_found", + stand: standName, + callsign: callsign, + icao: icao, + message: `Stand ${standName} does not exist at ${icao}`, + }; + } - if (standDef.Apron === undefined || standDef.Apron === false) { - if (registry.isOccupied(icao, standName)) { - warn( - `Cannot assign stand ${standName} at ${icao} to ${callsign} - already occupied, Requester: ${client}`, - { category: "Manual Assign", callsign: callsign, icao: icao } - ); - return { - action: "occupied", - stand: standName, - callsign: callsign, - icao: icao, - message: `Stand ${standName} could not be assigned to ${callsign} as it is already occupied`, - }; - } - if (registry.isAssigned(icao, standName)) { - warn( - `Cannot assign stand ${standName} at ${icao} to ${callsign} - already assigned, Requester: ${client}`, - { category: "Manual Assign", callsign: callsign, icao: icao } - ); - return { - action: "assigned", - stand: standName, - callsign: callsign, - icao: icao, - message: `Stand ${standName} could not be assigned to ${callsign} as it is already assigned`, - }; - } - if (registry.isBlocked(icao, standName)) { - warn( - `Cannot assign stand ${standName} at ${icao} to ${callsign} - already blocked, Requester: ${client}`, - { category: "Manual Assign", callsign: callsign, icao: icao } - ); - return { - action: "blocked", - stand: standName, - callsign: callsign, - icao: icao, - message: `Stand ${standName} could not be assigned to ${callsign} as it is blocked`, - }; - } + if (registry.isOccupied(icao, standName)) { + warn( + `Cannot assign stand ${standName} at ${icao} to ${callsign} - already occupied, Requester: ${client}`, + { category: "Manual Assign", callsign: callsign, icao: icao } + ); + return { + action: "occupied", + stand: standName, + callsign: callsign, + icao: icao, + message: `Stand ${standName} could not be assigned to ${callsign} as it is already occupied`, + }; + } + if (registry.isAssigned(icao, standName)) { + warn( + `Cannot assign stand ${standName} at ${icao} to ${callsign} - already assigned, Requester: ${client}`, + { category: "Manual Assign", callsign: callsign, icao: icao } + ); + return { + action: "assigned", + stand: standName, + callsign: callsign, + icao: icao, + message: `Stand ${standName} could not be assigned to ${callsign} as it is already assigned`, + }; } - const stand = new Stand(standName, icao, callsign); + if (registry.isBlocked(icao, standName)) { + warn( + `Cannot assign stand ${standName} at ${icao} to ${callsign} - already blocked, Requester: ${client}`, + { category: "Manual Assign", callsign: callsign, icao: icao } + ); + return { + action: "blocked", + stand: standName, + callsign: callsign, + icao: icao, + message: `Stand ${standName} could not be assigned to ${callsign} as it is blocked`, + }; + } + const stand = new Stand(standName, icao, callsign, "", standDef.Apron === undefined ? 0 : standDef.Apron.Size); registry.addAssigned(stand); // Block stands blockStands(standDef, icao, callsign); - info(`Manually assigned stand ${standName} at ${icao} to ${callsign}, Requester: ${client}`, { - category: "Manual Assign", - callsign: callsign, - icao: icao, - }); + info( + `Manually assigned stand ${standName} at ${icao} to ${callsign}, Requester: ${client}`, + { + category: "Manual Assign", + callsign: callsign, + icao: icao, + } + ); + return { action: "assign", stand: standName, @@ -970,10 +1067,10 @@ module.exports = { processDatafeed, assignStandToPilot, getGlobalOccupied, - getAllOccupied: registry.getAllOccupied.bind(registry), - getAllAssigned: registry.getAllAssigned.bind(registry), - getAllBlocked: registry.getAllBlocked.bind(registry), - isOccupied: registry.isOccupied.bind(registry), - isBlocked: registry.isBlocked.bind(registry), - isBlocked: registry.isBlocked.bind(registry), + getAllOccupied: () => registry.getAllOccupied(), + getAllAssigned: () => registry.getAllAssigned(), + getAllBlocked: () => registry.getAllBlocked(), + isOccupied: (icao, name) => registry.isOccupied(icao, name), + isAssigned: (icao, name) => registry.isAssigned(icao, name), + isBlocked: (icao, name) => registry.isBlocked(icao, name), }; diff --git a/services/redisService.js b/services/redisService.js index 6933e8d..81ec69c 100644 --- a/services/redisService.js +++ b/services/redisService.js @@ -282,7 +282,7 @@ class RedisService { } } - VALID_ROLES = ["admin", "user"]; + VALID_ROLES = ["admin"]; async validateRole(role) { return VALID_ROLES.includes(role); @@ -394,165 +394,6 @@ class RedisService { user.roles = user.roles.filter((r) => r !== role); return await this.updateLocalUser(cid, user); } - - async getAllKeys() { - if (!this.isConnected) return []; - - try { - // Get all keys except metadata keys - const keys = await this.client.keys("*"); - const nonMetaKeys = keys.filter((key) => !key.startsWith("meta:")); - - // Get metadata for each key - const keysWithMetadata = await Promise.all( - nonMetaKeys.map(async (key) => { - const metadata = await this.getKeyMetadata(key); - return { - key, - metadata, - }; - }) - ); - - return keysWithMetadata; - } catch (err) { - logger.warn(`Failed to get all keys: ${err.message}`, { - category: "System", - }); - return []; - } - } - - async createKey(key, expireIn = RedisService.KEY_EXPIRATION) { - if (!this.isConnected) return false; - try { - const raw = crypto.randomBytes(bytes); // cryptographically secure - // base64url: replace +/ with -_ and remove trailing = padding - const value = raw - .toString("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, ""); - // Store the key value with expiration - await this.client.set(key, value, { EX: expireIn }); - - // Store metadata about the key - const metadata = { - created_at: Date.now(), - last_used: Date.now(), - expires_at: Date.now() + expireIn * 1000, - value_type: typeof value, - }; - - await this.client.set(`meta:${key}`, JSON.stringify(metadata), { - EX: RedisService.KEY_METADATA_EXPIRATION, - }); - - return true; - } catch (err) { - logger.warn(`Failed to create key ${key}: ${err.message}`, { - category: "System", - }); - return false; - } - } - - async renewKey(key, value, expireIn = RedisService.KEY_EXPIRATION) { - if (!this.isConnected) return false; - try { - const exists = await this.client.exists(key); - if (exists) { - // Update the key value and reset expiration - await this.client.set(key, value, { EX: expireIn }); - - // Update metadata - const metaKey = `meta:${key}`; - const existingMeta = await this.client.get(metaKey); - const metadata = existingMeta ? JSON.parse(existingMeta) : {}; - - metadata.last_used = Date.now(); - metadata.expires_at = Date.now() + expireIn * 1000; - - await this.client.set(metaKey, JSON.stringify(metadata), { - EX: RedisService.KEY_METADATA_EXPIRATION, - }); - - return true; - } - return false; - } catch (err) { - logger.warn(`Failed to renew key ${key}: ${err.message}`, { - category: "System", - }); - return false; - } - } - - async getKeyMetadata(key) { - if (!this.isConnected) return null; - try { - const metaKey = `meta:${key}`; - const metadata = await this.client.get(metaKey); - return metadata ? JSON.parse(metadata) : null; - } catch (err) { - logger.warn(`Failed to get metadata for key ${key}: ${err.message}`, { - category: "System", - }); - return null; - } - } - - async deleteKey(key) { - if (!this.isConnected) return false; - try { - const exists = await this.client.exists(key); - if (exists) { - // Delete both the key and its metadata - await Promise.all([ - this.client.del(key), - this.client.del(`meta:${key}`), - ]); - return true; - } - return false; - } catch (err) { - logger.warn(`Failed to delete key ${key}: ${err.message}`, { - category: "System", - }); - return false; - } - } - - async getKeyById(id) { - if (!this.isConnected) return null; - try { - // Get key value - const value = await this.client.get(id); - if (!value) return null; - - // Get metadata separately - const metadata = await this.getKeyMetadata(id); - - // Try to parse value if it's JSON, otherwise return as is - let parsedValue; - try { - parsedValue = JSON.parse(value); - } catch { - parsedValue = value; - } - - return { - value: parsedValue, - expires_at: metadata?.expires_at || null, - metadata, - }; - } catch (err) { - logger.warn(`Failed to get key ${id}: ${err.message}`, { - category: "System", - }); - return null; - } - } } // Singleton instance diff --git a/viewer/script.js b/viewer/script.js index 93ce53f..b62f9dc 100644 --- a/viewer/script.js +++ b/viewer/script.js @@ -15,7 +15,7 @@ function closeNav() { } document.addEventListener("click", function (event) { - if (event.x <= 200) return; + if (event.x <= 200) return; const sidenav = document.getElementById("mySidenav"); if (sidenav && sidenav.style.width !== "0") { if (!sidenav.contains(event.target)) { @@ -40,7 +40,7 @@ function toggleDarkMode() { document.addEventListener("DOMContentLoaded", function () { // Check localStorage for dark mode preference const darkMode = localStorage.getItem("darkMode"); - + // Restore manual performance-mode preference from previous session const performanceModeStored = localStorage.getItem("performanceModeManual"); manualToggle = performanceModeStored === "true"; @@ -58,7 +58,7 @@ document.addEventListener("DOMContentLoaded", function () { window.switchMapLayer(); } }, 100); - + checkAuthAndUpdateUI(); updateApiKeyCount(); }); @@ -70,7 +70,8 @@ const HIGH_VOLUME_THRESHOLD = 50; // Number of stands that triggers performance function checkVolumeAndTogglePerformanceMode(standCount) { let shouldBeInPerformanceMode = standCount >= HIGH_VOLUME_THRESHOLD; - if (window.innerWidth < 700) { // Mobiles always in performance mode since lower power + if (window.innerWidth < 700) { + // Mobiles always in performance mode since lower power shouldBeInPerformanceMode = true; } @@ -387,21 +388,21 @@ function updateChartColors(isDarkMode) { // Update grid colors reportsChart.options.scales.x.grid.color = gridColor; reportsChart.options.scales.y.grid.color = gridColor; - + // Update tick colors reportsChart.options.scales.x.ticks.color = axisTextColor; reportsChart.options.scales.y.ticks.color = axisTextColor; - + // Update legend color reportsChart.options.plugins.legend.labels.color = legendTextColor; - - reportsChart.update('active'); + + reportsChart.update("active"); } // Update airport chart if it exists if (airportChart) { airportChart.options.plugins.legend.labels.color = legendTextColor; - airportChart.update('active'); + airportChart.update("active"); } } @@ -1102,8 +1103,6 @@ document.addEventListener("DOMContentLoaded", () => { }, 2000); }); -// Event listeners for filter changes are now set up inside DOMContentLoaded - // Navigation routing - wrapped to execute after DOM is ready (function () { function initNavigation() { @@ -1115,10 +1114,11 @@ document.addEventListener("DOMContentLoaded", () => { ); function showPage(page) { - sections.forEach((s) => { + sections.forEach(async (s) => { if (page === "log" || page === "configs") { // Block access to logs and configs if not authenticated - if (!isUserAdmin(fetchCurrentUser())) { + const currentUser = await fetchCurrentUser(); + if (!isUserAdmin(currentUser)) { console.log("Access denied to page:", page); s.style.display = "none"; // redirect to status page @@ -1198,11 +1198,20 @@ function fetchOccupiedStands() { }) .then((stands) => { if (Array.isArray(stands)) { - // Store as ICAO-StandName format instead of just name - occupiedStands = stands.map((s) => ({ - id: s.icao + "-" + s.name, - callsign: s.callsign, - })); + // Group apron stands by ICAO-StandName, keep others as-is + const grouped = new Map(); + stands.forEach((s) => { + const id = s.icao + "-" + s.name; + if (s.apronSize > 0) { + if (!grouped.has(id)) { + grouped.set(id, { id, callsigns: [], isApron: true }); + } + grouped.get(id).callsigns.push(s.callsign); + } else { + grouped.set(id, { id, callsign: s.callsign, isApron: false }); + } + }); + occupiedStands = Array.from(grouped.values()); } }) .catch((err) => { @@ -1220,11 +1229,20 @@ function fetchAssignedStands() { }) .then((stands) => { if (Array.isArray(stands)) { - // Store as ICAO-StandName format instead of just name - assignedStands = stands.map((s) => ({ - id: s.icao + "-" + s.name, - callsign: s.callsign, - })); + // Group apron stands by ICAO-StandName, keep others as-is + const grouped = new Map(); + stands.forEach((s) => { + const id = s.icao + "-" + s.name; + if (s.apronSize > 0) { + if (!grouped.has(id)) { + grouped.set(id, { id, callsigns: [], isApron: true }); + } + grouped.get(id).callsigns.push(s.callsign); + } else { + grouped.set(id, { id, callsign: s.callsign, isApron: false }); + } + }); + assignedStands = Array.from(grouped.values()); } }) .catch((err) => { @@ -1242,11 +1260,20 @@ function fetchBlockedStands() { }) .then((stands) => { if (Array.isArray(stands)) { - // Store as ICAO-StandName format instead of just name - blockedStands = stands.map((s) => ({ - id: s.icao + "-" + s.name, - callsign: s.callsign, - })); + // Blocked stands are usually not aprons, but handle just in case + const grouped = new Map(); + stands.forEach((s) => { + const id = s.icao + "-" + s.name; + if (s.apronSize > 0) { + if (!grouped.has(id)) { + grouped.set(id, { id, callsigns: [], isApron: true }); + } + grouped.get(id).callsigns.push(s.callsign); + } else { + grouped.set(id, { id, callsign: s.callsign, isApron: false }); + } + }); + blockedStands = Array.from(grouped.values()); } }) .catch((err) => { @@ -1256,6 +1283,10 @@ function fetchBlockedStands() { function getStandColor(standName, apron) { // Now both standName and the arrays are in ICAO-StandName format + if (apron) { + return ["#4682B4", "#87CEEB"]; // steel blue border, sky blue fill (apron) + } + if (occupiedStands.some((s) => s.id === standName)) { return ["#B22222", "#FF6B6B"]; // dark red border, light red fill (occupied) } @@ -1268,15 +1299,11 @@ function getStandColor(standName, apron) { return ["#9c7c22ff", "#cdc54eff"]; // dark teal border, light teal fill (blocked) } - if (apron) { - return ["#4682B4", "#87CEEB"]; // steel blue border, sky blue fill (apron) - } - return ["#78BFA0", "#96CEB4"]; // darker green border, light green fill (default) } // Map variables and constants -var zoomThreshold = 5; // <= show meter circle, > show screen-sized marker +var zoomThreshold = 6; // <= show meter circle, > show screen-sized marker var zoomHideThreshold = 13; // > hide marker entirely var meterRadius = 50000; // meters for the L.Circle when zoomed out var labelZoomThreshold = 17; // show stand labels at this zoom level and above @@ -1322,9 +1349,6 @@ function updateMarkerSizes() { }); } } -// map.on("zoomend", updateMarkerSizes); // Moved to initializeMap() - -// Home button control and map.whenReady moved to initializeMap() // Configs page // generate buttons for available config presets @@ -1553,16 +1577,67 @@ function createStandPopupContent(standId) { div.className = "stand-popup-content"; div.innerHTML = "

" + standId + "

"; - // Check occupied/assigned/blocked arrays for callsign + // Check occupied/assigned/blocked arrays for callsign(s) const occupied = occupiedStands.find((s) => s.id === standId); const assigned = assignedStands.find((s) => s.id === standId); const blocked = blockedStands.find((s) => s.id === standId); - if (occupied) { + + // For aprons, combine occupied and assigned callsigns + if (occupied && occupied.isApron && assigned && assigned.isApron) { + // Both occupied and assigned callsigns exist + const occupiedCallsigns = Array.isArray(occupied.callsigns) ? occupied.callsigns : []; + const assignedCallsigns = Array.isArray(assigned.callsigns) ? assigned.callsigns : []; + const totalCount = occupiedCallsigns.length + assignedCallsigns.length; + + div.innerHTML += `

Aircraft (${totalCount}):

`; + + if (occupiedCallsigns.length > 0) { + div.innerHTML += `

Occupied (${occupiedCallsigns.length}):

`; + } + + if (assignedCallsigns.length > 0) { + div.innerHTML += `

Assigned (${assignedCallsigns.length}):

`; + } + } else if (occupied && occupied.isApron) { + // Only occupied callsigns + const occupiedCallsigns = Array.isArray(occupied.callsigns) ? occupied.callsigns : []; + div.innerHTML += `

Occupied (${occupiedCallsigns.length}):

`; + } else if (assigned && assigned.isApron) { + // Only assigned callsigns + const assignedCallsigns = Array.isArray(assigned.callsigns) ? assigned.callsigns : []; + div.innerHTML += `

Assigned (${assignedCallsigns.length}):

`; + } else if (occupied) { + // Non-apron occupied stand div.innerHTML += `

Occupied by ${occupied.callsign}

`; } else if (assigned) { + // Non-apron assigned stand div.innerHTML += `

Assigned to ${assigned.callsign}

`; } else if (blocked) { - div.innerHTML += `

Blocked by ${blocked.callsign}

`; + if (blocked.isApron && Array.isArray(blocked.callsigns)) { + div.innerHTML += `

Blocked by (${blocked.callsigns.length}):

`; + } else { + div.innerHTML += `

Blocked by ${blocked.callsign}

`; + } } else { div.innerHTML += `

Free

`; } @@ -1581,7 +1656,6 @@ function loadMapData() { .then((data) => { if (!Array.isArray(data)) throw new Error("Stands response is not an array"); - stands = data.filter((s) => { return ( Array.isArray(s.coords) && @@ -1599,15 +1673,58 @@ function loadMapData() { } else { stands.forEach((stand) => { const color = getStandColor(stand.name, stand.apron); - stand.circle = L.circle(stand.coords, { - color: color[0], - fillColor: color[1], - fillOpacity: 0.8, - radius: stand.radius, - weight: 3, - }).bindPopup(() => { - return createStandPopupContent(stand.name); - }); + if (stand.apron && stand.apron.Coordinates) { + // Parse and validate apron coordinates + // Currently, coordinates are "lat:lng" strings - convert to [lat, lng] arrays + if (Array.isArray(stand.apron.Coordinates)) { + stand.apron.Coordinates = stand.apron.Coordinates.map((coord) => { + const [lat, lng] = coord.split(":").map(Number); + return [lat, lng]; + }); + } + + const apronCoordsValid = + stand && + stand.apron && + Array.isArray(stand.apron.Coordinates) && + stand.apron.Coordinates.length > 0 && + stand.apron.Coordinates.every( + (c) => + Array.isArray(c) && + c.length === 2 && + Number.isFinite(c[0]) && + Number.isFinite(c[1]) + ); + + if (!apronCoordsValid) { + console.warn( + "Invalid apron coordinates for stand:", + stand + ); + return; + } + stand.polygon = L.polygon(stand.apron.Coordinates, { + color: color[0], + fillColor: color[1], + fillOpacity: 0.5, + weight: 2, + lineJoin: "round", // <- round joins + lineCap: "round", // <- round end caps + smoothFactor: 1.5 + }).bindPopup(() => { + return createStandPopupContent(stand.name); + }); + } else { + stand.circle = L.circle(stand.coords, { + color: color[0], + fillColor: color[1], + fillOpacity: 0.5, + radius: stand.radius, + weight: 3, + }).bindPopup(() => { + return createStandPopupContent(stand.name); + }); + } stand.label = L.marker(stand.coords, { interactive: false, @@ -1617,7 +1734,11 @@ function loadMapData() { }), }); - stand.circle.addTo(map); + if (stand.circle) { + stand.circle.addTo(map); + } else if (stand.polygon) { + stand.polygon.addTo(map); + } }); } }) @@ -1666,7 +1787,7 @@ function loadMapData() { const zoomThreshold = 5; const zoomHideThreshold = 13; - const meterRadius = 50000; + const meterRadius = 25000; airports.forEach(function (airport) { airport.circle = L.circle(airport.coords, { @@ -1724,16 +1845,16 @@ if (document.readyState === "loading") { initializeMap(); } - - // Dashboard async function fetchCurrentUser() { try { - const res = await fetch(API_BASE_URL + '/api/auth/session', { credentials: 'same-origin' }); + const res = await fetch(API_BASE_URL + "/api/auth/session", { + credentials: "same-origin", + }); if (!res.ok) return null; return await res.json(); } catch (err) { - console.warn('fetchCurrentUser error', err); + console.warn("fetchCurrentUser error", err); return null; } } @@ -1744,7 +1865,12 @@ function isUserConnected(user) { function isUserAdmin(user) { if (!user) return false; - if (user.local && Array.isArray(user.local.roles) && user.local.roles.includes('admin')) return true; + if ( + user.local && + Array.isArray(user.local.roles) && + user.local.roles.includes("admin") + ) + return true; return false; } @@ -1755,21 +1881,25 @@ async function checkAuthAndUpdateUI() { } async function fetchLocalUsers() { - const res = await fetch(API_BASE_URL + '/api/auth/internal/localusers', { credentials: 'same-origin' }); - if (!res.ok) throw new Error('Failed to fetch users'); + const res = await fetch(API_BASE_URL + "/api/auth/internal/localusers", { + credentials: "same-origin", + }); + if (!res.ok) throw new Error("Failed to fetch users"); return res.json(); } async function toggleAdminRole(cid, add) { - const url = API_BASE_URL + `/api/auth/internal/localuser/${encodeURIComponent(cid)}/roles`; - const method = add ? 'POST' : 'DELETE'; + const url = + API_BASE_URL + + `/api/auth/internal/localuser/${encodeURIComponent(cid)}/roles`; + const method = add ? "POST" : "DELETE"; const res = await fetch(url, { method, - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ role: 'admin' }) + credentials: "same-origin", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ role: "admin" }), }); - if (!res.ok) throw new Error('Failed to update role'); + if (!res.ok) throw new Error("Failed to update role"); return res.json(); } @@ -1779,25 +1909,29 @@ async function renderAdminList(containerId) { if (!container) return; try { const users = await fetchLocalUsers(); - container.innerHTML = users.map(u => { - const isAdmin = Array.isArray(u.roles) && u.roles.includes('admin'); - return `
- ${u.cid} ${u.full_name ? '- ' + u.full_name : ''} -
`; - }).join(''); - container.querySelectorAll('.role-btn').forEach(btn => { - btn.addEventListener('click', async (e) => { + }) + .join(""); + container.querySelectorAll(".role-btn").forEach((btn) => { + btn.addEventListener("click", async (e) => { const cid = btn.dataset.cid; - const add = btn.dataset.action === 'grant'; + const add = btn.dataset.action === "grant"; await toggleAdminRole(cid, add); await renderAdminList(containerId); }); }); } catch (err) { - container.textContent = 'Error loading users'; + container.textContent = "Error loading users"; console.error(err); } } @@ -1806,14 +1940,12 @@ async function renderAdminList(containerId) { function displayDashboard(user) { const isAdmin = isUserAdmin(user); if (isAdmin) { - renderAdminList('adminUserList'); + renderAdminList("adminUserList"); document.getElementById("dashboardAdmin").style.display = "block"; - document.getElementById("dashboardUser").style.display = "block"; updateControllerNumber(); updateApiKeyList(); } else { document.getElementById("dashboardAdmin").style.display = "none"; - document.getElementById("dashboardUser").style.display = "block"; } } @@ -1822,25 +1954,38 @@ function renderLoginLayout(user) { const isAdmin = isUserAdmin(user); // Handle sidenav items visibility - const adminOnlyItems = document.querySelectorAll('.sidenav a[data-admin-only]'); - adminOnlyItems.forEach(item => { - item.style.display = isConnected && isAdmin ? 'block' : 'none'; + const adminOnlyItems = document.querySelectorAll( + ".sidenav a[data-admin-only]" + ); + adminOnlyItems.forEach((item) => { + item.style.display = isConnected && isAdmin ? "block" : "none"; }); // Rest of existing login layout logic if (!isConnected) { - Array.from(document.getElementsByClassName("loginLayout")).forEach(el => el.style.display = "flex"); - Array.from(document.getElementsByClassName("connectedLayout")).forEach(el => el.style.display = "none"); + Array.from(document.getElementsByClassName("loginLayout")).forEach( + (el) => (el.style.display = "flex") + ); + Array.from(document.getElementsByClassName("connectedLayout")).forEach( + (el) => (el.style.display = "none") + ); } else { - Array.from(document.getElementsByClassName("loginLayout")).forEach(el => el.style.display = "none"); + Array.from(document.getElementsByClassName("loginLayout")).forEach( + (el) => (el.style.display = "none") + ); // Check maxwidth to adjust layout if (window.innerWidth <= 600) { - Array.from(document.getElementsByClassName("connectedLayout")).forEach(el => el.style.display = "flex"); + Array.from(document.getElementsByClassName("connectedLayout")).forEach( + (el) => (el.style.display = "flex") + ); } else { - Array.from(document.getElementsByClassName("connectedLayout")).forEach(el => el.style.display = "inline"); + Array.from(document.getElementsByClassName("connectedLayout")).forEach( + (el) => (el.style.display = "inline") + ); } - document.getElementById("usernameUser").textContent = user ? user.core.firstName : "Guest"; - document.getElementById("usernameAdmin").textContent = user ? user.core.firstName : "Guest"; + document.getElementById("username").textContent = user + ? user.core.firstName + : "Guest"; apiKeyDisplay(user); } } @@ -1857,22 +2002,22 @@ function apiKeyDisplay(user) { function generateApiKey() { console.log("Generating new API key..."); - fetchLocalUsers().then(user => { + fetchLocalUsers().then((user) => { if (!user) { console.error("Cannot generate API key: user not found"); return; } - fetch(API_BASE_URL + `/api/auth/key/${user.cid}`, { + fetch(API_BASE_URL + `/api/auth/key/${user.core.cid}`, { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, - body: JSON.stringify({ userId: user.cid }) + body: JSON.stringify({ userId: user.core.cid }), }); }); // Refresh the dashboard to show new key - fetchLocalUsers().then(user => { + fetchLocalUsers().then((user) => { apiKeyDisplay(user); }); } @@ -1880,11 +2025,11 @@ function generateApiKey() { function updateApiKeyList() { const tbody = document.querySelector("#apiKeyListTable tbody"); if (!tbody) return; - fetch(API_BASE_URL + "/api/auth/keys", { + fetch(API_BASE_URL + "/api/apikey/", { headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, - credentials: "same-origin" + credentials: "same-origin", }) .then((res) => { if (!res.ok) throw new Error("Network response was not ok"); @@ -1900,7 +2045,12 @@ function updateApiKeyList() { console.log("Fetched API keys:", data); // Populate table with API keys - data.keys.forEach(key => { + // Make sure it is an array + if (!Array.isArray(data.keys)) { + console.error("API keys data is not an array:", data.keys); + return; + } + data.keys.forEach((key) => { const row = document.createElement("tr"); row.id = key.cid; row.innerHTML = ` @@ -1916,16 +2066,18 @@ function updateApiKeyList() { tbody.appendChild(row); }); updateApiKeyCount(); - showNoApiKeysMessageIfEmpty(); }) .catch((err) => { console.error("Failed to fetch API key list", err); }); + showNoApiKeysMessageIfEmpty(); } function updateApiKeyCount() { const countElem = document.getElementById("apiKeyCount"); - const apiKeyCounter = document.querySelectorAll("#apiKeyListTable tbody tr").length; + const apiKeyCounter = document.querySelectorAll( + "#apiKeyListTable tbody tr" + ).length; if (countElem) { countElem.textContent = apiKeyCounter; } @@ -1934,26 +2086,26 @@ function updateApiKeyCount() { // API actions function renewApiKey(cid) { console.log("Renewing API key of CID:", cid); - fetch(API_BASE_URL + `/api/auth/key/${cid}/renew`, { + fetch(API_BASE_URL + `/api/apikey/${cid}/renew`, { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, credentials: "same-origin", - body: JSON.stringify({ cid }) + body: JSON.stringify({ cid }), }); } function revokeApiKey(cid) { console.log("Revoking API key of CID:", cid); - fetch(API_BASE_URL + `/api/auth/key/${cid}/revoke`, { + fetch(API_BASE_URL + `/api/apikey/${cid}/revoke`, { method: "DELETE", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, credentials: "same-origin", - body: JSON.stringify({ cid }) + body: JSON.stringify({ cid }), }); // Remove entire row from table @@ -1968,10 +2120,10 @@ function revokeApiKey(cid) { } function showNoApiKeysMessageIfEmpty() { - const tbody = document.querySelector('#apiKeyListTable tbody'); + const tbody = document.querySelector("#apiKeyListTable tbody"); if (tbody && tbody.children.length === 0) { - const noKeysRow = document.createElement('tr'); - const noKeysCell = document.createElement('td'); + const noKeysRow = document.createElement("tr"); + const noKeysCell = document.createElement("td"); noKeysCell.colSpan = 5; noKeysCell.textContent = "No API keys found"; noKeysRow.appendChild(noKeysCell); @@ -2004,57 +2156,66 @@ setInterval(updateControllerNumber, 15000); // update every 15 seconds // Swipe buttons (function enableRowSwipeActions() { - const tbody = document.querySelector('#apiKeyListTable tbody'); + const tbody = document.querySelector("#apiKeyListTable tbody"); if (!tbody) return; - let startX = 0, startY = 0, activeRow = null; + let startX = 0, + startY = 0, + activeRow = null; let dragging = false; const HORIZONTAL_THRESHOLD = 50; // px needed to count as swipe const MAX_TRANSLATE = 120; // px maximum visual translation function getRow(el) { - while (el && el !== tbody && el.tagName !== 'TR') el = el.parentElement; - return (el && el.tagName === 'TR') ? el : null; + while (el && el !== tbody && el.tagName !== "TR") el = el.parentElement; + return el && el.tagName === "TR" ? el : null; } function ensureIndicators(row) { if (!row) return; - if (!row.querySelector('.swipe-indicator.left')) { - const left = document.createElement('div'); - left.className = 'swipe-indicator left'; - left.innerHTML = 'Renew'; + if (!row.querySelector(".swipe-indicator.left")) { + const left = document.createElement("div"); + left.className = "swipe-indicator left"; + left.innerHTML = "Renew"; row.appendChild(left); } - if (!row.querySelector('.swipe-indicator.right')) { - const right = document.createElement('div'); - right.className = 'swipe-indicator right'; - right.innerHTML = 'Revoke'; + if (!row.querySelector(".swipe-indicator.right")) { + const right = document.createElement("div"); + right.className = "swipe-indicator right"; + right.innerHTML = "Revoke"; row.appendChild(right); } } function startDrag(x, y, target) { - startX = x; startY = y; + startX = x; + startY = y; activeRow = getRow(target); if (!activeRow) return; ensureIndicators(activeRow); dragging = true; - activeRow.classList.add('swipe-dragging'); + activeRow.classList.add("swipe-dragging"); // guard everything that touches style with a check if (activeRow) { - activeRow.style.transition = 'none'; - activeRow.style.willChange = 'transform'; - activeRow.style.zIndex = '1500'; - activeRow.style.boxShadow = '0 12px 30px rgba(0,0,0,0.18)'; - activeRow.style.transform = 'translateX(0) scale(1.01)'; - activeRow.style.userSelect = 'none'; + activeRow.style.transition = "none"; + activeRow.style.willChange = "transform"; + activeRow.style.zIndex = "1500"; + activeRow.style.boxShadow = "0 12px 30px rgba(0,0,0,0.18)"; + activeRow.style.transform = "translateX(0) scale(1.01)"; + activeRow.style.userSelect = "none"; } // initialize indicators - const left = activeRow.querySelector('.swipe-indicator.left'); - const right = activeRow.querySelector('.swipe-indicator.right'); - if (left) { left.style.width = '0px'; left.style.opacity = '0'; } - if (right) { right.style.width = '0px'; right.style.opacity = '0'; } + const left = activeRow.querySelector(".swipe-indicator.left"); + const right = activeRow.querySelector(".swipe-indicator.right"); + if (left) { + left.style.width = "0px"; + left.style.opacity = "0"; + } + if (right) { + right.style.width = "0px"; + right.style.opacity = "0"; + } } function moveDrag(x, y) { @@ -2066,8 +2227,8 @@ setInterval(updateControllerNumber, 15000); // update every 15 seconds const scale = 1 + Math.min(Math.abs(limited) / 800, 0.03); activeRow.style.transform = `translateX(${limited}px) scale(${scale})`; - const left = activeRow.querySelector('.swipe-indicator.left'); - const right = activeRow.querySelector('.swipe-indicator.right'); + const left = activeRow.querySelector(".swipe-indicator.left"); + const right = activeRow.querySelector(".swipe-indicator.right"); if (limited > 0) { // reveal left indicator proportionally if (left) { @@ -2075,8 +2236,8 @@ setInterval(updateControllerNumber, 15000); // update every 15 seconds left.style.opacity = String(Math.min(1, Math.abs(limited) / 20)); } if (right) { - right.style.width = '0px'; - right.style.opacity = '0'; + right.style.width = "0px"; + right.style.opacity = "0"; } } else if (limited < 0) { // reveal right indicator proportionally @@ -2086,17 +2247,26 @@ setInterval(updateControllerNumber, 15000); // update every 15 seconds right.style.opacity = String(Math.min(1, Math.abs(limited) / 20)); } if (left) { - left.style.width = '0px'; - left.style.opacity = '0'; + left.style.width = "0px"; + left.style.opacity = "0"; } } else { - if (left) { left.style.width = '0px'; left.style.opacity = '0'; } - if (right) { right.style.width = '0px'; right.style.opacity = '0'; } + if (left) { + left.style.width = "0px"; + left.style.opacity = "0"; + } + if (right) { + right.style.width = "0px"; + right.style.opacity = "0"; + } } } function endDrag(x, y) { - if (!activeRow) { dragging = false; return; } + if (!activeRow) { + dragging = false; + return; + } const dx = x - startX; const dy = y - startY; dragging = false; @@ -2104,97 +2274,145 @@ setInterval(updateControllerNumber, 15000); // update every 15 seconds // use a localRef to avoid race if activeRow is cleared/removed later const rowRef = activeRow; - if (rowRef) rowRef.style.transition = 'transform 220ms ease, box-shadow 180ms ease'; + if (rowRef) + rowRef.style.transition = "transform 220ms ease, box-shadow 180ms ease"; - const left = rowRef ? rowRef.querySelector('.swipe-indicator.left') : null; - const right = rowRef ? rowRef.querySelector('.swipe-indicator.right') : null; + const left = rowRef ? rowRef.querySelector(".swipe-indicator.left") : null; + const right = rowRef + ? rowRef.querySelector(".swipe-indicator.right") + : null; if (Math.abs(dx) >= HORIZONTAL_THRESHOLD && Math.abs(dx) > Math.abs(dy)) { - const apiCell = rowRef ? rowRef.querySelector('.apiValue') : null; + const apiCell = rowRef ? rowRef.querySelector(".apiValue") : null; const apiKey = apiCell ? apiCell.textContent.trim() : null; if (apiKey && rowRef) { - const direction = dx > 0 ? 'right' : 'left'; + const direction = dx > 0 ? "right" : "left"; const finishTranslate = dx > 0 ? MAX_TRANSLATE : -MAX_TRANSLATE; rowRef.style.transform = `translateX(${finishTranslate}px) scale(1.02)`; - if (direction === 'right' && left) { left.style.width = `${MAX_TRANSLATE}px`; left.style.opacity = '1'; } - if (direction === 'left' && right) { right.style.width = `${MAX_TRANSLATE}px`; right.style.opacity = '1'; } + if (direction === "right" && left) { + left.style.width = `${MAX_TRANSLATE}px`; + left.style.opacity = "1"; + } + if (direction === "left" && right) { + right.style.width = `${MAX_TRANSLATE}px`; + right.style.opacity = "1"; + + } setTimeout(() => { - if (direction === 'right') { - try { renewApiKey(apiKey); } catch (err) { console.error(err); } + if (direction === "right") { + try { + renewApiKey(apiKey); + } catch (err) { + console.error(err); + } } else { - try { revokeApiKey(apiKey); } catch (err) { console.error(err); } + try { + revokeApiKey(apiKey); + } catch (err) { + console.error(err); + } } if (rowRef) { - rowRef.style.transform = 'translateX(0) scale(1)'; - if (left) { left.style.width = '0px'; left.style.opacity = '0'; } - if (right) { right.style.width = '0px'; right.style.opacity = '0'; } + rowRef.style.transform = "translateX(0) scale(1)"; + if (left) { + left.style.width = "0px"; + left.style.opacity = "0"; + } + if (right) { + right.style.width = "0px"; + right.style.opacity = "0"; + } } }, 180); } else if (rowRef) { - rowRef.style.transform = 'translateX(0) scale(1)'; + rowRef.style.transform = "translateX(0) scale(1)"; } } else { if (rowRef) { - rowRef.style.transform = 'translateX(0) scale(1)'; - if (left) { left.style.width = '0px'; left.style.opacity = '0'; } - if (right) { right.style.width = '0px'; right.style.opacity = '0'; } + rowRef.style.transform = "translateX(0) scale(1)"; + if (left) { + left.style.width = "0px"; + left.style.opacity = "0"; + } + if (right) { + right.style.width = "0px"; + right.style.opacity = "0"; + } } } const cleanup = () => { if (!rowRef) return; - rowRef.classList.remove('swipe-dragging'); + rowRef.classList.remove("swipe-dragging"); // clear inline styles safely - rowRef.style.transition = ''; - rowRef.style.transform = ''; - rowRef.style.willChange = ''; - rowRef.style.boxShadow = ''; - rowRef.style.zIndex = ''; - rowRef.style.userSelect = ''; - const l = rowRef.querySelector('.swipe-indicator.left'); - const r = rowRef.querySelector('.swipe-indicator.right'); + rowRef.style.transition = ""; + rowRef.style.transform = ""; + rowRef.style.willChange = ""; + rowRef.style.boxShadow = ""; + rowRef.style.zIndex = ""; + rowRef.style.userSelect = ""; + const l = rowRef.querySelector(".swipe-indicator.left"); + const r = rowRef.querySelector(".swipe-indicator.right"); if (l) l.remove(); if (r) r.remove(); - rowRef.removeEventListener('transitionend', cleanup); + rowRef.removeEventListener("transitionend", cleanup); // only null the shared activeRow after cleanup finishes if (activeRow === rowRef) activeRow = null; }; - if (rowRef) rowRef.addEventListener('transitionend', cleanup); + if (rowRef) rowRef.addEventListener("transitionend", cleanup); } // Touch handlers - tbody.addEventListener('touchstart', (e) => { - const t = e.changedTouches[0]; - startDrag(t.clientX, t.clientY, e.target); - }, { passive: true }); - - tbody.addEventListener('touchmove', (e) => { - if (!dragging) return; - const t = e.changedTouches[0]; - moveDrag(t.clientX, t.clientY); - }, { passive: true }); - - tbody.addEventListener('touchend', (e) => { - const t = e.changedTouches[0]; - endDrag(t.clientX, t.clientY); - }, { passive: true }); - - tbody.addEventListener('touchcancel', () => { - if (activeRow) { - activeRow.style.transition = 'transform 150ms ease'; - activeRow.style.transform = 'translateX(0) scale(1)'; - activeRow.addEventListener('transitionend', () => { - if (activeRow) { - activeRow.classList.remove('swipe-dragging'); - activeRow.style.transition = ''; - activeRow.style.transform = ''; - activeRow = null; - } - }, { once: true }); - } - dragging = false; - }, { passive: true }); + tbody.addEventListener( + "touchstart", + (e) => { + const t = e.changedTouches[0]; + startDrag(t.clientX, t.clientY, e.target); + }, + { passive: true } + ); - -})(); \ No newline at end of file + tbody.addEventListener( + "touchmove", + (e) => { + if (!dragging) return; + const t = e.changedTouches[0]; + moveDrag(t.clientX, t.clientY); + }, + { passive: true } + ); + + tbody.addEventListener( + "touchend", + (e) => { + const t = e.changedTouches[0]; + endDrag(t.clientX, t.clientY); + }, + { passive: true } + ); + + tbody.addEventListener( + "touchcancel", + () => { + if (activeRow) { + activeRow.style.transition = "transform 150ms ease"; + activeRow.style.transform = "translateX(0) scale(1)"; + activeRow.addEventListener( + "transitionend", + () => { + if (activeRow) { + activeRow.classList.remove("swipe-dragging"); + activeRow.style.transition = ""; + activeRow.style.transform = ""; + activeRow = null; + } + }, + { once: true } + ); + } + dragging = false; + }, + { passive: true } + ); +})(); diff --git a/viewer/styles.css b/viewer/styles.css index e63ca3d..362a7df 100644 --- a/viewer/styles.css +++ b/viewer/styles.css @@ -383,6 +383,11 @@ body.dark-mode .log-container::-webkit-scrollbar-track { z-index: 0; } +.leaflet-interactive { + stroke-linejoin: round; + stroke-linecap: round; +} + .map-layer { filter: brightness(1) contrast(1); } @@ -1888,22 +1893,22 @@ body.dark-mode .letter::after { transform: rotateX(0deg); } 20% { - content: "R"; + content: "K"; transform: rotateX(360deg); } 40% { color: #afafaf; - content: "S"; + content: "O"; transform: rotateX(0deg); } 60% { color: #afafaf; - content: "C"; + content: "S"; transform: rotateX(360deg); } 80% { color: #afafaf; - content: "U"; + content: "2"; transform: rotateX(0deg); } 100% { diff --git a/viewer/styles.css.map b/viewer/styles.css.map index 5df145e..7f7634c 100644 --- a/viewer/styles.css.map +++ b/viewer/styles.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["styles.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;;;AAEF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAIF;EACE;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;AACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;AACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAIF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAIF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;AACA;EAAe;;;AACf;EAAe;;;AACf;EAAe;;;AACf;EAAe;;;AACf;EAAe;;;AAGf;AAUA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE,kBA3BgB;EA4BhB;EACA,OA7BgB;EA8BhB;EACA;EACA;EACA;EACA,OAjCU;EAkCV;;;AAEF;EACE,kBAnC0B;EAoC1B,OAnCoB;;;AAsCtB;EACE,kBA3CgB;EA4ChB;EACA;EACA;EACA;EACA;EACA;EACA,OAjDU;EAkDV;;;AAEF;EACE,kBAnD0B;EAoD1B,OAnDoB;EAoDpB;;;AAEF;EACE,kBA3DgB;EA4DhB;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAlEU;EAmEV;;;AAEF;EACE,kBApE0B;EAqE1B,OApEoB;;;AAsEtB;AACA;EACE;;;AAEF;EACE;;;AAGF;AAEE;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AAqFR;AAEE;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACE;IACE;;EAEF;IACE;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;;;AAIJ;AACA;AAAA;EAEE;EACA;;;AAGF;EACE;EACA;;;AAGF;AAAA;EAEE;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;AAEA;AAEA;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;AACA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;AACA;EACE;EACA;;;AAGF;AACA;EACE;;;AAEF;EACE;;;AAGF;AACA;EACE;IACE;;;AAIJ;EACE;IACE;;;AAIJ;EACE;IACE;;;AAIJ;AACI;EACF;AAAA;IAEE;;EAGF;IACE;;AAGF;EACA;IACE;IACA;IACA;;;AAIJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;IACA;;EAEF;IACE;;;AAIJ;EACE;IACE","file":"styles.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["styles.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;;;AAEF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAIF;EACE;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;AACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;AACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAIF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAIF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAGF;AACA;EAAe;;;AACf;EAAe;;;AACf;EAAe;;;AACf;EAAe;;;AACf;EAAe;;;AAGf;AAUA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE,kBA3BgB;EA4BhB;EACA,OA7BgB;EA8BhB;EACA;EACA;EACA;EACA,OAjCU;EAkCV;;;AAEF;EACE,kBAnC0B;EAoC1B,OAnCoB;;;AAsCtB;EACE,kBA3CgB;EA4ChB;EACA;EACA;EACA;EACA;EACA;EACA,OAjDU;EAkDV;;;AAEF;EACE,kBAnD0B;EAoD1B,OAnDoB;EAoDpB;;;AAEF;EACE,kBA3DgB;EA4DhB;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAlEU;EAmEV;;;AAEF;EACE,kBApE0B;EAqE1B,OApEoB;;;AAsEtB;AACA;EACE;;;AAEF;EACE;;;AAGF;AAEE;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AA8EN;EACE,SA/EI;;;AAiFN;EACE,SAlFI;;;AAqFR;AAEE;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAdF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACE;IACE;;EAEF;IACE;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;;;AAIJ;AACA;AAAA;EAEE;EACA;;;AAGF;EACE;EACA;;;AAGF;AAAA;EAEE;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;AAEA;AAEA;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;AACA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;AACA;EACE;EACA;;;AAGF;AACA;EACE;;;AAEF;EACE;;;AAGF;AACA;EACE;IACE;;;AAIJ;EACE;IACE;;;AAIJ;EACE;IACE;;;AAIJ;AACI;EACF;AAAA;IAEE;;EAGF;IACE;;AAGF;EACA;IACE;IACA;IACA;;;AAIJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;IACA;;EAEF;IACE;;;AAIJ;EACE;IACE","file":"styles.css"} \ No newline at end of file diff --git a/viewer/styles.scss b/viewer/styles.scss index 0b6ef63..4e5c0c1 100644 --- a/viewer/styles.scss +++ b/viewer/styles.scss @@ -364,6 +364,11 @@ body.dark-mode .log-container::-webkit-scrollbar-track { z-index: 0; } +.leaflet-interactive { + stroke-linejoin: round; + stroke-linecap: round; +} + .map-layer { filter: brightness(1) contrast(1); } diff --git a/viewer/viewer.html b/viewer/viewer.html index 1201e31..75028ee 100644 --- a/viewer/viewer.html +++ b/viewer/viewer.html @@ -57,7 +57,7 @@

Ramp Agent API

Logs Configs Dashboard -

Version 1.0.6

+

Version 1.0.7

@@ -133,7 +133,7 @@

Configs

-

Welcome back, User!

+

Welcome back, User!

Your API Key: