refactor: update dependencies and remove unused code
- Added new dependencies: `adler2`, `crc32fast`, `flate2`, `miniz_oxide`, and `libredox`. - Updated existing dependencies: `tokio-rustls` to version 0.26.4 and `filetime` to version 0.2.27. - Removed the `backup.rs` file as it is no longer needed. - Introduced tests for configuration and credential management. - Enhanced the `identity` module to generate W3C compliant DID documents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
172
neode-ui/src/components/LineChart.vue
Normal file
172
neode-ui/src/components/LineChart.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="monitoring-chart"
|
||||
:width="width"
|
||||
:height="height"
|
||||
></canvas>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export interface ChartDataset {
|
||||
label: string
|
||||
data: number[]
|
||||
color: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
datasets: ChartDataset[]
|
||||
labels?: string[]
|
||||
width?: number
|
||||
height?: number
|
||||
yMax?: number
|
||||
yLabel?: string
|
||||
showGrid?: boolean
|
||||
}>(),
|
||||
{
|
||||
width: 400,
|
||||
height: 180,
|
||||
showGrid: true,
|
||||
},
|
||||
)
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
function draw() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
canvas.width = props.width * dpr
|
||||
canvas.height = props.height * dpr
|
||||
canvas.style.width = `${props.width}px`
|
||||
canvas.style.height = `${props.height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const w = props.width
|
||||
const h = props.height
|
||||
const pad = { top: 10, right: 12, bottom: 24, left: 44 }
|
||||
const plotW = w - pad.left - pad.right
|
||||
const plotH = h - pad.top - pad.bottom
|
||||
|
||||
// Clear
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
|
||||
if (!props.datasets.length || !props.datasets[0]?.data.length) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.3)'
|
||||
ctx.font = '12px system-ui'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText('No data yet', w / 2, h / 2)
|
||||
return
|
||||
}
|
||||
|
||||
// Compute y range
|
||||
let yMax = props.yMax ?? 0
|
||||
if (!yMax) {
|
||||
for (const ds of props.datasets) {
|
||||
for (const v of ds.data) {
|
||||
if (v > yMax) yMax = v
|
||||
}
|
||||
}
|
||||
yMax = yMax * 1.1 || 1
|
||||
}
|
||||
|
||||
const maxPoints = Math.max(...props.datasets.map((d) => d.data.length))
|
||||
|
||||
// Grid lines
|
||||
if (props.showGrid) {
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.06)'
|
||||
ctx.lineWidth = 1
|
||||
const gridCount = 4
|
||||
for (let i = 0; i <= gridCount; i++) {
|
||||
const y = pad.top + (plotH / gridCount) * i
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(pad.left, y)
|
||||
ctx.lineTo(pad.left + plotW, y)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Y-axis labels
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.4)'
|
||||
ctx.font = '10px system-ui'
|
||||
ctx.textAlign = 'right'
|
||||
for (let i = 0; i <= gridCount; i++) {
|
||||
const y = pad.top + (plotH / gridCount) * i
|
||||
const val = yMax - (yMax / gridCount) * i
|
||||
ctx.fillText(formatValue(val), pad.left - 6, y + 3)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw each dataset
|
||||
for (const ds of props.datasets) {
|
||||
if (!ds.data.length) continue
|
||||
|
||||
ctx.strokeStyle = ds.color
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.lineCap = 'round'
|
||||
|
||||
ctx.beginPath()
|
||||
for (let i = 0; i < ds.data.length; i++) {
|
||||
const x = pad.left + (i / Math.max(maxPoints - 1, 1)) * plotW
|
||||
const y = pad.top + plotH - (ds.data[i]! / yMax) * plotH
|
||||
if (i === 0) {
|
||||
ctx.moveTo(x, y)
|
||||
} else {
|
||||
ctx.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
ctx.stroke()
|
||||
|
||||
// Area fill
|
||||
ctx.globalAlpha = 0.08
|
||||
ctx.fillStyle = ds.color
|
||||
ctx.lineTo(pad.left + ((ds.data.length - 1) / Math.max(maxPoints - 1, 1)) * plotW, pad.top + plotH)
|
||||
ctx.lineTo(pad.left, pad.top + plotH)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.globalAlpha = 1.0
|
||||
}
|
||||
|
||||
// X-axis labels (first, middle, last)
|
||||
if (props.labels && props.labels.length > 0) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.4)'
|
||||
ctx.font = '10px system-ui'
|
||||
ctx.textAlign = 'center'
|
||||
const indices = [0, Math.floor(props.labels.length / 2), props.labels.length - 1]
|
||||
for (const idx of indices) {
|
||||
if (idx >= 0 && idx < props.labels.length) {
|
||||
const x = pad.left + (idx / Math.max(props.labels.length - 1, 1)) * plotW
|
||||
ctx.fillText(props.labels[idx]!, pad.left + plotW + pad.right > w ? x : x, h - 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatValue(val: number): string {
|
||||
if (val >= 1_000_000_000) return `${(val / 1_000_000_000).toFixed(1)}G`
|
||||
if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(1)}M`
|
||||
if (val >= 1_000) return `${(val / 1_000).toFixed(1)}K`
|
||||
return val.toFixed(val < 10 ? 1 : 0)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.datasets, props.labels, props.width, props.height],
|
||||
() => draw(),
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
draw()
|
||||
window.addEventListener('resize', draw)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', draw)
|
||||
})
|
||||
</script>
|
||||
@@ -46,7 +46,7 @@ onMounted(() => {
|
||||
// Don't show if already dismissed this session or if already installed
|
||||
if (sessionStorage.getItem(DISMISS_KEY) === '1') return
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) return
|
||||
if ((window.navigator as any).standalone) return
|
||||
if ((window.navigator as Navigator & { standalone?: boolean }).standalone) return
|
||||
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault()
|
||||
@@ -55,11 +55,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handler)
|
||||
;(window as any).__beforeinstallpromptHandler = handler
|
||||
;(window as Window & { __beforeinstallpromptHandler?: EventListener }).__beforeinstallpromptHandler = handler
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('beforeinstallprompt', (window as any).__beforeinstallpromptHandler)
|
||||
window.removeEventListener('beforeinstallprompt', (window as Window & { __beforeinstallpromptHandler?: EventListener }).__beforeinstallpromptHandler as EventListener)
|
||||
})
|
||||
|
||||
function dismiss() {
|
||||
|
||||
@@ -205,7 +205,7 @@ watch([showWelcome, showLogo], ([welcome, logo]) => {
|
||||
if ((welcome || logo) && videoElement.value) {
|
||||
if (videoElement.value.paused) {
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video autoplay failed:', err)
|
||||
if (import.meta.env.DEV) console.warn('Video autoplay failed:', err)
|
||||
})
|
||||
}
|
||||
// Add pause prevention handler once, remove when no longer needed
|
||||
@@ -228,7 +228,7 @@ watch(showWelcome, (isShowing) => {
|
||||
if (isShowing && videoElement.value) {
|
||||
// Start video immediately when welcome appears
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video autoplay failed on welcome:', err)
|
||||
if (import.meta.env.DEV) console.warn('Video autoplay failed on welcome:', err)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -414,7 +414,7 @@ function startAlienIntro() {
|
||||
playWelcomeNoderunnerSpeech()
|
||||
if (videoElement.value) {
|
||||
videoElement.value.play().catch(err => {
|
||||
console.warn('Video autoplay failed on welcome:', err)
|
||||
if (import.meta.env.DEV) console.warn('Video autoplay failed on welcome:', err)
|
||||
})
|
||||
}
|
||||
backgroundOpacity.value = 0.3
|
||||
|
||||
117
neode-ui/src/components/__tests__/LineChart.test.ts
Normal file
117
neode-ui/src/components/__tests__/LineChart.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import LineChart from '../LineChart.vue'
|
||||
|
||||
// Mock canvas context
|
||||
const mockContext = {
|
||||
clearRect: vi.fn(),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
lineTo: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
fillRect: vi.fn(),
|
||||
fillText: vi.fn(),
|
||||
closePath: vi.fn(),
|
||||
setLineDash: vi.fn(),
|
||||
save: vi.fn(),
|
||||
restore: vi.fn(),
|
||||
scale: vi.fn(),
|
||||
createLinearGradient: vi.fn().mockReturnValue({
|
||||
addColorStop: vi.fn(),
|
||||
}),
|
||||
canvas: { width: 600, height: 200 },
|
||||
strokeStyle: '',
|
||||
fillStyle: '',
|
||||
lineWidth: 0,
|
||||
font: '',
|
||||
textAlign: '',
|
||||
textBaseline: '',
|
||||
globalAlpha: 1,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue(mockContext)
|
||||
})
|
||||
|
||||
describe('LineChart', () => {
|
||||
const sampleDatasets = [
|
||||
{ label: 'CPU', data: [10, 20, 30, 40, 50], color: '#fb923c' },
|
||||
]
|
||||
|
||||
it('renders a canvas element', () => {
|
||||
const wrapper = shallowMount(LineChart, {
|
||||
props: { datasets: sampleDatasets },
|
||||
})
|
||||
expect(wrapper.find('canvas').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts datasets prop', () => {
|
||||
const wrapper = shallowMount(LineChart, {
|
||||
props: { datasets: sampleDatasets },
|
||||
})
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders with empty datasets', () => {
|
||||
const wrapper = shallowMount(LineChart, {
|
||||
props: { datasets: [] },
|
||||
})
|
||||
expect(wrapper.find('canvas').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders with multiple datasets', () => {
|
||||
const wrapper = shallowMount(LineChart, {
|
||||
props: {
|
||||
datasets: [
|
||||
{ label: 'CPU', data: [10, 20, 30], color: '#fb923c' },
|
||||
{ label: 'Memory', data: [50, 60, 70], color: '#4ade80' },
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts optional height and width props', () => {
|
||||
const wrapper = shallowMount(LineChart, {
|
||||
props: {
|
||||
datasets: sampleDatasets,
|
||||
height: 300,
|
||||
width: 600,
|
||||
},
|
||||
})
|
||||
const canvas = wrapper.find('canvas')
|
||||
expect(canvas.attributes('width')).toBe('600')
|
||||
expect(canvas.attributes('height')).toBe('300')
|
||||
})
|
||||
|
||||
it('uses default width of 400 and height of 180', () => {
|
||||
const wrapper = shallowMount(LineChart, {
|
||||
props: { datasets: sampleDatasets },
|
||||
})
|
||||
const canvas = wrapper.find('canvas')
|
||||
expect(canvas.attributes('width')).toBe('400')
|
||||
expect(canvas.attributes('height')).toBe('180')
|
||||
})
|
||||
|
||||
it('renders with dataset containing single data point', () => {
|
||||
const wrapper = shallowMount(LineChart, {
|
||||
props: {
|
||||
datasets: [{ label: 'Test', data: [42], color: '#3b82f6' }],
|
||||
},
|
||||
})
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts yMax and yLabel props', () => {
|
||||
const wrapper = shallowMount(LineChart, {
|
||||
props: {
|
||||
datasets: sampleDatasets,
|
||||
yMax: 100,
|
||||
yLabel: 'Percent',
|
||||
},
|
||||
})
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
103
neode-ui/src/components/__tests__/PWAInstallPrompt.test.ts
Normal file
103
neode-ui/src/components/__tests__/PWAInstallPrompt.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import PWAInstallPrompt from '../PWAInstallPrompt.vue'
|
||||
|
||||
describe('PWAInstallPrompt', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
sessionStorage.clear()
|
||||
// Mock matchMedia to return non-standalone
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
})
|
||||
})
|
||||
|
||||
it('renders without errors', () => {
|
||||
const wrapper = shallowMount(PWAInstallPrompt, {
|
||||
global: { stubs: { Teleport: true, Transition: true } },
|
||||
})
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('does not show prompt initially', () => {
|
||||
const wrapper = shallowMount(PWAInstallPrompt, {
|
||||
global: { stubs: { Teleport: true, Transition: true } },
|
||||
})
|
||||
expect(wrapper.text()).not.toContain('Install Archipelago')
|
||||
})
|
||||
|
||||
it('shows prompt after beforeinstallprompt event', async () => {
|
||||
const wrapper = shallowMount(PWAInstallPrompt, {
|
||||
global: { stubs: { Teleport: true, Transition: true } },
|
||||
})
|
||||
|
||||
// Fire the beforeinstallprompt event
|
||||
const event = new Event('beforeinstallprompt')
|
||||
Object.defineProperty(event, 'preventDefault', { value: vi.fn() })
|
||||
window.dispatchEvent(event)
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.text()).toContain('Install Archipelago')
|
||||
})
|
||||
|
||||
it('hides prompt when dismissed', async () => {
|
||||
const wrapper = shallowMount(PWAInstallPrompt, {
|
||||
global: { stubs: { Teleport: true, Transition: true } },
|
||||
})
|
||||
|
||||
// Show prompt
|
||||
const event = new Event('beforeinstallprompt')
|
||||
Object.defineProperty(event, 'preventDefault', { value: vi.fn() })
|
||||
window.dispatchEvent(event)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Click dismiss button
|
||||
const dismissBtn = wrapper.findAll('button').find(b => b.text().includes('Not now'))
|
||||
expect(dismissBtn).toBeDefined()
|
||||
await dismissBtn!.trigger('click')
|
||||
expect(sessionStorage.getItem('archipelago_pwa_install_dismissed')).toBe('1')
|
||||
})
|
||||
|
||||
it('does not show if already dismissed this session', async () => {
|
||||
sessionStorage.setItem('archipelago_pwa_install_dismissed', '1')
|
||||
const wrapper = shallowMount(PWAInstallPrompt, {
|
||||
global: { stubs: { Teleport: true, Transition: true } },
|
||||
})
|
||||
|
||||
// Fire beforeinstallprompt — should not show
|
||||
const event = new Event('beforeinstallprompt')
|
||||
Object.defineProperty(event, 'preventDefault', { value: vi.fn() })
|
||||
window.dispatchEvent(event)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).not.toContain('Install Archipelago')
|
||||
})
|
||||
|
||||
it('does not show in standalone mode', async () => {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockReturnValue({ matches: true }),
|
||||
})
|
||||
|
||||
const wrapper = shallowMount(PWAInstallPrompt, {
|
||||
global: { stubs: { Teleport: true, Transition: true } },
|
||||
})
|
||||
|
||||
const event = new Event('beforeinstallprompt')
|
||||
Object.defineProperty(event, 'preventDefault', { value: vi.fn() })
|
||||
window.dispatchEvent(event)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).not.toContain('Install Archipelago')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user