add destroy button

Changed files
+86 -68
src
-66
GEMINI.md
··· 1 - # Gemini Code Assistant Context 2 - 3 - This document provides an overview of the "skypod" project to be used as context for AI-assisted development. 4 - 5 - ## Project Overview 6 - 7 - **Skypod** is an offline-first, peer-to-peer RSS and podcast Progressive Web App (PWA). The primary goal is to allow a user to manage their subscriptions and listening history across multiple devices, with data synchronizing directly between them. 8 - 9 - ### Architecture 10 - 11 - The application is a full-stack TypeScript project with three main components: 12 - 13 - 1. **Frontend Client:** A Preact-based single-page application built with Vite. It is responsible for all UI, managing local data in IndexedDB via **Dexie.js**, and handling the peer-to-peer communication. 14 - 2. **Backend Server:** A lightweight Node.js server using Express. Its primary roles are to act as a proxy for fetching and parsing external RSS feeds and to run a WebSocket-based signaling server for establishing WebRTC connections between clients. 15 - 3. **P2P Sync Protocol:** The core of the application is a custom peer-to-peer synchronization protocol. It uses WebRTC (via `simple-peer`) for direct communication between a user's devices. Data consistency is achieved by syncing an immutable log of actions, with ordering guaranteed by a **Hybrid Logical Clock (HLC)** implementation. 16 - 17 - ### Key Technologies 18 - 19 - - **Language:** TypeScript 20 - - **Frontend:** Preact, Vite, Dexie.js 21 - - **Backend:** Node.js, Express, `ws` for WebSockets 22 - - **P2P & Sync:** WebRTC (`simple-peer`), Hybrid Logical Clocks 23 - - **Schema & Validation:** Zod 24 - - **Build & Tooling:** `npm` with `wireit`, ESLint, Prettier, Jest 25 - 26 - ## Building and Running 27 - 28 - The project uses `npm` and `wireit` for robust script and dependency management. 29 - 30 - - **Installation:** 31 - ```bash 32 - npm install 33 - ``` 34 - 35 - - **Development:** 36 - ```bash 37 - npm run dev 38 - ``` 39 - This is the primary development command. It uses `wireit` to concurrently run the Vite dev server, the backend server with `tsx` for live reloading, and watch modes for linting and type-checking. The frontend is available at `http://127.0.0.1:4000` and the backend at `http://127.0.0.1:4001`. 40 - 41 - - **Production:** 42 - ```bash 43 - npm run start:prod 44 - ``` 45 - This command builds the production frontend assets with Vite and starts the backend server. 46 - 47 - - **Testing:** 48 - ```bash 49 - npm run test 50 - ``` 51 - Runs the entire Jest test suite once. 52 - 53 - - **Linting & Type-Checking:** 54 - ```bash 55 - npm run lint 56 - npm run types 57 - ``` 58 - 59 - ## Development Conventions 60 - 61 - - **Code Style:** The project enforces a strict code style using Prettier and ESLint. Configurations can be found in `prettier.config.js` and `eslint.config.js`. 62 - - **Git Hooks:** A `pre-commit` hook is provided in `.githooks/` to automatically run linting and type-checking. Enable it with `git config core.hooksPath .githooks`. 63 - - **Path Aliases:** The codebase uses import aliases like `#common/*` and `#client/*` for clean, absolute-style imports. These are defined in `package.json` and `tsconfig.json`. 64 - - **P2P Sync Model:** The synchronization protocol follows a specific hybrid model: 65 - - **PULL for Catch-up:** A new client PULLS the full action history from a single, deterministically chosen `syncPartner` to efficiently get up to date. 66 - - **PUSH for Updates:** All clients PUSH their own new or offline-generated changes to all connected peers. This is not a simple broadcast; the client sends a *tailored* set of missing actions to each peer based on a handshake where "knowledge vectors" are exchanged.
+80
src/client/components/debug-nuke.tsx
··· 1 + import {useState} from 'preact/hooks' 2 + 3 + export const DebugNuke: preact.FunctionComponent = () => { 4 + const [confirming, setConfirming] = useState(false) 5 + 6 + const handleNuke = () => { 7 + if (!confirming) { 8 + setConfirming(true) 9 + return 10 + } 11 + 12 + try { 13 + window.indexedDB.deleteDatabase('skypod') 14 + window.location.reload() 15 + } catch (error) { 16 + console.error('Failed to nuke database:', error) 17 + alert(`Failed to delete database: ${error}`) 18 + setConfirming(false) 19 + } 20 + } 21 + 22 + const handleCancel = () => { 23 + setConfirming(false) 24 + } 25 + 26 + return ( 27 + <div style={{position: 'fixed', bottom: '10px', right: '10px', zIndex: 1000}}> 28 + {!confirming ? ( 29 + <button 30 + onClick={handleNuke} 31 + style={{ 32 + padding: '8px 16px', 33 + backgroundColor: '#dc3545', 34 + color: 'white', 35 + border: 'none', 36 + borderRadius: '4px', 37 + cursor: 'pointer', 38 + fontSize: '14px', 39 + fontWeight: 'bold', 40 + }} 41 + > 42 + Nuke DB 43 + </button> 44 + ) : ( 45 + <div style={{display: 'flex', gap: '8px'}}> 46 + <button 47 + onClick={handleNuke} 48 + style={{ 49 + padding: '8px 16px', 50 + backgroundColor: '#dc3545', 51 + color: 'white', 52 + border: '2px solid #fff', 53 + borderRadius: '4px', 54 + cursor: 'pointer', 55 + fontSize: '14px', 56 + fontWeight: 'bold', 57 + animation: 'pulse 1s infinite', 58 + }} 59 + > 60 + CONFIRM DELETE 61 + </button> 62 + <button 63 + onClick={handleCancel} 64 + style={{ 65 + padding: '8px 16px', 66 + backgroundColor: '#6c757d', 67 + color: 'white', 68 + border: 'none', 69 + borderRadius: '4px', 70 + cursor: 'pointer', 71 + fontSize: '14px', 72 + }} 73 + > 74 + Cancel 75 + </button> 76 + </div> 77 + )} 78 + </div> 79 + ) 80 + }
+2
src/client/page-app.tsx
··· 7 7 import {DatabaseProvider} from '#client/root/context-database.js' 8 8 import {SkypodProvider} from '#client/skypod/context' 9 9 10 + import {DebugNuke} from './components/debug-nuke' 10 11 import {Messenger} from './components/messenger' 11 12 import {PeerList} from './components/peer-list' 12 13 ··· 29 30 <RealmConnectionManager /> 30 31 <PeerList /> 31 32 <Messenger /> 33 + <DebugNuke /> 32 34 </SkypodProvider> 33 35 </RealmConnectionProvider> 34 36 </RealmIdentityProvider>
+2
src/client/realm/service-connection.ts
··· 349 349 #socketLoop = async () => { 350 350 this.#abort.signal.throwIfAborted() 351 351 352 + await sleep(1_000) 352 353 await this.#pingSocket() 354 + 353 355 while (!this.#abort.signal.aborted) { 354 356 await sleep(30_000) 355 357 await this.#pingSocket()
+2 -2
src/server/realm-storage.ts
··· 164 164 165 165 try { 166 166 // Iterate through all actions (leveldb stores by clock which is sortable) 167 - for await (const [, value] of this.#db.values({reverse: true})) { 167 + for await (const value of this.#db.values({reverse: true})) { 168 168 const stored = storedActionSchema.parse(JSON.parse(value)) 169 169 if (!states[stored.actor]) { 170 170 states[stored.actor] = stored.clock ··· 180 180 async buildSyncDelta(clocks: Record<IdentID, LCTimestamp | null>): Promise<StoredAction[]> { 181 181 const results: StoredAction[] = [] 182 182 try { 183 - for await (const [, value] of this.#db.values()) { 183 + for await (const value of this.#db.values()) { 184 184 const stored = storedActionSchema.parse(JSON.parse(value)) 185 185 const knownClock = clocks[stored.actor] 186 186