${template.description}
++ The interactive tour is unavailable, but you can explore the dashboard freely. + Check the documentation for help getting started. +
+ `; + + document.body.appendChild(fallbackMessage); + + // Auto-remove after 10 seconds + setTimeout(() => { + if (fallbackMessage.parentNode) { + fallbackMessage.parentNode.removeChild(fallbackMessage); + } + }, 10000); + + return true; + } + + /** + * Handle storage unavailable scenario + * @returns {Object} In-memory storage fallback + */ + handleStorageUnavailable() { + this.logError('Storage Unavailable', 'Local storage is not available'); + + // Create in-memory storage + const memoryStorage = { + data: {}, + getItem(key) { + return this.data[key] || null; + }, + setItem(key, value) { + this.data[key] = value; + }, + removeItem(key) { + delete this.data[key]; + }, + clear() { + this.data = {}; + } + }; + + console.warn('[ErrorHandler] Using in-memory storage - progress will not persist'); + return memoryStorage; + } + + /** + * Send error to tracking service (placeholder) + * @private + * @param {Object} errorEntry - Error entry to send + */ + sendToErrorTracking(errorEntry) { + // Placeholder for error tracking integration + // Could integrate with Sentry, LogRocket, etc. + // Example: + // if (window.Sentry) { + // Sentry.captureException(new Error(errorEntry.message), { + // extra: errorEntry.metadata + // }); + // } + } + } + + window.ErrorHandler = ErrorHandler; + console.log('[ErrorHandler] Module loaded'); + +})(window); diff --git a/dashcaddy-api/assets/favicon.ico b/dashcaddy-api/assets/favicon.ico new file mode 100644 index 0000000..77e6e9c Binary files /dev/null and b/dashcaddy-api/assets/favicon.ico differ diff --git a/dashcaddy-api/assets/favicon.png b/dashcaddy-api/assets/favicon.png new file mode 100644 index 0000000..e385d2a Binary files /dev/null and b/dashcaddy-api/assets/favicon.png differ diff --git a/dashcaddy-api/assets/favicon.svg b/dashcaddy-api/assets/favicon.svg new file mode 100644 index 0000000..adcf3b4 --- /dev/null +++ b/dashcaddy-api/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/filebrowser.png b/dashcaddy-api/assets/filebrowser.png new file mode 100644 index 0000000..0899c43 Binary files /dev/null and b/dashcaddy-api/assets/filebrowser.png differ diff --git a/dashcaddy-api/assets/fonts.css b/dashcaddy-api/assets/fonts.css new file mode 100644 index 0000000..f5e3463 --- /dev/null +++ b/dashcaddy-api/assets/fonts.css @@ -0,0 +1,91 @@ +/* Sami Sans Font Family - External CSS */ + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-Regular.woff2') format('woff2'), + url('fonts/SamiSans-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-Regular.woff2') format('woff2'), + url('fonts/SamiSans-Italic.ttf') format('truetype'); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-Medium.woff2') format('woff2'), + url('fonts/SamiSans-Medium.ttf') format('truetype'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-SemiBold.woff2') format('woff2'), + url('fonts/SamiSans-SemiBold.ttf') format('truetype'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-Bold.woff2') format('woff2'), + url('fonts/SamiSans-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-ExtraBold.woff2') format('woff2'), + url('fonts/SamiSans-ExtraBold.ttf') format('truetype'); + font-weight: 800; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-Black.woff2') format('woff2'), + url('fonts/SamiSans-Black.ttf') format('truetype'); + font-weight: 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-Light.woff2') format('woff2'), + url('fonts/SamiSans-Light.ttf') format('truetype'); + font-weight: 300; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-ExtraLight.woff2') format('woff2'), + url('fonts/SamiSans-ExtraLight.ttf') format('truetype'); + font-weight: 200; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Sami Sans'; + src: url('fonts/SamiSans-Thin.woff2') format('woff2'), + url('fonts/SamiSans-Thin.ttf') format('truetype'); + font-weight: 100; + font-style: normal; + font-display: swap; +} diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-Black.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-Black.ttf new file mode 100644 index 0000000..ea4b04f Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-Black.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-BlackItalic.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-BlackItalic.ttf new file mode 100644 index 0000000..005a92a Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-BlackItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-Bold.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-Bold.ttf new file mode 100644 index 0000000..7ee1c38 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-Bold.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-BoldItalic.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-BoldItalic.ttf new file mode 100644 index 0000000..6f2f6ba Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-BoldItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-Light.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-Light.ttf new file mode 100644 index 0000000..269b97d Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-Light.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-LightItalic.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-LightItalic.ttf new file mode 100644 index 0000000..d10a4c6 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-LightItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-Medium.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-Medium.ttf new file mode 100644 index 0000000..0dca4b4 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-Medium.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-MediumItalic.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-MediumItalic.ttf new file mode 100644 index 0000000..4ab921f Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-MediumItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-Regular.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-Regular.ttf new file mode 100644 index 0000000..2d3b18d Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-Regular.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiGrotesk-RegularItalic.ttf b/dashcaddy-api/assets/fonts/SamiGrotesk-RegularItalic.ttf new file mode 100644 index 0000000..6c15c7e Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiGrotesk-RegularItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Black.ttf b/dashcaddy-api/assets/fonts/SamiSans-Black.ttf new file mode 100644 index 0000000..c44894b Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Black.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Black.woff2 b/dashcaddy-api/assets/fonts/SamiSans-Black.woff2 new file mode 100644 index 0000000..6929b36 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Black.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-BlackItalic.ttf b/dashcaddy-api/assets/fonts/SamiSans-BlackItalic.ttf new file mode 100644 index 0000000..eccde7d Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-BlackItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-BlackItalic.woff2 b/dashcaddy-api/assets/fonts/SamiSans-BlackItalic.woff2 new file mode 100644 index 0000000..3048ee4 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-BlackItalic.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Bold.ttf b/dashcaddy-api/assets/fonts/SamiSans-Bold.ttf new file mode 100644 index 0000000..6bc519f Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Bold.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Bold.woff2 b/dashcaddy-api/assets/fonts/SamiSans-Bold.woff2 new file mode 100644 index 0000000..185b3a3 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Bold.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-BoldItalic.ttf b/dashcaddy-api/assets/fonts/SamiSans-BoldItalic.ttf new file mode 100644 index 0000000..a47ea80 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-BoldItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-BoldItalic.woff2 b/dashcaddy-api/assets/fonts/SamiSans-BoldItalic.woff2 new file mode 100644 index 0000000..62f36be Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-BoldItalic.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ExtraBold.ttf b/dashcaddy-api/assets/fonts/SamiSans-ExtraBold.ttf new file mode 100644 index 0000000..8cdb5a9 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ExtraBold.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ExtraBold.woff2 b/dashcaddy-api/assets/fonts/SamiSans-ExtraBold.woff2 new file mode 100644 index 0000000..7ce126b Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ExtraBold.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ExtraBoldItalic.ttf b/dashcaddy-api/assets/fonts/SamiSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000..9e0cc55 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ExtraBoldItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ExtraBoldItalic.woff2 b/dashcaddy-api/assets/fonts/SamiSans-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..521db52 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ExtraBoldItalic.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ExtraLight.ttf b/dashcaddy-api/assets/fonts/SamiSans-ExtraLight.ttf new file mode 100644 index 0000000..b39c51e Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ExtraLight.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ExtraLight.woff2 b/dashcaddy-api/assets/fonts/SamiSans-ExtraLight.woff2 new file mode 100644 index 0000000..a0890de Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ExtraLight.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ExtraLightItalic.ttf b/dashcaddy-api/assets/fonts/SamiSans-ExtraLightItalic.ttf new file mode 100644 index 0000000..5222446 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ExtraLightItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ExtraLightItalic.woff2 b/dashcaddy-api/assets/fonts/SamiSans-ExtraLightItalic.woff2 new file mode 100644 index 0000000..f23459a Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ExtraLightItalic.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Italic.ttf b/dashcaddy-api/assets/fonts/SamiSans-Italic.ttf new file mode 100644 index 0000000..97fcbe3 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Italic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Italic.woff2 b/dashcaddy-api/assets/fonts/SamiSans-Italic.woff2 new file mode 100644 index 0000000..d7e72d1 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Italic.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Light.ttf b/dashcaddy-api/assets/fonts/SamiSans-Light.ttf new file mode 100644 index 0000000..446e73c Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Light.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Light.woff2 b/dashcaddy-api/assets/fonts/SamiSans-Light.woff2 new file mode 100644 index 0000000..9c4227f Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Light.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-LightItalic.ttf b/dashcaddy-api/assets/fonts/SamiSans-LightItalic.ttf new file mode 100644 index 0000000..ab054fc Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-LightItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-LightItalic.woff2 b/dashcaddy-api/assets/fonts/SamiSans-LightItalic.woff2 new file mode 100644 index 0000000..6ea5bc7 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-LightItalic.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Medium.ttf b/dashcaddy-api/assets/fonts/SamiSans-Medium.ttf new file mode 100644 index 0000000..d440388 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Medium.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Medium.woff2 b/dashcaddy-api/assets/fonts/SamiSans-Medium.woff2 new file mode 100644 index 0000000..bc2048c Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Medium.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-MediumItalic.ttf b/dashcaddy-api/assets/fonts/SamiSans-MediumItalic.ttf new file mode 100644 index 0000000..d61074b Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-MediumItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-MediumItalic.woff2 b/dashcaddy-api/assets/fonts/SamiSans-MediumItalic.woff2 new file mode 100644 index 0000000..05f6488 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-MediumItalic.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Regular.ttf b/dashcaddy-api/assets/fonts/SamiSans-Regular.ttf new file mode 100644 index 0000000..09cdb41 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Regular.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Regular.woff2 b/dashcaddy-api/assets/fonts/SamiSans-Regular.woff2 new file mode 100644 index 0000000..55b58bd Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Regular.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-SemiBold.ttf b/dashcaddy-api/assets/fonts/SamiSans-SemiBold.ttf new file mode 100644 index 0000000..956a313 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-SemiBold.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-SemiBold.woff2 b/dashcaddy-api/assets/fonts/SamiSans-SemiBold.woff2 new file mode 100644 index 0000000..e14f439 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-SemiBold.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-SemiBoldItalic.ttf b/dashcaddy-api/assets/fonts/SamiSans-SemiBoldItalic.ttf new file mode 100644 index 0000000..494082a Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-SemiBoldItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-SemiBoldItalic.woff2 b/dashcaddy-api/assets/fonts/SamiSans-SemiBoldItalic.woff2 new file mode 100644 index 0000000..406aa7d Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-SemiBoldItalic.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Thin.ttf b/dashcaddy-api/assets/fonts/SamiSans-Thin.ttf new file mode 100644 index 0000000..1e50ae1 Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Thin.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-Thin.woff2 b/dashcaddy-api/assets/fonts/SamiSans-Thin.woff2 new file mode 100644 index 0000000..281a96a Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-Thin.woff2 differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ThinItalic.ttf b/dashcaddy-api/assets/fonts/SamiSans-ThinItalic.ttf new file mode 100644 index 0000000..8eb93ba Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ThinItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/SamiSans-ThinItalic.woff2 b/dashcaddy-api/assets/fonts/SamiSans-ThinItalic.woff2 new file mode 100644 index 0000000..03a9b3f Binary files /dev/null and b/dashcaddy-api/assets/fonts/SamiSans-ThinItalic.woff2 differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Black.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Black.ttf new file mode 100644 index 0000000..ea4b04f Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Black.ttf differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-BlackItalic.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-BlackItalic.ttf new file mode 100644 index 0000000..005a92a Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-BlackItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Bold.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Bold.ttf new file mode 100644 index 0000000..7ee1c38 Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Bold.ttf differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-BoldItalic.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-BoldItalic.ttf new file mode 100644 index 0000000..6f2f6ba Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-BoldItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Light.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Light.ttf new file mode 100644 index 0000000..269b97d Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Light.ttf differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-LightItalic.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-LightItalic.ttf new file mode 100644 index 0000000..d10a4c6 Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-LightItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Medium.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Medium.ttf new file mode 100644 index 0000000..0dca4b4 Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Medium.ttf differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-MediumItalic.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-MediumItalic.ttf new file mode 100644 index 0000000..4ab921f Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-MediumItalic.ttf differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Regular.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Regular.ttf new file mode 100644 index 0000000..2d3b18d Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-Regular.ttf differ diff --git a/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-RegularItalic.ttf b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-RegularItalic.ttf new file mode 100644 index 0000000..6c15c7e Binary files /dev/null and b/dashcaddy-api/assets/fonts/sami-grotesk/SamiGrotesk-RegularItalic.ttf differ diff --git a/dashcaddy-api/assets/icon-192.png b/dashcaddy-api/assets/icon-192.png new file mode 100644 index 0000000..07a6369 Binary files /dev/null and b/dashcaddy-api/assets/icon-192.png differ diff --git a/dashcaddy-api/assets/icon-512.png b/dashcaddy-api/assets/icon-512.png new file mode 100644 index 0000000..af4acae Binary files /dev/null and b/dashcaddy-api/assets/icon-512.png differ diff --git a/dashcaddy-api/assets/jellyfin.png b/dashcaddy-api/assets/jellyfin.png new file mode 100644 index 0000000..c8c07ef Binary files /dev/null and b/dashcaddy-api/assets/jellyfin.png differ diff --git a/dashcaddy-api/assets/nginx.png b/dashcaddy-api/assets/nginx.png new file mode 100644 index 0000000..07a6369 Binary files /dev/null and b/dashcaddy-api/assets/nginx.png differ diff --git a/dashcaddy-api/assets/onboarding.css b/dashcaddy-api/assets/onboarding.css new file mode 100644 index 0000000..f8dc06b --- /dev/null +++ b/dashcaddy-api/assets/onboarding.css @@ -0,0 +1,354 @@ +/** + * Onboarding Tooltip Styles + * Custom styling for Driver.js tooltips to match DashCaddy theme + */ + +/* Driver.js overrides are injected dynamically by ThemeAdapter */ +/* This file contains additional custom styles */ + +.driver-popover { + max-width: 500px !important; + z-index: 10000 !important; +} + +.driver-popover-title { + font-size: 1.2rem !important; + margin-bottom: 12px !important; +} + +.driver-popover-description { + font-size: 0.95rem !important; + line-height: 1.6 !important; +} + +.driver-popover-description p { + margin: 8px 0 !important; +} + +.driver-popover-description ul { + margin: 8px 0 !important; + padding-left: 20px !important; +} + +.driver-popover-description li { + margin: 4px 0 !important; +} + +.driver-popover-description code { + background: rgba(0, 0, 0, 0.1) !important; + padding: 2px 6px !important; + border-radius: 3px !important; + font-family: 'Courier New', monospace !important; + font-size: 0.9em !important; +} + +.driver-popover-footer { + margin-top: 16px !important; + display: flex !important; + gap: 8px !important; + justify-content: flex-end !important; +} + +.driver-popover-footer button { + padding: 8px 16px !important; + border-radius: 8px !important; + font-size: 0.9rem !important; + cursor: pointer !important; + transition: all 0.2s ease !important; +} + +.driver-popover-footer button:hover { + transform: translateY(-1px) !important; +} + +.driver-popover-close-btn { + position: absolute !important; + top: 12px !important; + right: 12px !important; + width: 24px !important; + height: 24px !important; + border-radius: 50% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + cursor: pointer !important; + opacity: 0.6 !important; + transition: opacity 0.2s ease !important; +} + +.driver-popover-close-btn:hover { + opacity: 1 !important; +} + +.driver-popover-arrow { + border-width: 8px !important; +} + +/* Progress indicator */ +.driver-popover-progress-text { + font-size: 0.85rem !important; + margin-bottom: 8px !important; +} + +/* Mobile responsive */ +@media (max-width: 768px) { + .driver-popover { + max-width: calc(100vw - 32px) !important; + } + + .driver-popover-title { + font-size: 1.1rem !important; + } + + .driver-popover-description { + font-size: 0.9rem !important; + } + + .driver-popover-footer button { + padding: 6px 12px !important; + font-size: 0.85rem !important; + } +} + +/* Restart tour button in dashboard */ +#restart-tour-btn { + display: inline-flex; + align-items: center; + gap: 6px; +} + +#restart-tour-btn::before { + content: "🎓"; + font-size: 1.1em; +} + + +/* DNS Template Selector Modal */ +.dns-template-modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + z-index: 10000; + align-items: center; + justify-content: center; + padding: 20px; +} + +.dns-template-modal-content { + background: var(--card-base); + border-radius: 12px; + max-width: 900px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.dns-template-header { + padding: 30px; + border-bottom: 1px solid var(--border); + position: relative; +} + +.dns-template-header h2 { + margin: 0 0 10px 0; + color: var(--fg); + font-size: 28px; +} + +.dns-template-header p { + margin: 0; + color: var(--fg-muted); + font-size: 14px; +} + +.dns-template-close { + position: absolute; + top: 20px; + right: 20px; + background: none; + border: none; + font-size: 32px; + color: var(--fg-muted); + cursor: pointer; + padding: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s; +} + +.dns-template-close:hover { + background: var(--hover); + color: var(--fg); +} + +.dns-template-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + padding: 30px; +} + +.dns-template-card { + background: var(--card-hover); + border: 2px solid var(--border); + border-radius: 12px; + padding: 20px; + transition: all 0.3s; + position: relative; + display: flex; + flex-direction: column; +} + +.dns-template-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); + border-color: var(--accent); +} + +.dns-template-card.recommended { + border-color: var(--accent); + background: linear-gradient(135deg, var(--card-hover) 0%, var(--card-base) 100%); +} + +.recommended-badge { + position: absolute; + top: -10px; + right: 20px; + background: var(--accent); + color: white; + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.dns-template-icon { + font-size: 48px; + margin-bottom: 15px; + text-align: center; +} + +.dns-template-card h3 { + margin: 0 0 10px 0; + color: var(--fg); + font-size: 18px; + text-align: center; +} + +.dns-template-description { + color: var(--fg-muted); + font-size: 13px; + margin: 0 0 15px 0; + text-align: center; + flex-grow: 1; +} + +.dns-template-difficulty { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 11px; + font-weight: bold; + text-align: center; + margin: 0 auto 15px auto; +} + +.difficulty-easy { + background: #2ecc71; + color: white; +} + +.difficulty-intermediate { + background: #f39c12; + color: white; +} + +.difficulty-advanced { + background: #e74c3c; + color: white; +} + +.dns-template-features { + list-style: none; + padding: 0; + margin: 0 0 20px 0; + font-size: 12px; + color: var(--fg-muted); +} + +.dns-template-features li { + padding: 6px 0; + padding-left: 20px; + position: relative; +} + +.dns-template-features li:before { + content: "✓"; + position: absolute; + left: 0; + color: var(--accent); + font-weight: bold; +} + +.dns-template-select-btn { + background: var(--accent); + color: white; + border: none; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + width: 100%; +} + +.dns-template-select-btn:hover { + background: var(--accent-strong); + transform: scale(1.02); +} + +.dns-template-footer { + padding: 20px 30px; + border-top: 1px solid var(--border); + text-align: center; +} + +.dns-template-later-btn { + background: transparent; + color: var(--fg-muted); + border: 1px solid var(--border); + padding: 10px 24px; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.dns-template-later-btn:hover { + background: var(--hover); + color: var(--fg); + border-color: var(--fg-muted); +} + +/* Responsive design */ +@media (max-width: 768px) { + .dns-template-grid { + grid-template-columns: 1fr; + } + + .dns-template-modal-content { + max-height: 95vh; + } +} diff --git a/dashcaddy-api/assets/onboarding.js b/dashcaddy-api/assets/onboarding.js new file mode 100644 index 0000000..98949ac --- /dev/null +++ b/dashcaddy-api/assets/onboarding.js @@ -0,0 +1,177 @@ +/** + * DashCaddy User Onboarding System + * Main entry point for the tooltip-based onboarding experience + * + * This file initializes the onboarding system and coordinates between + * the various components (TourManager, ProgressTracker, ThemeAdapter, etc.) + */ + +(function() { + 'use strict'; + + let progressTracker; + let themeAdapter; + let tourManager; + let dnsTemplateSelector; + let errorHandler; + + /** + * Initialize the onboarding system + */ + async function initializeOnboarding() { + try { + console.log('[Onboarding] Initializing system...'); + + // Initialize Error Handler first + errorHandler = new ErrorHandler(); + console.log('[Onboarding] Error Handler initialized'); + + // Initialize Progress Tracker + progressTracker = new ProgressTracker('dashcaddy_onboarding'); + console.log('[Onboarding] Progress Tracker initialized'); + + // Initialize Theme Adapter + themeAdapter = new ThemeAdapter(); + console.log('[Onboarding] Theme Adapter initialized'); + + // Initialize DNS Template Selector + dnsTemplateSelector = new DnsTemplateSelector(progressTracker); + console.log('[Onboarding] DNS Template Selector initialized'); + + // Initialize Tour Manager + tourManager = new TourManager(progressTracker, themeAdapter, dnsTemplateSelector); + console.log('[Onboarding] Tour Manager initialized'); + + // Check if tour should auto-start + if (tourManager.shouldAutoStart()) { + console.log('[Onboarding] Auto-starting tour for first-time user'); + // Wait a bit for page to fully load + setTimeout(() => { + tourManager.startTour(); + }, 1000); + } else { + const tourCompleted = progressTracker.isTourCompleted(); + const currentStep = progressTracker.getCurrentStep(); + console.log(`[Onboarding] Tour not auto-starting (completed: ${tourCompleted}, step: ${currentStep})`); + + // If tour is in progress, offer to resume + if (!tourCompleted && currentStep > 0) { + console.log('[Onboarding] Tour in progress, can be resumed manually'); + } + } + + // Add restart tour button to tools row + addRestartTourButton(); + + // Expose to global scope for manual triggering + window.DashCaddyOnboarding = { + startTour: () => tourManager.startTour(), + restartTour: () => tourManager.restartTour(), + showTooltip: (id) => tourManager.showTooltip(id), + showWhatsNew: () => tourManager.showWhatsNew(), + resetProgress: () => progressTracker.resetProgress(), + getErrors: () => errorHandler.getErrors(), + getErrorStats: () => errorHandler.getStatistics() + }; + + console.log('[Onboarding] System initialized successfully'); + } catch (error) { + console.error('[Onboarding] Initialization error:', error); + + // Use error handler if available + if (errorHandler) { + errorHandler.logError('Initialization', error); + } + + // Graceful degradation - don't break the dashboard + console.warn('[Onboarding] System failed to initialize, dashboard will continue without onboarding'); + } + } + + /** + * Add restart tour button to tools row + */ + function addRestartTourButton() { + const toolsRow = document.querySelector('.tools'); + if (!toolsRow) return; + + const clickHandler = () => { + if (tourManager) { + console.log('[Onboarding] Starting tour via button click'); + tourManager.restartTour(); + } else { + console.error('[Onboarding] Tour manager not initialized'); + alert('Tour is not available. Check browser console for errors.\n\nPossible issues:\n- Driver.js library failed to load\n- JavaScript errors during initialization'); + } + }; + + // If button already exists in the HTML, just attach the handler + const existing = document.getElementById('restart-tour-btn'); + if (existing) { + existing.onclick = clickHandler; + return; + } + + const button = document.createElement('button'); + button.id = 'restart-tour-btn'; + button.textContent = 'Help Tour'; + button.title = 'Restart the onboarding tour'; + button.onclick = clickHandler; + toolsRow.appendChild(button); + } + + /** + * Check if Driver.js is loaded + */ + function checkDriverLoaded() { + // Driver.js v1.x IIFE: window.driver.js.driver is the factory function + const driverFactory = window.driver?.js?.driver || window.driver?.driver || window.driver; + if (typeof driverFactory !== 'function') { + console.warn('[Onboarding] Driver.js not loaded yet, will retry... window.driver:', window.driver); + return false; + } + return true; + } + + /** + * Wait for Driver.js to load, then initialize + */ + function waitForDriver() { + let retries = 0; + const maxRetries = 10; + + function attemptInit() { + if (checkDriverLoaded()) { + initializeOnboarding(); + } else { + retries++; + if (retries < maxRetries) { + // Retry after a short delay + setTimeout(attemptInit, 500); + } else { + // Max retries reached, show fallback + console.error('[Onboarding] Driver.js failed to load after multiple attempts'); + if (errorHandler) { + errorHandler.handleDriverLoadFailure(); + } else { + // Create temporary error handler for fallback + const tempHandler = new ErrorHandler(); + tempHandler.handleDriverLoadFailure(); + } + } + } + } + + attemptInit(); + } + + // Start initialization when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', waitForDriver); + } else { + waitForDriver(); + } + + console.log('[Onboarding] System loaded'); + +})(); diff --git a/dashcaddy-api/assets/pics.png b/dashcaddy-api/assets/pics.png new file mode 100644 index 0000000..951ecfe Binary files /dev/null and b/dashcaddy-api/assets/pics.png differ diff --git a/dashcaddy-api/assets/plex.png b/dashcaddy-api/assets/plex.png new file mode 100644 index 0000000..f30efe3 Binary files /dev/null and b/dashcaddy-api/assets/plex.png differ diff --git a/dashcaddy-api/assets/portainer.png b/dashcaddy-api/assets/portainer.png new file mode 100644 index 0000000..c0b0436 Binary files /dev/null and b/dashcaddy-api/assets/portainer.png differ diff --git a/dashcaddy-api/assets/progress-tracker.js b/dashcaddy-api/assets/progress-tracker.js new file mode 100644 index 0000000..cbfe3db --- /dev/null +++ b/dashcaddy-api/assets/progress-tracker.js @@ -0,0 +1,282 @@ +/** + * Progress Tracker + * Manages persistent storage of user progress through the onboarding flow + * using browser local storage. + * + * Storage Schema: + * { + * "version": "1.0", + * "tourCompleted": false, + * "completedTooltips": ["welcome", "dns-priority", ...], + * "currentStep": 3, + * "completionTimestamp": "2024-01-15T10:30:00Z", + * "dnsSetupDeferred": false, + * "lastVisit": "2024-01-15T10:30:00Z" + * } + */ + +(function(window) { + 'use strict'; + + /** + * ProgressTracker class + * Manages persistent storage of onboarding progress + * + * @class + * @param {string} storageKey - The key to use for local storage (default: 'dashcaddy_onboarding') + */ + class ProgressTracker { + constructor(storageKey = 'dashcaddy_onboarding') { + this.storageKey = storageKey; + this.storageVersion = '1.0'; + + // Initialize storage if it doesn't exist + this._initializeStorage(); + + // Update last visit timestamp + this._updateLastVisit(); + } + + /** + * Initialize storage with default values if it doesn't exist + * @private + */ + _initializeStorage() { + const existing = this._getStorage(); + if (!existing || existing.version !== this.storageVersion) { + const defaultState = { + version: this.storageVersion, + tourCompleted: false, + completedTooltips: [], + currentStep: 0, + completionTimestamp: null, + dnsSetupDeferred: false, + lastVisit: new Date().toISOString() + }; + this._setStorage(defaultState); + } + } + + /** + * Get the current storage state + * @private + * @returns {Object|null} The storage state or null if unavailable + */ + _getStorage() { + try { + const data = localStorage.getItem(this.storageKey); + return data ? JSON.parse(data) : null; + } catch (error) { + console.error('[ProgressTracker] Error reading from storage:', error); + return null; + } + } + + /** + * Set the storage state + * @private + * @param {Object} state - The state to save + */ + _setStorage(state) { + try { + localStorage.setItem(this.storageKey, JSON.stringify(state)); + } catch (error) { + console.error('[ProgressTracker] Error writing to storage:', error); + // Handle quota exceeded or storage unavailable + // Fall back to session storage or in-memory storage + this._handleStorageError(error); + } + } + + /** + * Handle storage errors (quota exceeded, unavailable, etc.) + * @private + * @param {Error} error - The error that occurred + */ + _handleStorageError(error) { + // Try session storage as fallback + try { + sessionStorage.setItem(this.storageKey, JSON.stringify(this._getStorage())); + console.warn('[ProgressTracker] Falling back to session storage'); + } catch (sessionError) { + console.error('[ProgressTracker] Session storage also unavailable:', sessionError); + // Could implement in-memory fallback here if needed + } + } + + /** + * Update the last visit timestamp + * @private + */ + _updateLastVisit() { + const state = this._getStorage(); + if (state) { + state.lastVisit = new Date().toISOString(); + this._setStorage(state); + } + } + + /** + * Check if a specific tooltip has been completed + * @param {string} tooltipId - The ID of the tooltip to check + * @returns {boolean} True if the tooltip has been completed + */ + isTooltipCompleted(tooltipId) { + const state = this._getStorage(); + if (!state) return false; + return state.completedTooltips.includes(tooltipId); + } + + /** + * Mark a tooltip as completed with timestamp + * @param {string} tooltipId - The ID of the tooltip to mark as completed + */ + markTooltipCompleted(tooltipId) { + const state = this._getStorage(); + if (!state) return; + + // Add tooltip to completed list if not already there + if (!state.completedTooltips.includes(tooltipId)) { + state.completedTooltips.push(tooltipId); + + // Store timestamp for this specific tooltip + if (!state.tooltipTimestamps) { + state.tooltipTimestamps = {}; + } + state.tooltipTimestamps[tooltipId] = new Date().toISOString(); + + this._setStorage(state); + } + } + + /** + * Check if the entire tour has been completed + * @returns {boolean} True if the tour is completed + */ + isTourCompleted() { + const state = this._getStorage(); + if (!state) return false; + return state.tourCompleted === true; + } + + /** + * Mark the entire tour as completed + */ + markTourCompleted() { + const state = this._getStorage(); + if (!state) return; + + state.tourCompleted = true; + state.completionTimestamp = new Date().toISOString(); + this._setStorage(state); + } + + /** + * Get the current step index + * @returns {number} The current step index (0-based) + */ + getCurrentStep() { + const state = this._getStorage(); + if (!state) return 0; + return state.currentStep || 0; + } + + /** + * Set the current step index + * @param {number} stepIndex - The step index to set (0-based) + */ + setCurrentStep(stepIndex) { + const state = this._getStorage(); + if (!state) return; + + state.currentStep = stepIndex; + this._setStorage(state); + } + + /** + * Reset all progress and clear storage + */ + resetProgress() { + const defaultState = { + version: this.storageVersion, + tourCompleted: false, + completedTooltips: [], + currentStep: 0, + completionTimestamp: null, + dnsSetupDeferred: false, + lastVisit: new Date().toISOString() + }; + this._setStorage(defaultState); + } + + /** + * Get the completion timestamp + * @returns {Date|null} The completion timestamp or null if not completed + */ + getCompletionTimestamp() { + const state = this._getStorage(); + if (!state || !state.completionTimestamp) return null; + return new Date(state.completionTimestamp); + } + + /** + * Check if DNS setup was deferred + * @returns {boolean} True if DNS setup was deferred + */ + isDnsSetupDeferred() { + const state = this._getStorage(); + if (!state) return false; + return state.dnsSetupDeferred === true; + } + + /** + * Mark DNS setup as deferred + */ + markDnsSetupDeferred() { + const state = this._getStorage(); + if (!state) return; + + state.dnsSetupDeferred = true; + this._setStorage(state); + } + + /** + * Get the timestamp for a specific tooltip completion + * @param {string} tooltipId - The ID of the tooltip + * @returns {Date|null} The timestamp or null if not completed + */ + getTooltipTimestamp(tooltipId) { + const state = this._getStorage(); + if (!state || !state.tooltipTimestamps || !state.tooltipTimestamps[tooltipId]) { + return null; + } + return new Date(state.tooltipTimestamps[tooltipId]); + } + + /** + * Get all completed tooltip IDs + * @returns {string[]} Array of completed tooltip IDs + */ + getCompletedTooltips() { + const state = this._getStorage(); + if (!state) return []; + return state.completedTooltips || []; + } + + /** + * Get the last visit timestamp + * @returns {Date|null} The last visit timestamp + */ + getLastVisit() { + const state = this._getStorage(); + if (!state || !state.lastVisit) return null; + return new Date(state.lastVisit); + } + } + + // Export to global scope + window.ProgressTracker = ProgressTracker; + + console.log('[ProgressTracker] Module loaded'); + +})(window); diff --git a/dashcaddy-api/assets/prowlarr.png b/dashcaddy-api/assets/prowlarr.png new file mode 100644 index 0000000..964f18b Binary files /dev/null and b/dashcaddy-api/assets/prowlarr.png differ diff --git a/dashcaddy-api/assets/qBittorrent.png b/dashcaddy-api/assets/qBittorrent.png new file mode 100644 index 0000000..97bdb6c Binary files /dev/null and b/dashcaddy-api/assets/qBittorrent.png differ diff --git a/dashcaddy-api/assets/radarr.png b/dashcaddy-api/assets/radarr.png new file mode 100644 index 0000000..49943a5 Binary files /dev/null and b/dashcaddy-api/assets/radarr.png differ diff --git a/dashcaddy-api/assets/router.png b/dashcaddy-api/assets/router.png new file mode 100644 index 0000000..241b9aa Binary files /dev/null and b/dashcaddy-api/assets/router.png differ diff --git a/dashcaddy-api/assets/sami-favicon.png b/dashcaddy-api/assets/sami-favicon.png new file mode 100644 index 0000000..1b8d421 Binary files /dev/null and b/dashcaddy-api/assets/sami-favicon.png differ diff --git a/dashcaddy-api/assets/sami-logo.png b/dashcaddy-api/assets/sami-logo.png new file mode 100644 index 0000000..2c6685e Binary files /dev/null and b/dashcaddy-api/assets/sami-logo.png differ diff --git a/dashcaddy-api/assets/site.webmanifest b/dashcaddy-api/assets/site.webmanifest new file mode 100644 index 0000000..e83acb0 --- /dev/null +++ b/dashcaddy-api/assets/site.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "SAMI-CLOUD Status", + "short_name": "SAMI-CLOUD", + "start_url": "index.html", + "display": "standalone", + "background_color": "#0b0f1a", + "theme_color": "#0e1116", + "icons": [ + { + "src": "assets/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "assets/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/dashcaddy-api/assets/sonarr.png b/dashcaddy-api/assets/sonarr.png new file mode 100644 index 0000000..c904551 Binary files /dev/null and b/dashcaddy-api/assets/sonarr.png differ diff --git a/dashcaddy-api/assets/syncthing.png b/dashcaddy-api/assets/syncthing.png new file mode 100644 index 0000000..ee5e1ec Binary files /dev/null and b/dashcaddy-api/assets/syncthing.png differ diff --git a/dashcaddy-api/assets/test-upload4.png b/dashcaddy-api/assets/test-upload4.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/dashcaddy-api/assets/test-upload4.png differ diff --git a/dashcaddy-api/assets/theme-adapter.js b/dashcaddy-api/assets/theme-adapter.js new file mode 100644 index 0000000..f58248f --- /dev/null +++ b/dashcaddy-api/assets/theme-adapter.js @@ -0,0 +1,308 @@ +/** + * Theme Adapter + * Ensures tooltips match the current dashboard theme + * Integrates with Driver.js to apply theme-specific styling + */ + +(function(window) { + 'use strict'; + + /** + * Theme configuration mapping for Driver.js + * Maps dashboard themes to Driver.js styling + */ + const THEME_CONFIGS = { + dark: { + backgroundColor: 'var(--card-base)', + textColor: 'var(--fg)', + primaryColor: 'var(--accent)', + overlayColor: 'rgba(0, 0, 0, 0.7)', + borderColor: 'var(--border)', + highlightColor: 'var(--accent)', + fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" + }, + light: { + backgroundColor: 'var(--card-base)', + textColor: 'var(--fg)', + primaryColor: 'var(--accent-strong)', + overlayColor: 'rgba(0, 0, 0, 0.5)', + borderColor: 'var(--border)', + highlightColor: 'var(--accent-strong)', + fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" + }, + blue: { + backgroundColor: 'var(--card-base)', + textColor: 'var(--fg)', + primaryColor: 'var(--accent)', + overlayColor: 'rgba(25, 8, 172, 0.7)', + borderColor: 'var(--border)', + highlightColor: 'var(--accent)', + fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" + }, + nord: { + backgroundColor: 'var(--card-base)', + textColor: 'var(--fg)', + primaryColor: 'var(--accent)', + overlayColor: 'rgba(46, 52, 64, 0.7)', + borderColor: 'var(--border)', + highlightColor: 'var(--accent)', + fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" + }, + dracula: { + backgroundColor: 'var(--card-base)', + textColor: 'var(--fg)', + primaryColor: 'var(--accent)', + overlayColor: 'rgba(40, 42, 54, 0.7)', + borderColor: 'var(--border)', + highlightColor: 'var(--accent)', + fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" + }, + 'solarized-dark': { + backgroundColor: 'var(--card-base)', + textColor: 'var(--fg)', + primaryColor: 'var(--accent)', + overlayColor: 'rgba(0, 43, 54, 0.7)', + borderColor: 'var(--border)', + highlightColor: 'var(--accent)', + fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" + }, + 'solarized-light': { + backgroundColor: 'var(--card-base)', + textColor: 'var(--fg)', + primaryColor: 'var(--accent)', + overlayColor: 'rgba(253, 246, 227, 0.7)', + borderColor: 'var(--border)', + highlightColor: 'var(--accent)', + fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" + } + }; + + /** + * ThemeAdapter class + * Manages theme integration for the tooltip system + */ + class ThemeAdapter { + constructor() { + this.currentTheme = this.getCurrentTheme(); + this.themeChangeCallbacks = []; + this._setupThemeChangeListener(); + } + + /** + * Get the current theme name from document root class + * @returns {string} Current theme name (e.g., 'dark', 'light', 'blue') + */ + getCurrentTheme() { + const root = document.documentElement; + const classList = Array.from(root.classList); + + // Check for theme classes + const themeClasses = ['light', 'blue', 'nord', 'dracula', 'solarized-dark', 'solarized-light']; + const foundTheme = themeClasses.find(theme => classList.includes(theme)); + + // Default to 'dark' if no theme class found + return foundTheme || 'dark'; + } + + /** + * Get Driver.js theme configuration for current theme + * @returns {Object} Theme configuration object + */ + getDriverTheme() { + const themeName = this.getCurrentTheme(); + const config = THEME_CONFIGS[themeName] || THEME_CONFIGS.dark; + + // Resolve CSS variables to actual values + const resolvedConfig = {}; + for (const [key, value] of Object.entries(config)) { + if (typeof value === 'string' && value.startsWith('var(')) { + // Extract CSS variable name + const varName = value.match(/var\((--[^)]+)\)/)?.[1]; + if (varName) { + const computedValue = getComputedStyle(document.documentElement) + .getPropertyValue(varName) + .trim(); + resolvedConfig[key] = computedValue || value; + } else { + resolvedConfig[key] = value; + } + } else { + resolvedConfig[key] = value; + } + } + + return resolvedConfig; + } + + /** + * Register a callback for theme changes + * @param {Function} callback - Function to call when theme changes + */ + onThemeChange(callback) { + if (typeof callback === 'function') { + this.themeChangeCallbacks.push(callback); + } + } + + /** + * Setup theme change listener using MutationObserver + * @private + */ + _setupThemeChangeListener() { + const root = document.documentElement; + + // Create observer to watch for class changes on root element + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + const newTheme = this.getCurrentTheme(); + if (newTheme !== this.currentTheme) { + const oldTheme = this.currentTheme; + this.currentTheme = newTheme; + this._notifyThemeChange(newTheme, oldTheme); + } + } + }); + }); + + // Start observing + observer.observe(root, { + attributes: true, + attributeFilter: ['class'] + }); + + console.log('[ThemeAdapter] Theme change listener initialized'); + } + + /** + * Notify all registered callbacks of theme change + * @private + * @param {string} newTheme - New theme name + * @param {string} oldTheme - Old theme name + */ + _notifyThemeChange(newTheme, oldTheme) { + console.log(`[ThemeAdapter] Theme changed: ${oldTheme} → ${newTheme}`); + + this.themeChangeCallbacks.forEach(callback => { + try { + callback(newTheme, oldTheme); + } catch (error) { + console.error('[ThemeAdapter] Error in theme change callback:', error); + } + }); + } + + /** + * Apply theme to Driver.js instance + * @param {Object} driver - Driver.js instance + */ + applyTheme(driver) { + if (!driver) { + console.warn('[ThemeAdapter] No driver instance provided'); + return; + } + + const themeConfig = this.getDriverTheme(); + + // Apply theme configuration to driver + // Note: Driver.js v1.0+ uses CSS variables, so we inject a style element + this._injectDriverStyles(themeConfig); + + console.log('[ThemeAdapter] Theme applied to driver:', this.currentTheme); + } + + /** + * Inject custom styles for Driver.js based on theme + * @private + * @param {Object} themeConfig - Theme configuration + */ + _injectDriverStyles(themeConfig) { + // Remove existing theme styles + const existingStyle = document.getElementById('driver-theme-styles'); + if (existingStyle) { + existingStyle.remove(); + } + + // Create new style element + const style = document.createElement('style'); + style.id = 'driver-theme-styles'; + style.textContent = ` + .driver-popover { + background: ${themeConfig.backgroundColor} !important; + color: ${themeConfig.textColor} !important; + border: 1px solid ${themeConfig.borderColor} !important; + border-radius: 12px !important; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4) !important; + font-family: ${themeConfig.fontFamily} !important; + } + + .driver-popover-title { + color: ${themeConfig.textColor} !important; + font-weight: 600 !important; + font-family: ${themeConfig.fontFamily} !important; + } + + .driver-popover-description { + color: ${themeConfig.textColor} !important; + font-family: ${themeConfig.fontFamily} !important; + } + + .driver-popover-footer button { + background: ${themeConfig.primaryColor} !important; + color: ${themeConfig.backgroundColor} !important; + border: none !important; + font-family: ${themeConfig.fontFamily} !important; + font-weight: 500 !important; + } + + .driver-popover-footer button:hover { + opacity: 0.9 !important; + } + + .driver-popover-close-btn { + color: ${themeConfig.textColor} !important; + } + + .driver-overlay { + background: ${themeConfig.overlayColor} !important; + } + + .driver-highlighted-element { + outline: 2px solid ${themeConfig.highlightColor} !important; + outline-offset: 4px !important; + } + + .driver-popover-progress-text { + color: ${themeConfig.textColor} !important; + opacity: 0.7 !important; + font-family: ${themeConfig.fontFamily} !important; + } + `; + + document.head.appendChild(style); + } + + /** + * Get all available theme names + * @returns {string[]} Array of theme names + */ + getAvailableThemes() { + return Object.keys(THEME_CONFIGS); + } + + /** + * Check if a theme is available + * @param {string} themeName - Theme name to check + * @returns {boolean} True if theme is available + */ + isThemeAvailable(themeName) { + return THEME_CONFIGS.hasOwnProperty(themeName); + } + } + + // Export to global scope + window.ThemeAdapter = ThemeAdapter; + + console.log('[ThemeAdapter] Module loaded'); + +})(window); diff --git a/dashcaddy-api/assets/tooltip-definitions.js b/dashcaddy-api/assets/tooltip-definitions.js new file mode 100644 index 0000000..8ef36d2 --- /dev/null +++ b/dashcaddy-api/assets/tooltip-definitions.js @@ -0,0 +1,337 @@ +/** + * Tooltip Definitions + * Defines all tooltip content, positioning, and behavior for the onboarding system + */ + +(function(window) { + 'use strict'; + + /** + * Validate a tooltip definition + * @param {Object} tooltip - The tooltip definition to validate + * @returns {Object} { valid: boolean, errors: string[] } + */ + function validateTooltipDefinition(tooltip) { + const errors = []; + + // Required fields + if (!tooltip.id || typeof tooltip.id !== 'string') { + errors.push('Tooltip must have a valid string id'); + } + + if (!tooltip.element) { + errors.push('Tooltip must have an element selector or HTMLElement'); + } + + if (!tooltip.popover || typeof tooltip.popover !== 'object') { + errors.push('Tooltip must have a popover object'); + } else { + // Validate popover fields + if (!tooltip.popover.title || typeof tooltip.popover.title !== 'string') { + errors.push('Tooltip popover must have a valid string title'); + } + + if (!tooltip.popover.description || typeof tooltip.popover.description !== 'string') { + errors.push('Tooltip popover must have a valid string description'); + } + + // Validate position if provided + if (tooltip.popover.position) { + const validPositions = ['top', 'bottom', 'left', 'right', 'center']; + if (!validPositions.includes(tooltip.popover.position)) { + errors.push(`Invalid position: ${tooltip.popover.position}. Must be one of: ${validPositions.join(', ')}`); + } + } + + // Validate align if provided + if (tooltip.popover.align) { + const validAligns = ['start', 'center', 'end']; + if (!validAligns.includes(tooltip.popover.align)) { + errors.push(`Invalid align: ${tooltip.popover.align}. Must be one of: ${validAligns.join(', ')}`); + } + } + + // Validate showButtons if provided + if (tooltip.popover.showButtons && !Array.isArray(tooltip.popover.showButtons)) { + errors.push('showButtons must be an array'); + } + + // Validate callbacks if provided + const callbacks = ['onNext', 'onPrevious', 'onClose', 'onSetupNow', 'onLater']; + callbacks.forEach(callback => { + if (tooltip.popover[callback] && typeof tooltip.popover[callback] !== 'function') { + errors.push(`${callback} must be a function`); + } + }); + } + + // Validate condition if provided + if (tooltip.condition && typeof tooltip.condition !== 'function') { + errors.push('condition must be a function'); + } + + // Validate priority if provided + if (tooltip.priority !== undefined && typeof tooltip.priority !== 'number') { + errors.push('priority must be a number'); + } + + return { + valid: errors.length === 0, + errors + }; + } + + /** + * Validate an array of tooltip definitions + * @param {Array} tooltips - Array of tooltip definitions + * @returns {Object} { valid: boolean, errors: Object[] } + */ + function validateTooltipDefinitions(tooltips) { + if (!Array.isArray(tooltips)) { + return { + valid: false, + errors: [{ tooltip: null, errors: ['tooltips must be an array'] }] + }; + } + + const allErrors = []; + const ids = new Set(); + + tooltips.forEach((tooltip, index) => { + const validation = validateTooltipDefinition(tooltip); + + if (!validation.valid) { + allErrors.push({ + tooltip: tooltip.id || `index ${index}`, + errors: validation.errors + }); + } + + // Check for duplicate IDs + if (tooltip.id) { + if (ids.has(tooltip.id)) { + allErrors.push({ + tooltip: tooltip.id, + errors: [`Duplicate tooltip ID: ${tooltip.id}`] + }); + } + ids.add(tooltip.id); + } + }); + + return { + valid: allErrors.length === 0, + errors: allErrors + }; + } + + /** + * Error handler for tooltip system + */ + class TooltipError extends Error { + constructor(message, tooltipId = null) { + super(message); + this.name = 'TooltipError'; + this.tooltipId = tooltipId; + } + } + + /** + * Handle tooltip definition errors + * @param {Object} validation - Validation result + * @throws {TooltipError} If validation fails + */ + function handleValidationErrors(validation) { + if (!validation.valid) { + const errorMessages = validation.errors.map(e => + `${e.tooltip}: ${e.errors.join(', ')}` + ).join('\n'); + + console.error('[TooltipDefinitions] Validation errors:', errorMessages); + throw new TooltipError(`Tooltip validation failed:\n${errorMessages}`); + } + } + + // Export to global scope + window.TooltipValidation = { + validateTooltipDefinition, + validateTooltipDefinitions, + handleValidationErrors, + TooltipError + }; + + console.log('[TooltipDefinitions] Validation module loaded'); + +})(window); + + +/** + * Tooltip Definitions Array + * Defines all tooltips for the onboarding tour + */ +const TOOLTIP_DEFINITIONS = [ + // 1. Welcome tooltip pointing to logo + { + id: 'welcome', + element: '#brand', + popover: { + title: 'Welcome to DashCaddy!', + description: ` +Your personal dashboard for managing services with Caddy reverse proxy.
+Let's take a quick tour to help you get started.
+Tip: You can customize this logo in Settings.
+ `, + position: 'bottom', + align: 'start', + showButtons: ['next'], + showProgress: true + }, + priority: 1, + isNewFeature: false + }, + + // 2. Add Service button + { + id: 'add-service', + element: '#add-service-btn', + popover: { + title: 'Adding New Services', + description: ` +Click + Add Service to deploy new apps or add existing services to your dashboard.
+Choose from 50+ templates including:
+This is your service grid where all your deployed applications appear.
+Each card shows:
+DashCaddy comes with 7 themes. Click here to switch between them.
+Your preference is saved automatically.
+ `, + position: 'bottom', + showButtons: ['previous', 'close'], + showProgress: true + }, + priority: 4, + isNewFeature: false, + condition: () => { + return document.getElementById('theme') !== null; + } + } +]; + +/** + * Get tooltip definitions + * @returns {Array} Array of tooltip definitions + */ +function getTooltipDefinitions() { + return TOOLTIP_DEFINITIONS; +} + +/** + * Get a specific tooltip by ID + * @param {string} id - Tooltip ID + * @returns {Object|null} Tooltip definition or null if not found + */ +function getTooltipById(id) { + return TOOLTIP_DEFINITIONS.find(t => t.id === id) || null; +} + +/** + * Get tooltips filtered by condition + * @returns {Array} Array of tooltips that pass their condition check + */ +function getActiveTooltips() { + return TOOLTIP_DEFINITIONS.filter(tooltip => { + if (tooltip.condition && typeof tooltip.condition === 'function') { + try { + return tooltip.condition(); + } catch (error) { + console.error(`[TooltipDefinitions] Error evaluating condition for ${tooltip.id}:`, error); + return false; + } + } + return true; + }); +} + +/** + * Get tooltips sorted by priority + * @returns {Array} Array of tooltips sorted by priority (ascending) + */ +function getSortedTooltips() { + const tooltips = getActiveTooltips(); + return tooltips.sort((a, b) => { + const priorityA = a.priority || 999; + const priorityB = b.priority || 999; + return priorityA - priorityB; + }); +} + +/** + * Get tooltips marked as new features + * @returns {Array} Array of tooltips marked with isNewFeature flag + */ +function getNewFeatureTooltips() { + const tooltips = getActiveTooltips(); + return tooltips.filter(tooltip => tooltip.isNewFeature === true) + .sort((a, b) => { + const priorityA = a.priority || 999; + const priorityB = b.priority || 999; + return priorityA - priorityB; + }); +} + +// Export to global scope +window.TooltipDefinitions = { + TOOLTIP_DEFINITIONS, + getTooltipDefinitions, + getTooltipById, + getActiveTooltips, + getSortedTooltips, + getNewFeatureTooltips +}; + +console.log('[TooltipDefinitions] Definitions loaded:', TOOLTIP_DEFINITIONS.length, 'tooltips'); + diff --git a/dashcaddy-api/assets/tour-manager.js b/dashcaddy-api/assets/tour-manager.js new file mode 100644 index 0000000..d972a7c --- /dev/null +++ b/dashcaddy-api/assets/tour-manager.js @@ -0,0 +1,363 @@ +/** + * Tour Manager + * Orchestrates the onboarding tour using Driver.js + */ + +(function(window) { + 'use strict'; + + class TourManager { + constructor(progressTracker, themeAdapter, dnsTemplateSelector) { + this.progressTracker = progressTracker; + this.themeAdapter = themeAdapter; + this.dnsTemplateSelector = dnsTemplateSelector; + this.driver = null; + this.currentStepIndex = 0; + this.isActive = false; + this.resizeHandler = null; + this.layoutChangeHandler = null; + } + + /** + * Initialize Driver.js with theme-aware configuration + */ + async initializeDriver() { + // Driver.js v1.x IIFE: window.driver.js.driver is the factory function + const driverFactory = window.driver?.js?.driver || window.driver?.driver || window.driver; + + if (typeof driverFactory !== 'function') { + console.error('[TourManager] Driver.js not loaded or invalid. window.driver:', window.driver); + return false; + } + + const themeConfig = this.themeAdapter.getDriverTheme(); + + this.driver = driverFactory({ + showProgress: true, + showButtons: ['next', 'previous', 'close'], + allowClose: true, + overlayClickNext: false, + overlayOpacity: 0, + stagePadding: 0, + stageRadius: 0, + allowKeyboardControl: true, + popoverClass: 'dashcaddy-popover', + onDestroyed: () => this.onTourComplete(), + onDestroyStarted: () => { + if (!this.progressTracker.isTourCompleted()) { + this.onTourSkip(); + } + } + }); + + // Apply theme + this.themeAdapter.applyTheme(this.driver); + + // Listen for theme changes + this.themeAdapter.onThemeChange(() => { + this.themeAdapter.applyTheme(this.driver); + }); + + // Set up dynamic repositioning + this.setupDynamicRepositioning(); + + return true; + } + + /** + * Check if tour should auto-start + */ + shouldAutoStart() { + return !this.progressTracker.isTourCompleted() && + this.progressTracker.getCurrentStep() === 0; + } + + /** + * Start the onboarding tour + */ + async startTour() { + if (!this.driver) { + const initialized = await this.initializeDriver(); + if (!initialized) return; + } + + // Get active tooltips (filtered by conditions) + const allTooltips = window.TooltipDefinitions.getSortedTooltips(); + + // Filter out completed tooltips + const completedIds = this.progressTracker.getCompletedTooltips(); + const activeTooltips = allTooltips.filter(t => !completedIds.includes(t.id)); + + if (activeTooltips.length === 0) { + console.log('[TourManager] No tooltips to show'); + this.progressTracker.markTourCompleted(); + return; + } + + // Convert to Driver.js steps with navigation logic + const steps = activeTooltips.map((tooltip, index) => { + const isFirst = index === 0; + const isLast = index === activeTooltips.length - 1; + + const step = { + element: tooltip.element, + popover: { + title: tooltip.popover.title, + description: tooltip.popover.description, + side: tooltip.popover.position || 'bottom', + align: tooltip.popover.align || 'start', + showButtons: this._getButtonsForStep(tooltip, isFirst, isLast), + showProgress: tooltip.popover.showProgress !== false, + onNextClick: () => { + this.progressTracker.markTooltipCompleted(tooltip.id); + this.progressTracker.setCurrentStep(index + 1); + this.currentStepIndex = index + 1; + this.driver.moveNext(); + }, + onPrevClick: () => { + this.progressTracker.setCurrentStep(Math.max(0, index - 1)); + this.currentStepIndex = Math.max(0, index - 1); + this.driver.movePrevious(); + }, + onCloseClick: () => { + this.skipTour(); + } + } + }; + + // Add custom handlers for DNS tooltip + if (tooltip.id === 'dns-priority' && this.dnsTemplateSelector) { + step.popover.onSetupNowClick = () => { + console.log('[TourManager] Opening DNS template selector'); + this.dnsTemplateSelector.showTemplateSelector(); + // Mark tooltip as completed and move to next + this.progressTracker.markTooltipCompleted(tooltip.id); + this.progressTracker.setCurrentStep(index + 1); + this.currentStepIndex = index + 1; + this.driver.moveNext(); + }; + + step.popover.onLaterClick = () => { + console.log('[TourManager] DNS setup deferred'); + this.progressTracker.markDnsSetupDeferred(); + // Mark tooltip as completed and move to next + this.progressTracker.markTooltipCompleted(tooltip.id); + this.progressTracker.setCurrentStep(index + 1); + this.currentStepIndex = index + 1; + this.driver.moveNext(); + }; + } + + return step; + }); + + this.isActive = true; + this.driver.setSteps(steps); + this.driver.drive(); + } + + /** + * Resume tour from last step + */ + async resumeTour() { + const currentStep = this.progressTracker.getCurrentStep(); + if (currentStep > 0) { + await this.startTour(); + // Driver.js will start from beginning, we'd need to skip to current step + // This is a simplified implementation + } else { + await this.startTour(); + } + } + + /** + * Skip the entire tour + */ + skipTour() { + if (this.driver) { + this.driver.destroy(); + } + this.cleanupDynamicRepositioning(); + this.isActive = false; + } + + /** + * Restart tour from beginning + */ + async restartTour() { + this.progressTracker.resetProgress(); + await this.startTour(); + } + + /** + * Show a specific tooltip by ID + */ + async showTooltip(tooltipId) { + const tooltip = window.TooltipDefinitions.getTooltipById(tooltipId); + if (!tooltip) { + console.error(`[TourManager] Tooltip not found: ${tooltipId}`); + return; + } + + if (!this.driver) { + await this.initializeDriver(); + } + + const step = { + element: tooltip.element, + popover: { + title: tooltip.popover.title, + description: tooltip.popover.description, + side: tooltip.popover.position || 'bottom', + align: tooltip.popover.align || 'start' + } + }; + + this.driver.highlight(step); + } + + /** + * Show "What's New" tour - only tooltips marked as new features + */ + async showWhatsNew() { + if (!this.driver) { + const initialized = await this.initializeDriver(); + if (!initialized) return; + } + + // Get only new feature tooltips + const newFeatureTooltips = window.TooltipDefinitions.getNewFeatureTooltips(); + + if (newFeatureTooltips.length === 0) { + console.log('[TourManager] No new features to show'); + return; + } + + console.log(`[TourManager] Showing ${newFeatureTooltips.length} new features`); + + // Convert to Driver.js steps + const steps = newFeatureTooltips.map((tooltip, index) => { + const isFirst = index === 0; + const isLast = index === newFeatureTooltips.length - 1; + + return { + element: tooltip.element, + popover: { + title: `✨ NEW: ${tooltip.popover.title}`, + description: tooltip.popover.description, + side: tooltip.popover.position || 'bottom', + align: tooltip.popover.align || 'start', + showButtons: this._getButtonsForStep(tooltip, isFirst, isLast), + showProgress: true, + onNextClick: () => { + this.driver.moveNext(); + }, + onPrevClick: () => { + this.driver.movePrevious(); + }, + onCloseClick: () => { + this.skipTour(); + } + } + }; + }); + + this.isActive = true; + this.driver.setSteps(steps); + this.driver.drive(); + } + + /** + * Set up dynamic repositioning for window resize and layout changes + */ + setupDynamicRepositioning() { + // Window resize handler with debouncing + let resizeTimeout; + this.resizeHandler = () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + if (this.isActive && this.driver) { + console.log('[TourManager] Window resized, repositioning tooltip'); + this.driver.refresh(); + } + }, 150); // Debounce for 150ms + }; + + // Layout change handler (for theme changes, DOM mutations) + this.layoutChangeHandler = () => { + if (this.isActive && this.driver) { + console.log('[TourManager] Layout changed, repositioning tooltip'); + // Small delay to allow layout to settle + setTimeout(() => { + if (this.driver) { + this.driver.refresh(); + } + }, 100); + } + }; + + // Add event listeners + window.addEventListener('resize', this.resizeHandler); + + // Listen for theme changes (already handled by ThemeAdapter, but also trigger reposition) + this.themeAdapter.onThemeChange(this.layoutChangeHandler); + } + + /** + * Clean up dynamic repositioning listeners + */ + cleanupDynamicRepositioning() { + if (this.resizeHandler) { + window.removeEventListener('resize', this.resizeHandler); + } + } + + /** + * Get buttons to show for a specific step + * @private + */ + _getButtonsForStep(tooltip, isFirst, isLast) { + // Check if tooltip has custom buttons defined + if (tooltip.popover.showButtons) { + return tooltip.popover.showButtons; + } + + // Default button configuration + const buttons = []; + + if (!isFirst) { + buttons.push('previous'); + } + + if (!isLast) { + buttons.push('next'); + } else { + buttons.push('close'); + } + + return buttons; + } + + /** + * Handle tour completion + */ + onTourComplete() { + this.progressTracker.markTourCompleted(); + this.isActive = false; + console.log('[TourManager] Tour completed'); + } + + /** + * Handle tour skip + */ + onTourSkip() { + // Save current progress but don't mark as completed + console.log('[TourManager] Tour skipped'); + this.isActive = false; + } + } + + window.TourManager = TourManager; + console.log('[TourManager] Module loaded'); + +})(window); diff --git a/dashcaddy-api/assets/transmission.png b/dashcaddy-api/assets/transmission.png new file mode 100644 index 0000000..57bedae Binary files /dev/null and b/dashcaddy-api/assets/transmission.png differ diff --git a/dashcaddy-api/assets/weather/clear-day.svg b/dashcaddy-api/assets/weather/clear-day.svg new file mode 100644 index 0000000..d0d36ca --- /dev/null +++ b/dashcaddy-api/assets/weather/clear-day.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/clear-night.svg b/dashcaddy-api/assets/weather/clear-night.svg new file mode 100644 index 0000000..bd3f1cb --- /dev/null +++ b/dashcaddy-api/assets/weather/clear-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/cloudy.svg b/dashcaddy-api/assets/weather/cloudy.svg new file mode 100644 index 0000000..b868d87 --- /dev/null +++ b/dashcaddy-api/assets/weather/cloudy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/drizzle.svg b/dashcaddy-api/assets/weather/drizzle.svg new file mode 100644 index 0000000..27513c8 --- /dev/null +++ b/dashcaddy-api/assets/weather/drizzle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/fog.svg b/dashcaddy-api/assets/weather/fog.svg new file mode 100644 index 0000000..12208db --- /dev/null +++ b/dashcaddy-api/assets/weather/fog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/partly-cloudy-day.svg b/dashcaddy-api/assets/weather/partly-cloudy-day.svg new file mode 100644 index 0000000..6fcec43 --- /dev/null +++ b/dashcaddy-api/assets/weather/partly-cloudy-day.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/partly-cloudy-night.svg b/dashcaddy-api/assets/weather/partly-cloudy-night.svg new file mode 100644 index 0000000..2c49905 --- /dev/null +++ b/dashcaddy-api/assets/weather/partly-cloudy-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/rain.svg b/dashcaddy-api/assets/weather/rain.svg new file mode 100644 index 0000000..74b33d3 --- /dev/null +++ b/dashcaddy-api/assets/weather/rain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/sleet.svg b/dashcaddy-api/assets/weather/sleet.svg new file mode 100644 index 0000000..03d6a3a --- /dev/null +++ b/dashcaddy-api/assets/weather/sleet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/snow.svg b/dashcaddy-api/assets/weather/snow.svg new file mode 100644 index 0000000..e444068 --- /dev/null +++ b/dashcaddy-api/assets/weather/snow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/thunderstorm.svg b/dashcaddy-api/assets/weather/thunderstorm.svg new file mode 100644 index 0000000..390f11b --- /dev/null +++ b/dashcaddy-api/assets/weather/thunderstorm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/assets/weather/wind.svg b/dashcaddy-api/assets/weather/wind.svg new file mode 100644 index 0000000..55d168c --- /dev/null +++ b/dashcaddy-api/assets/weather/wind.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashcaddy-api/audit-logger.js b/dashcaddy-api/audit-logger.js index 60f614b..3fdf5b8 100644 --- a/dashcaddy-api/audit-logger.js +++ b/dashcaddy-api/audit-logger.js @@ -111,7 +111,7 @@ class AuditLogger { action: action || '', resource: resource || '', details: details || {}, - outcome: outcome || 'unknown', + outcome: outcome || 'unknown' }; await this.stateManager.update(entries => { diff --git a/dashcaddy-api/auth-manager.js b/dashcaddy-api/auth-manager.js index a7039b5..bb0a9bc 100644 --- a/dashcaddy-api/auth-manager.js +++ b/dashcaddy-api/auth-manager.js @@ -8,10 +8,8 @@ const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const credentialManager = require('./credential-manager'); const cryptoUtils = require('./crypto-utils'); -const { safeLog } = require('./logger-utils'); // JWT signing secret - derived from encryption key for consistency -// SECURITY: Loaded from secure storage, never logged const JWT_SECRET = cryptoUtils.loadOrCreateKey(); // Namespace for API keys in credential manager @@ -40,13 +38,12 @@ class AuthManager { { ...payload, iat: Math.floor(Date.now() / 1000), - scope: payload.scope || ['read', 'write'], + scope: payload.scope || ['read', 'write'] }, JWT_SECRET, - { expiresIn }, + { expiresIn } ); - // SECURITY: Log event only, never log the actual token console.log(`[AuthManager] Generated JWT for user: ${payload.sub}, expires in: ${expiresIn}`); return token; } catch (error) { @@ -67,14 +64,13 @@ class AuthManager { userId: decoded.sub, scope: decoded.scope || [], iat: decoded.iat, - exp: decoded.exp, + exp: decoded.exp }; } catch (error) { if (error.name === 'TokenExpiredError') { console.log('[AuthManager] JWT token expired'); } else if (error.name === 'JsonWebTokenError') { - // SECURITY: Never log the actual token - console.log('[AuthManager] JWT token invalid'); + console.log('[AuthManager] JWT token invalid:', error.message); } else { console.error('[AuthManager] JWT verification failed:', error.message); } @@ -111,7 +107,7 @@ class AuthManager { name, scopes, createdAt: new Date().toISOString(), - lastUsed: null, + lastUsed: null }; const metadataKey = `${API_KEY_METADATA_NAMESPACE}.${keyId}`; @@ -120,7 +116,6 @@ class AuthManager { // Cache metadata this.keyMetadataCache.set(keyId, metadata); - // SECURITY: Log event only, never log the actual API key console.log(`[AuthManager] Generated API key: ${name} (${keyId})`); return { @@ -128,7 +123,7 @@ class AuthManager { id: keyId, name, scopes, - createdAt: metadata.createdAt, + createdAt: metadata.createdAt }; } catch (error) { console.error('[AuthManager] API key generation failed:', error.message); @@ -179,7 +174,7 @@ class AuthManager { // Update last used timestamp (non-blocking) this.updateLastUsed(keyId, metadata).catch(err => - console.error(`[AuthManager] Failed to update lastUsed for ${keyId}:`, err.message), + console.error(`[AuthManager] Failed to update lastUsed for ${keyId}:`, err.message) ); console.log(`[AuthManager] API key verified: ${metadata.name} (${keyId})`); @@ -187,7 +182,7 @@ class AuthManager { return { keyId, scopes: metadata.scopes || [], - name: metadata.name, + name: metadata.name }; } catch (error) { console.error('[AuthManager] API key verification failed:', error.message); @@ -282,7 +277,7 @@ class AuthManager { try { const updatedMetadata = { ...metadata, - lastUsed: new Date().toISOString(), + lastUsed: new Date().toISOString() }; const metadataKey = `${API_KEY_METADATA_NAMESPACE}.${keyId}`; diff --git a/dashcaddy-api/backup-manager.js b/dashcaddy-api/backup-manager.js index 3ec8b28..9a30d12 100644 --- a/dashcaddy-api/backup-manager.js +++ b/dashcaddy-api/backup-manager.js @@ -165,7 +165,7 @@ class BackupManager extends EventEmitter { locations: savedLocations, encrypted: !!backup.encrypt, compressed: true, - status: 'success', + status: 'success' }; this.addToHistory(historyEntry); @@ -187,7 +187,7 @@ class BackupManager extends EventEmitter { timestamp: new Date().toISOString(), duration, status: 'failed', - error: error.message, + error: error.message }; this.addToHistory(historyEntry); @@ -205,7 +205,7 @@ class BackupManager extends EventEmitter { version: '1.0', timestamp: new Date().toISOString(), hostname: require('os').hostname(), - data: {}, + data: {} }; for (const source of include) { @@ -332,10 +332,10 @@ class BackupManager extends EventEmitter { HostConfig: { Binds: [ `${volumeName}:/volume:ro`, - `${backupDir}:/backup`, + `${backupDir}:/backup` ], - AutoRemove: true, - }, + AutoRemove: true + } }); // Start and wait for completion @@ -354,7 +354,7 @@ class BackupManager extends EventEmitter { path: backupFile, size: stats.size, timestamp: new Date().toISOString(), - status: 'success', + status: 'success' }); } } catch (volumeError) { @@ -362,7 +362,7 @@ class BackupManager extends EventEmitter { backupResults.push({ name: volume.Name, status: 'failed', - error: volumeError.message, + error: volumeError.message }); } } @@ -371,7 +371,7 @@ class BackupManager extends EventEmitter { timestamp: new Date().toISOString(), totalVolumes: volumes.length, successCount: backupResults.filter(r => r.status === 'success').length, - volumes: backupResults, + volumes: backupResults }; } catch (error) { console.error('[BackupManager] Error backing up volumes:', error.message); @@ -425,10 +425,10 @@ class BackupManager extends EventEmitter { HostConfig: { Binds: [ `${volumeName}:/volume`, - `${backupDir}:/backup:ro`, + `${backupDir}:/backup:ro` ], - AutoRemove: true, - }, + AutoRemove: true + } }); await container.start(); @@ -442,7 +442,7 @@ class BackupManager extends EventEmitter { restoreResults.push({ name: volumeName, status: 'success', - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }); console.log(`[BackupManager] Volume ${volumeName} restored successfully`); @@ -451,7 +451,7 @@ class BackupManager extends EventEmitter { restoreResults.push({ name: volBackup.name, status: 'failed', - error: restoreError.message, + error: restoreError.message }); } } @@ -460,7 +460,7 @@ class BackupManager extends EventEmitter { timestamp: new Date().toISOString(), results: restoreResults, successCount: restoreResults.filter(r => r.status === 'success').length, - failedCount: restoreResults.filter(r => r.status === 'failed').length, + failedCount: restoreResults.filter(r => r.status === 'failed').length }; } @@ -498,7 +498,7 @@ class BackupManager extends EventEmitter { // Return: iv:authTag:encrypted (all base64) return Buffer.from( - `${iv.toString('base64') }:${ authTag.toString('base64') }:${ encrypted.toString('base64')}`, + iv.toString('base64') + ':' + authTag.toString('base64') + ':' + encrypted.toString('base64') ); } @@ -566,7 +566,7 @@ class BackupManager extends EventEmitter { return { type: 'local', path: filepath, - size: data.length, + size: data.length }; } @@ -652,7 +652,7 @@ class BackupManager extends EventEmitter { this.emit('restore-complete', { backupId, restored, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }); console.log('[BackupManager] Restore completed successfully'); @@ -661,7 +661,7 @@ class BackupManager extends EventEmitter { this.emit('restore-failed', { backupId, error: error.message, - timestamp: new Date().toISOString(), + timestamp: new Date().toISOString() }); throw error; } @@ -790,7 +790,7 @@ class BackupManager extends EventEmitter { return { backups: {}, - defaultRetention: { keep: 7 }, + defaultRetention: { keep: 7 } }; } diff --git a/dashcaddy-api/ca/CERTIFICATE-API.md b/dashcaddy-api/ca/CERTIFICATE-API.md new file mode 100644 index 0000000..fb5452a --- /dev/null +++ b/dashcaddy-api/ca/CERTIFICATE-API.md @@ -0,0 +1,306 @@ +# DashCA Certificate Generation API + +## Overview + +DashCA now provides automatic SSL certificate generation for services on your network. This allows services like Technitium DNS to automatically obtain valid SSL certificates signed by your internal CA. + +## Features + +- **Automatic Certificate Generation**: Generate SSL certificates for any domain on demand +- **Multiple Formats**: PFX, PEM, CRT, KEY, and full chain certificates +- **Certificate Caching**: Certificates are cached and reused if still valid (>30 days remaining) +- **No Authentication Required**: Public API endpoints for easy automation +- **Automatic Renewal**: Certificates are regenerated when they expire in less than 30 days + +## API Endpoints + +### 1. Generate/Download Certificate + +**GET** `/api/ca/cert/:domain` + +Generate and download an SSL certificate for the specified domain. + +**Parameters:** +- `:domain` (required) - The domain name (e.g., `dns1.sami`, `dns2.sami`) +- `format` (optional) - Certificate format: `pfx`, `pem`, `crt`, `key`, or `fullchain` (default: `pfx`) +- `password` (optional) - Password for PFX files (default: `dashcaddy`) + +**Examples:** + +```bash +# Download PFX certificate for dns1.sami +curl -O "https://ca.sami/api/ca/cert/dns1.sami?format=pfx&password=dashcaddy" + +# Download PEM bundle (private key + certificate + chain) +curl -O "https://ca.sami/api/ca/cert/dns1.sami?format=pem" + +# Download certificate only +curl -O "https://ca.sami/api/ca/cert/dns1.sami?format=crt" + +# Download private key only +curl -O "https://ca.sami/api/ca/cert/dns1.sami?format=key" + +# Download full certificate chain (cert + intermediate + root) +curl -O "https://ca.sami/api/ca/cert/dns1.sami?format=fullchain" +``` + +**PowerShell:** + +```powershell +Invoke-WebRequest -Uri "https://ca.sami/api/ca/cert/dns1.sami?format=pfx" -OutFile "dns1.pfx" +``` + +### 2. List Generated Certificates + +**GET** `/api/ca/certs` + +List all generated certificates with their status and expiration information. + +**Example:** + +```bash +curl https://ca.sami/api/ca/certs +``` + +**Response:** + +```json +{ + "success": true, + "certificates": [ + { + "domain": "dns1.sami", + "subject": "CN=dns1.sami", + "validFrom": "Feb 11 21:37:02 2026 GMT", + "validUntil": "Feb 11 21:37:02 2027 GMT", + "daysUntilExpiration": 364, + "fingerprint": "4E:74:F8:49:...", + "status": "valid" + } + ] +} +``` + +**Status values:** +- `valid` - Certificate is valid and has >30 days remaining +- `expiring-soon` - Certificate expires in less than 30 days +- `expired` - Certificate has expired + +## Certificate Details + +- **Validity**: 365 days (1 year) +- **Algorithm**: RSA 2048-bit (for broad compatibility) +- **Signature**: SHA-256 +- **Key Usage**: Key Encipherment, Data Encipherment, Digital Signature +- **Extended Key Usage**: Server Authentication +- **Subject Alternative Names**: Primary domain + wildcard subdomain (if applicable) + +Example: A certificate for `dns1.sami` includes both `dns1.sami` and `*.dns1.sami`. + +## Automation Scripts + +### PowerShell - Update Technitium DNS Certificate + +Located at: `C:\caddy\scripts\update-technitium-cert.ps1` + +```powershell +# Update certificate for a single DNS server +.\update-technitium-cert.ps1 ` + -Domain "dns1.sami" ` + -TechnitiumUrl "http://192.168.254.204:5380" ` + -CertPath "C:\ProgramData\Technitium\DnsServer\cert.pfx" + +# Update all DNS servers at once +.\update-all-dns-certs.ps1 +``` + +### Schedule Automatic Updates + +Create a Windows scheduled task to update certificates monthly: + +```powershell +# Create scheduled task for monthly certificate updates +schtasks /create ` + /tn "Update DNS Certificates" ` + /tr "powershell -ExecutionPolicy Bypass -File C:\caddy\scripts\update-all-dns-certs.ps1" ` + /sc monthly ` + /d 1 ` + /st 03:00 ` + /ru SYSTEM +``` + +## Manual Certificate Installation + +### Technitium DNS + +1. Download PFX certificate: + ```bash + curl -k -O "https://ca.sami/api/ca/cert/dns1.sami?format=pfx&password=dashcaddy" + ``` + +2. Copy to Technitium directory: + ``` + Copy dns1.sami.pfx to C:\ProgramData\Technitium\DnsServer\cert.pfx + ``` + +3. Restart Technitium DNS Server service: + ```powershell + Restart-Service "Technitium DNS Server" + ``` + +4. Access Technitium DNS web interface via HTTPS: + ``` + https://dns1.sami:5380 + ``` + +### Other Services + +For other services requiring SSL certificates, use the appropriate format: + +- **PFX**: Windows services, .NET applications, IIS +- **PEM**: Most Linux services (Apache, Nginx, HAProxy) +- **CRT + KEY**: Separate certificate and key files (Nginx, Apache) +- **Fullchain**: Full certificate chain for maximum compatibility + +## Security Considerations + +1. **Root CA Protection**: The root CA private key is mounted read-only in the API container +2. **Serial Number Tracking**: Each certificate gets a unique serial number stored per domain +3. **Password Protection**: PFX files are password-protected (default: `dashcaddy`) +4. **Network Access**: Certificate API is available on your Tailscale network only +5. **No Revocation**: Certificates cannot be revoked; wait for expiration or delete manually + +## Troubleshooting + +### Certificate Generation Fails + +Check if the PKI directory is accessible: +```bash +docker exec dashcaddy-api ls -la /app/pki/ +``` + +Should show: `root.crt`, `root.key`, `intermediate.crt`, `intermediate.key` + +### Certificate Not Trusted by Browser + +Install the root CA certificate on your device: +```bash +curl -O https://ca.sami/root.crt +``` + +Follow platform-specific installation instructions on the DashCA homepage. + +### Certificate Expired + +Request a new certificate - it will automatically regenerate: +```bash +curl -O "https://ca.sami/api/ca/cert/dns1.sami?format=pfx" +``` + +### Generated Certificates Location + +On the host system: +``` +C:\caddy\generated-certs\ + ├── dns1.sami\ + │ ├── server.key + │ ├── server.csr + │ ├── server.crt + │ ├── server.pfx + │ ├── server.pem + │ └── fullchain.pem + ├── dns2.sami\ + └── dns3.sami\ +``` + +## Integration Examples + +### curl + +```bash +# Simple download +curl -O https://ca.sami/api/ca/cert/myservice.sami?format=pfx + +# With custom password +curl -O "https://ca.sami/api/ca/cert/myservice.sami?format=pfx&password=mypassword" +``` + +### PowerShell + +```powershell +# Download and install +$certPath = "C:\certs\myservice.pfx" +Invoke-WebRequest -Uri "https://ca.sami/api/ca/cert/myservice.sami?format=pfx" -OutFile $certPath + +# Verify certificate +$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath, "dashcaddy") +Write-Host "Certificate valid until: $($cert.NotAfter)" +``` + +### Bash/Linux + +```bash +#!/bin/bash +# Download and extract PEM files +curl -k "https://ca.sami/api/ca/cert/myservice.sami?format=pem" -o myservice.pem + +# Extract key and cert +openssl pkey -in myservice.pem -out myservice.key +openssl x509 -in myservice.pem -out myservice.crt +``` + +### Docker Compose + +```yaml +services: + myservice: + image: myimage + volumes: + - ./certs:/certs + environment: + SSL_CERT_FILE: /certs/myservice.crt + SSL_KEY_FILE: /certs/myservice.key + command: | + sh -c " + curl -k https://ca.sami/api/ca/cert/myservice.sami?format=crt -o /certs/myservice.crt && + curl -k https://ca.sami/api/ca/cert/myservice.sami?format=key -o /certs/myservice.key && + exec myservice + " +``` + +## DNS Server Configuration + +### DNS1 (Primary) +- **IP**: 192.168.254.204 +- **Domain**: dns1.sami +- **Certificate**: https://ca.sami/api/ca/cert/dns1.sami?format=pfx + +### DNS2 (Secondary) +- **IP**: 100.74.102.61 (Tailscale) +- **Domain**: dns2.sami +- **Certificate**: https://ca.sami/api/ca/cert/dns2.sami?format=pfx + +### DNS3 (Tertiary) +- **IP**: 100.89.216.23 (Tailscale) +- **Domain**: dns3.sami +- **Certificate**: https://ca.sami/api/ca/cert/dns3.sami?format=pfx + +## Future Enhancements + +Potential features for future versions: + +- Certificate revocation lists (CRL) +- OCSP responder +- Certificate renewal webhooks +- Email notifications for expiring certificates +- Web UI for certificate management +- Custom certificate validity periods +- Certificate templates for different service types +- Automatic DNS record validation + +## Support + +For issues or questions, check: +- DashCA homepage: https://ca.sami +- Certificate list: https://ca.sami/api/ca/certs +- Server logs: `docker logs dashcaddy-api` diff --git a/dashcaddy-api/ca/README.md b/dashcaddy-api/ca/README.md new file mode 100644 index 0000000..aa09881 --- /dev/null +++ b/dashcaddy-api/ca/README.md @@ -0,0 +1,281 @@ +# DashCA - Certificate Authority Distribution + +A self-hosted landing page for distributing your root CA certificate with one-click installation across all major platforms. + +## Quick Start + +### Regenerate All Certificate Formats + +```bash +cd scripts +bash generate-all.sh +``` + +This will: +1. Copy root.crt and intermediate.crt from Caddy PKI +2. Generate root.der (DER format for Windows) +3. Generate root.mobileconfig (Apple profile for iOS/macOS) +4. Extract certificate metadata to cert-info.json + +### Deploy to Production + +```bash +# Copy all files to production directory +cp -r e:/CaddyCerts/sites/ca/* C:/caddy/sites/ca/ +``` + +Or deploy via the dashboard app selector (preferred method). + +## File Structure + +``` +ca/ +├── index.html # Landing page with OS detection +├── root.crt # Root CA certificate (PEM format) +├── root.der # Root CA certificate (DER format) +├── root.mobileconfig # Apple configuration profile +├── intermediate.crt # Intermediate CA certificate +├── cert-info.json # Certificate metadata (auto-generated) +├── scripts/ +│ ├── install.ps1 # Windows PowerShell installer +│ ├── install.sh # Linux/macOS shell installer +│ ├── generate-cert-info.js # Extract certificate metadata +│ ├── generate-mobileconfig.js # Generate Apple profile +│ └── generate-all.sh # Wrapper script to regenerate all +└── assets/ + └── (icons, logos, etc.) +``` + +## Certificate Information + +**Source:** Caddy's built-in PKI at `C:/caddy/certs/pki/authorities/local/` + +- **Name:** Sami Home Network Root CA +- **Algorithm:** ECDSA P-256 with SHA-256 +- **Valid Until:** Dec 22, 2034 +- **Fingerprint:** `08:98:A5:63:F5:A1:A2:58:5F:02:D7:A8:A2:54:87:E6:BC:33:96:21:29:0E` + +## Installation Scripts + +### Windows (install.ps1) + +Features: +- Requires Administrator privileges +- Downloads certificate from ca.sami +- Verifies SHA-256 fingerprint +- Installs to LocalMachine\Root store +- Checks for existing installation + +**One-liner:** +```powershell +irm https://ca.sami/install.ps1 | iex +``` + +### Linux/macOS (install.sh) + +Features: +- Requires sudo/root +- Auto-detects OS (Debian, RedHat, Arch, macOS) +- Platform-specific installation commands +- Fingerprint verification with OpenSSL +- Checks for existing installation + +**One-liner:** +```bash +curl -fsSL https://ca.sami/install.sh | sudo bash +``` + +### Apple Devices (root.mobileconfig) + +Features: +- Works on both iOS and macOS +- XML configuration profile format +- Contains base64-encoded certificate +- Unique UUIDs per generation +- User must manually trust after installation (iOS) + +**Installation:** +1. Download root.mobileconfig +2. iOS: Settings prompts automatically +3. macOS: System Settings → Profiles → Install +4. iOS: Enable trust in Certificate Trust Settings + +## Landing Page Features + +The landing page (`index.html`) includes: + +- **OS Detection:** Automatically detects Windows, macOS, Linux, iOS, Android +- **Certificate Info Display:** Shows name, fingerprint, expiration, algorithm +- **QR Code:** For easy mobile access (powered by qrcodejs library) +- **Download Links:** All certificate formats and installation scripts +- **Platform Tabs:** Detailed instructions for each operating system +- **Copy-to-Clipboard:** For fingerprint and command-line scripts +- **DashCaddy Theme:** Dark mode with Sami Grotesk font + +**API Integration:** +- Loads certificate info from `/api/ca/info` endpoint +- Falls back to static info if API unavailable + +## Development Workflow + +1. **Edit Files:** Make changes in `e:/CaddyCerts/sites/ca/` +2. **Test Locally:** Open `index.html` in browser (file:// protocol works) +3. **Regenerate Certificates:** Run `scripts/generate-all.sh` if CA renewed +4. **Deploy:** Copy to production or use dashboard deployment +5. **Verify:** Visit https://ca.sami and test on target platforms + +## Updating After CA Renewal + +When Caddy regenerates its CA certificate (every ~10 years): + +### 1. Regenerate Certificate Formats + +```bash +cd e:/CaddyCerts/sites/ca/scripts +bash generate-all.sh +``` + +### 2. Update Fingerprints in Scripts + +The new fingerprint will be in `cert-info.json`. Update these files: + +**install.ps1** (line 17): +```powershell +$ExpectedFingerprint = "NEW:FIN:GER:PRINT:HERE" +``` + +**install.sh** (line 13): +```bash +EXPECTED_FP="NEW:FIN:GER:PRINT:HERE" +``` + +### 3. Deploy to Production + +```bash +cp -r e:/CaddyCerts/sites/ca/* C:/caddy/sites/ca/ +``` + +### 4. Notify Users + +- Add banner to dashboard +- Send notification via configured channels +- Update documentation with new expiration date + +## API Endpoints + +DashCA integrates with DashCaddy API: + +### GET /api/ca/info + +Returns certificate metadata: + +```json +{ + "success": true, + "certificate": { + "name": "Sami Home Network Root CA", + "fingerprint": "08:98:A5:...", + "validFrom": "Feb 12 07:44:51 2025 GMT", + "validUntil": "Dec 22 07:44:51 2034 GMT", + "daysUntilExpiration": 3235, + "algorithm": "ECDSA P-256 with SHA-256", + "serialNumber": "c1:dc:48:...", + "downloadUrl": "https://ca.sami/root.crt" + } +} +``` + +### GET /api/health/ca + +Returns CA expiration health status: + +```json +{ + "status": "healthy", + "message": "CA certificate valid for 3235 days", + "daysUntilExpiration": 3235, + "expiresAt": "Dec 22 07:44:51 2034 GMT" +} +``` + +**Status values:** +- `healthy`: >90 days remaining +- `warning`: 30-90 days +- `critical`: <30 days or expired +- `error`: Certificate not found or error reading + +## Troubleshooting + +### Certificate Not Found Error + +**Symptom:** Scripts fail with "certificate not found" +**Cause:** Caddy hasn't generated the local CA yet +**Solution:** Visit any *.sami domain to trigger CA generation + +### Fingerprint Mismatch + +**Symptom:** Install scripts reject certificate with fingerprint mismatch +**Cause:** CA was renewed but scripts not updated +**Solution:** Run `generate-all.sh` and update fingerprints in install scripts + +### iOS Profile Won't Install + +**Symptom:** .mobileconfig shows error when installing +**Cause:** Invalid XML or missing UUIDs +**Solution:** Regenerate with `node generate-mobileconfig.js` + +### Android Shows "Not Trusted" + +**Symptom:** Certificate installs but sites still show warnings +**Cause:** Android installs as "user" certificate; some apps don't trust user CAs +**Solution:** This is by design. System CA installation requires root access. + +### Landing Page Shows "Loading..." + +**Symptom:** Certificate info stuck on loading state +**Cause:** API endpoint not accessible +**Solution:** Check that dashcaddy-api server is running and `/api/ca/info` responds + +## Testing Checklist + +Before deploying to production: + +- [ ] All certificate formats generated successfully +- [ ] Landing page loads correctly in browser +- [ ] OS detection works (test multiple user agents) +- [ ] QR code renders and scans correctly +- [ ] Download links work for all file types +- [ ] API endpoint returns valid certificate info +- [ ] Copy-to-clipboard buttons work +- [ ] Platform instruction tabs function correctly +- [ ] Responsive design works on mobile viewport +- [ ] HTTPS access works after deployment + +## Security Notes + +- **Private Key:** NEVER serve the CA private key (`root.key`). Only public certificates are safe to distribute. +- **Fingerprint Verification:** Install scripts verify fingerprint to prevent MITM attacks +- **Access Control:** ca.sami should only be accessible on your Tailnet/internal network +- **HTTPS Enforcement:** The page itself uses HTTPS (via Caddy's internal CA) to protect the distribution +- **No Auto-Execution:** All installation methods require explicit user action + +## Contributing + +When adding features to DashCA: + +1. Test on multiple platforms before committing +2. Update this README with new features +3. Add relevant sections to troubleshooting guide +4. Update CLAUDE.md if deployment process changes +5. Ensure backward compatibility with existing certificates + +## Resources + +- **Caddy PKI Documentation:** https://caddyserver.com/docs/caddyfile/directives/tls#pki +- **mobileconfig Format:** https://developer.apple.com/documentation/devicemanagement +- **OpenSSL Certificate Commands:** https://www.openssl.org/docs/man1.1.1/man1/x509.html +- **QR Code Library:** https://github.com/davidshimjs/qrcodejs + +--- + +**Part of the DashCaddy project** - Unified management for Docker + Caddy + DNS diff --git a/dashcaddy-api/ca/index.html b/dashcaddy-api/ca/index.html new file mode 100644 index 0000000..3d83d78 --- /dev/null +++ b/dashcaddy-api/ca/index.html @@ -0,0 +1,1243 @@ + + + + + +Certificate Authority Distribution
+Scan with your phone's camera to visit this page
++ SSL certificates generated for services on your network +
+Right-click the Start button and select "Windows PowerShell (Admin)" or "Terminal (Admin)"
+Copy and paste this command into PowerShell:
+Visit any *.sami domain (like https://status.sami) - you should see a secure connection with no warnings.
+Click "Apple Profile" above to download root.mobileconfig
+Open System Settings → Privacy & Security → Profiles. Click the downloaded profile and click "Install".
+Open Terminal and run:
+Open your terminal application (usually Ctrl+Alt+T)
+Copy and paste this command (will prompt for sudo password):
+The installer supports Debian/Ubuntu, RedHat/CentOS/Fedora, and Arch Linux. It will automatically detect your distribution and use the appropriate commands.
+Use your iPhone's camera to scan the QR code above, or visit https://ca.sami directly in Safari
+Tap "Apple Profile" to download root.mobileconfig. You'll see a notification that the profile was downloaded.
+Go to Settings (you should see a notification), tap on the downloaded profile, and tap "Install". Enter your passcode if prompted.
+Go to Settings → General → About → Certificate Trust Settings. Enable full trust for "Sami Home Network Root CA".
+Tap "Root Certificate (.crt)" above to download the certificate to your device
+Open Settings → Security → Encryption & credentials → Install a certificate → CA certificate. Select the downloaded file.
+Android installs this as a "user" certificate. Some apps may not trust user certificates for security reasons. For full trust, you would need to install it as a system certificate (requires root access).
+