fix(routes): complete post-refactor dependency wiring cleanup
This commit is contained in:
@@ -53,5 +53,16 @@ module.exports = {
|
|||||||
'max-depth': 'off',
|
'max-depth': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Frontend assets use browser globals
|
||||||
|
files: ['assets/**/*.js', 'frontend/**/*.js'],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-undef': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,308 +1,308 @@
|
|||||||
/**
|
/**
|
||||||
* Theme Adapter
|
* Theme Adapter
|
||||||
* Ensures tooltips match the current dashboard theme
|
* Ensures tooltips match the current dashboard theme
|
||||||
* Integrates with Driver.js to apply theme-specific styling
|
* Integrates with Driver.js to apply theme-specific styling
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function(window) {
|
(function(window) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Theme configuration mapping for Driver.js
|
* Theme configuration mapping for Driver.js
|
||||||
* Maps dashboard themes to Driver.js styling
|
* Maps dashboard themes to Driver.js styling
|
||||||
*/
|
*/
|
||||||
const THEME_CONFIGS = {
|
const THEME_CONFIGS = {
|
||||||
dark: {
|
dark: {
|
||||||
backgroundColor: 'var(--card-base)',
|
backgroundColor: 'var(--card-base)',
|
||||||
textColor: 'var(--fg)',
|
textColor: 'var(--fg)',
|
||||||
primaryColor: 'var(--accent)',
|
primaryColor: 'var(--accent)',
|
||||||
overlayColor: 'rgba(0, 0, 0, 0.7)',
|
overlayColor: 'rgba(0, 0, 0, 0.7)',
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
highlightColor: 'var(--accent)',
|
highlightColor: 'var(--accent)',
|
||||||
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
||||||
},
|
},
|
||||||
light: {
|
light: {
|
||||||
backgroundColor: 'var(--card-base)',
|
backgroundColor: 'var(--card-base)',
|
||||||
textColor: 'var(--fg)',
|
textColor: 'var(--fg)',
|
||||||
primaryColor: 'var(--accent-strong)',
|
primaryColor: 'var(--accent-strong)',
|
||||||
overlayColor: 'rgba(0, 0, 0, 0.5)',
|
overlayColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
highlightColor: 'var(--accent-strong)',
|
highlightColor: 'var(--accent-strong)',
|
||||||
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
||||||
},
|
},
|
||||||
blue: {
|
blue: {
|
||||||
backgroundColor: 'var(--card-base)',
|
backgroundColor: 'var(--card-base)',
|
||||||
textColor: 'var(--fg)',
|
textColor: 'var(--fg)',
|
||||||
primaryColor: 'var(--accent)',
|
primaryColor: 'var(--accent)',
|
||||||
overlayColor: 'rgba(25, 8, 172, 0.7)',
|
overlayColor: 'rgba(25, 8, 172, 0.7)',
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
highlightColor: 'var(--accent)',
|
highlightColor: 'var(--accent)',
|
||||||
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
||||||
},
|
},
|
||||||
nord: {
|
nord: {
|
||||||
backgroundColor: 'var(--card-base)',
|
backgroundColor: 'var(--card-base)',
|
||||||
textColor: 'var(--fg)',
|
textColor: 'var(--fg)',
|
||||||
primaryColor: 'var(--accent)',
|
primaryColor: 'var(--accent)',
|
||||||
overlayColor: 'rgba(46, 52, 64, 0.7)',
|
overlayColor: 'rgba(46, 52, 64, 0.7)',
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
highlightColor: 'var(--accent)',
|
highlightColor: 'var(--accent)',
|
||||||
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
||||||
},
|
},
|
||||||
dracula: {
|
dracula: {
|
||||||
backgroundColor: 'var(--card-base)',
|
backgroundColor: 'var(--card-base)',
|
||||||
textColor: 'var(--fg)',
|
textColor: 'var(--fg)',
|
||||||
primaryColor: 'var(--accent)',
|
primaryColor: 'var(--accent)',
|
||||||
overlayColor: 'rgba(40, 42, 54, 0.7)',
|
overlayColor: 'rgba(40, 42, 54, 0.7)',
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
highlightColor: 'var(--accent)',
|
highlightColor: 'var(--accent)',
|
||||||
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
||||||
},
|
},
|
||||||
'solarized-dark': {
|
'solarized-dark': {
|
||||||
backgroundColor: 'var(--card-base)',
|
backgroundColor: 'var(--card-base)',
|
||||||
textColor: 'var(--fg)',
|
textColor: 'var(--fg)',
|
||||||
primaryColor: 'var(--accent)',
|
primaryColor: 'var(--accent)',
|
||||||
overlayColor: 'rgba(0, 43, 54, 0.7)',
|
overlayColor: 'rgba(0, 43, 54, 0.7)',
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
highlightColor: 'var(--accent)',
|
highlightColor: 'var(--accent)',
|
||||||
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
||||||
},
|
},
|
||||||
'solarized-light': {
|
'solarized-light': {
|
||||||
backgroundColor: 'var(--card-base)',
|
backgroundColor: 'var(--card-base)',
|
||||||
textColor: 'var(--fg)',
|
textColor: 'var(--fg)',
|
||||||
primaryColor: 'var(--accent)',
|
primaryColor: 'var(--accent)',
|
||||||
overlayColor: 'rgba(253, 246, 227, 0.7)',
|
overlayColor: 'rgba(253, 246, 227, 0.7)',
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
highlightColor: 'var(--accent)',
|
highlightColor: 'var(--accent)',
|
||||||
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ThemeAdapter class
|
* ThemeAdapter class
|
||||||
* Manages theme integration for the tooltip system
|
* Manages theme integration for the tooltip system
|
||||||
*/
|
*/
|
||||||
class ThemeAdapter {
|
class ThemeAdapter {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.currentTheme = this.getCurrentTheme();
|
this.currentTheme = this.getCurrentTheme();
|
||||||
this.themeChangeCallbacks = [];
|
this.themeChangeCallbacks = [];
|
||||||
this._setupThemeChangeListener();
|
this._setupThemeChangeListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current theme name from document root class
|
* Get the current theme name from document root class
|
||||||
* @returns {string} Current theme name (e.g., 'dark', 'light', 'blue')
|
* @returns {string} Current theme name (e.g., 'dark', 'light', 'blue')
|
||||||
*/
|
*/
|
||||||
getCurrentTheme() {
|
getCurrentTheme() {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const classList = Array.from(root.classList);
|
const classList = Array.from(root.classList);
|
||||||
|
|
||||||
// Check for theme classes
|
// Check for theme classes
|
||||||
const themeClasses = ['light', 'blue', 'nord', 'dracula', 'solarized-dark', 'solarized-light'];
|
const themeClasses = ['light', 'blue', 'nord', 'dracula', 'solarized-dark', 'solarized-light'];
|
||||||
const foundTheme = themeClasses.find(theme => classList.includes(theme));
|
const foundTheme = themeClasses.find(theme => classList.includes(theme));
|
||||||
|
|
||||||
// Default to 'dark' if no theme class found
|
// Default to 'dark' if no theme class found
|
||||||
return foundTheme || 'dark';
|
return foundTheme || 'dark';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Driver.js theme configuration for current theme
|
* Get Driver.js theme configuration for current theme
|
||||||
* @returns {Object} Theme configuration object
|
* @returns {Object} Theme configuration object
|
||||||
*/
|
*/
|
||||||
getDriverTheme() {
|
getDriverTheme() {
|
||||||
const themeName = this.getCurrentTheme();
|
const themeName = this.getCurrentTheme();
|
||||||
const config = THEME_CONFIGS[themeName] || THEME_CONFIGS.dark;
|
const config = THEME_CONFIGS[themeName] || THEME_CONFIGS.dark;
|
||||||
|
|
||||||
// Resolve CSS variables to actual values
|
// Resolve CSS variables to actual values
|
||||||
const resolvedConfig = {};
|
const resolvedConfig = {};
|
||||||
for (const [key, value] of Object.entries(config)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
if (typeof value === 'string' && value.startsWith('var(')) {
|
if (typeof value === 'string' && value.startsWith('var(')) {
|
||||||
// Extract CSS variable name
|
// Extract CSS variable name
|
||||||
const varName = value.match(/var\((--[^)]+)\)/)?.[1];
|
const varName = value.match(/var\((--[^)]+)\)/)?.[1];
|
||||||
if (varName) {
|
if (varName) {
|
||||||
const computedValue = getComputedStyle(document.documentElement)
|
const computedValue = getComputedStyle(document.documentElement)
|
||||||
.getPropertyValue(varName)
|
.getPropertyValue(varName)
|
||||||
.trim();
|
.trim();
|
||||||
resolvedConfig[key] = computedValue || value;
|
resolvedConfig[key] = computedValue || value;
|
||||||
} else {
|
} else {
|
||||||
resolvedConfig[key] = value;
|
resolvedConfig[key] = value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolvedConfig[key] = value;
|
resolvedConfig[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvedConfig;
|
return resolvedConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a callback for theme changes
|
* Register a callback for theme changes
|
||||||
* @param {Function} callback - Function to call when theme changes
|
* @param {Function} callback - Function to call when theme changes
|
||||||
*/
|
*/
|
||||||
onThemeChange(callback) {
|
onThemeChange(callback) {
|
||||||
if (typeof callback === 'function') {
|
if (typeof callback === 'function') {
|
||||||
this.themeChangeCallbacks.push(callback);
|
this.themeChangeCallbacks.push(callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup theme change listener using MutationObserver
|
* Setup theme change listener using MutationObserver
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_setupThemeChangeListener() {
|
_setupThemeChangeListener() {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
// Create observer to watch for class changes on root element
|
// Create observer to watch for class changes on root element
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||||
const newTheme = this.getCurrentTheme();
|
const newTheme = this.getCurrentTheme();
|
||||||
if (newTheme !== this.currentTheme) {
|
if (newTheme !== this.currentTheme) {
|
||||||
const oldTheme = this.currentTheme;
|
const oldTheme = this.currentTheme;
|
||||||
this.currentTheme = newTheme;
|
this.currentTheme = newTheme;
|
||||||
this._notifyThemeChange(newTheme, oldTheme);
|
this._notifyThemeChange(newTheme, oldTheme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start observing
|
// Start observing
|
||||||
observer.observe(root, {
|
observer.observe(root, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ['class']
|
attributeFilter: ['class']
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[ThemeAdapter] Theme change listener initialized');
|
console.log('[ThemeAdapter] Theme change listener initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify all registered callbacks of theme change
|
* Notify all registered callbacks of theme change
|
||||||
* @private
|
* @private
|
||||||
* @param {string} newTheme - New theme name
|
* @param {string} newTheme - New theme name
|
||||||
* @param {string} oldTheme - Old theme name
|
* @param {string} oldTheme - Old theme name
|
||||||
*/
|
*/
|
||||||
_notifyThemeChange(newTheme, oldTheme) {
|
_notifyThemeChange(newTheme, oldTheme) {
|
||||||
console.log(`[ThemeAdapter] Theme changed: ${oldTheme} → ${newTheme}`);
|
console.log(`[ThemeAdapter] Theme changed: ${oldTheme} → ${newTheme}`);
|
||||||
|
|
||||||
this.themeChangeCallbacks.forEach(callback => {
|
this.themeChangeCallbacks.forEach(callback => {
|
||||||
try {
|
try {
|
||||||
callback(newTheme, oldTheme);
|
callback(newTheme, oldTheme);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ThemeAdapter] Error in theme change callback:', error);
|
console.error('[ThemeAdapter] Error in theme change callback:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply theme to Driver.js instance
|
* Apply theme to Driver.js instance
|
||||||
* @param {Object} driver - Driver.js instance
|
* @param {Object} driver - Driver.js instance
|
||||||
*/
|
*/
|
||||||
applyTheme(driver) {
|
applyTheme(driver) {
|
||||||
if (!driver) {
|
if (!driver) {
|
||||||
console.warn('[ThemeAdapter] No driver instance provided');
|
console.warn('[ThemeAdapter] No driver instance provided');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeConfig = this.getDriverTheme();
|
const themeConfig = this.getDriverTheme();
|
||||||
|
|
||||||
// Apply theme configuration to driver
|
// Apply theme configuration to driver
|
||||||
// Note: Driver.js v1.0+ uses CSS variables, so we inject a style element
|
// Note: Driver.js v1.0+ uses CSS variables, so we inject a style element
|
||||||
this._injectDriverStyles(themeConfig);
|
this._injectDriverStyles(themeConfig);
|
||||||
|
|
||||||
console.log('[ThemeAdapter] Theme applied to driver:', this.currentTheme);
|
console.log('[ThemeAdapter] Theme applied to driver:', this.currentTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inject custom styles for Driver.js based on theme
|
* Inject custom styles for Driver.js based on theme
|
||||||
* @private
|
* @private
|
||||||
* @param {Object} themeConfig - Theme configuration
|
* @param {Object} themeConfig - Theme configuration
|
||||||
*/
|
*/
|
||||||
_injectDriverStyles(themeConfig) {
|
_injectDriverStyles(themeConfig) {
|
||||||
// Remove existing theme styles
|
// Remove existing theme styles
|
||||||
const existingStyle = document.getElementById('driver-theme-styles');
|
const existingStyle = document.getElementById('driver-theme-styles');
|
||||||
if (existingStyle) {
|
if (existingStyle) {
|
||||||
existingStyle.remove();
|
existingStyle.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new style element
|
// Create new style element
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.id = 'driver-theme-styles';
|
style.id = 'driver-theme-styles';
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.driver-popover {
|
.driver-popover {
|
||||||
background: ${themeConfig.backgroundColor} !important;
|
background: ${themeConfig.backgroundColor} !important;
|
||||||
color: ${themeConfig.textColor} !important;
|
color: ${themeConfig.textColor} !important;
|
||||||
border: 1px solid ${themeConfig.borderColor} !important;
|
border: 1px solid ${themeConfig.borderColor} !important;
|
||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4) !important;
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4) !important;
|
||||||
font-family: ${themeConfig.fontFamily} !important;
|
font-family: ${themeConfig.fontFamily} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-title {
|
.driver-popover-title {
|
||||||
color: ${themeConfig.textColor} !important;
|
color: ${themeConfig.textColor} !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
font-family: ${themeConfig.fontFamily} !important;
|
font-family: ${themeConfig.fontFamily} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-description {
|
.driver-popover-description {
|
||||||
color: ${themeConfig.textColor} !important;
|
color: ${themeConfig.textColor} !important;
|
||||||
font-family: ${themeConfig.fontFamily} !important;
|
font-family: ${themeConfig.fontFamily} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-footer button {
|
.driver-popover-footer button {
|
||||||
background: ${themeConfig.primaryColor} !important;
|
background: ${themeConfig.primaryColor} !important;
|
||||||
color: ${themeConfig.backgroundColor} !important;
|
color: ${themeConfig.backgroundColor} !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
font-family: ${themeConfig.fontFamily} !important;
|
font-family: ${themeConfig.fontFamily} !important;
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-footer button:hover {
|
.driver-popover-footer button:hover {
|
||||||
opacity: 0.9 !important;
|
opacity: 0.9 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-close-btn {
|
.driver-popover-close-btn {
|
||||||
color: ${themeConfig.textColor} !important;
|
color: ${themeConfig.textColor} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-overlay {
|
.driver-overlay {
|
||||||
background: ${themeConfig.overlayColor} !important;
|
background: ${themeConfig.overlayColor} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-highlighted-element {
|
.driver-highlighted-element {
|
||||||
outline: 2px solid ${themeConfig.highlightColor} !important;
|
outline: 2px solid ${themeConfig.highlightColor} !important;
|
||||||
outline-offset: 4px !important;
|
outline-offset: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.driver-popover-progress-text {
|
.driver-popover-progress-text {
|
||||||
color: ${themeConfig.textColor} !important;
|
color: ${themeConfig.textColor} !important;
|
||||||
opacity: 0.7 !important;
|
opacity: 0.7 !important;
|
||||||
font-family: ${themeConfig.fontFamily} !important;
|
font-family: ${themeConfig.fontFamily} !important;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all available theme names
|
* Get all available theme names
|
||||||
* @returns {string[]} Array of theme names
|
* @returns {string[]} Array of theme names
|
||||||
*/
|
*/
|
||||||
getAvailableThemes() {
|
getAvailableThemes() {
|
||||||
return Object.keys(THEME_CONFIGS);
|
return Object.keys(THEME_CONFIGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a theme is available
|
* Check if a theme is available
|
||||||
* @param {string} themeName - Theme name to check
|
* @param {string} themeName - Theme name to check
|
||||||
* @returns {boolean} True if theme is available
|
* @returns {boolean} True if theme is available
|
||||||
*/
|
*/
|
||||||
isThemeAvailable(themeName) {
|
isThemeAvailable(themeName) {
|
||||||
return THEME_CONFIGS.hasOwnProperty(themeName);
|
return Object.hasOwn(THEME_CONFIGS, themeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export to global scope
|
// Export to global scope
|
||||||
window.ThemeAdapter = ThemeAdapter;
|
window.ThemeAdapter = ThemeAdapter;
|
||||||
|
|
||||||
console.log('[ThemeAdapter] Module loaded');
|
console.log('[ThemeAdapter] Module loaded');
|
||||||
|
|
||||||
})(window);
|
})(window);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class BackupManager extends EventEmitter {
|
|||||||
case 'monthly':
|
case 'monthly':
|
||||||
intervalMs = 30 * 24 * 60 * 60 * 1000;
|
intervalMs = 30 * 24 * 60 * 60 * 1000;
|
||||||
break;
|
break;
|
||||||
default:
|
default: {
|
||||||
// Custom interval in minutes
|
// Custom interval in minutes
|
||||||
const minutes = parseInt(backup.schedule, 10);
|
const minutes = parseInt(backup.schedule, 10);
|
||||||
if (!isNaN(minutes) && minutes > 0) {
|
if (!isNaN(minutes) && minutes > 0) {
|
||||||
@@ -84,6 +84,7 @@ class BackupManager extends EventEmitter {
|
|||||||
console.error(`[BackupManager] Invalid schedule for ${name}: ${backup.schedule}`);
|
console.error(`[BackupManager] Invalid schedule for ${name}: ${backup.schedule}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule the job
|
// Schedule the job
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const colors = {
|
|||||||
magenta: '\x1b[35m'
|
magenta: '\x1b[35m'
|
||||||
};
|
};
|
||||||
|
|
||||||
let testResults = {
|
const testResults = {
|
||||||
passed: 0,
|
passed: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
warnings: 0,
|
warnings: 0,
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ async function validateSecurePath(requestedPath, allowedRoots, auditLogger = nul
|
|||||||
const suspiciousPatterns = [
|
const suspiciousPatterns = [
|
||||||
/\.\./, // ..
|
/\.\./, // ..
|
||||||
/%2e%2e/i, // URL encoded ..
|
/%2e%2e/i, // URL encoded ..
|
||||||
/\.\%2f/i, // .%2F (encoded ./)
|
/\.%2f/i, // .%2F (encoded ./)
|
||||||
/%2e\./i, // %2E.
|
/%2e\./i, // %2E.
|
||||||
/\.\\/, // .\ (Windows)
|
/\.\\/, // .\ (Windows)
|
||||||
/%5c/i // URL encoded backslash
|
/%5c/i // URL encoded backslash
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ const platformPaths = require('../../platform-paths');
|
|||||||
* @param {Object} deps.log - Logger instance
|
* @param {Object} deps.log - Logger instance
|
||||||
* @returns {Object} Helper functions
|
* @returns {Object} Helper functions
|
||||||
*/
|
*/
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ docker, caddy, credentialManager, servicesStateManager, fetchT, log, ctx }) {
|
||||||
const { docker, caddy, credentialManager, servicesStateManager, fetchT, log } = ctx;
|
|
||||||
|
|
||||||
async function checkPortConflicts(ports, excludeContainerName = null) {
|
async function checkPortConflicts(ports, excludeContainerName = null) {
|
||||||
const conflicts = [];
|
const conflicts = [];
|
||||||
|
|||||||
@@ -27,17 +27,21 @@ module.exports = function(ctx) {
|
|||||||
log: ctx.log,
|
log: ctx.log,
|
||||||
// Additional context properties needed by routes
|
// Additional context properties needed by routes
|
||||||
APP_TEMPLATES: ctx.APP_TEMPLATES,
|
APP_TEMPLATES: ctx.APP_TEMPLATES,
|
||||||
|
TEMPLATE_CATEGORIES: ctx.TEMPLATE_CATEGORIES,
|
||||||
|
DIFFICULTY_LEVELS: ctx.DIFFICULTY_LEVELS,
|
||||||
siteConfig: ctx.siteConfig,
|
siteConfig: ctx.siteConfig,
|
||||||
buildDomain: ctx.buildDomain,
|
buildDomain: ctx.buildDomain,
|
||||||
buildServiceUrl: ctx.buildServiceUrl,
|
buildServiceUrl: ctx.buildServiceUrl,
|
||||||
addServiceToConfig: ctx.addServiceToConfig,
|
addServiceToConfig: ctx.addServiceToConfig,
|
||||||
dns: ctx.dns,
|
dns: ctx.dns,
|
||||||
notification: ctx.notification,
|
notification: ctx.notification,
|
||||||
safeErrorMessage: ctx.safeErrorMessage
|
safeErrorMessage: ctx.safeErrorMessage,
|
||||||
|
SERVICES_FILE: ctx.SERVICES_FILE,
|
||||||
|
ctx: ctx
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize helpers with dependencies
|
// Initialize helpers with dependencies (ctx is the Koa context)
|
||||||
const helpers = initHelpers(ctx);
|
const helpers = initHelpers({ ...deps, ctx });
|
||||||
|
|
||||||
// Mount sub-routes — pass full ctx so sub-routes can reference ctx.* properties
|
// Mount sub-routes — pass full ctx so sub-routes can reference ctx.* properties
|
||||||
const subCtx = Object.assign({}, ctx, { helpers });
|
const subCtx = Object.assign({}, ctx, { helpers });
|
||||||
|
|||||||
@@ -1,20 +1,40 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { exists } = require('../../fs-helpers');
|
const { exists } = require('../../fs-helpers');
|
||||||
|
const { logError } = require('../../src/utils/logging');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function({
|
||||||
const { docker, caddy, servicesStateManager, asyncHandler, errorResponse, log, helpers } = ctx;
|
docker, caddy, servicesStateManager, asyncHandler, log, helpers,
|
||||||
|
errorResponse, dns, siteConfig, buildDomain, SERVICES_FILE, safeErrorMessage
|
||||||
|
}) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
/**
|
|
||||||
* Apps removal routes factory
|
// Ctx shim for backward compatibility with existing route code
|
||||||
* @param {Object} deps - Explicit dependencies
|
const ctx = {
|
||||||
* @param {Object} deps.docker - Docker client wrapper
|
dns,
|
||||||
* @param {Object} deps.caddy - Caddy client
|
siteConfig,
|
||||||
* @param {Object} deps.servicesStateManager - Services state manager
|
buildDomain,
|
||||||
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
SERVICES_FILE,
|
||||||
* @param {Object} deps.log - Logger instance
|
safeErrorMessage
|
||||||
* @param {Object} deps.helpers - Apps helpers module
|
};
|
||||||
* @returns {express.Router}
|
|
||||||
*/
|
// Remove deployed app
|
||||||
|
/**
|
||||||
|
* Apps removal routes factory
|
||||||
|
* @param {Object} deps - Explicit dependencies
|
||||||
|
* @param {Object} deps.docker - Docker client wrapper
|
||||||
|
* @param {Object} deps.caddy - Caddy client
|
||||||
|
* @param {Object} deps.servicesStateManager - Services state manager
|
||||||
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
|
* @param {Object} deps.log - Logger instance
|
||||||
|
* @param {Object} deps.helpers - Apps helpers module
|
||||||
|
* @param {Function} deps.errorResponse - Error response helper
|
||||||
|
* @param {Object} deps.dns - DNS context
|
||||||
|
* @param {Object} deps.siteConfig - Site configuration
|
||||||
|
* @param {Function} deps.buildDomain - Build domain helper
|
||||||
|
* @param {string} deps.SERVICES_FILE - Services file path
|
||||||
|
* @param {Function} deps.safeErrorMessage - Safe error message formatter
|
||||||
|
* @returns {express.Router}
|
||||||
|
*/
|
||||||
router.delete('/apps/:appId', asyncHandler(async (req, res) => {
|
router.delete('/apps/:appId', asyncHandler(async (req, res) => {
|
||||||
const { appId } = req.params;
|
const { appId } = req.params;
|
||||||
const { containerId, subdomain, ip, deleteContainer } = req.query;
|
const { containerId, subdomain, ip, deleteContainer } = req.query;
|
||||||
|
|||||||
@@ -8,14 +8,23 @@ const { DOCKER } = require('../../constants');
|
|||||||
* @param {Object} deps.caddy - Caddy client
|
* @param {Object} deps.caddy - Caddy client
|
||||||
* @param {Object} deps.servicesStateManager - Services state manager
|
* @param {Object} deps.servicesStateManager - Services state manager
|
||||||
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
|
* @param {Function} deps.errorResponse - Error response helper
|
||||||
* @param {Object} deps.log - Logger instance
|
* @param {Object} deps.log - Logger instance
|
||||||
* @param {Object} deps.helpers - Apps helpers module
|
* @param {Object} deps.helpers - Apps helpers module
|
||||||
|
* @param {Object} deps.APP_TEMPLATES - App templates registry
|
||||||
|
* @param {Object} deps.dns - DNS client
|
||||||
|
* @param {Function} deps.buildServiceUrl - Service URL builder
|
||||||
* @returns {express.Router}
|
* @returns {express.Router}
|
||||||
*/
|
*/
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, errorResponse, log, helpers, APP_TEMPLATES, dns, buildServiceUrl }) {
|
||||||
const { docker, caddy, servicesStateManager, asyncHandler, log, helpers } = ctx;
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
APP_TEMPLATES,
|
||||||
|
dns,
|
||||||
|
buildServiceUrl
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore a single service from its deployment manifest.
|
* Restore a single service from its deployment manifest.
|
||||||
* Pulls image, creates container, starts it, recreates Caddy config.
|
* Pulls image, creates container, starts it, recreates Caddy config.
|
||||||
@@ -125,11 +134,6 @@ module.exports = function(ctx) {
|
|||||||
// Static sites: just recreate Caddy config
|
// Static sites: just recreate Caddy config
|
||||||
if (template?.isStaticSite) {
|
if (template?.isStaticSite) {
|
||||||
log.info('restore', `Restoring static site Caddy config: ${service.name}`);
|
log.info('restore', `Restoring static site Caddy config: ${service.name}`);
|
||||||
const caddyOptions = {
|
|
||||||
tailscaleOnly: manifest.caddy.tailscaleOnly,
|
|
||||||
allowedIPs: manifest.caddy.allowedIPs,
|
|
||||||
subpathSupport: manifest.caddy.subpathSupport,
|
|
||||||
};
|
|
||||||
// Static site Caddy config would need to be regenerated
|
// Static site Caddy config would need to be regenerated
|
||||||
// For now, just confirm the service entry exists
|
// For now, just confirm the service entry exists
|
||||||
return {
|
return {
|
||||||
@@ -288,7 +292,7 @@ module.exports = function(ctx) {
|
|||||||
const svc = services.find(s => s.id === service.id);
|
const svc = services.find(s => s.id === service.id);
|
||||||
if (svc) {
|
if (svc) {
|
||||||
svc.containerId = container.id;
|
svc.containerId = container.id;
|
||||||
svc.url = ctx.buildServiceUrl(manifest.config.subdomain);
|
svc.url = buildServiceUrl(manifest.config.subdomain);
|
||||||
}
|
}
|
||||||
return services;
|
return services;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,14 +6,40 @@ const { exists } = require('../../fs-helpers');
|
|||||||
* @param {Object} deps.servicesStateManager - Services state manager
|
* @param {Object} deps.servicesStateManager - Services state manager
|
||||||
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
* @param {Object} deps.helpers - Apps helpers module
|
* @param {Object} deps.helpers - Apps helpers module
|
||||||
|
* @param {Object} deps.APP_TEMPLATES - App templates registry
|
||||||
|
* @param {Object} deps.TEMPLATE_CATEGORIES - Template categories
|
||||||
|
* @param {Object} deps.DIFFICULTY_LEVELS - Difficulty levels
|
||||||
|
* @param {Object} deps.docker - Docker client
|
||||||
|
* @param {Object} deps.caddy - Caddy client
|
||||||
|
* @param {Object} deps.dns - DNS context
|
||||||
|
* @param {Object} deps.siteConfig - Site configuration
|
||||||
|
* @param {Function} deps.buildDomain - Build domain helper
|
||||||
|
* @param {Function} deps.errorResponse - Error response helper
|
||||||
|
* @param {Object} deps.log - Logger instance
|
||||||
|
* @param {string} deps.SERVICES_FILE - Services file path
|
||||||
* @returns {express.Router}
|
* @returns {express.Router}
|
||||||
*/
|
*/
|
||||||
const { REGEX } = require('../../constants');
|
const { REGEX } = require('../../constants');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function({
|
||||||
const { servicesStateManager, asyncHandler, helpers, docker, caddy, log, errorResponse } = ctx;
|
servicesStateManager, asyncHandler, helpers,
|
||||||
|
APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS,
|
||||||
|
docker, caddy, dns, siteConfig, buildDomain,
|
||||||
|
errorResponse, log, SERVICES_FILE
|
||||||
|
}) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Ctx shim for backward compatibility with existing route code
|
||||||
|
const ctx = {
|
||||||
|
APP_TEMPLATES,
|
||||||
|
TEMPLATE_CATEGORIES,
|
||||||
|
DIFFICULTY_LEVELS,
|
||||||
|
dns,
|
||||||
|
siteConfig,
|
||||||
|
buildDomain,
|
||||||
|
SERVICES_FILE
|
||||||
|
};
|
||||||
|
|
||||||
// Get available app templates
|
// Get available app templates
|
||||||
router.get('/apps/templates', asyncHandler(async (req, res) => {
|
router.get('/apps/templates', asyncHandler(async (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
@@ -64,6 +90,8 @@ module.exports = function(ctx) {
|
|||||||
// Update subdomain for deployed app
|
// Update subdomain for deployed app
|
||||||
router.post('/apps/update-subdomain', asyncHandler(async (req, res) => {
|
router.post('/apps/update-subdomain', asyncHandler(async (req, res) => {
|
||||||
const { serviceId, oldSubdomain, newSubdomain, containerId, ip } = req.body;
|
const { serviceId, oldSubdomain, newSubdomain, containerId, ip } = req.body;
|
||||||
|
const { ValidationError } = require('../../errors');
|
||||||
|
|
||||||
if (!oldSubdomain || typeof oldSubdomain !== 'string') {
|
if (!oldSubdomain || typeof oldSubdomain !== 'string') {
|
||||||
throw new ValidationError('oldSubdomain is required');
|
throw new ValidationError('oldSubdomain is required');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ module.exports = function(ctx) {
|
|||||||
const results = { radarr: null, sonarr: null };
|
const results = { radarr: null, sonarr: null };
|
||||||
|
|
||||||
// Step 1: Authenticate with Overseerr via Plex token
|
// Step 1: Authenticate with Overseerr via Plex token
|
||||||
let overseerrUrl = `http://host.docker.internal:${APP_PORTS.overseerr}`;
|
const overseerrUrl = `http://host.docker.internal:${APP_PORTS.overseerr}`;
|
||||||
const overseerrSession = await helpers.getOverseerrSession();
|
const overseerrSession = await helpers.getOverseerrSession();
|
||||||
|
|
||||||
if (!overseerrSession) {
|
if (!overseerrSession) {
|
||||||
@@ -227,7 +227,7 @@ module.exports = function(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normalize URL - remove trailing slash
|
// Normalize URL - remove trailing slash
|
||||||
let baseUrl = url.replace(/\/+$/, '');
|
const baseUrl = url.replace(/\/+$/, '');
|
||||||
|
|
||||||
// Build the API endpoint
|
// Build the API endpoint
|
||||||
let apiEndpoint;
|
let apiEndpoint;
|
||||||
|
|||||||
@@ -9,12 +9,18 @@ const { APP_PORTS } = require('../../constants');
|
|||||||
* @param {Function} deps.errorResponse - Error response helper
|
* @param {Function} deps.errorResponse - Error response helper
|
||||||
* @param {Object} deps.log - Logger instance
|
* @param {Object} deps.log - Logger instance
|
||||||
* @param {Object} deps.helpers - Arr helpers module
|
* @param {Object} deps.helpers - Arr helpers module
|
||||||
|
* @param {Object} deps.credentialManager - Credential manager
|
||||||
|
* @param {Object} deps.servicesStateManager - Services state manager
|
||||||
* @returns {express.Router}
|
* @returns {express.Router}
|
||||||
*/
|
*/
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ fetchT, asyncHandler, errorResponse, log: _log, helpers, credentialManager, servicesStateManager }) {
|
||||||
const { fetchT, asyncHandler, errorResponse, log, helpers } = ctx;
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
credentialManager,
|
||||||
|
servicesStateManager
|
||||||
|
};
|
||||||
|
|
||||||
// Plex Libraries endpoint
|
// Plex Libraries endpoint
|
||||||
router.get('/plex/libraries', asyncHandler(async (req, res) => {
|
router.get('/plex/libraries', asyncHandler(async (req, res) => {
|
||||||
// Get Plex token
|
// Get Plex token
|
||||||
|
|||||||
@@ -10,12 +10,18 @@ const { APP_PORTS } = require('../../constants');
|
|||||||
* @param {Function} deps.errorResponse - Error response helper
|
* @param {Function} deps.errorResponse - Error response helper
|
||||||
* @param {Object} deps.log - Logger instance
|
* @param {Object} deps.log - Logger instance
|
||||||
* @param {Object} deps.helpers - Arr helpers module
|
* @param {Object} deps.helpers - Arr helpers module
|
||||||
|
* @param {Object} deps.servicesStateManager - Services state manager
|
||||||
|
* @param {Object} deps.notification - Notification helper
|
||||||
* @returns {express.Router}
|
* @returns {express.Router}
|
||||||
*/
|
*/
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ credentialManager, servicesStateManager, fetchT, asyncHandler, errorResponse: _errorResponse, log: _log, helpers, notification }) {
|
||||||
const { credentialManager, fetchT, asyncHandler, errorResponse, log, helpers } = ctx;
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
servicesStateManager,
|
||||||
|
notification
|
||||||
|
};
|
||||||
|
|
||||||
// Smart Connect: Unified orchestration endpoint
|
// Smart Connect: Unified orchestration endpoint
|
||||||
router.post('/arr/smart-connect', asyncHandler(async (req, res) => {
|
router.post('/arr/smart-connect', asyncHandler(async (req, res) => {
|
||||||
const { services: inputServices, configurePlex, configureProwlarr, configureSeerr, saveCredentials } = req.body;
|
const { services: inputServices, configurePlex, configureProwlarr, configureSeerr, saveCredentials } = req.body;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants');
|
const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants');
|
||||||
const { createCache, CACHE_CONFIGS } = require('../../cache-config');
|
const { createCache, CACHE_CONFIGS } = require('../../cache-config');
|
||||||
|
|
||||||
module.exports = function({ authManager, credentialManager, fetchT, asyncHandler, errorResponse, log }) {
|
|
||||||
// App session cache for auto-login
|
|
||||||
/**
|
/**
|
||||||
* Auth session handlers routes factory
|
* Auth session handlers routes factory
|
||||||
* @param {Object} deps - Explicit dependencies
|
* @param {Object} deps - Explicit dependencies
|
||||||
@@ -11,8 +9,11 @@ module.exports = function({ authManager, credentialManager, fetchT, asyncHandler
|
|||||||
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
* @param {Function} deps.errorResponse - Error response helper
|
* @param {Function} deps.errorResponse - Error response helper
|
||||||
* @param {Object} deps.log - Logger instance
|
* @param {Object} deps.log - Logger instance
|
||||||
* @returns {express.Router}
|
* @param {Function} deps.fetchT - Timeout-wrapped fetch
|
||||||
|
* @returns {{ getAppSession: Function, appSessionCache: Object }}
|
||||||
*/
|
*/
|
||||||
|
module.exports = function({ authManager: _authManager, credentialManager: _credentialManager, asyncHandler: _asyncHandler, errorResponse: _errorResponse, log, fetchT }) {
|
||||||
|
const ctx = { fetchT };
|
||||||
const appSessionCache = createCache(CACHE_CONFIGS.appSessions);
|
const appSessionCache = createCache(CACHE_CONFIGS.appSessions);
|
||||||
|
|
||||||
async function getAppSession(serviceId, baseUrl, username, password) {
|
async function getAppSession(serviceId, baseUrl, username, password) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const fsp = require('fs').promises;
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { exists, isAccessible } = require('../fs-helpers');
|
const { exists, isAccessible } = require('../fs-helpers');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
const { ValidationError } = require('../errors');
|
const { ValidationError, ForbiddenError } = require('../errors');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browse route factory
|
* Browse route factory
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const fsp = require('fs').promises;
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
const { exists } = require('../fs-helpers');
|
const { exists } = require('../fs-helpers');
|
||||||
|
const { ValidationError } = require('../errors');
|
||||||
const platformPaths = require('../platform-paths');
|
const platformPaths = require('../platform-paths');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function(ctx) {
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ try {
|
|||||||
// Image processing libraries not available — favicon conversion disabled
|
// Image processing libraries not available — favicon conversion disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ servicesStateManager: _servicesStateManager, asyncHandler, log: _log, CONFIG_FILE, readConfig, saveConfig, errorResponse }) {
|
||||||
const { servicesStateManager, asyncHandler, log } = ctx;
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const ctx = { CONFIG_FILE, readConfig, saveConfig, errorResponse };
|
||||||
|
|
||||||
// ===== ASSET UPLOAD =====
|
// ===== ASSET UPLOAD =====
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ module.exports = function(ctx) {
|
|||||||
throw new ValidationError('Invalid image data format');
|
throw new ValidationError('Invalid image data format');
|
||||||
}
|
}
|
||||||
|
|
||||||
const extension = matches[1] === 'svg+xml' ? 'svg' : matches[1];
|
|
||||||
const base64Data = matches[2];
|
const base64Data = matches[2];
|
||||||
const buffer = Buffer.from(base64Data, 'base64');
|
const buffer = Buffer.from(base64Data, 'base64');
|
||||||
|
|
||||||
@@ -109,6 +108,7 @@ module.exports = function(ctx) {
|
|||||||
|
|
||||||
// Upload custom logo(s) and/or update position and title
|
// Upload custom logo(s) and/or update position and title
|
||||||
// Supports: dataDark/dataLight (separate variants) or data (single logo for both)
|
// Supports: dataDark/dataLight (separate variants) or data (single logo for both)
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
router.post('/logo', express.json({ limit: LIMITS.BODY_UPLOAD }), asyncHandler(async (req, res) => {
|
router.post('/logo', express.json({ limit: LIMITS.BODY_UPLOAD }), asyncHandler(async (req, res) => {
|
||||||
const { data, dataDark, dataLight, position, dashboardTitle } = req.body;
|
const { data, dataDark, dataLight, position, dashboardTitle } = req.body;
|
||||||
|
|
||||||
@@ -231,7 +231,6 @@ module.exports = function(ctx) {
|
|||||||
throw new ValidationError('Invalid image data format');
|
throw new ValidationError('Invalid image data format');
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageType = matches[1];
|
|
||||||
const base64Data = matches[2];
|
const base64Data = matches[2];
|
||||||
const buffer = Buffer.from(base64Data, 'base64');
|
const buffer = Buffer.from(base64Data, 'base64');
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ module.exports = function(ctx) {
|
|||||||
configStateManager: ctx.configStateManager,
|
configStateManager: ctx.configStateManager,
|
||||||
servicesStateManager: ctx.servicesStateManager,
|
servicesStateManager: ctx.servicesStateManager,
|
||||||
asyncHandler: ctx.asyncHandler,
|
asyncHandler: ctx.asyncHandler,
|
||||||
log: ctx.log
|
log: ctx.log,
|
||||||
|
CONFIG_FILE: ctx.CONFIG_FILE,
|
||||||
|
errorResponse: ctx.errorResponse,
|
||||||
|
loadSiteConfig: ctx.loadSiteConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
// Additional deps for backup route
|
// Additional deps for backup route
|
||||||
@@ -35,8 +38,8 @@ module.exports = function(ctx) {
|
|||||||
saveTotpConfig: ctx.saveTotpConfig
|
saveTotpConfig: ctx.saveTotpConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
router.use(require('./settings')(ctx));
|
router.use(require('./settings')(baseDeps));
|
||||||
router.use(require('./assets')(ctx));
|
router.use(require('./assets')({ ...baseDeps, readConfig: ctx.readConfig, saveConfig: ctx.saveConfig }));
|
||||||
router.use(require('./backup')(backupDeps));
|
router.use(require('./backup')(backupDeps));
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ const { ValidationError } = require('../../errors');
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Config settings routes factory
|
* Config settings routes factory
|
||||||
* @param {Object} ctx - Application context
|
* @param {Object} deps - Explicit dependencies
|
||||||
|
* @param {Object} deps.configStateManager - Config state manager
|
||||||
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
|
* @param {Object} deps.log - Logger instance
|
||||||
|
* @param {string} deps.CONFIG_FILE - Config file path
|
||||||
|
* @param {Function} deps.errorResponse - Error response helper
|
||||||
|
* @param {Function} deps.loadSiteConfig - Site config reload helper
|
||||||
* @returns {express.Router}
|
* @returns {express.Router}
|
||||||
*/
|
*/
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ configStateManager: _configStateManager, asyncHandler, log, CONFIG_FILE, errorResponse, loadSiteConfig }) {
|
||||||
const { configStateManager, asyncHandler, log } = ctx;
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const ctx = { CONFIG_FILE, errorResponse, loadSiteConfig };
|
||||||
|
|
||||||
// ===== DASHCADDY CONFIG ENDPOINTS =====
|
// ===== DASHCADDY CONFIG ENDPOINTS =====
|
||||||
// Server-side config storage for setup wizard (shared across all browsers/machines)
|
// Server-side config storage for setup wizard (shared across all browsers/machines)
|
||||||
@@ -60,7 +66,9 @@ module.exports = function(ctx) {
|
|||||||
config.updatedAt = new Date().toISOString();
|
config.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
||||||
ctx.loadSiteConfig(); // Refresh in-memory config
|
if (typeof ctx.loadSiteConfig === 'function') {
|
||||||
|
ctx.loadSiteConfig(); // Refresh in-memory config
|
||||||
|
}
|
||||||
log.info('config', 'Config saved', { path: ctx.CONFIG_FILE });
|
log.info('config', 'Config saved', { path: ctx.CONFIG_FILE });
|
||||||
|
|
||||||
res.json({ success: true, message: 'Configuration saved', config, warnings });
|
res.json({ success: true, message: 'Configuration saved', config, warnings });
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { paginate, parsePaginationParams } = require('../pagination');
|
|||||||
const platformPaths = require('../platform-paths');
|
const platformPaths = require('../platform-paths');
|
||||||
const { resolveServiceUrl } = require('../url-resolver');
|
const { resolveServiceUrl } = require('../url-resolver');
|
||||||
const { success, error: errorResponse } = require('../response-helpers');
|
const { success, error: errorResponse } = require('../response-helpers');
|
||||||
|
const { ValidationError } = require('../errors');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Health routes factory
|
* Health routes factory
|
||||||
@@ -54,7 +55,7 @@ module.exports = function({
|
|||||||
url,
|
url,
|
||||||
checkedAt: new Date().toISOString()
|
checkedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
} catch (_) {}
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
// Fallback to GET
|
// Fallback to GET
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const fsp = require('fs').promises;
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { exists } = require('../fs-helpers');
|
const { exists } = require('../fs-helpers');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
const { NotFoundError } = require('../errors');
|
const { NotFoundError, ValidationError, ForbiddenError } = require('../errors');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs route factory
|
* Logs route factory
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const { DOCKER } = require('../../constants');
|
|||||||
* @param {Object} deps.log - Logger instance
|
* @param {Object} deps.log - Logger instance
|
||||||
* @returns {express.Router}
|
* @returns {express.Router}
|
||||||
*/
|
*/
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ docker, credentialManager: _credentialManager, servicesStateManager: _servicesStateManager, asyncHandler, errorResponse: _errorResponse, log, addServiceToConfig, notification, APP_TEMPLATES, siteConfig, caddy, buildDomain }) {
|
||||||
const { docker, credentialManager, servicesStateManager, asyncHandler, errorResponse, log } = ctx;
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const ctx = { addServiceToConfig, notification, APP_TEMPLATES, siteConfig, caddy, buildDomain };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deploy a recipe — creates multiple containers as a coordinated stack
|
* Deploy a recipe — creates multiple containers as a coordinated stack
|
||||||
@@ -24,6 +24,7 @@ module.exports = function(ctx) {
|
|||||||
* POST /api/recipes/deploy
|
* POST /api/recipes/deploy
|
||||||
* Body: { recipeId, config: { selectedComponents, sharedConfig, componentOverrides } }
|
* Body: { recipeId, config: { selectedComponents, sharedConfig, componentOverrides } }
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
router.post('/deploy', asyncHandler(async (req, res) => {
|
router.post('/deploy', asyncHandler(async (req, res) => {
|
||||||
const { recipeId, config } = req.body;
|
const { recipeId, config } = req.body;
|
||||||
const { RECIPE_TEMPLATES } = require('../../recipe-templates');
|
const { RECIPE_TEMPLATES } = require('../../recipe-templates');
|
||||||
@@ -44,7 +45,6 @@ module.exports = function(ctx) {
|
|||||||
|
|
||||||
// Generate shared passwords for the recipe (consistent across components)
|
// Generate shared passwords for the recipe (consistent across components)
|
||||||
const generatedPasswords = {};
|
const generatedPasswords = {};
|
||||||
const passwordKey = `recipe-${recipeId}-${Date.now()}`;
|
|
||||||
generatedPasswords.default = crypto.randomBytes(24).toString('base64url');
|
generatedPasswords.default = crypto.randomBytes(24).toString('base64url');
|
||||||
|
|
||||||
// Create Docker network if defined
|
// Create Docker network if defined
|
||||||
@@ -185,6 +185,7 @@ module.exports = function(ctx) {
|
|||||||
/**
|
/**
|
||||||
* Deploy a single component of a recipe
|
* Deploy a single component of a recipe
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
async function deployComponent(component, recipe, config, passwords, networkName) {
|
async function deployComponent(component, recipe, config, passwords, networkName) {
|
||||||
const sharedConfig = config.sharedConfig || {};
|
const sharedConfig = config.sharedConfig || {};
|
||||||
const overrides = config.componentOverrides?.[component.id] || {};
|
const overrides = config.componentOverrides?.[component.id] || {};
|
||||||
@@ -364,7 +365,7 @@ module.exports = function(ctx) {
|
|||||||
/**
|
/**
|
||||||
* Run auto-connect steps after recipe deployment
|
* Run auto-connect steps after recipe deployment
|
||||||
*/
|
*/
|
||||||
async function runAutoConnect(recipe, deployedComponents, config) {
|
async function runAutoConnect(recipe, _deployedComponents, _config) {
|
||||||
if (!recipe.autoConnect?.steps) return;
|
if (!recipe.autoConnect?.steps) return;
|
||||||
|
|
||||||
// Wait for services to be fully ready
|
// Wait for services to be fully ready
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ module.exports = function(ctx) {
|
|||||||
servicesStateManager: ctx.servicesStateManager,
|
servicesStateManager: ctx.servicesStateManager,
|
||||||
asyncHandler: ctx.asyncHandler,
|
asyncHandler: ctx.asyncHandler,
|
||||||
errorResponse: ctx.errorResponse,
|
errorResponse: ctx.errorResponse,
|
||||||
log: ctx.log
|
log: ctx.log,
|
||||||
|
notification: ctx.notification,
|
||||||
|
buildDomain: ctx.buildDomain,
|
||||||
|
caddy: ctx.caddy,
|
||||||
|
addServiceToConfig: ctx.addServiceToConfig,
|
||||||
|
APP_TEMPLATES: ctx.APP_TEMPLATES,
|
||||||
|
siteConfig: ctx.siteConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
// All recipe routes require premium license
|
// All recipe routes require premium license
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ const express = require('express');
|
|||||||
const { DOCKER } = require('../../constants');
|
const { DOCKER } = require('../../constants');
|
||||||
const { NotFoundError } = require('../../errors');
|
const { NotFoundError } = require('../../errors');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ servicesStateManager, asyncHandler, log, docker, notification, buildDomain, caddy }) {
|
||||||
const { servicesStateManager, asyncHandler, log } = ctx;
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Ctx shim for backward compatibility
|
||||||
|
const ctx = { docker, notification, buildDomain, caddy };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recipes management routes factory
|
* Recipes management routes factory
|
||||||
* @param {Object} deps - Explicit dependencies
|
* @param {Object} deps - Explicit dependencies
|
||||||
@@ -303,7 +306,7 @@ module.exports = function(ctx) {
|
|||||||
*/
|
*/
|
||||||
async function removeCaddyBlock(subdomain) {
|
async function removeCaddyBlock(subdomain) {
|
||||||
const domain = ctx.buildDomain(subdomain);
|
const domain = ctx.buildDomain(subdomain);
|
||||||
let content = await ctx.caddy.read();
|
const content = await ctx.caddy.read();
|
||||||
|
|
||||||
// Find and remove the block for this domain
|
// Find and remove the block for this domain
|
||||||
const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { APP, REGEX, TIMEOUTS } = require('../constants');
|
|||||||
const { validateServiceConfig, isValidPort } = require('../input-validator');
|
const { validateServiceConfig, isValidPort } = require('../input-validator');
|
||||||
const { exists } = require('../fs-helpers');
|
const { exists } = require('../fs-helpers');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
|
const { ValidationError, NotFoundError } = require('../errors');
|
||||||
const { resolveServiceUrl } = require('../url-resolver');
|
const { resolveServiceUrl } = require('../url-resolver');
|
||||||
const { success, error: errorResponse } = require('../response-helpers');
|
const { success, error: errorResponse } = require('../response-helpers');
|
||||||
const { ConflictError, ValidationError, NotFoundError } = require('../errors');
|
const { ConflictError, ValidationError, NotFoundError } = require('../errors');
|
||||||
@@ -470,7 +471,7 @@ module.exports = function({
|
|||||||
const oldDomain = buildDomain(oldSubdomain);
|
const oldDomain = buildDomain(oldSubdomain);
|
||||||
const newDomain = buildDomain(newSubdomain);
|
const newDomain = buildDomain(newSubdomain);
|
||||||
|
|
||||||
let content = await caddy.read();
|
const content = await caddy.read();
|
||||||
|
|
||||||
const escapedOldDomain = oldDomain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escapedOldDomain = oldDomain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
const siteBlockRegex = new RegExp(
|
const siteBlockRegex = new RegExp(
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
const upstreamRegex = /^[a-z0-9.-]+:\d{1,5}$/i;
|
const upstreamRegex = /^[a-z0-9.-]+:\d{1,5}$/i;
|
||||||
if (!upstreamRegex.test(upstream)) throw new ValidationError('Invalid upstream format. Use host:port');
|
if (!upstreamRegex.test(upstream)) throw new ValidationError('Invalid upstream format. Use host:port');
|
||||||
|
|
||||||
let content = await caddy.read();
|
const content = await caddy.read();
|
||||||
const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
const siteBlockRegex = new RegExp(`\\n?${escapedDomain}\\s*\\{`, 'g');
|
const siteBlockRegex = new RegExp(`\\n?${escapedDomain}\\s*\\{`, 'g');
|
||||||
if (siteBlockRegex.test(content)) {
|
if (siteBlockRegex.test(content)) {
|
||||||
@@ -203,7 +203,6 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
const domain = buildDomain(subdomain);
|
const domain = buildDomain(subdomain);
|
||||||
let dnsWarning = null;
|
let dnsWarning = null;
|
||||||
|
|
||||||
try {
|
|
||||||
if (createDns) {
|
if (createDns) {
|
||||||
try {
|
try {
|
||||||
await dns.createRecord(subdomain, siteConfig.dnsServerIp);
|
await dns.createRecord(subdomain, siteConfig.dnsServerIp);
|
||||||
@@ -267,9 +266,6 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
};
|
};
|
||||||
if (dnsWarning) response.warning = dnsWarning;
|
if (dnsWarning) response.warning = dnsWarning;
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}, 'site-external'));
|
}, 'site-external'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ module.exports = function({
|
|||||||
throw new ValidationError('subdomain is required');
|
throw new ValidationError('subdomain is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = await caddy.read();
|
const content = await caddy.read();
|
||||||
const domain = buildDomain(subdomain);
|
const domain = buildDomain(subdomain);
|
||||||
|
|
||||||
const blockRegex = new RegExp(`(${domain.replace('.', '\\.')}\\s*\\{[^}]*\\})`, 's');
|
const blockRegex = new RegExp(`(${domain.replace('.', '\\.')}\\s*\\{[^}]*\\})`, 's');
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class SelfUpdater extends EventEmitter {
|
|||||||
let commit = null;
|
let commit = null;
|
||||||
try {
|
try {
|
||||||
commit = fs.readFileSync(path.join(__dirname, 'VERSION'), 'utf8').trim();
|
commit = fs.readFileSync(path.join(__dirname, 'VERSION'), 'utf8').trim();
|
||||||
} catch (_) {}
|
} catch { /* ignore */ }
|
||||||
return { version: pkg.version, commit };
|
return { version: pkg.version, commit };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { version: '0.0.0', commit: null };
|
return { version: '0.0.0', commit: null };
|
||||||
@@ -164,7 +164,7 @@ class SelfUpdater extends EventEmitter {
|
|||||||
} catch (dlErr) {
|
} catch (dlErr) {
|
||||||
console.warn('[SelfUpdater] Primary download failed:', dlErr.message, '— trying mirror');
|
console.warn('[SelfUpdater] Primary download failed:', dlErr.message, '— trying mirror');
|
||||||
// Ensure file is fully cleaned up before mirror attempt
|
// Ensure file is fully cleaned up before mirror attempt
|
||||||
try { fs.unlinkSync(tarballPath); } catch (_) {}
|
try { fs.unlinkSync(tarballPath); } catch { /* ignore */ }
|
||||||
await this._downloadFile(mirrorUrl, tarballPath);
|
await this._downloadFile(mirrorUrl, tarballPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +473,7 @@ class SelfUpdater extends EventEmitter {
|
|||||||
const sub = path.join(baseDir, entry, name);
|
const sub = path.join(baseDir, entry, name);
|
||||||
if (fs.existsSync(sub)) return sub;
|
if (fs.existsSync(sub)) return sub;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch { /* ignore */ }
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +512,7 @@ class SelfUpdater extends EventEmitter {
|
|||||||
async _cleanDir(dir) {
|
async _cleanDir(dir) {
|
||||||
try {
|
try {
|
||||||
await fsp.rm(dir, { recursive: true, force: true });
|
await fsp.rm(dir, { recursive: true, force: true });
|
||||||
} catch (_) {}
|
} catch { /* ignore */ }
|
||||||
await fsp.mkdir(dir, { recursive: true });
|
await fsp.mkdir(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ process.on('uncaughtException', (error) => {
|
|||||||
|
|
||||||
// Optional modules
|
// Optional modules
|
||||||
let dockerMaintenance, logDigest;
|
let dockerMaintenance, logDigest;
|
||||||
try { dockerMaintenance = require('./docker-maintenance'); } catch (_) {}
|
try { dockerMaintenance = require('./docker-maintenance'); } catch { /* optional */ }
|
||||||
try { logDigest = require('./log-digest'); } catch (_) {}
|
try { logDigest = require('./log-digest'); } catch { /* optional */ }
|
||||||
|
|
||||||
log.info('server', 'Starting feature modules');
|
log.info('server', 'Starting feature modules');
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ process.on('uncaughtException', (error) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
function shutdown(signal) {
|
const shutdown = (signal) => {
|
||||||
log.info('shutdown', `${signal} received, draining connections...`);
|
log.info('shutdown', `${signal} received, draining connections...`);
|
||||||
|
|
||||||
const resourceMonitor = require('./resource-monitor');
|
const resourceMonitor = require('./resource-monitor');
|
||||||
@@ -206,12 +206,12 @@ process.on('uncaughtException', (error) => {
|
|||||||
try {
|
try {
|
||||||
const dockerMaintenance = require('./docker-maintenance');
|
const dockerMaintenance = require('./docker-maintenance');
|
||||||
dockerMaintenance.stop();
|
dockerMaintenance.stop();
|
||||||
} catch (_) {}
|
} catch { /* optional */ }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const logDigest = require('./log-digest');
|
const logDigest = require('./log-digest');
|
||||||
logDigest.stop();
|
logDigest.stop();
|
||||||
} catch (_) {}
|
} catch { /* optional */ }
|
||||||
|
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
log.info('shutdown', 'HTTP server closed');
|
log.info('shutdown', 'HTTP server closed');
|
||||||
@@ -220,7 +220,7 @@ process.on('uncaughtException', (error) => {
|
|||||||
|
|
||||||
// Force exit after 5s if connections don't drain
|
// Force exit after 5s if connections don't drain
|
||||||
setTimeout(() => process.exit(0), 5000).unref();
|
setTimeout(() => process.exit(0), 5000).unref();
|
||||||
}
|
};
|
||||||
|
|
||||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ const { validateURL } = require('../input-validator');
|
|||||||
|
|
||||||
// Optional modules
|
// Optional modules
|
||||||
let dockerMaintenance, logDigest;
|
let dockerMaintenance, logDigest;
|
||||||
try { dockerMaintenance = require('../docker-maintenance'); } catch (_) {}
|
try { dockerMaintenance = require('../docker-maintenance'); } catch (_) { /* optional module */ }
|
||||||
try { logDigest = require('../log-digest'); } catch (_) {}
|
try { logDigest = require('../log-digest'); } catch (_) { /* optional module */ }
|
||||||
|
|
||||||
// Templates
|
// Templates
|
||||||
const { APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS } = require('../app-templates');
|
const { APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS } = require('../app-templates');
|
||||||
@@ -104,8 +104,8 @@ async function createApp() {
|
|||||||
log.warn('server', 'CA cert not found — HTTPS calls may fail', { path: CA_CERT_PATH });
|
log.warn('server', 'CA cert not found — HTTPS calls may fail', { path: CA_CERT_PATH });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOTP configuration (defaults, overridden by loadTotpConfig below)
|
// TOTP configuration
|
||||||
let totpConfig = {
|
const totpConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
sessionDuration: 'never',
|
sessionDuration: 'never',
|
||||||
isSetUp: false
|
isSetUp: false
|
||||||
@@ -124,7 +124,7 @@ async function createApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tailscale configuration
|
// Tailscale configuration
|
||||||
let tailscaleConfig = {
|
const tailscaleConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
requireAuth: false,
|
requireAuth: false,
|
||||||
allowedTailnet: null,
|
allowedTailnet: null,
|
||||||
@@ -137,7 +137,7 @@ async function createApp() {
|
|||||||
|
|
||||||
// Helper functions needed by middleware
|
// Helper functions needed by middleware
|
||||||
function isValidContainerId(id) {
|
function isValidContainerId(id) {
|
||||||
const CONTAINER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.\-]{0,127}$/;
|
const CONTAINER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
|
||||||
return typeof id === 'string' && CONTAINER_ID_RE.test(id);
|
return typeof id === 'string' && CONTAINER_ID_RE.test(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const fs = require('fs');
|
|||||||
const { validateConfig } = require('../../config-schema');
|
const { validateConfig } = require('../../config-schema');
|
||||||
const { CADDY } = require('../../constants');
|
const { CADDY } = require('../../constants');
|
||||||
|
|
||||||
let siteConfig = {
|
const siteConfig = {
|
||||||
tld: '.home',
|
tld: '.home',
|
||||||
caName: '',
|
caName: '',
|
||||||
dnsServerIp: '',
|
dnsServerIp: '',
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ async function requireDnsToken(providedToken, siteConfig, credentialManager, fet
|
|||||||
/**
|
/**
|
||||||
* Create DNS record
|
* Create DNS record
|
||||||
*/
|
*/
|
||||||
async function createDnsRecord(subdomain, ip, siteConfig, buildDomain, fetchT, httpsAgent, log) {
|
async function createDnsRecord(subdomain, ip, siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log) {
|
||||||
const tokenResult = await ensureValidDnsToken(siteConfig, credentialManager, fetchT, log);
|
const tokenResult = await ensureValidDnsToken(siteConfig, credentialManager, fetchT, log);
|
||||||
if (!tokenResult.success) {
|
if (!tokenResult.success) {
|
||||||
throw new Error(`DNS token not available: ${tokenResult.error}`);
|
throw new Error(`DNS token not available: ${tokenResult.error}`);
|
||||||
@@ -285,7 +285,7 @@ function createDnsContext(siteConfig, buildDomain, credentialManager, fetchT, ht
|
|||||||
const require = (providedToken) => requireDnsToken(providedToken, siteConfig, credentialManager, fetchT, log);
|
const require = (providedToken) => requireDnsToken(providedToken, siteConfig, credentialManager, fetchT, log);
|
||||||
const getForServer = (server, role) => getTokenForServer(server, siteConfig, credentialManager, fetchT, log, role);
|
const getForServer = (server, role) => getTokenForServer(server, siteConfig, credentialManager, fetchT, log, role);
|
||||||
const refresh = (username, password, server) => refreshDnsToken(username, password, server, fetchT, log);
|
const refresh = (username, password, server) => refreshDnsToken(username, password, server, fetchT, log);
|
||||||
const create = (subdomain, ip) => createDnsRecord(subdomain, ip, siteConfig, buildDomain, fetchT, httpsAgent, log);
|
const create = (subdomain, ip) => createDnsRecord(subdomain, ip, siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log);
|
||||||
const call = (server, apiPath, params) => callDns(server, apiPath, params, fetchT, httpsAgent);
|
const call = (server, apiPath, params) => callDns(server, apiPath, params, fetchT, httpsAgent);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user