diff --git a/pkg/console/assets/css/output.css b/pkg/console/assets/css/output.css index e28294cc..3f319537 100644 --- a/pkg/console/assets/css/output.css +++ b/pkg/console/assets/css/output.css @@ -1,4 +1,4 @@ -/*! tailwindcss v4.1.8 | MIT License | https://tailwindcss.com */ +/*! tailwindcss v4.1.14 | MIT License | https://tailwindcss.com */ @layer properties; @layer theme, base, components, utilities; @layer theme { @@ -12,7 +12,6 @@ --color-red-400: oklch(70.4% 0.191 22.216); --color-red-500: oklch(63.7% 0.237 25.331); --color-red-600: oklch(57.7% 0.245 27.325); - --color-red-700: oklch(50.5% 0.213 27.518); --color-red-800: oklch(44.4% 0.177 26.899); --color-red-900: oklch(39.6% 0.141 25.723); --color-orange-100: oklch(95.4% 0.038 75.164); @@ -61,7 +60,7 @@ --color-gray-700: oklch(37.3% 0.034 259.733); --color-gray-800: oklch(27.8% 0.033 256.848); --color-gray-900: oklch(21% 0.034 264.665); - --color-gray-950: oklch(13% 0.028 261.692); + --color-stone-700: oklch(37.4% 0.01 67.558); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; @@ -88,7 +87,6 @@ --font-weight-semibold: 600; --font-weight-bold: 700; --tracking-wide: 0.025em; - --tracking-wider: 0.05em; --leading-relaxed: 1.625; --radius-sm: 0.25rem; --radius-md: 0.375rem; @@ -235,6 +233,9 @@ ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { padding-block: 0; } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } :-moz-ui-invalid { box-shadow: none; } @@ -258,9 +259,6 @@ .absolute { position: absolute; } - .fixed { - position: fixed; - } .relative { position: relative; } @@ -273,9 +271,6 @@ .top-0 { top: calc(var(--spacing) * 0); } - .top-1 { - top: calc(var(--spacing) * 1); - } .top-1\/2 { top: calc(1/2 * 100%); } @@ -300,9 +295,6 @@ .bottom-full { bottom: 100%; } - .left-1 { - left: calc(var(--spacing) * 1); - } .left-1\/2 { left: calc(1/2 * 100%); } @@ -399,6 +391,9 @@ .hidden { display: none; } + .inline { + display: inline; + } .inline-block { display: inline-block; } @@ -423,9 +418,6 @@ .h-5 { height: calc(var(--spacing) * 5); } - .h-6 { - height: calc(var(--spacing) * 6); - } .h-8 { height: calc(var(--spacing) * 8); } @@ -450,9 +442,6 @@ .w-0 { width: calc(var(--spacing) * 0); } - .w-1 { - width: calc(var(--spacing) * 1); - } .w-1\/2 { width: calc(1/2 * 100%); } @@ -510,9 +499,6 @@ .w-10 { width: calc(var(--spacing) * 10); } - .w-11 { - width: calc(var(--spacing) * 11); - } .w-11\/12 { width: calc(11/12 * 100%); } @@ -552,30 +538,16 @@ .flex-none { flex: none; } - .flex-shrink { - flex-shrink: 1; - } .flex-shrink-0 { flex-shrink: 0; } .table-auto { table-layout: auto; } - .border-collapse { - border-collapse: collapse; - } - .-translate-x-1 { - --tw-translate-x: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-x-1\/2 { --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); } - .-translate-y-1 { - --tw-translate-y: calc(var(--spacing) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); - } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -604,9 +576,6 @@ .cursor-pointer { cursor: pointer; } - .resize { - resize: both; - } .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -634,6 +603,9 @@ .justify-center { justify-content: center; } + .justify-evenly { + justify-content: space-evenly; + } .gap-1 { gap: calc(var(--spacing) * 1); } @@ -698,13 +670,6 @@ margin-inline-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse))); } } - .space-x-2 { - :where(& > :not(:last-child)) { - --tw-space-x-reverse: 0; - margin-inline-start: calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse)); - margin-inline-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse))); - } - } .space-x-3 { :where(& > :not(:last-child)) { --tw-space-x-reverse: 0; @@ -719,13 +684,6 @@ margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); } } - .space-x-6 { - :where(& > :not(:last-child)) { - --tw-space-x-reverse: 0; - margin-inline-start: calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse)); - margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse))); - } - } .truncate { overflow: hidden; text-overflow: ellipsis; @@ -811,18 +769,12 @@ .border-gray-700 { border-color: var(--color-gray-700); } - .border-red-400 { - border-color: var(--color-red-400); - } .border-red-500 { border-color: var(--color-red-500); } .border-transparent { border-color: transparent; } - .border-white { - border-color: var(--color-white); - } .border-white\/20 { border-color: color-mix(in srgb, #fff 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -832,9 +784,6 @@ .border-t-gray-900 { border-top-color: var(--color-gray-900); } - .bg-black { - background-color: var(--color-black); - } .bg-black\/20 { background-color: color-mix(in srgb, #000 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -914,27 +863,15 @@ --tw-gradient-position: to left in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } - .bg-gradient-to-r { - --tw-gradient-position: to right in oklab; - background-image: linear-gradient(var(--tw-gradient-stops)); - } .from-gray-50 { --tw-gradient-from: var(--color-gray-50); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } - .from-white { - --tw-gradient-from: var(--color-white); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } .via-gray-50 { --tw-gradient-via: var(--color-gray-50); --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } - .to-gray-100 { - --tw-gradient-to: var(--color-gray-100); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } .to-transparent { --tw-gradient-to: transparent; --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); @@ -969,9 +906,6 @@ .px-30 { padding-inline: calc(var(--spacing) * 30); } - .py-0 { - padding-block: calc(var(--spacing) * 0); - } .py-0\.5 { padding-block: calc(var(--spacing) * 0.5); } @@ -1011,9 +945,6 @@ .pl-6 { padding-left: calc(var(--spacing) * 6); } - .pl-8 { - padding-left: calc(var(--spacing) * 8); - } .pl-12 { padding-left: calc(var(--spacing) * 12); } @@ -1142,9 +1073,6 @@ .text-red-600 { color: var(--color-red-600); } - .text-red-700 { - color: var(--color-red-700); - } .text-red-800 { color: var(--color-red-800); } @@ -1187,9 +1115,9 @@ --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .outline { - outline-style: var(--tw-outline-style); - outline-width: 1px; + .invert { + --tw-invert: invert(100%); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } .filter { filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); @@ -1204,10 +1132,6 @@ -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); } - .backdrop-filter { - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } .transition-all { transition-property: all; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); @@ -1423,17 +1347,17 @@ } } .dark\:border-gray-600 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { border-color: var(--color-gray-600); } } .dark\:border-gray-700 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { border-color: var(--color-gray-700); } } .dark\:border-gray-700\/50 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { border-color: color-mix(in srgb, oklch(37.3% 0.034 259.733) 50%, transparent); @supports (color: color-mix(in lab, red, red)) { border-color: color-mix(in oklab, var(--color-gray-700) 50%, transparent); @@ -1441,195 +1365,171 @@ } } .dark\:border-red-500 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { border-color: var(--color-red-500); } } .dark\:border-t-gray-700 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { border-top-color: var(--color-gray-700); } } - .dark\:bg-black { - @media (prefers-color-scheme: dark) { - background-color: var(--color-black); - } - } - .dark\:bg-blue-900 { - @media (prefers-color-scheme: dark) { - background-color: var(--color-blue-900); - } - } - .dark\:bg-gray-100 { - @media (prefers-color-scheme: dark) { - background-color: var(--color-gray-100); - } - } - .dark\:bg-gray-600 { - @media (prefers-color-scheme: dark) { - background-color: var(--color-gray-600); - } - } - .dark\:bg-gray-700 { - @media (prefers-color-scheme: dark) { - background-color: var(--color-gray-700); + .dark\:bg-\[\#000000\] { + &:is(.dark, .dark *) { + background-color: #000000; } } - .dark\:bg-gray-800 { - @media (prefers-color-scheme: dark) { - background-color: var(--color-gray-800); + .dark\:bg-\[\#141414\] { + &:is(.dark, .dark *) { + background-color: #141414; } } - .dark\:bg-gray-800\/90 { - @media (prefers-color-scheme: dark) { - background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 90%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-gray-800) 90%, transparent); - } + .dark\:bg-\[\#141414\]\/90 { + &:is(.dark, .dark *) { + background-color: color-mix(in oklab, #141414 90%, transparent); } } - .dark\:bg-gray-900 { - @media (prefers-color-scheme: dark) { - background-color: var(--color-gray-900); + .dark\:bg-blue-900 { + &:is(.dark, .dark *) { + background-color: var(--color-blue-900); } } - .dark\:bg-gray-950 { - @media (prefers-color-scheme: dark) { - background-color: var(--color-gray-950); + .dark\:bg-gray-100 { + &:is(.dark, .dark *) { + background-color: var(--color-gray-100); } } .dark\:bg-green-900 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { background-color: var(--color-green-900); } } .dark\:bg-orange-900 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { background-color: var(--color-orange-900); } } .dark\:bg-purple-900 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { background-color: var(--color-purple-900); } } .dark\:bg-red-900 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { background-color: var(--color-red-900); } } + .dark\:bg-stone-700 { + &:is(.dark, .dark *) { + background-color: var(--color-stone-700); + } + } .dark\:from-gray-800 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { --tw-gradient-from: var(--color-gray-800); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } } .dark\:via-gray-800 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { --tw-gradient-via: var(--color-gray-800); --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-via-stops); } } - .dark\:to-gray-900 { - @media (prefers-color-scheme: dark) { - --tw-gradient-to: var(--color-gray-900); - --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); - } - } .dark\:\!text-gray-400 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-gray-400) !important; } } .dark\:text-blue-300 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-blue-300); } } .dark\:text-gray-100 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-gray-100); } } .dark\:text-gray-200 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-gray-200); } } .dark\:text-gray-300 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-gray-300); } } .dark\:text-gray-400 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-gray-400); } } .dark\:text-gray-500 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-gray-500); } } .dark\:text-gray-600 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-gray-600); } } .dark\:text-gray-900 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-gray-900); } } .dark\:text-green-200 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-green-200); } } .dark\:text-green-400 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-green-400); } } .dark\:text-orange-200 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-orange-200); } } .dark\:text-purple-200 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-purple-200); } } .dark\:text-purple-400 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-purple-400); } } .dark\:text-red-200 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-red-200); } } .dark\:text-red-400 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-red-400); } } .dark\:text-yellow-400 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { color: var(--color-yellow-400); } } - .dark\:invert { - @media (prefers-color-scheme: dark) { - --tw-invert: invert(100%); + .dark\:invert-0 { + &:is(.dark, .dark *) { + --tw-invert: invert(0%); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } } .dark\:hover\:\!bg-purple-700 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { &:hover { @media (hover: hover) { background-color: var(--color-purple-700) !important; @@ -1638,7 +1538,7 @@ } } .dark\:hover\:bg-gray-600 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { &:hover { @media (hover: hover) { background-color: var(--color-gray-600); @@ -1647,7 +1547,7 @@ } } .dark\:hover\:bg-gray-700 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { &:hover { @media (hover: hover) { background-color: var(--color-gray-700); @@ -1655,8 +1555,17 @@ } } } + .dark\:hover\:bg-gray-800 { + &:is(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-800); + } + } + } + } .dark\:hover\:bg-purple-700 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { &:hover { @media (hover: hover) { background-color: var(--color-purple-700); @@ -1664,8 +1573,17 @@ } } } + .dark\:hover\:bg-stone-700 { + &:is(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: var(--color-stone-700); + } + } + } + } .dark\:hover\:\!text-gray-300 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { &:hover { @media (hover: hover) { color: var(--color-gray-300) !important; @@ -1674,7 +1592,7 @@ } } .dark\:hover\:text-purple-300 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { &:hover { @media (hover: hover) { color: var(--color-purple-300); @@ -1683,14 +1601,14 @@ } } .dark\:focus\:border-purple-500 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { &:focus { border-color: var(--color-purple-500); } } } .dark\:focus\:border-red-500 { - @media (prefers-color-scheme: dark) { + &:is(.dark, .dark *) { &:focus { border-color: var(--color-red-500); } @@ -1927,11 +1845,6 @@ inherits: false; initial-value: 0 0 #0000; } -@property --tw-outline-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} @property --tw-blur { syntax: "*"; inherits: false; @@ -2094,7 +2007,6 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; - --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; --tw-contrast: initial; diff --git a/pkg/console/assets/images/Glyph_Black.svg b/pkg/console/assets/images/Glyph_Black.svg deleted file mode 100755 index d2dd14a7..00000000 --- a/pkg/console/assets/images/Glyph_Black.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/pkg/console/assets/images/OAPLogo.svg b/pkg/console/assets/images/OAPLogo.svg new file mode 100644 index 00000000..7fe4c586 --- /dev/null +++ b/pkg/console/assets/images/OAPLogo.svg @@ -0,0 +1,3 @@ + diff --git a/pkg/console/assets/images/favicon.png b/pkg/console/assets/images/favicon.png new file mode 100644 index 00000000..0bdfdbc7 Binary files /dev/null and b/pkg/console/assets/images/favicon.png differ diff --git a/pkg/console/assets/input.css b/pkg/console/assets/input.css index 1f8f3b93..39e6667b 100644 --- a/pkg/console/assets/input.css +++ b/pkg/console/assets/input.css @@ -1,5 +1,7 @@ @import "tailwindcss"; +@variant dark (&:is(.dark, .dark *)); + /* JSON Syntax Highlighting */ .json-key { @apply text-blue-600 font-semibold; diff --git a/pkg/console/assets/js/theme.js b/pkg/console/assets/js/theme.js new file mode 100644 index 00000000..01d4b286 --- /dev/null +++ b/pkg/console/assets/js/theme.js @@ -0,0 +1,30 @@ +function themeToggle() { + return { + theme: 'light', + + init() { + console.log('Theme toggle initialized'); + // Read current state from document (set by inline script) + const isDark = document.documentElement.classList.contains('dark'); + this.theme = isDark ? 'dark' : 'light'; + console.log('Current theme:', this.theme); + }, + + toggleTheme() { + console.log('Toggle clicked, current theme:', this.theme); + this.theme = this.theme === 'light' ? 'dark' : 'light'; + console.log('New theme:', this.theme); + localStorage.setItem('theme', this.theme); + this.applyTheme(); + }, + + applyTheme() { + console.log('Applying theme:', this.theme); + if (this.theme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + } + } +} diff --git a/pkg/console/cache.go b/pkg/console/cache.go new file mode 100644 index 00000000..c6c786ee --- /dev/null +++ b/pkg/console/cache.go @@ -0,0 +1,79 @@ +package console + +import ( + "context" + "sync" + "time" + + "github.com/AudiusProject/audiusd/pkg/common" +) + +// Cache is a generic thread-safe cache that periodically refreshes data in the background +type Cache[T any] struct { + mu sync.RWMutex + data T + lastUpdated time.Time + updateFunc func(context.Context) (T, error) + refreshRate time.Duration + logger *common.Logger +} + +// NewCache creates a new cache with the given update function and refresh rate +func NewCache[T any](updateFunc func(context.Context) (T, error), refreshRate time.Duration, logger *common.Logger) *Cache[T] { + return &Cache[T]{ + updateFunc: updateFunc, + refreshRate: refreshRate, + logger: logger, + } +} + +// Get returns the cached data (thread-safe read) +func (c *Cache[T]) Get() T { + c.mu.RLock() + defer c.mu.RUnlock() + return c.data +} + +// GetLastUpdated returns when the cache was last updated +func (c *Cache[T]) GetLastUpdated() time.Time { + c.mu.RLock() + defer c.mu.RUnlock() + return c.lastUpdated +} + +// StartRefresh begins the background refresh loop +// Should be called in a goroutine +func (c *Cache[T]) StartRefresh(ctx context.Context) { + ticker := time.NewTicker(c.refreshRate) + defer ticker.Stop() + + // Do initial refresh immediately (non-blocking) + c.refresh(ctx) + + for { + select { + case <-ctx.Done(): + c.logger.Info("Cache refresh stopped") + return + case <-ticker.C: + c.refresh(ctx) + } + } +} + +// refresh updates the cached data by calling the update function +func (c *Cache[T]) refresh(ctx context.Context) { + start := time.Now() + data, err := c.updateFunc(ctx) + if err != nil { + c.logger.Warn("Cache refresh failed", "error", err, "duration", time.Since(start)) + return + } + + c.mu.Lock() + c.data = data + c.lastUpdated = time.Now() + c.mu.Unlock() + + c.logger.Debug("Cache refreshed", "duration", time.Since(start)) +} diff --git a/pkg/console/cmd/main.go b/pkg/console/cmd/main.go index 5ad7d02d..1e40a01b 100644 --- a/pkg/console/cmd/main.go +++ b/pkg/console/cmd/main.go @@ -1,19 +1,13 @@ package main import ( - "context" - "fmt" "log" - "os" - "path/filepath" - "time" "github.com/AudiusProject/audiusd/pkg/common" "github.com/AudiusProject/audiusd/pkg/console" "github.com/AudiusProject/audiusd/pkg/etl" "github.com/AudiusProject/audiusd/pkg/sdk" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" + "go.uber.org/zap" ) func main() { @@ -21,19 +15,14 @@ func main() { logger.Info("Starting Console") - // Start postgres container with volume - dbURL, err := startPostgres(context.Background()) - if err != nil { - log.Fatalf("Failed to start postgres: %s", err) - } + // Connect to postgres from Makefile (port 5444) + dbURL := "postgres://postgres:postgres@0.0.0.0:5444/audiusd?sslmode=disable" logger.Info("pgURL", "url", dbURL) - // wait for postgres to be ready - time.Sleep(5 * time.Second) - auds := sdk.NewAudiusdSDK("rpc.audius.engineering") + auds := sdk.NewAudiusdSDK("creatornode.audius.co") - etl := etl.NewETLService(auds.Core, nil) + etl := etl.NewETLService(auds.Core, zap.NewNop()) etl.SetDBURL(dbURL) etl.SetCheckReadiness(false) @@ -45,52 +34,3 @@ func main() { log.Fatal(err) } } - -func startPostgres(ctx context.Context) (string, error) { - pguser := "postgres" - pgpass := "postgres" - pgdb := "audiusd" - - // Get current working directory and create absolute path for postgres data - cwd, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to get current working directory: %w", err) - } - - postgresDataPath := filepath.Join(cwd, "tmp", "postgres-data") - - // Create the directory if it doesn't exist - if err := os.MkdirAll(postgresDataPath, 0755); err != nil { - return "", fmt.Errorf("failed to create postgres data directory: %w", err) - } - - req := testcontainers.ContainerRequest{ - Image: "postgres:15", - Name: "audiusd-console-postgres", - ExposedPorts: []string{"15432:5432/tcp"}, - Env: map[string]string{ - "POSTGRES_USER": pguser, - "POSTGRES_PASSWORD": pgpass, - "POSTGRES_DB": pgdb, - }, - WaitingFor: wait.ForListeningPort("5432/tcp"), - } - - postgresC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - Reuse: true, - }) - if err != nil { - return "", fmt.Errorf("failed to start container: %w", err) - } - - host, err := postgresC.Host(ctx) - if err != nil { - return "", fmt.Errorf("failed to get container host: %w", err) - } - - // Use fixed port 15432 for consistent TablePlus connections - dsn := fmt.Sprintf("postgres://%s:%s@%s:15432/%s?sslmode=disable", pguser, pgpass, host, pgdb) - return dsn, nil -} diff --git a/pkg/console/console.go b/pkg/console/console.go index 3fd08c3f..66640ba8 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -44,6 +44,7 @@ type Console struct { latestTrustedBlock atomic.Int64 lastRefreshTime atomic.Int64 // Unix timestamp of last refresh refreshInterval time.Duration // How often to refresh + dashboardCache *Cache[*pages.DashboardProps] } func NewConsole(etl *etl.ETLService, e *echo.Echo, env string) *Console { @@ -58,11 +59,11 @@ func NewConsole(etl *etl.ETLService, e *echo.Echo, env string) *Console { switch env { case "prod", "production", "mainnet": - trustedNodeURL = "rpc.audius.engineering" + trustedNodeURL = "creatornode.audius.co" case "staging", "stage", "testnet": - trustedNodeURL = "rpc.staging.audius.engineering" + trustedNodeURL = "creatornode11.staging.audius.co" case "dev": - trustedNodeURL = "rpc.dev.audius.engineering" + trustedNodeURL = "node2.oap.devnet" } return &Console{ @@ -76,7 +77,10 @@ func NewConsole(etl *etl.ETLService, e *echo.Echo, env string) *Console { } func (con *Console) Initialize() { - // start refresher + // Initialize dashboard cache with 5 second refresh rate + con.dashboardCache = NewCache(con.buildDashboardProps, 5*time.Second, con.logger.Child("dashboard-cache")) + + // Start background refreshers (but not the dashboard cache yet - that needs ETL to be ready) go con.refreshTrustedBlock() e := con.e @@ -153,7 +157,7 @@ func (con *Console) Initialize() { } func (con *Console) Run() error { - g, _ := errgroup.WithContext(context.Background()) + g, ctx := errgroup.WithContext(context.Background()) g.Go(func() error { info, err := con.trustedNode.Core.GetNodeInfo(context.Background(), &connect.Request[corev1.GetNodeInfoRequest]{}) @@ -189,6 +193,14 @@ func (con *Console) Run() error { return nil }) + // Start dashboard cache refresh after ETL is ready + g.Go(func() error { + // Wait a moment for ETL to initialize + time.Sleep(2 * time.Second) + con.dashboardCache.StartRefresh(ctx) + return nil + }) + g.Go(func() error { if err := con.e.Start(":3000"); err != nil { return err @@ -237,9 +249,9 @@ func (con *Console) Hello(c echo.Context) error { return p.Render(ctx, c.Response().Writer) } -func (con *Console) Dashboard(c echo.Context) error { - ctx := c.Request().Context() - +// buildDashboardProps builds the dashboard props by querying the database +// This is used by the cache to refresh dashboard data periodically +func (con *Console) buildDashboardProps(ctx context.Context) (*pages.DashboardProps, error) { // Get dashboard transaction stats from materialized view txStats, err := con.etl.GetDB().GetDashboardTransactionStats(ctx) if err != nil { @@ -271,20 +283,12 @@ func (con *Console) Dashboard(c echo.Context) error { var blockDelta int64 syncProgressPercentage := float64(100) // Default to fully synced - con.logger.Info("Latest block height", "height", latestBlockHeight) - con.logger.Info("Trusted block height", "height", trustedBlockHeight) - if trustedBlockHeight >= 0 && latestBlockHeight >= 0 { - blockDelta = trustedBlockHeight - latestBlockHeight isSyncing = blockDelta > syncThreshold - con.logger.Info("Block delta", "delta", blockDelta) - con.logger.Info("Is syncing", "isSyncing", isSyncing) - if isSyncing && trustedBlockHeight > 0 { syncProgressPercentage = (float64(latestBlockHeight) / float64(trustedBlockHeight)) * 100 - con.logger.Info("Sync progress percentage", "percentage", syncProgressPercentage) // Ensure percentage doesn't exceed 100 if syncProgressPercentage > 100 { syncProgressPercentage = 100 @@ -293,7 +297,6 @@ func (con *Console) Dashboard(c echo.Context) error { syncProgressPercentage = 100 // Fully synced } } else { - con.logger.Info("No trusted block info, assuming synced") // If we don't have trusted block info, assume synced isSyncing = false blockDelta = 0 @@ -335,7 +338,7 @@ func (con *Console) Dashboard(c echo.Context) error { }) if err != nil { con.logger.Warn("Failed to get blocks", "error", err) - return c.String(http.StatusInternalServerError, "Failed to get blocks") + blocks = []db.EtlBlock{} } blockPointers := make([]*db.EtlBlock, len(blocks)) @@ -398,14 +401,11 @@ func (con *Console) Dashboard(c echo.Context) error { slaRollupsData = []db.EtlSlaRollup{} } - con.logger.Info("SLA rollups data retrieved", "count", len(slaRollupsData)) - // Build SLA performance data points for chart - Initialize as empty slice, not nil slaPerformanceData := make([]*pages.SLAPerformanceDataPoint, 0) // Build chart data if we have any rollups if len(slaRollupsData) > 0 { - con.logger.Info("Building SLA performance chart data", "rollupCount", len(slaRollupsData)) // Extract rollup IDs for healthy validators query rollupIDs := make([]int32, len(slaRollupsData)) @@ -432,50 +432,17 @@ func (con *Console) Dashboard(c echo.Context) error { // Filter out invalid rollups and build valid data points validDataPoints := make([]*pages.SLAPerformanceDataPoint, 0, len(slaRollupsData)) - for i, rollup := range slaRollupsData { - // Log the first rollup to see what data we're getting - if i == 0 { - con.logger.Info("First rollup data sample", - "id", rollup.ID, - "blockHeight", rollup.BlockHeight, - "validatorCount", rollup.ValidatorCount, - "bps", rollup.Bps, - "tps", rollup.Tps, - "createdAtValid", rollup.CreatedAt.Valid, - "blockStart", rollup.BlockStart, - "blockEnd", rollup.BlockEnd) - } - - // Comprehensive validation of rollup data - if rollup.ID <= 0 { - con.logger.Debug("Skipping rollup with invalid ID", "index", i, "rollupId", rollup.ID) - continue - } - - if !rollup.CreatedAt.Valid { - con.logger.Debug("Skipping rollup with invalid timestamp", "rollupId", rollup.ID) - continue - } - - if rollup.BlockHeight <= 0 { - con.logger.Debug("Skipping rollup with invalid block height", "rollupId", rollup.ID, "blockHeight", rollup.BlockHeight) - continue - } - - if rollup.Bps < 0 || rollup.Tps < 0 { - con.logger.Debug("Skipping rollup with invalid performance data", "rollupId", rollup.ID, "bps", rollup.Bps, "tps", rollup.Tps) - continue - } - - if rollup.BlockStart < 0 || rollup.BlockEnd <= 0 || rollup.BlockStart > rollup.BlockEnd { - con.logger.Debug("Skipping rollup with invalid block range", "rollupId", rollup.ID, "start", rollup.BlockStart, "end", rollup.BlockEnd) + for _, rollup := range slaRollupsData { + // Skip invalid rollups + if rollup.ID <= 0 || !rollup.CreatedAt.Valid || rollup.BlockHeight <= 0 || + rollup.Bps < 0 || rollup.Tps < 0 || + rollup.BlockStart < 0 || rollup.BlockEnd <= 0 || rollup.BlockStart > rollup.BlockEnd { continue } // Use the validator count from the rollup data itself validatorCount := rollup.ValidatorCount if validatorCount <= 0 { - con.logger.Debug("Invalid validator count in rollup, using fallback", "rollupId", rollup.ID, "count", validatorCount) validatorCount = 1 // Minimum of 1 validator } @@ -507,25 +474,16 @@ func (con *Console) Dashboard(c echo.Context) error { // Use the data if we have any valid points after filtering if len(validDataPoints) > 0 { slaPerformanceData = validDataPoints - con.logger.Info("Successfully built SLA performance data", "validPoints", len(validDataPoints)) - } else { - con.logger.Warn("No valid rollup data points after filtering", "valid", len(validDataPoints), "total", len(slaRollupsData)) - // Keep the empty slice - don't set to nil } - } else { - con.logger.Info("No rollups available for chart", "rollupCount", len(slaRollupsData)) } - // Final debug of what we're passing to template - con.logger.Info("Final SLA performance data for template", "dataPoints", len(slaPerformanceData)) - // Convert rollups to pointers for template recentSLARollups := make([]*db.EtlSlaRollup, len(slaRollupsData)) for i := range slaRollupsData { recentSLARollups[i] = &slaRollupsData[i] } - props := pages.DashboardProps{ + props := &pages.DashboardProps{ Stats: stats, TransactionBreakdown: transactionBreakdown, RecentBlocks: blockPointers, @@ -536,10 +494,26 @@ func (con *Console) Dashboard(c echo.Context) error { SyncProgressPercentage: syncProgressPercentage, } - p := pages.Dashboard(props) + return props, nil +} - // Use context with environment - return p.Render(ctx, c.Response().Writer) +func (con *Console) Dashboard(c echo.Context) error { + // Get cached dashboard props + props := con.dashboardCache.Get() + + // If cache isn't ready yet, build props on-demand + if props == nil { + var err error + props, err = con.buildDashboardProps(c.Request().Context()) + if err != nil { + con.logger.Error("Failed to build dashboard props", "error", err) + return c.String(http.StatusInternalServerError, "Failed to load dashboard") + } + } + + // Render the dashboard + p := pages.Dashboard(*props) + return p.Render(c.Request().Context(), c.Response().Writer) } func (con *Console) Validators(c echo.Context) error { diff --git a/pkg/console/templates/layouts/base.templ b/pkg/console/templates/layouts/base.templ index 1ed38475..aadcbd53 100644 --- a/pkg/console/templates/layouts/base.templ +++ b/pkg/console/templates/layouts/base.templ @@ -4,18 +4,26 @@ import "fmt" templ Base(pageTitle string) { -
{ props.Stats.ChainID }
-{ fmt.Sprintf("%.2fs", props.Stats.AvgBlockTime) }
} else if props.Stats.BPS > 0 { @@ -193,9 +188,9 @@ templ Dashboard(props DashboardProps) {-
No data
} +Synced
Up to date
} +