diff --git a/neode-ui/public/assets/img/phone-mockups/pixel-10.webp b/neode-ui/public/assets/img/phone-mockups/pixel-10.webp new file mode 100644 index 00000000..0f3ba1ff Binary files /dev/null and b/neode-ui/public/assets/img/phone-mockups/pixel-10.webp differ diff --git a/neode-ui/public/assets/img/phone-mockups/pixel-9.webp b/neode-ui/public/assets/img/phone-mockups/pixel-9.webp new file mode 100644 index 00000000..7689d967 Binary files /dev/null and b/neode-ui/public/assets/img/phone-mockups/pixel-9.webp differ diff --git a/neode-ui/public/assets/img/phone-mockups/pixel-9a.webp b/neode-ui/public/assets/img/phone-mockups/pixel-9a.webp new file mode 100644 index 00000000..40726152 Binary files /dev/null and b/neode-ui/public/assets/img/phone-mockups/pixel-9a.webp differ diff --git a/neode-ui/public/assets/img/router-mockups/beryl-ax.webp b/neode-ui/public/assets/img/router-mockups/beryl-ax.webp new file mode 100644 index 00000000..547c03ce Binary files /dev/null and b/neode-ui/public/assets/img/router-mockups/beryl-ax.webp differ diff --git a/neode-ui/public/assets/img/router-mockups/flint-2.webp b/neode-ui/public/assets/img/router-mockups/flint-2.webp new file mode 100644 index 00000000..f367c48e Binary files /dev/null and b/neode-ui/public/assets/img/router-mockups/flint-2.webp differ diff --git a/neode-ui/public/assets/img/router-mockups/nanopi-r5s.webp b/neode-ui/public/assets/img/router-mockups/nanopi-r5s.webp new file mode 100644 index 00000000..f747c258 Binary files /dev/null and b/neode-ui/public/assets/img/router-mockups/nanopi-r5s.webp differ diff --git a/neode-ui/src/components/DeviceModel.vue b/neode-ui/src/components/DeviceModel.vue index d2eaddac..fe66431b 100644 --- a/neode-ui/src/components/DeviceModel.vue +++ b/neode-ui/src/components/DeviceModel.vue @@ -192,6 +192,6 @@ onBeforeUnmount(() => { .device-model__caption { position: absolute; inset: auto 0 0; display: flex; justify-content: center; align-items: center; gap: 14px; color: rgba(255,255,255,.4); font-size: 11px; } .device-model__caption button { padding: 4px 9px; color: rgba(255,255,255,.65); border: 1px solid rgba(255,255,255,.12); border-radius: 20px; background: transparent; cursor: pointer; } .device-model__counter { color: rgba(255,255,255,.3); font-variant-numeric: tabular-nums; } -.device-model--viewport { height: 330px; } +.device-model--viewport { height: 290px; } @media (min-width: 1024px) { .device-model--viewport { height: var(--device-height); min-height: 260px; } } diff --git a/neode-ui/src/components/EasyHome.vue b/neode-ui/src/components/EasyHome.vue index 3fad4a04..739c3cb4 100644 --- a/neode-ui/src/components/EasyHome.vue +++ b/neode-ui/src/components/EasyHome.vue @@ -94,6 +94,7 @@ function goalIcon(icon: string): string { lightning: '⚡', identity: '🔑', backup: '💾', + phone: '📱', } return icons[icon] || '📦' } diff --git a/neode-ui/src/components/HardwareMarketplace.vue b/neode-ui/src/components/HardwareMarketplace.vue new file mode 100644 index 00000000..78365ac7 --- /dev/null +++ b/neode-ui/src/components/HardwareMarketplace.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/neode-ui/src/components/RouterMarketplace.vue b/neode-ui/src/components/RouterMarketplace.vue deleted file mode 100644 index 495a92ba..00000000 --- a/neode-ui/src/components/RouterMarketplace.vue +++ /dev/null @@ -1,214 +0,0 @@ - - - - - diff --git a/neode-ui/src/data/goals.ts b/neode-ui/src/data/goals.ts index 77ece715..5f87698f 100644 --- a/neode-ui/src/data/goals.ts +++ b/neode-ui/src/data/goals.ts @@ -67,6 +67,13 @@ export const GOALS: GoalDefinition[] = [ route: '/dashboard/setup/external-access', estimatedTime: 'Guided setup', difficulty: 'beginner', steps: [{ id: 'external-access', title: 'Configure access', description: 'Choose and verify each connection.', action: 'configure', isAutomatic: false }], }, + { + id: 'non-surveillance-phone', title: 'Get a non surveillance phone', + subtitle: 'Choose an unlockable Pixel and take control with GrapheneOS', + icon: 'phone', category: 'identity', requiredApps: [], + route: '/dashboard/setup/non-surveillance-phone', estimatedTime: 'Browse phones', difficulty: 'beginner', + steps: [{ id: 'choose-private-phone', title: 'Choose a compatible Pixel', description: 'Check that the bootloader can be unlocked, then install GrapheneOS and lock it again for verified boot.', action: 'info', isAutomatic: false }], + }, { id: 'publish-website', title: 'Publish a website', subtitle: 'Create a website on your node and choose where to publish it', diff --git a/neode-ui/src/graphics/deviceModels.ts b/neode-ui/src/graphics/deviceModels.ts index efd2978f..e90087ca 100644 --- a/neode-ui/src/graphics/deviceModels.ts +++ b/neode-ui/src/graphics/deviceModels.ts @@ -4,7 +4,7 @@ import { RoundedBoxGeometry } from 'three/addons/geometries/RoundedBoxGeometry.j // Original, procedural display models. Dimensions/layouts follow the existing // device illustrations and manufacturer references listed in device-models.md. // These are recognition aids, not mechanical CAD or revision-specific pinouts. -type Kind = 'board' | 'echo' | 'deck' | 'card' | 'case' | 'router' +type Kind = 'board' | 'echo' | 'deck' | 'card' | 'case' | 'router' | 'phone' interface DeviceSpec { kind: Kind; name: string; w: number; h: number; d: number; color: number screen?: 'oled' | 'paper'; antenna?: boolean; battery?: boolean; pins?: number @@ -36,6 +36,7 @@ export const deviceSpecs: Record = { 'rak4631_case': { kind: 'case', name: 'RAK WisBlock', w: 1.4, h: 2.3, d: .55, color: 0xd5dad5, screen: 'oled', antenna: true }, 'unknown-new': { kind: 'case', name: 'LoRa mesh radio', w: 1.8, h: 2.6, d: .5, color: 0x182623, screen: 'oled' }, 'beryl-ax': { kind: 'router', name: 'Beryl AX · GL-MT3000', w: 3, h: .8, d: 2.05, color: 0x171b20 }, + 'pixel-graphene': { kind: 'phone', name: 'Pixel · GrapheneOS', w: 1.82, h: 3.7, d: .26, color: 0x24282c }, } export function createDeviceModel(id: string) { @@ -157,6 +158,41 @@ export function createDeviceModel(id: string) { box(.02, .05, .05, -1.51, .14, -.77, dark) const underside = label('GL-MT3000 / BERYL AX', 1.25, .15, 0, -.413, 0) underside.rotation.x = Math.PI / 2 + } else if (spec.kind === 'phone') { + // Pixel-inspired handset: metal edge, side keys, rear camera bar and a + // conceptual GrapheneOS verified-boot display. This is not device CAD. + box(w + .035, h + .035, d + .025, 0, 0, 0, silver, .22) + box(w, h, d, 0, 0, 0, body, .21) + box(w - .08, h - .09, .025, 0, 0, d / 2 + .018, black, .19) + box(w - .15, h - .18, .008, 0, 0, d / 2 + .036, glass, .15) + const bootCanvas = document.createElement('canvas') + bootCanvas.width = 512; bootCanvas.height = 1024 + const boot = bootCanvas.getContext('2d')! + const glow = boot.createLinearGradient(0, 0, 512, 1024) + glow.addColorStop(0, '#080d11'); glow.addColorStop(1, '#121c1d') + boot.fillStyle = glow; boot.fillRect(0, 0, 512, 1024) + boot.strokeStyle = '#e6c775'; boot.lineWidth = 12 + boot.beginPath(); boot.moveTo(256, 288); boot.lineTo(326, 314); boot.lineTo(315, 395) + boot.quadraticCurveTo(294, 433, 256, 451); boot.quadraticCurveTo(218, 433, 197, 395) + boot.lineTo(186, 314); boot.closePath(); boot.stroke() + boot.fillStyle = '#e8ede9'; boot.textAlign = 'center' + boot.font = '500 57px sans-serif'; boot.fillText('GrapheneOS', 256, 590) + boot.fillStyle = '#d4bc78'; boot.font = '30px sans-serif'; boot.fillText('Verified boot', 256, 644) + boot.fillStyle = '#859699'; boot.font = '23px sans-serif'; boot.fillText('Bootloader locked', 256, 684) + const bootTexture = new THREE.CanvasTexture(bootCanvas); bootTexture.colorSpace = THREE.SRGBColorSpace; textures.push(bootTexture) + const bootMaterial = new THREE.MeshBasicMaterial({ map: bootTexture, toneMapped: false }); materials.push(bootMaterial) + const bootScreen = new THREE.Mesh(geometry('pixel-boot-screen', () => new THREE.PlaneGeometry(w - .19, h - .23)), bootMaterial) + bootScreen.position.z = d / 2 + .043; root.add(bootScreen) + disk(.035, 0, h / 2 - .13, d / 2 + .05, dark) + box(.045, .31, .07, w / 2 + .019, .57, 0, silver, .018) + box(.045, .17, .07, w / 2 + .019, .15, 0, silver, .018) + box(w * .82, .42, .07, 0, h * .29, -d / 2 - .04, black, .18) + for (const x of [-.43, .03, .47]) { + disk(.135, x, h * .29, -d / 2 - .085, silver) + disk(.095, x, h * .29, -d / 2 - .098, dark) + disk(.04, x, h * .29, -d / 2 - .11, glass) + } + box(.13, .045, .025, 0, -h / 2 - .014, 0, dark, .014) } else if (spec.kind === 'board') { box(w, h, d, 0, 0, 0, body, .06) // Plated through holes, copper traces, solder joints and populated reverse. diff --git a/neode-ui/src/router/index.ts b/neode-ui/src/router/index.ts index c21c63fe..aa3edf57 100644 --- a/neode-ui/src/router/index.ts +++ b/neode-ui/src/router/index.ts @@ -255,6 +255,11 @@ const router = createRouter({ name: 'publish-website', component: () => import('@/views/publishing/PublishingSetup.vue'), }, + { + path: 'setup/non-surveillance-phone', + name: 'non-surveillance-phone', + component: () => import('@/views/goals/NonSurveillancePhone.vue'), + }, { path: 'goals/:goalId', name: 'goal-detail', diff --git a/neode-ui/src/views/Server.vue b/neode-ui/src/views/Server.vue index ea582ebb..27debe9e 100644 --- a/neode-ui/src/views/Server.vue +++ b/neode-ui/src/views/Server.vue @@ -163,13 +163,22 @@ - - Firewalls & tunnels - - +
+ + Firewalls & tunnels + + + + OpenWrt Gateway + + +
diff --git a/neode-ui/src/views/goals/NonSurveillancePhone.vue b/neode-ui/src/views/goals/NonSurveillancePhone.vue new file mode 100644 index 00000000..7ff0c04b --- /dev/null +++ b/neode-ui/src/views/goals/NonSurveillancePhone.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/neode-ui/src/views/server/OpenWrtGateway.vue b/neode-ui/src/views/server/OpenWrtGateway.vue index 7b47104b..6488d0d3 100644 --- a/neode-ui/src/views/server/OpenWrtGateway.vue +++ b/neode-ui/src/views/server/OpenWrtGateway.vue @@ -5,7 +5,7 @@ import { rpcClient } from '@/api/rpc-client' import { useCachedResource } from '@/composables/useCachedResource' import BackButton from '@/components/BackButton.vue' import DeviceModel from '@/components/DeviceModel.vue' -import RouterMarketplace from '@/components/RouterMarketplace.vue' +import HardwareMarketplace from '@/components/HardwareMarketplace.vue' const router = useRouter() function goBack() { @@ -549,7 +549,7 @@ onMounted(() => {
- +
@@ -1038,10 +1038,10 @@ onMounted(() => { .router-model-card { margin: 24px 0 48px; } -.router-model-card > .grid { height: 100%; } +.router-model-card > .grid { height: 100%; min-height: 0; grid-template-rows: minmax(0, 1fr); } @media (min-width: 1024px) { - .router-model-card > .grid > .router-marketplace { + .router-model-card > .grid > :deep(.router-marketplace) { align-self: end; margin-top: 0; }