@bakery-framework/plugin-dashboard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,327 @@
1
+ export let refreshShimmerCache = () => {}
2
+
3
+ /**
4
+ * Both effects below run on import. Without this guard, importing anything that
5
+ * transitively reaches this file outside a browser throws on the first `window`
6
+ * access — which made every module in `client/parts/` untestable, including the
7
+ * hand-built HTML in `database.ts` that has produced every XSS found in this
8
+ * repo. A DOM effect with no DOM is a no-op, not an error.
9
+ */
10
+ const HAS_DOM = typeof window !== 'undefined' && typeof document !== 'undefined'
11
+
12
+ ;(function initDashboardShimmer() {
13
+ if (!HAS_DOM) return
14
+ if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches) return
15
+
16
+ let cachedCards: {
17
+ el: HTMLElement
18
+ width: number
19
+ height: number
20
+ pageLeft: number
21
+ pageTop: number
22
+ }[] = []
23
+ const mouse = { x: -9999, y: -9999 }
24
+ let rafId = 0
25
+
26
+ refreshShimmerCache = function updateRects() {
27
+ const cards = Array.from(
28
+ document.querySelectorAll('.glass-effect'),
29
+ ) as HTMLElement[]
30
+
31
+ cachedCards = cards.map(el => {
32
+ const rect = el.getBoundingClientRect()
33
+ return {
34
+ el,
35
+ width: rect.width,
36
+ height: rect.height,
37
+ pageLeft: rect.left + window.scrollX,
38
+ pageTop: rect.top + window.scrollY,
39
+ }
40
+ })
41
+ }
42
+
43
+ /**
44
+ * The observer below fires for every subtree mutation on the page, and
45
+ * `refreshShimmerCache` reads `getBoundingClientRect` once per glass card —
46
+ * a forced synchronous layout. The log console appends one node per
47
+ * websocket frame, each in its own task, so a busy server bought one full
48
+ * layout pass per log line. Coalescing into a frame makes that cost
49
+ * proportional to frames, not to mutations.
50
+ */
51
+ let refreshRafId = 0
52
+ function scheduleRefresh() {
53
+ if (refreshRafId) return
54
+ refreshRafId = requestAnimationFrame(() => {
55
+ refreshRafId = 0
56
+ refreshShimmerCache()
57
+ })
58
+ }
59
+
60
+ window.addEventListener('resize', scheduleRefresh, { passive: true })
61
+
62
+ const observer = new MutationObserver(scheduleRefresh)
63
+ observer.observe(document.body, {
64
+ childList: true,
65
+ subtree: true,
66
+ attributes: true,
67
+ attributeFilter: ['class'],
68
+ })
69
+
70
+ refreshShimmerCache()
71
+
72
+ function updateStyles() {
73
+ const scrollX = window.scrollX
74
+ const scrollY = window.scrollY
75
+
76
+ for (let i = 0; i < cachedCards.length; i++) {
77
+ const c = cachedCards[i]
78
+ const left = c.pageLeft - scrollX
79
+ const top = c.pageTop - scrollY
80
+
81
+ const rx = mouse.x - left
82
+ const ry = mouse.y - top
83
+
84
+ const closestX = Math.max(left, Math.min(mouse.x, left + c.width))
85
+ const closestY = Math.max(top, Math.min(mouse.y, top + c.height))
86
+ const dx = mouse.x - closestX
87
+ const dy = mouse.y - closestY
88
+ const dist = Math.sqrt(dx * dx + dy * dy)
89
+
90
+ if (dist < 200) {
91
+ c.el.style.setProperty('--mouse-x', `${rx}px`)
92
+ c.el.style.setProperty('--mouse-y', `${ry}px`)
93
+ } else {
94
+ c.el.style.removeProperty('--mouse-x')
95
+ c.el.style.removeProperty('--mouse-y')
96
+ }
97
+ }
98
+ rafId = 0
99
+ }
100
+
101
+ window.addEventListener(
102
+ 'pointermove',
103
+ e => {
104
+ mouse.x = e.clientX
105
+ mouse.y = e.clientY
106
+ if (!rafId) rafId = requestAnimationFrame(updateStyles)
107
+ },
108
+ { passive: true },
109
+ )
110
+
111
+ window.addEventListener(
112
+ 'scroll',
113
+ () => {
114
+ if (mouse.x === -9999) return
115
+ if (!rafId) rafId = requestAnimationFrame(updateStyles)
116
+ },
117
+ { passive: true },
118
+ )
119
+
120
+ window.addEventListener('mouseleave', () => {
121
+ mouse.x = -9999
122
+ mouse.y = -9999
123
+ if (!rafId) rafId = requestAnimationFrame(updateStyles)
124
+ })
125
+ })()
126
+
127
+ ;(function initDotPattern() {
128
+ if (!HAS_DOM) return
129
+
130
+ const canvas = document.createElement('canvas')
131
+ canvas.id = 'dot-pattern-canvas'
132
+ canvas.style.cssText = [
133
+ 'position: fixed',
134
+ 'inset: 0',
135
+ 'width: 100%',
136
+ 'height: 100%',
137
+ 'z-index: -1',
138
+ 'pointer-events: none',
139
+ ].join(';')
140
+ document.documentElement.prepend(canvas)
141
+
142
+ const ctx = canvas.getContext('2d')!
143
+ const DOT_SPACING = 28
144
+ const DOT_RADIUS = 1.1
145
+ const GLOW_RADIUS = 130
146
+
147
+ interface Dot {
148
+ ox: number
149
+ oy: number
150
+ x: number
151
+ y: number
152
+ }
153
+
154
+ let dots: Dot[] = []
155
+ const mouse = { x: -9999, y: -9999 }
156
+ let isAnimating = false
157
+ let isPressed = false
158
+ /**
159
+ * The loop below keeps requesting frames while the pointer is anywhere on
160
+ * the page, which on a hidden tab is pure waste — and this console is the
161
+ * kind of page that lives in a background tab for hours. Ported from the
162
+ * example app's copy of this effect, which had the throttle where this one
163
+ * did not; the two files are separate copies on purpose (a plugin's client
164
+ * bundle and an app's script cannot share a module), so a fix to one is
165
+ * worth checking against the other.
166
+ */
167
+ let isTabVisible = !document.hidden
168
+
169
+ function initDots() {
170
+ dots = []
171
+ const cols = Math.ceil(canvas.width / DOT_SPACING) + 1
172
+ const rows = Math.ceil(canvas.height / DOT_SPACING) + 1
173
+
174
+ for (let r = 0; r < rows; r++) {
175
+ for (let c = 0; c < cols; c++) {
176
+ const ox = c * DOT_SPACING
177
+ const oy = r * DOT_SPACING
178
+ dots.push({ ox, oy, x: ox, y: oy })
179
+ }
180
+ }
181
+ }
182
+
183
+ function resize() {
184
+ canvas.width = window.innerWidth
185
+ canvas.height = window.innerHeight
186
+ initDots()
187
+ if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
188
+ draw()
189
+ } else {
190
+ if (!isAnimating) {
191
+ isAnimating = true
192
+ requestAnimationFrame(draw)
193
+ }
194
+ }
195
+ }
196
+
197
+ interface DotTarget {
198
+ x: number
199
+ y: number
200
+ alpha: number
201
+ radius: number
202
+ }
203
+
204
+ function computeDotTarget(dot: Dot): DotTarget {
205
+ const dx = mouse.x - dot.ox
206
+ const dy = mouse.y - dot.oy
207
+ const dist = Math.sqrt(dx * dx + dy * dy)
208
+
209
+ let targetX = dot.ox
210
+ let targetY = dot.oy
211
+ let alpha = 0.07
212
+ let radius = DOT_RADIUS
213
+
214
+ if (dist < GLOW_RADIUS) {
215
+ const t = 1 - dist / GLOW_RADIUS
216
+ alpha = 0.07 + t * 0.35
217
+ radius = DOT_RADIUS + t * 0.9
218
+
219
+ if (dist > 0.01) {
220
+ const force = t * (isPressed ? 32 : 18)
221
+ if (isPressed) {
222
+ targetX = dot.ox + (dx / dist) * force
223
+ targetY = dot.oy + (dy / dist) * force
224
+ } else {
225
+ targetX = dot.ox - (dx / dist) * force
226
+ targetY = dot.oy - (dy / dist) * force
227
+ }
228
+ }
229
+ }
230
+
231
+ return { x: targetX, y: targetY, alpha, radius }
232
+ }
233
+
234
+ function moveDot(dot: Dot, target: DotTarget): boolean {
235
+ const dx = target.x - dot.x
236
+ const dy = target.y - dot.y
237
+ if (Math.abs(dx) > 0.01 || Math.abs(dy) > 0.01) {
238
+ dot.x += dx * 0.15
239
+ dot.y += dy * 0.15
240
+ return true
241
+ }
242
+ dot.x = target.x
243
+ dot.y = target.y
244
+ return false
245
+ }
246
+
247
+ function draw() {
248
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
249
+
250
+ let needsMoreFrames = false
251
+
252
+ for (let i = 0; i < dots.length; i++) {
253
+ const dot = dots[i]
254
+ const target = computeDotTarget(dot)
255
+ if (moveDot(dot, target)) needsMoreFrames = true
256
+
257
+ ctx.beginPath()
258
+ ctx.arc(dot.x, dot.y, target.radius, 0, Math.PI * 2)
259
+ ctx.fillStyle = `rgba(255, 255, 255, ${target.alpha})`
260
+ ctx.fill()
261
+ }
262
+
263
+ if (
264
+ window.matchMedia('(hover: hover) and (pointer: fine)').matches &&
265
+ isTabVisible
266
+ ) {
267
+ if (mouse.x !== -9999 || needsMoreFrames) {
268
+ requestAnimationFrame(draw)
269
+ } else {
270
+ isAnimating = false
271
+ }
272
+ } else {
273
+ // Hidden tab: stop the loop and let visibilitychange restart it, or the
274
+ // flag would stay true and the resume below would never fire.
275
+ isAnimating = false
276
+ }
277
+ }
278
+
279
+ if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
280
+ window.addEventListener('pointermove', e => {
281
+ mouse.x = e.clientX
282
+ mouse.y = e.clientY
283
+ if (!isAnimating) {
284
+ isAnimating = true
285
+ requestAnimationFrame(draw)
286
+ }
287
+ })
288
+
289
+ window.addEventListener('mouseleave', () => {
290
+ mouse.x = -9999
291
+ mouse.y = -9999
292
+ isPressed = false
293
+ if (!isAnimating) {
294
+ isAnimating = true
295
+ requestAnimationFrame(draw)
296
+ }
297
+ })
298
+
299
+ window.addEventListener('pointerdown', () => {
300
+ isPressed = true
301
+ if (!isAnimating) {
302
+ isAnimating = true
303
+ requestAnimationFrame(draw)
304
+ }
305
+ })
306
+
307
+ window.addEventListener('pointerup', () => {
308
+ isPressed = false
309
+ if (!isAnimating) {
310
+ isAnimating = true
311
+ requestAnimationFrame(draw)
312
+ }
313
+ })
314
+ }
315
+
316
+ document.addEventListener('visibilitychange', () => {
317
+ isTabVisible = !document.hidden
318
+ if (isTabVisible && !isAnimating) {
319
+ isAnimating = true
320
+ requestAnimationFrame(draw)
321
+ }
322
+ })
323
+
324
+ window.addEventListener('resize', resize)
325
+ resize()
326
+ draw()
327
+ })()
@@ -0,0 +1,132 @@
1
+ import { colorizeHtml, getWebSocketUrl } from './utils'
2
+
3
+ export let logsWs: WebSocket | null = null
4
+ export let logsPaused = false
5
+
6
+ /**
7
+ * The console keeps the most recent rows only. It previously appended
8
+ * without ever removing, so a long-lived dashboard tab on a chatty server
9
+ * grew the DOM without bound — and every append also triggered a shimmer
10
+ * cache refresh, so the per-line cost climbed with the backlog.
11
+ */
12
+ const MAX_LOG_ROWS = 500
13
+
14
+ function appendLogRow(cEl: HTMLElement, row: HTMLElement) {
15
+ cEl.appendChild(row)
16
+ while (cEl.childElementCount > MAX_LOG_ROWS) {
17
+ cEl.removeChild(cEl.firstElementChild!)
18
+ }
19
+ scrollConsoleToBottom(cEl)
20
+ }
21
+
22
+ function scrollConsoleToBottom(cEl: HTMLElement) {
23
+ const scrollCheck = document.getElementById(
24
+ 'logs-autoscroll',
25
+ ) as HTMLInputElement | null
26
+ if (scrollCheck?.checked) {
27
+ cEl.scrollTop = cEl.scrollHeight
28
+ }
29
+ }
30
+
31
+ function getLogLevelColor(level: string): string {
32
+ if (level === 'WARN') return '#f59e0b'
33
+ if (level === 'ERROR' || level === 'FATAL') return '#ef4444'
34
+ if (level === 'DEBUG') return '#a855f7'
35
+ return '#34d399'
36
+ }
37
+
38
+ function renderLogEntry(cEl: HTMLElement, parsed: any) {
39
+ const timestamp = new Date(
40
+ parsed.timestamp || Date.now(),
41
+ ).toLocaleTimeString()
42
+ const level = (parsed.level || 'info').toUpperCase()
43
+ const by = parsed.by || 'global'
44
+ const payload = parsed.payload || ''
45
+
46
+ const levelColor = getLogLevelColor(level)
47
+
48
+ const logRow = document.createElement('div')
49
+ logRow.style.padding = '0.15rem 0'
50
+ logRow.style.borderBottom = '1px solid rgba(255, 255, 255, 0.02)'
51
+
52
+ // `level` and `by` arrive over the websocket. LiveReloadHandler rebroadcasts
53
+ // client_log frames from any connected client, so both are untrusted —
54
+ // `payload` was already escaped by colorizeHtml, these were not.
55
+ logRow.innerHTML = `
56
+ <span style="color: var(--text-secondary); margin-right: 0.5rem;">[${escapeHTML(String(timestamp))}]</span>
57
+ <span style="color: ${levelColor}; font-weight: bold; margin-right: 0.5rem;">[${escapeHTML(String(level))}]</span>
58
+ <span style="color: #60a5fa; font-weight: 500; margin-right: 0.5rem;">${escapeHTML(String(by))}:</span>
59
+ <span style="color: #f1f5f9; white-space: pre-wrap;">${colorizeHtml(payload)}</span>
60
+ `
61
+
62
+ appendLogRow(cEl, logRow)
63
+ }
64
+
65
+ function renderRawLogEntry(cEl: HTMLElement, rawData: string) {
66
+ const logRow = document.createElement('div')
67
+ logRow.style.color = '#cbd5e1'
68
+ logRow.innerText = rawData
69
+ appendLogRow(cEl, logRow)
70
+ }
71
+
72
+ export function initLogsWebSocket() {
73
+ if (logsWs && logsWs.readyState === WebSocket.OPEN) return
74
+
75
+ const consoleEl = document.getElementById('logs-console')
76
+ if (!consoleEl) return
77
+ consoleEl.innerHTML =
78
+ '<div style="color: var(--text-secondary);">Connecting to server log stream...</div>'
79
+
80
+ try {
81
+ logsWs = new WebSocket(getWebSocketUrl('/_livereload'))
82
+
83
+ logsWs.onopen = () => {
84
+ consoleEl.innerHTML =
85
+ '<div style="color: var(--accent-green); display: flex; align-items: center; gap: 0.25rem;"><iconify-icon icon="lucide:check-circle-2" style="font-size: 1.1rem;"></iconify-icon><span>Connected to logs pipeline. Listening for events...</span></div>'
86
+ logsWs?.send(JSON.stringify({ type: 'subscribe_logger' }))
87
+ }
88
+
89
+ logsWs.onmessage = event => {
90
+ if (logsPaused) return
91
+
92
+ try {
93
+ const parsed = JSON.parse(event.data)
94
+ if (parsed.type === 'server_log' || parsed.type === 'client_log') {
95
+ renderLogEntry(consoleEl, parsed)
96
+ }
97
+ } catch (_e) {
98
+ renderRawLogEntry(consoleEl, event.data)
99
+ }
100
+ }
101
+
102
+ logsWs.onclose = () => {
103
+ const logRow = document.createElement('div')
104
+ logRow.style.color = '#f59e0b'
105
+ logRow.innerHTML =
106
+ '<span style="display: flex; align-items: center; gap: 0.25rem;"><iconify-icon icon="lucide:alert-triangle" style="font-size: 1rem;"></iconify-icon><span>Logs pipeline disconnected. Reconnecting in 3s...</span></span>'
107
+ consoleEl.appendChild(logRow)
108
+ setTimeout(initLogsWebSocket, 3000)
109
+ }
110
+ } catch (_err) {
111
+ consoleEl.innerHTML =
112
+ '<div style="color: var(--accent-red);">Failed to establish log stream connection.</div>'
113
+ }
114
+ }
115
+
116
+ export function toggleLogsPlay() {
117
+ logsPaused = !logsPaused
118
+ const btn = document.getElementById('btn-logs-play')
119
+ if (btn) {
120
+ btn.innerHTML = logsPaused
121
+ ? '<iconify-icon icon="lucide:play" style="font-size: 1.1rem;"></iconify-icon><span>Resume</span>'
122
+ : '<iconify-icon icon="lucide:pause" style="font-size: 1.1rem;"></iconify-icon><span>Pause</span>'
123
+ btn.classList.toggle('btn-success', logsPaused)
124
+ }
125
+ }
126
+
127
+ export function clearLogs() {
128
+ const consoleEl = document.getElementById('logs-console')
129
+ if (consoleEl)
130
+ consoleEl.innerHTML =
131
+ '<div style="color: var(--text-secondary);">Console cleared.</div>'
132
+ }