main d300ff7074bd cached
78 files
260.7 KB
68.0k tokens
95 symbols
1 requests
Download .txt
Showing preview only (281K chars total). Download the full file or copy to clipboard to get everything.
Repository: LovesWorking/rn-better-dev-tools
Branch: main
Commit: d300ff7074bd
Files: 78
Total size: 260.7 KB

Directory structure:
gitextract_s7vy5tt0/

├── .eslintignore
├── .eslintrc.json
├── .github/
│   └── workflows/
│       └── build.yml
├── .gitignore
├── .npmrc
├── DEVELOPMENT.md
├── GITHUB_RELEASE.md
├── README.md
├── RELEASE.md
├── assets/
│   └── icon.icns
├── auto-release.sh
├── build-and-pack.sh
├── build-macos.sh
├── config.js
├── copy-to-desktop.sh
├── entitlements.plist
├── forge.config.ts
├── forge.env.d.ts
├── index.html
├── package.json
├── postcss.config.js
├── release-version.sh
├── socket-client.js
├── src/
│   ├── auto-updater.ts
│   ├── components/
│   │   ├── App.tsx
│   │   └── external-dash/
│   │       ├── Dash.tsx
│   │       ├── DeviceSelection.tsx
│   │       ├── LogConsole.tsx
│   │       ├── Main.tsx
│   │       ├── NoDevicesConnected.tsx
│   │       ├── UserInfo/
│   │       │   ├── DeviceSpecificationsSection.tsx
│   │       │   ├── EnvironmentVariablesSection.tsx
│   │       │   ├── InfoRow.tsx
│   │       │   ├── StorageControlsSection.tsx
│   │       │   ├── TargetGlowEffect.tsx
│   │       │   ├── UserCardDetails.tsx
│   │       │   ├── UserCardHeader.tsx
│   │       │   └── index.ts
│   │       ├── UserInfo.tsx
│   │       ├── _hooks/
│   │       │   └── useConnectedUsers.ts
│   │       ├── hooks/
│   │       │   └── useDevToolsEventHandler.ts
│   │       ├── providers.tsx
│   │       ├── shared/
│   │       │   ├── hydration.ts
│   │       │   └── types.ts
│   │       ├── types/
│   │       │   ├── ClientQuery.ts
│   │       │   └── User.ts
│   │       ├── useSyncQueriesWeb.ts
│   │       └── utils/
│   │           ├── devToolsEvents.ts
│   │           ├── logStore.ts
│   │           ├── logger.ts
│   │           ├── platformUtils.tsx
│   │           ├── storageQueryKeys.ts
│   │           └── storageStore.ts
│   ├── config.ts
│   ├── index.css
│   ├── main.ts
│   ├── preload.ts
│   ├── react-query-external-sync/
│   │   ├── README.md
│   │   ├── User.ts
│   │   ├── hydration.ts
│   │   ├── index.ts
│   │   ├── platformUtils.ts
│   │   ├── types.ts
│   │   ├── useMySocket.ts
│   │   ├── useSyncQueries.ts
│   │   └── utils/
│   │       └── logger.ts
│   ├── renderer.ts
│   ├── renderer.tsx
│   ├── server/
│   │   └── socketHandle.ts
│   ├── server.ts
│   └── types.d.ts
├── tailwind.config.js
├── tanstack-query-devtools-5.74.7.tgz
├── tanstack-react-query-devtools-5.75.7.tgz
├── tsconfig.json
├── vite.main.config.ts
├── vite.preload.config.ts
└── vite.renderer.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
# Dependencies
node_modules/

# Build outputs
build/
dist/
.next/
out/

# IDE Specific
.idea/
.vscode/

# Specific files with TypeScript issues
src/components/external-dash/LogConsole.tsx 

================================================
FILE: .eslintrc.json
================================================
{
  "env": {
    "browser": true,
    "es6": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:import/recommended",
    "plugin:import/electron",
    "plugin:import/typescript"
  ],
  "parser": "@typescript-eslint/parser",
  "settings": {
    "import/resolver": {
      "node": {
        "extensions": [".js", ".jsx", ".ts", ".tsx"]
      }
    },
    "import/ignore": ["@tanstack/react-query-devtools"]
  },
  "rules": {
    "import/no-unresolved": [
      "error",
      {
        "ignore": ["^@tanstack/react-query-devtools"]
      }
    ]
  }
}


================================================
FILE: .github/workflows/build.yml
================================================
---
name: Build/release

on:
  push:
    tags:
      - "v*"

# Add permissions block
permissions:
  contents: write

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]
        arch: [x64, arm64]

    steps:
      - name: Check out Git repository
        uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v3
        with:
          version: 10.4.1

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "pnpm"

      - name: Install dependencies
        run: pnpm install

      - name: Import certificates
        if: runner.os == 'macOS'
        env:
          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
          MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          # Write certificate to file and decode it
          echo "$MACOS_CERTIFICATE" | base64 -D > certificate.p12

          # Create keychain
          security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain

          # Import certificate
          security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain

          # Verify certificate import
          security find-identity -v -p codesigning build.keychain

      - name: Build and package app
        env:
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          ARCH: ${{ matrix.arch }}
        run: pnpm run make --arch=${{ matrix.arch }}

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-${{ matrix.os }}-${{ matrix.arch }}
          path: "out/**/*.zip"
          if-no-files-found: error

  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download all uploaded artifacts
        uses: actions/download-artifact@v4
        with:
          path: ./downloaded-artifacts

      - name: Create or update release
        uses: ncipollo/release-action@v1
        if: startsWith(github.ref, 'refs/tags/')
        with:
          allowUpdates: true
          replacesArtifacts: true
          removeArtifacts: true
          artifacts: "downloaded-artifacts/**/*.zip"
          draft: false
          prerelease: false
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# Webpack
.webpack/

# Vite
.vite/

# Electron-Forge
out/

# Yalc
.yalc/.env

# Environment variables
.env.local
.env.*


================================================
FILE: .npmrc
================================================
node-linker = hoisted


================================================
FILE: DEVELOPMENT.md
================================================
# Development Guide

This guide covers everything you need to know about developing React Native DevTools.

## 🛠 Prerequisites

- Node.js 20 or later
- pnpm 10.4.1 or later
- macOS for building and signing
- Apple Developer account for signing
- GitHub CLI (`gh`) for releases

## 🚀 Getting Started

1. Clone the repository:

   ```bash
   git clone https://github.com/LovesWorking/rn-better-dev-tools.git
   cd rn-better-dev-tools
   ```

2. Install dependencies:

   ```bash
   pnpm install
   ```

3. Create a `.env` file based on `.env.example`:

   ```bash
   cp .env.example .env
   ```

4. Fill in your Apple Developer credentials in `.env`:
   ```
   APPLE_ID=your.email@example.com
   APPLE_PASSWORD=app-specific-password
   APPLE_TEAM_ID=your-team-id
   ```

## 🏗 Building

### Development Build

```bash
# Start in development mode with hot reload
pnpm start

# Build and copy to desktop for testing
pnpm run make:desktop

# Build only
pnpm run make
```

### Release Build

We provide an automated release script that:

- Bumps version (minor)
- Builds locally to verify
- Commits changes
- Creates and pushes tag
- Monitors GitHub Action progress

```bash
# Using npm script
pnpm run auto-release

# Or directly
./auto-release.sh
```

## 🐛 Debugging

### Enable Debug Logs

Add to your `.env`:

```bash
DEBUG=electron-osx-sign*
```

### Common Issues

1. **Build Hanging on "Finalizing package"**

   - Check Apple Developer credentials
   - Verify keychain access
   - Run with debug logs enabled

2. **Permission Issues**

   ```bash
   # Fix directory permissions
   sudo chown -R $(whoami) .

   # Clean build artifacts
   rm -rf .vite out
   ```

3. **Certificate Issues**
   - Verify Apple Developer membership is active
   - Check Team ID matches in `.env`
   - Ensure app-specific password is correct

### Development Commands

```bash
# Clean install
pnpm run nuke

# Package without making distributables
pnpm run package

# Run linter
pnpm run lint
```

## 📦 Project Structure

```
.
├── src/                  # Source code
│   ├── main.ts          # Main process
│   ├── preload.ts       # Preload scripts
│   └── components/      # React components
├── assets/              # Static assets
├── .github/workflows/   # GitHub Actions
└── forge.config.ts      # Electron Forge config
```

## 🔄 Release Process

### Automatic Release

```bash
./auto-release.sh
```

### Manual Release Steps

1. Update version in `package.json`
2. Build and test locally
3. Create and push tag
4. GitHub Action will build and publish

## 🧪 Testing

Before submitting a PR:

1. Test in development mode (`pnpm start`)
2. Build and test locally (`pnpm run make:desktop`)
3. Verify all features work
4. Check console for errors
5. Run linter (`pnpm run lint`)


================================================
FILE: GITHUB_RELEASE.md
================================================
# GitHub Release Process

This guide explains how to release updates to GitHub and enable auto-updating for React Native DevTools.

## Automated Release Process

The recommended way to create a new release is to use the automated script:

1. Make sure your changes are committed to the main branch
2. Run the release script:
   ```bash
   pnpm run release
   # or directly with
   ./release-version.sh
   ```
3. Follow the interactive prompts to:
   - Select the version bump type (patch, minor, major, or custom)
   - Enter release notes
   - Confirm the release
4. The script will:
   - Update the version in package.json
   - Commit and push the changes
   - Create and push a git tag
   - Monitor the GitHub Actions workflow
   - Automatically publish the release when complete

## Manual Releases

If you need to manually publish a release:

1. Ensure you have a GitHub access token with "repo" permissions
2. Set the token as an environment variable:
   ```bash
   export GITHUB_TOKEN=your_github_token
   ```
3. Run the pack script:
   ```bash
   pnpm run pack
   # or directly with
   ./build-and-pack.sh
   ```

## GitHub Actions Automated Releases

For automated releases using GitHub Actions:

1. Create a new tag following semantic versioning:

   ```bash
   git tag v1.x.x
   git push origin v1.x.x
   ```

2. The GitHub Actions workflow will automatically build and release the app
3. The release will initially be created as a draft
4. Review the release and publish it when ready

## Auto-Updates

The app is configured to automatically check for updates when running. When a new version is released on GitHub:

1. Users with the previous version will automatically receive update notifications
2. Updates are downloaded in the background
3. The update will be installed when the user restarts the app

## Configuration Files

The auto-update system is configured in several files:

- `forge.config.ts`: Contains the GitHub publisher configuration
- `package.json`: Contains the electron-builder configuration
- `src/auto-updater.ts`: Implements the auto-update checking logic
- `.github/workflows/build.yml`: Defines the GitHub Actions workflow for automated builds

## Troubleshooting

- If the auto-updater isn't working, check the log file at:
  - macOS: `~/Library/Logs/react-native-devtools/main.log`
- Make sure your repository is public, or if it's private, ensure users have proper access tokens configured

- To debug update issues, run the app with the `DEBUG` environment variable:
  ```bash
  DEBUG=electron-updater npm start
  ```

## Version Updates

The automated release script handles version updates for you, but if you need to do it manually:

1. Update the version in `package.json`
2. Commit the changes
3. Create a new tag matching the version number
4. Push the tag to GitHub

The version format should follow semantic versioning: `MAJOR.MINOR.PATCH`


================================================
FILE: README.md
================================================
# React Native DevTools

Enhanced developer tools for React Native applications, supporting React Query DevTools and device storage monitoring with a beautiful native interface.

![ios pokemon](https://github.com/user-attachments/assets/25ffb38c-2e41-4aa9-a3c7-6f74383a75fc)

https://github.com/user-attachments/assets/5c0c5748-e031-427a-8ebf-9c085434e8ba

## Example app

https://github.com/LovesWorking/RN-Dev-Tools-Example

### If you need internal React Query dev tools within the device you can use my other package here!

https://github.com/LovesWorking/react-native-react-query-devtools

## ✨ Features

- 🔄 Real-time React Query state monitoring
- 💾 **Device storage monitoring with CRUD operations** - MMKV, AsyncStorage, and SecureStorage
- 🌐 **Environment variables monitoring** - View and track public environment variables
- 🎨 Beautiful native macOS interface
- 🚀 Automatic connection to React apps
- 📊 Query status visualization
- 🔌 Socket.IO integration for reliable communication
- ⚡️ Simple setup with NPM package
- 📱 Works with **any React-based platform**: React Native, React Web, Next.js, Expo, tvOS, VR, etc.
- 🛑 Zero-config production safety - automatically disabled in production builds

## 📦 Installation

### DevTools Desktop Application (macOS)

> **⚠️ Important**: The desktop app has currently only been tested on Apple Silicon Macs (M1/M2/M3).
> If you encounter issues on Intel-based Macs, please [open an issue](https://github.com/LovesWorking/rn-better-dev-tools/issues)
> and we'll work together to fix it.

1. Download the latest release from the [Releases page](https://github.com/LovesWorking/rn-better-dev-tools/releases)
2. Extract the ZIP file
3. Move the app to your Applications folder
4. Launch the app

### React Application Integration

The easiest way to connect your React application to the DevTools is by installing the npm package:

```bash
# Using npm
npm install --save-dev react-query-external-sync socket.io-client
npm install expo-device  # For automatic device detection

# Using yarn
yarn add -D react-query-external-sync socket.io-client
yarn add expo-device  # For automatic device detection

# Using pnpm (recommended)
pnpm add -D react-query-external-sync socket.io-client
pnpm add expo-device  # For automatic device detection
```

## 🚀 Quick Start

1. Download and launch the React Native DevTools application
2. Add the hook to your application where you set up your React Query context:

```jsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useSyncQueriesExternal } from "react-query-external-sync";
// Import Platform for React Native or use other platform detection for web/desktop
import { Platform } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as SecureStore from "expo-secure-store";
import * as ExpoDevice from "expo-device";
import { storage } from "./mmkv"; // Your MMKV instance

// Create your query client
const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AppContent />
    </QueryClientProvider>
  );
}

function AppContent() {
  // Set up the sync hook - automatically disabled in production!
  useSyncQueriesExternal({
    queryClient,
    socketURL: "http://localhost:42831", // Default port for React Native DevTools
    deviceName: Platform?.OS || "web", // Platform detection
    platform: Platform?.OS || "web", // Use appropriate platform identifier
    deviceId: Platform?.OS || "web", // Use a PERSISTENT identifier (see note below)
    isDevice: ExpoDevice.isDevice, // Automatically detects real devices vs emulators
    extraDeviceInfo: {
      // Optional additional info about your device
      appVersion: "1.0.0",
      // Add any relevant platform info
    },
    enableLogs: false,
    envVariables: {
      NODE_ENV: process.env.NODE_ENV,
      // Add any private environment variables you want to monitor
      // Public environment variables are automatically loaded
    },
    // Storage monitoring with CRUD operations
    mmkvStorage: storage, // MMKV storage for ['#storage', 'mmkv', 'key'] queries + monitoring
    asyncStorage: AsyncStorage, // AsyncStorage for ['#storage', 'async', 'key'] queries + monitoring
    secureStorage: SecureStore, // SecureStore for ['#storage', 'secure', 'key'] queries + monitoring
    secureStorageKeys: [
      "userToken",
      "refreshToken",
      "biometricKey",
      "deviceId",
    ], // SecureStore keys to monitor
  });

  // Your app content
  return <YourApp />;
}
```

3. Start your React application
4. DevTools will automatically detect and connect to your running application

## 🔒 Production Safety

The React Query External Sync package is automatically disabled in production builds.

```jsx
// The package handles this internally:
if (process.env.NODE_ENV !== "production") {
  useSyncQueries = require("./new-sync/useSyncQueries").useSyncQueries;
} else {
  // In production, this becomes a no-op function
  useSyncQueries = () => ({
    isConnected: false,
    connect: () => {},
    disconnect: () => {},
    socket: null,
    users: [],
  });
}
```

### 📱 Using with Real Devices (Local Network)

When testing on real devices connected to your local network, you'll need to use your host machine's IP address instead of `localhost`. Here's a helpful setup for Expo apps (contributed by [ShoeBoom](https://github.com/ShoeBoom)):

```jsx
import Constants from "expo-constants";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as SecureStore from "expo-secure-store";
import { storage } from "./mmkv"; // Your MMKV instance

// Get the host IP address dynamically
const hostIP =
  Constants.expoGoConfig?.debuggerHost?.split(`:`)[0] ||
  Constants.expoConfig?.hostUri?.split(`:`)[0];

function AppContent() {
  useSyncQueriesExternal({
    queryClient,
    socketURL: `http://${hostIP}:42831`, // Use local network IP
    deviceName: Platform?.OS || "web",
    platform: Platform?.OS || "web",
    deviceId: Platform?.OS || "web",
    extraDeviceInfo: {
      appVersion: "1.0.0",
    },
    enableLogs: false,
    envVariables: {
      NODE_ENV: process.env.NODE_ENV,
    },
    // Storage monitoring
    mmkvStorage: storage,
    asyncStorage: AsyncStorage,
    secureStorage: SecureStore,
    secureStorageKeys: ["userToken", "refreshToken"],
  });

  return <YourApp />;
}
```

> **Note**: For optimal connection, launch DevTools before starting your application.

## 💡 Usage Tips

- Keep DevTools running while developing
- Monitor query states in real-time
- View detailed query information
- Track cache updates and invalidations
- **Monitor device storage**: View and modify MMKV, AsyncStorage, and SecureStorage data in real-time
- **Track environment variables**: Monitor public environment variables across your application
- **Use storage queries**: Access storage data via React Query with keys like `['#storage', 'mmkv', 'key']`
- The hook is automatically disabled in production builds, no configuration needed

## 📱 Platform Support

React Native DevTools works with **any React-based application**, regardless of platform:

- 📱 Mobile: iOS, Android
- 🖥️ Web: React, Next.js, Remix, etc.
- 🖥️ Desktop: Electron, Tauri
- 📺 TV: tvOS, Android TV
- 🥽 VR/AR: Meta Quest, etc.
- 💻 Cross-platform: Expo, React Native Web

If your platform can run React and connect to a socket server, it will work with these DevTools!

## 💾 Storage Monitoring

React Native DevTools now includes powerful storage monitoring capabilities with full CRUD operations:

### Supported Storage Types

- **MMKV**: High-performance key-value storage
- **AsyncStorage**: React Native's standard async storage
- **SecureStorage**: Secure storage for sensitive data (Expo SecureStore)

### Features

- **Real-time monitoring**: See storage changes as they happen
- **CRUD operations**: Create, read, update, and delete storage entries directly from DevTools
- **React Query integration**: Access storage data via queries like `['#storage', 'mmkv', 'keyName']`
- **Type-safe operations**: Automatic serialization/deserialization of complex data types
- **Secure data handling**: SecureStorage keys are monitored securely

### Usage Example

```jsx
// In your app, use React Query to interact with storage
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

// Read from MMKV storage
const { data: userData } = useQuery({
  queryKey: ["#storage", "mmkv", "user"],
  // Data will be automatically synced with DevTools
});

// Write to AsyncStorage
const queryClient = useQueryClient();
const updateAsyncStorage = useMutation({
  mutationFn: async ({ key, value }) => {
    await AsyncStorage.setItem(key, JSON.stringify(value));
    // Invalidate to trigger sync
    queryClient.invalidateQueries(["#storage", "async", key]);
  },
});
```

## ⚙️ Configuration Options

The `useSyncQueriesExternal` hook accepts the following options:

| Option              | Type         | Required | Description                                                                                                       |
| ------------------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `queryClient`       | QueryClient  | Yes      | Your React Query client instance                                                                                  |
| `socketURL`         | string       | Yes      | URL of the socket server (e.g., 'http://localhost:42831')                                                         |
| `deviceName`        | string       | Yes      | Human-readable name for your device                                                                               |
| `platform`          | string       | Yes      | Platform identifier ('ios', 'android', 'web', 'macos', 'windows', etc.)                                           |
| `deviceId`          | string       | Yes      | Unique identifier for your device                                                                                 |
| `isDevice`          | boolean      | No       | Whether running on a real device (true) or emulator (false). Used for Android socket URL handling (default: true) |
| `extraDeviceInfo`   | object       | No       | Additional device metadata to display in DevTools                                                                 |
| `enableLogs`        | boolean      | No       | Enable console logging for debugging (default: false)                                                             |
| `envVariables`      | object       | No       | Private environment variables to sync with DevTools (public vars are auto-loaded)                                 |
| `mmkvStorage`       | MmkvStorage  | No       | MMKV storage instance for real-time monitoring                                                                    |
| `asyncStorage`      | AsyncStorage | No       | AsyncStorage instance for polling-based monitoring                                                                |
| `secureStorage`     | SecureStore  | No       | SecureStore instance for secure data monitoring                                                                   |
| `secureStorageKeys` | string[]     | No       | Array of SecureStore keys to monitor (required if using secureStorage)                                            |

## 🔮 Future Plans

React Native DevTools is actively being developed with exciting features on the roadmap:

- ✅ **Storage Viewers**: Beautiful interfaces for viewing and modifying storage (AsyncStorage, MMKV, SecureStorage) - **Now Available!**
- 🌐 **Network Request Monitoring**: Track API calls, WebSockets, and GraphQL requests
- ❌ **Failed Request Tracking**: Easily identify and debug network failures
- 🔄 **Remote Expo DevTools**: Trigger Expo DevTools commands remotely without using the command line
- 🧩 **Plugin System**: Allow community extensions for specialized debugging tasks
- 🗄️ **Drizzle Studio Plugin**: Integration with Drizzle ORM for database management

Stay tuned for updates!

## 🤝 Contributing

I welcome contributions! See [Development Guide](DEVELOPMENT.md) for details on:

- Setting up the development environment
- Building and testing
- Release process
- Contribution guidelines

## 🐛 Troubleshooting

Having issues? Check these common solutions:

1. **App Not Connecting**

   - Ensure DevTools is launched before your React app
   - Check that your React app is running
   - Verify you're on the same network
   - Make sure the `socketURL` is correctly pointing to localhost:42831
   - Verify the Socket.IO client is properly installed in your app
   - Check that the `useSyncQueriesExternal` hook is properly implemented

2. **App Not Starting**

   - Verify you're using the latest version
   - Check system requirements (macOS with Apple Silicon chip)
   - Try reinstalling the application
   - If using an Intel Mac and encountering issues, please report them

3. **Socket Connection Issues**

   - Make sure no firewall is blocking the connection on port 42831
   - Restart both the DevTools app and your React app
   - Check the console logs with `enableLogs: true` for any error messages
   - If the React Query data is too large the socket connection will crash! If you see the device connect and then disconnect with no logs this is what's happening. You'll need to fix your query cache to not be so large.

4. **Data Not Syncing**

   - Confirm you're passing the correct `queryClient` instance
   - Set `enableLogs: true` to see connection information

5. **Android Real Device Connection Issues**

   - If using a real Android device with React Native CLI and ADB, ensure `isDevice: true`
   - The package transforms `localhost` to `10.0.2.2` for emulators only
   - Use `ExpoDevice.isDevice` for automatic detection: `import * as ExpoDevice from "expo-device"`
   - Check network connectivity between your device and development machine

6. **DevTools App Issues**

   - Make sure your `deviceId` is persistent (see below)
   - Verify you're using the latest version
   - Check system requirements (macOS with Apple Silicon chip)
   - Try reinstalling the application
   - If using an Intel Mac and encountering issues, please report them

7. **Storage Monitoring Issues**

   - Ensure storage instances are properly passed to the hook
   - For SecureStorage, make sure `secureStorageKeys` array is provided
   - Check that storage permissions are granted on the device
   - Verify storage libraries are properly installed and configured
   - Use `enableLogs: true` to see storage sync information

That's it! If you're still having issues, visit the [GitHub repository](https://github.com/LovesWorking/rn-better-dev-tools/issues) for support.

## 🏷️ Device Type Detection

The `isDevice` prop helps the DevTools distinguish between real devices and simulators/emulators. This is **crucial for Android connectivity** - the package automatically handles URL transformation for Android emulators (localhost → 10.0.2.2) but needs to know if you're running on a real device to avoid this transformation.

### ⚠️ Android Connection Issue

On real Android devices using React Native CLI and ADB, the automatic emulator detection can incorrectly transform `localhost` to `10.0.2.2`, breaking WebSocket connections. Setting `isDevice: true` prevents this transformation.

**Recommended approaches:**

```jsx
// Best approach using Expo Device (works with bare React Native too)
import * as ExpoDevice from "expo-device";

useSyncQueriesExternal({
  queryClient,
  socketURL: "http://localhost:42831",
  deviceName: Platform.OS,
  platform: Platform.OS,
  deviceId: Platform.OS,
  isDevice: ExpoDevice.isDevice, // Automatically detects real devices vs emulators
  // ... other props
});

// Alternative: Simple approach using React Native's __DEV__ flag
isDevice: !__DEV__, // true for production/real devices, false for development/simulators

// Alternative: More sophisticated detection using react-native-device-info
import DeviceInfo from 'react-native-device-info';
isDevice: !DeviceInfo.isEmulator(), // Automatically detects if running on emulator

// Manual control for specific scenarios
isDevice: Platform.OS === 'ios' ? !Platform.isPad : Platform.OS !== 'web',
```

## ⚠️ Important Note About Device IDs

The `deviceId` parameter must be **persistent** across app restarts and re-renders. Using a value that changes (like `Date.now()`) will cause each render to be treated as a new device.

**Recommended approaches:**

```jsx
// Simple approach for single devices
deviceId: Platform.OS, // Works if you only have one device per platform

// Better approach for multiple simulators/devices of same type
// Using AsyncStorage, MMKV, or another storage solution
const [deviceId, setDeviceId] = useState(Platform.OS);

useEffect(() => {
  const loadOrCreateDeviceId = async () => {
    // Try to load existing ID
    const storedId = await AsyncStorage.getItem('deviceId');

    if (storedId) {
      setDeviceId(storedId);
    } else {
      // First launch - generate and store a persistent ID
      const newId = `${Platform.OS}-${Date.now()}`;
      await AsyncStorage.setItem('deviceId', newId);
      setDeviceId(newId);
    }
  };

  loadOrCreateDeviceId();
}, []);
```

For more detailed troubleshooting, see our [Development Guide](DEVELOPMENT.md).

## 📄 License

MIT
---

Made with ❤️ by [LovesWorking](https://github.com/LovesWorking)



## 🚀 More

**Take a shortcut from web developer to mobile development fluency with guided learning**

Enjoyed this project? Learn to use React Native to build production-ready, native mobile apps for both iOS and Android based on your existing web development skills.

<img width="1800" height="520" alt="banner" src="https://github.com/user-attachments/assets/cdf63dea-464f-44fe-bed1-a517785bfd99" />


================================================
FILE: RELEASE.md
================================================
# Releasing React Native DevTools

This document outlines the process for creating a new release of React Native DevTools.

## Automated Release Process (Recommended)

The easiest way to create a new release is to use our automated script:

```bash
pnpm run release
```

This interactive script will:

1. Prompt you to select a version bump type (patch, minor, major, or custom)
2. Update the version in package.json
3. Ask for release notes
4. Commit and push the changes
5. Create and push a git tag
6. Monitor the GitHub Actions workflow
7. Automatically publish the release when complete

## Manual Release Process

If you need to perform the release manually, follow these steps:

1. Update the version in `package.json`:

```json
{
  "version": "x.y.z",
  ...
}
```

2. Commit your changes:

```bash
git add package.json
git commit -m "Bump version to x.y.z"
```

3. Create and push a new tag:

```bash
git tag -a vx.y.z -m "Version x.y.z"
git push origin vx.y.z
```

4. The GitHub Actions workflow will automatically:

   - Build the app for macOS
   - Create a draft release with all the built installers
   - Add the release notes

5. Go to the GitHub Releases page, review the draft release, add any additional notes, and publish it.

## Local Build and Package

If you want to build the app locally without publishing:

```bash
pnpm run pack
```

This will:

- Build the app
- Create installation packages
- Copy them to your Desktop in a "release rn better tools" folder

## Testing Before Release

Before creating a new release tag, make sure to:

1. Test the app thoroughly on your local machine
2. Run `pnpm run make` locally to ensure the build process completes without errors
3. Test the generated installers

## Troubleshooting

If the GitHub Actions build fails:

1. Check the workflow logs for errors
2. Make sure the repository has the necessary secrets and permissions set up
3. Try running the build locally to isolate the issue

## Auto-Update Feature

The app includes auto-update functionality. When a new release is published:

1. Existing users will be automatically notified of the update
2. The update will be downloaded in the background
3. The update will be installed when the user restarts the app

See GITHUB_RELEASE.md for more details on the auto-update configuration.


================================================
FILE: assets/icon.icns
================================================
// This is a placeholder - you need to replace this with an actual .icns file
// For now, you'll need to create or convert your icon to .icns format manually
// You can use tools like iconutil or online converters to create .icns files 

================================================
FILE: auto-release.sh
================================================
#!/bin/bash

# Set error handling
set -e

# Colors for better logging
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Function for logging
log() {
    echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}

error() {
    echo -e "${RED}[ERROR]${NC} $1"
    exit 1
}

success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

warning() {
    echo -e "${YELLOW}[WARNING]${NC} $1"
}

# Check if we're in the right directory
if [ ! -f "package.json" ]; then
    error "Must be run from project root directory"
fi

# Get current version from package.json
CURRENT_VERSION=$(node -p "require('./package.json').version")
log "Current version: $CURRENT_VERSION"

# Calculate new version (minor bump)
NEW_VERSION=$(node -p "
    const [major, minor, patch] = '$CURRENT_VERSION'.split('.');
    \`\${major}.\${parseInt(minor) + 1}.0\`
")
log "New version will be: $NEW_VERSION"

# Update version in package.json
log "Updating package.json version..."
npm version $NEW_VERSION --no-git-tag-version

# Build locally first to ensure everything works
log "Building locally to verify..."
pnpm run make:desktop || error "Local build failed"
success "Local build successful!"

# Stage all changes
log "Staging changes..."
git add . || error "Failed to stage changes"

# Commit with conventional commit message
log "Committing changes..."
git commit -m "chore: release v$NEW_VERSION" || error "Failed to commit changes"

# Create and push tag
log "Creating tag v$NEW_VERSION..."
git tag -d "v$NEW_VERSION" 2>/dev/null || true
git push origin ":refs/tags/v$NEW_VERSION" 2>/dev/null || true
git tag "v$NEW_VERSION" || error "Failed to create tag"

# Push changes and tags
log "Pushing changes and tags..."
git push && git push --tags || error "Failed to push changes"
success "Changes pushed successfully!"

# Wait for GitHub Action to start
log "Waiting for GitHub Action to start..."
sleep 5

# Monitor GitHub Action progress
MAX_ATTEMPTS=30
ATTEMPT=1
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
    STATUS=$(gh run list --json status,name --jq '.[0].status' 2>/dev/null || echo "unknown")
    if [ "$STATUS" = "completed" ]; then
        success "GitHub Action completed successfully!"
        break
    elif [ "$STATUS" = "failed" ]; then
        error "GitHub Action failed. Check the logs with: gh run view"
    fi
    log "Build still in progress... (Attempt $ATTEMPT/$MAX_ATTEMPTS)"
    sleep 10
    ATTEMPT=$((ATTEMPT + 1))
done

if [ $ATTEMPT -gt $MAX_ATTEMPTS ]; then
    warning "Timed out waiting for GitHub Action to complete. Check status manually with: gh run view"
fi

success "Release process completed!"
log "New version v$NEW_VERSION is being published"
log "You can check the release status at: https://github.com/LovesWorking/rn-better-dev-tools/releases" 

================================================
FILE: build-and-pack.sh
================================================
#!/bin/bash

# Exit on any error
set -e

# Display what's happening
echo "🚀 Building and packaging React Native DevTools..."

# Variables
VERSION=$(node -e "console.log(require('./package.json').version)")
OUTPUT_DIR="$HOME/Desktop/release rn better tools"
APP_NAME="React Native DevTools"

# Ensure the output directory exists
mkdir -p "$OUTPUT_DIR"

# Clean previous builds
echo "🧹 Cleaning previous builds..."
rm -rf out/

# Check if GITHUB_TOKEN is set
if [ -n "$GITHUB_TOKEN" ]; then
  # If GITHUB_TOKEN is set, publish to GitHub
  echo "🌐 Publishing to GitHub..."
  npx electron-forge publish
else
  # If GITHUB_TOKEN is not set, just build locally
  echo "🔨 Building application locally..."
  npx electron-forge make --targets=@electron-forge/maker-zip

  # Copy the output to the destination folder
  echo "📦 Copying packaged app to destination folder..."
  cp "out/make/zip/darwin/arm64/$APP_NAME-darwin-arm64-$VERSION.zip" "$OUTPUT_DIR/"

  # Create a dated copy for versioning/archiving purposes
  DATED_FILENAME="$APP_NAME-darwin-arm64-$VERSION-$(date +%Y%m%d%H%M).zip"
  cp "out/make/zip/darwin/arm64/$APP_NAME-darwin-arm64-$VERSION.zip" "$OUTPUT_DIR/$DATED_FILENAME"

  echo "✅ Build and packaging complete!"
  echo "📝 Files created:"
  echo "   - $OUTPUT_DIR/$APP_NAME-darwin-arm64-$VERSION.zip"
  echo "   - $OUTPUT_DIR/$DATED_FILENAME"
  echo ""
  echo "🖥️  Open folder using: open \"$OUTPUT_DIR\""
  echo ""
  echo "⚠️  Note: To publish to GitHub, set the GITHUB_TOKEN environment variable and run this script again."
fi 

================================================
FILE: build-macos.sh
================================================
#!/bin/bash

# Exit on error
set -e

echo "=== Building React Native DevTools for macOS ==="

# Clean previous builds
rm -rf out || true
echo "✅ Cleaned previous builds"

# Install dependencies
pnpm install
echo "✅ Dependencies installed"

# Build package
echo "🔨 Building macOS package..."
pnpm run make

# Check if ZIP was created
ZIP_PATH=$(find out/make -name "*.zip" | head -n 1)

if [ -f "$ZIP_PATH" ]; then
    echo "✅ ZIP package created at: $ZIP_PATH"
    echo "✅ Total size: $(du -h "$ZIP_PATH" | cut -f1)"
    
    echo ""
    echo "To run the app:"
    echo "1. Extract the ZIP file"
    echo "2. Move the app to your Applications folder"
    echo "3. Open the app (you may need to right-click and select Open for the first time)"
    
    echo ""
    echo "To release:"
    echo "1. Update version in package.json"
    echo "2. Commit changes"
    echo "3. Create and push tag: git tag -a v1.0.0 -m 'v1.0.0' && git push origin v1.0.0"
    
    echo ""
    echo "Build complete! 🎉"
else
    echo "❌ ZIP package was not created. Check for errors above."
    exit 1
fi 

================================================
FILE: config.js
================================================
/**
 * Socket.IO Test Client Config
 * CommonJS version for use with the test script
 */

// Server configuration
const SERVER_PORT = 42831; // Using an uncommon port to avoid conflicts

// Client configuration
const CLIENT_URL = `http://localhost:${SERVER_PORT}`;

module.exports = {
  SERVER_PORT,
  CLIENT_URL,
};


================================================
FILE: copy-to-desktop.sh
================================================
#!/bin/bash

# Get the version from package.json
VERSION=$(node -p "require('./package.json').version")

# Create desktop directory if it doesn't exist
DESKTOP_DIR="$HOME/Desktop/rn-dev-tools-releases"
mkdir -p "$DESKTOP_DIR"

# Copy the zip file to desktop
cp "out/make/zip/darwin/arm64/React Native DevTools-darwin-arm64-$VERSION.zip" "$DESKTOP_DIR/"

echo "✅ Release copied to $DESKTOP_DIR" 

================================================
FILE: entitlements.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.device.audio-input</key>
    <true/>
    <key>com.apple.security.device.bluetooth</key>
    <true/>
    <key>com.apple.security.device.camera</key>
    <true/>
    <key>com.apple.security.device.print</key>
    <true/>
    <key>com.apple.security.device.usb</key>
    <true/>
    <key>com.apple.security.personal-information.location</key>
    <true/>
  </dict>
</plist> 

================================================
FILE: forge.config.ts
================================================
import { config as dotenvConfig } from "dotenv";
import * as path from "path";

// Load .env file from the project root
dotenvConfig({ path: path.resolve(process.cwd(), ".env") });

// Debug logging to verify environment variables
console.log("Environment variables loaded:", {
  APPLE_ID: process.env.APPLE_ID,
  TEAM_ID: process.env.APPLE_TEAM_ID,
  DEBUG: process.env.DEBUG,
});

import type { ForgeConfig } from "@electron-forge/shared-types";
import { MakerZIP } from "@electron-forge/maker-zip";
import { VitePlugin } from "@electron-forge/plugin-vite";
import { FusesPlugin } from "@electron-forge/plugin-fuses";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
import { PublisherGithub } from "@electron-forge/publisher-github";

const config: ForgeConfig = {
  packagerConfig: {
    asar: true,
    icon: "./assets/icon", // No file extension required
    appBundleId: "com.lovesworking.rn-dev-tools",
    appCategoryType: "public.app-category.developer-tools",
    executableName: "React Native DevTools",
    osxSign: {
      identity: "6EC9AE0A608BB7CBBA6BCC7936689773E76D63F0",
    },
    // The osxSign config comes with defaults that work out of the box in most cases, so we recommend you start with an empty configuration object.
    // For a full list of configuration options, see  https://js.electronforge.io/modules/_electron_forge_shared_types.InternalOptions.html#OsxSignOptions
    osxNotarize: {
      appleId: process.env.APPLE_ID!,
      appleIdPassword: process.env.APPLE_PASSWORD!,
      teamId: process.env.APPLE_TEAM_ID!,
    },
  },
  rebuildConfig: {},
  makers: [
    // Build a ZIP for all platforms
    new MakerZIP({}, ["darwin", "linux", "win32"]),
    // The following makers are commented out for now
    // new MakerSquirrel({}),
    // new MakerRpm({}),
    // new MakerDeb({})
  ],
  publishers: [
    new PublisherGithub({
      repository: {
        owner: "lovesworking", // Replace with your GitHub username or organization
        name: "rn-better-dev-tools", // Replace with your repository name
      },
      prerelease: false, // Set to true if you want to mark releases as pre-releases
    }),
  ],
  plugins: [
    new VitePlugin({
      // `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
      // If you are familiar with Vite configuration, it will look really familiar.
      build: [
        {
          // `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
          entry: "src/main.ts",
          config: "vite.main.config.ts",
          target: "main",
        },
        {
          entry: "src/preload.ts",
          config: "vite.preload.config.ts",
          target: "preload",
        },
      ],
      renderer: [
        {
          name: "main_window",
          config: "vite.renderer.config.ts",
        },
      ],
    }),
    // Fuses are used to enable/disable various Electron functionality
    // at package time, before code signing the application
    new FusesPlugin({
      version: FuseVersion.V1,
      [FuseV1Options.RunAsNode]: false,
      [FuseV1Options.EnableCookieEncryption]: true,
      [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
      [FuseV1Options.EnableNodeCliInspectArguments]: false,
      [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
      [FuseV1Options.OnlyLoadAppFromAsar]: true,
    }),
  ],
};

export default config;


================================================
FILE: forge.env.d.ts
================================================
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>React Native Dev Tools</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/renderer.tsx"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "rn-better-dev-tools",
  "productName": "React Native DevTools",
  "version": "1.1.0",
  "description": "Enhanced developer tools for React Native applications (Only supports React Query DevTools Right Now)",
  "main": ".vite/build/main.js",
  "scripts": {
    "release": "./release-version.sh",
    "pack": "./build-and-pack.sh",
    "nuke": "rm -rf node_modules .vite/build pnpm-lock.yaml && pnpm store prune && pnpm cache clean && pnpm install",
    "start": "electron-forge start",
    "package": "electron-forge package",
    "make": "electron-forge make",
    "make:desktop": "pnpm run make && ./copy-to-desktop.sh",
    "publish": "electron-forge publish",
    "lint": "eslint --ext .ts,.tsx .",
    "auto-release": "./auto-release.sh"
  },
  "keywords": [],
  "author": {
    "name": "lovesworking",
    "email": "austinlovesworking@gmail.com"
  },
  "license": "MIT",
  "pnpm": {
    "onlyBuiltDependencies": [
      "electron"
    ]
  },
  "packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
  "devDependencies": {
    "@electron-forge/cli": "^7.8.0",
    "@electron-forge/maker-deb": "^7.8.0",
    "@electron-forge/maker-dmg": "^7.8.0",
    "@electron-forge/maker-rpm": "^7.8.0",
    "@electron-forge/maker-squirrel": "^7.8.0",
    "@electron-forge/maker-zip": "^7.8.0",
    "@electron-forge/plugin-auto-unpack-natives": "^7.8.0",
    "@electron-forge/plugin-fuses": "^7.8.0",
    "@electron-forge/plugin-vite": "^7.8.0",
    "@electron-forge/publisher-github": "^7.8.0",
    "@electron/fuses": "^1.8.0",
    "@types/express": "^5.0.1",
    "@types/react": "^19.0.12",
    "@types/react-dom": "^19.0.4",
    "@types/socket.io": "^3.0.2",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0",
    "@vitejs/plugin-react": "^4.3.4",
    "autoprefixer": "^10.4.16",
    "dotenv": "^16.4.7",
    "electron": "35.1.2",
    "eslint": "^8.0.1",
    "eslint-plugin-import": "^2.25.0",
    "postcss": "^8.4.31",
    "tailwindcss": "^3.3.5",
    "ts-node": "^10.0.0",
    "typescript": "~4.5.4",
    "vite": "^5.0.12"
  },
  "dependencies": {
    "@tanstack/query-devtools": "file:tanstack-query-devtools-5.74.7.tgz",
    "@tanstack/react-query": "^5.75.7",
    "@tanstack/react-query-devtools": "file:tanstack-react-query-devtools-5.75.7.tgz",
    "bufferutil": "^4.0.9",
    "electron-log": "^5.3.3",
    "electron-squirrel-startup": "^1.0.1",
    "electron-updater": "^6.6.2",
    "express": "^4.21.2",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-use": "^17.6.0",
    "socket.io": "^4.8.1",
    "socket.io-client": "^4.8.1",
    "utf-8-validate": "^6.0.5",
    "zustand": "^5.0.3"
  },
  "build": {
    "appId": "com.lovesworking.rn-dev-tools",
    "productName": "React Native DevTools",
    "mac": {
      "category": "public.app-category.developer-tools",
      "target": "zip"
    },
    "publish": {
      "provider": "github",
      "owner": "lovesworking",
      "repo": "rn-better-dev-tools"
    }
  }
}


================================================
FILE: postcss.config.js
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};


================================================
FILE: release-version.sh
================================================
#!/bin/bash

# Exit on any error
set -e

# Colors for better output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Function to display status messages
function echo_status() {
  echo -e "${BLUE}[INFO]${NC} $1"
}

# Function to display success messages
function echo_success() {
  echo -e "${GREEN}[SUCCESS]${NC} $1"
}

# Function to display error messages
function echo_error() {
  echo -e "${RED}[ERROR]${NC} $1"
}

# Function to display warning messages
function echo_warning() {
  echo -e "${YELLOW}[WARNING]${NC} $1"
}

# Check if gh CLI is installed
if ! command -v gh &> /dev/null; then
  echo_error "GitHub CLI (gh) is not installed. Please install it first."
  echo "You can install it with: brew install gh"
  exit 1
fi

# Check if gh is authenticated
if ! gh auth status &> /dev/null; then
  echo_error "You're not authenticated with GitHub CLI."
  echo "Please run: gh auth login"
  exit 1
fi

# Ensure we're on the main branch
current_branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$current_branch" != "main" ]; then
  echo_warning "You are not on the main branch. Switching to main..."
  git checkout main
fi

# Make sure the working directory is clean
if ! git diff-index --quiet HEAD --; then
  echo_error "Your working directory has uncommitted changes."
  echo "Please commit or stash them before running this script."
  exit 1
fi

# Pull latest changes
echo_status "Pulling latest changes from main..."
git pull origin main

# Get current version from package.json
current_version=$(node -e "console.log(require('./package.json').version)")
echo_status "Current version: ${current_version}"

# Prompt for version bump type
echo "Select version bump type:"
echo "1) Patch (1.0.0 -> 1.0.1) - For bug fixes"
echo "2) Minor (1.0.0 -> 1.1.0) - For new features"
echo "3) Major (1.0.0 -> 2.0.0) - For breaking changes"
echo "4) Custom (Enter a specific version)"

read -p "Enter your choice (1-4): " version_choice

case $version_choice in
  1)
    bump_type="patch"
    ;;
  2)
    bump_type="minor"
    ;;
  3)
    bump_type="major"
    ;;
  4)
    read -p "Enter the new version (e.g., 1.2.3): " custom_version
    bump_type="custom"
    new_version=$custom_version
    ;;
  *)
    echo_error "Invalid choice. Exiting."
    exit 1
    ;;
esac

# Calculate new version for non-custom bumps
if [ "$bump_type" != "custom" ]; then
  # Split version by dots
  IFS='.' read -ra VERSION_PARTS <<< "$current_version"
  
  major=${VERSION_PARTS[0]}
  minor=${VERSION_PARTS[1]}
  patch=${VERSION_PARTS[2]}
  
  case $bump_type in
    patch)
      patch=$((patch + 1))
      ;;
    minor)
      minor=$((minor + 1))
      patch=0
      ;;
    major)
      major=$((major + 1))
      minor=0
      patch=0
      ;;
  esac
  
  new_version="${major}.${minor}.${patch}"
fi

echo_status "New version will be: ${new_version}"

# Prompt for confirmation
read -p "Proceed with release? (y/n): " confirm
if [[ $confirm != "y" && $confirm != "Y" ]]; then
  echo_warning "Release canceled."
  exit 0
fi

# Update version in package.json
echo_status "Updating package.json version to ${new_version}..."
# Use a temporary file to avoid issues with inline editing
node -e "
const fs = require('fs');
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
packageJson.version = '${new_version}';
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2) + '\n');
"

# Prompt for release notes
echo "Enter release notes (or leave empty for default message):"
echo "Type your notes and press Ctrl+D when finished (or Ctrl+C to cancel)"
release_notes=$(cat)

if [ -z "$release_notes" ]; then
  release_notes="Version ${new_version}"
fi

# Commit the version change
echo_status "Committing version change..."
git add package.json
git commit -m "Bump version to ${new_version}"

# Push the changes
echo_status "Pushing changes to main..."
git push origin main

# Create and push tag
tag_name="v${new_version}"
echo_status "Creating and pushing tag ${tag_name}..."
git tag $tag_name
git push origin $tag_name

echo_success "Version ${new_version} has been released!"
echo "GitHub Actions should now be building and publishing your release."

# Monitor the GitHub Actions workflow
echo_status "Monitoring workflow run..."
sleep 3  # Give GitHub a moment to register the workflow

# Find the latest run ID for the tag we just pushed
echo_status "Getting the latest workflow run ID..."
run_id=$(gh run list --workflow "Build and Release macOS App" --limit 1 --json databaseId --jq ".[0].databaseId")

if [ -z "$run_id" ]; then
  echo_warning "Could not find the workflow run. Please check GitHub Actions manually."
else
  echo_status "Workflow run ID: ${run_id}"
  echo_status "Watching workflow run (press Ctrl+C to stop watching)..."
  gh run watch $run_id

  # Check the status of the run
  run_status=$(gh run view $run_id --json conclusion --jq ".conclusion")
  
  if [ "$run_status" == "success" ]; then
    echo_success "Workflow completed successfully!"
    
    # Update the release notes
    echo_status "Updating release notes..."
    gh release edit $tag_name --title "Version ${new_version}" --notes "$release_notes"
    
    # Publish the release (remove the draft status)
    echo_status "Publishing release..."
    gh release edit $tag_name --draft=false
    
    echo_success "Release v${new_version} has been published!"
    echo "URL: https://github.com/lovesworking/rn-better-dev-tools/releases/tag/${tag_name}"
  else
    echo_error "Workflow failed or was canceled. Please check GitHub Actions."
    echo "URL: https://github.com/lovesworking/rn-better-dev-tools/actions"
  fi
fi 

================================================
FILE: socket-client.js
================================================
/**
 * Socket.IO Test Client
 *
 * This simple client connects to the Socket.IO server and allows
 * sending messages from the command line.
 */

// We need to use CommonJS require for this standalone script
const { io } = require("socket.io-client");

// Import config values from CommonJS config file
const { CLIENT_URL } = require("./config.js");

console.log("Starting Socket.IO test client...");
console.log(`Connecting to: ${CLIENT_URL}`);

// Connect to the Socket.IO server
const socket = io(CLIENT_URL);

// Event handler for successful connection
socket.on("connect", () => {
  console.log("✅ Connected to Socket.IO server");
  console.log("Socket ID:", socket.id);

  // Send a test message after connection
  setTimeout(() => {
    sendMessage("Hello from test client!");
  }, 1000);
});

// Event handler for disconnection
socket.on("disconnect", () => {
  console.log("❌ Disconnected from Socket.IO server");
});

// Event handler for connection errors
socket.on("connect_error", (err) => {
  console.error("❌ Connection error:", err.message);
});

// Event handler for receiving messages
socket.on("message", (data) => {
  console.log(`📨 Received: "${data}"`);
});

// Add handler for query-action events
socket.on("query-action", (data) => {
  console.log(`📨 Received query-action:`, data);
});

// Function to send a message
function sendMessage(message) {
  socket.emit("message", message);
  console.log(`📤 Sent: "${message}"`);
}

// Allow sending messages from the command line
process.stdin.on("data", (data) => {
  const message = data.toString().trim();
  if (message) {
    sendMessage(message);
  }
});

console.log("Type messages and press Enter to send. Press Ctrl+C to exit.");


================================================
FILE: src/auto-updater.ts
================================================
import { autoUpdater, UpdateInfo, ProgressInfo } from "electron-updater";
import log from "electron-log";

// Configure logger
log.transports.file.level = "info";
autoUpdater.logger = log;

export function setupAutoUpdater() {
  // Check for updates on startup
  autoUpdater.checkForUpdatesAndNotify();

  // Set up auto updater events
  autoUpdater.on("checking-for-update", () => {
    log.info("Checking for update...");
  });

  autoUpdater.on("update-available", (info: UpdateInfo) => {
    log.info("Update available:", info);
  });

  autoUpdater.on("update-not-available", (info: UpdateInfo) => {
    log.info("Update not available:", info);
  });

  autoUpdater.on("error", (err: Error) => {
    log.error("Error in auto-updater:", err);
  });

  autoUpdater.on("download-progress", (progressObj: ProgressInfo) => {
    let logMessage = `Download speed: ${progressObj.bytesPerSecond}`;
    logMessage = `${logMessage} - Downloaded ${progressObj.percent}%`;
    logMessage = `${logMessage} (${progressObj.transferred}/${progressObj.total})`;
    log.info(logMessage);
  });

  autoUpdater.on("update-downloaded", (info: UpdateInfo) => {
    log.info("Update downloaded:", info);
    // Install the update when the app is quit
    // Alternatively, you could prompt the user here
  });

  // Check for updates periodically
  const CHECK_INTERVAL = 1000 * 60 * 60; // Check every hour
  setInterval(() => {
    autoUpdater.checkForUpdatesAndNotify();
  }, CHECK_INTERVAL);
}


================================================
FILE: src/components/App.tsx
================================================
import Providers from "./external-dash/providers";
import Main from "./external-dash/Main";
export const App: React.FC = () => {
  return (
    <Providers>
      <Main />
    </Providers>
  );
};


================================================
FILE: src/components/external-dash/Dash.tsx
================================================
import React, { useEffect, useState } from "react";
import { User } from "./types/User";
import "../../index.css";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools/production";
import { useLogStore } from "./utils/logStore";
import { onDevToolsEvent } from "./utils/devToolsEvents";
import { useDevToolsEventHandler } from "./hooks/useDevToolsEventHandler";

import { DeviceSelection } from "./DeviceSelection";
import { UserInfo } from "./UserInfo";
import { LogConsole } from "./LogConsole";
import { NoDevicesConnected } from "./NoDevicesConnected";
import { StorageControlsSection } from "./UserInfo/StorageControlsSection";

export const PlatformIcon: React.FC<{ platform: string }> = ({ platform }) => {
  const normalizedPlatform = platform?.toLowerCase() || "";

  switch (normalizedPlatform) {
    case "ios":
      return (
        <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
          <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
        </svg>
      );
    case "android":
      return (
        <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
          <path d="M16.61 15.15c-.46 0-.84-.37-.84-.83s.37-.83.84-.83.83.37.83.83-.37.83-.83.83m-9.22 0c-.46 0-.83-.37-.83-.83s.37-.83.83-.83.84.37.84.83-.37.83-.84.83m9.42-5.89l1.67-2.89c.09-.17.03-.38-.13-.47-.17-.09-.38-.03-.47.13l-1.69 2.93A9.973 9.973 0 0012 7.75c-1.89 0-3.63.52-5.19 1.37L5.12 6.19c-.09-.17-.3-.22-.47-.13-.17.09-.22.3-.13.47l1.67 2.89C3.44 11.15 1.62 14.56 1.62 18h20.76c0-3.44-1.82-6.85-4.57-8.74z" />
        </svg>
      );
    case "web":
      return (
        <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
          <path d="M16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2m-5.15 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 01-4.33 3.56M14.34 14H9.66c-.1-.66-.16-1.32-.16-2 0-.68.06-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2M12 19.96c-.83-1.2-1.5-2.53-1.91-3.96h3.82c-.41 1.43-1.08 2.76-1.91 3.96M8 8H5.08A7.923 7.923 0 019.4 4.44C8.8 5.55 8.35 6.75 8 8m-2.92 8H8c.35 1.25.8 2.45 1.4 3.56A8.008 8.008 0 015.08 16m-.82-2C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2M12 4.03c.83 1.2 1.5 2.54 1.91 3.97h-3.82c.41-1.43 1.08-2.77 1.91-3.97M18.92 8h-2.95a15.65 15.65 0 00-1.38-3.56c1.84.63 3.37 1.9 4.33 3.56M12 2C6.47 2 2 6.5 2 12a10 10 0 0010 10 10 10 0 0010-10A10 10 0 0012 2z" />
        </svg>
      );
    case "tv":
    case "tvos":
      return (
        <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
          <path d="M21 17H3V5h18m0-2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
        </svg>
      );
    default:
      return (
        <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
          <path d="M17.25 18H6.75V4h10.5M14 21h-4v-1h4m2-19H8C6.34 1 5 2.34 5 4v16c0 1.66 1.34 3 3 3h8c1.66 0 3-1.34 3-3V4c0-1.66-1.34-3-3-3z" />
        </svg>
      );
  }
};

export const getPlatformColor = (platform: string): string => {
  const normalizedPlatform = platform?.toLowerCase() || "";
  switch (normalizedPlatform) {
    case "ios":
      return "text-gray-100";
    case "android":
      return "text-green-300";
    case "web":
      return "text-blue-300";
    case "tv":
    case "tvos":
      return "text-purple-300";
    default:
      return "text-gray-300";
  }
};

export const getPlatformBgColor = (platform: string): string => {
  const normalizedPlatform = platform?.toLowerCase() || "";
  switch (normalizedPlatform) {
    case "ios":
      return "bg-blue-900/30 text-blue-200";
    case "android":
      return "bg-green-900/30 text-green-200";
    case "web":
      return "bg-cyan-900/30 text-cyan-200";
    case "tv":
    case "tvos":
      return "bg-purple-900/30 text-purple-200";
    default:
      return "bg-gray-800/60 text-gray-300";
  }
};

interface DashProps {
  allDevices: User[];
  isDashboardConnected: boolean;
  targetDevice: User;
  setTargetDevice: (device: User) => void;
}

export const Dash: React.FC<DashProps> = ({
  isDashboardConnected,
  allDevices,
  targetDevice,
  setTargetDevice,
}) => {
  const [showOfflineDevices, setShowOfflineDevices] = useState(true);
  const { isEnabled, setIsEnabled, isVisible, setIsVisible } = useLogStore();

  const filteredDevices = showOfflineDevices
    ? allDevices
    : allDevices.filter((device) => {
        if (typeof device === "string") {
          return false;
        }
        return device.isConnected;
      });

  // Find the target device
  useEffect(() => {
    const foundDevice = filteredDevices?.find((device) => {
      return device.deviceId === targetDevice.deviceId;
    });
    foundDevice && setTargetDevice(foundDevice);
  }, [setTargetDevice, filteredDevices, targetDevice]);

  useDevToolsEventHandler();

  return (
    <div className="font-sf-pro">
      <div className="flex flex-col w-full h-screen overflow-hidden bg-[#0A0A0C] text-white">
        <header className="w-full px-5 py-4 border-b border-[#2D2D2F]/50 flex justify-between items-center flex-shrink-0 bg-[#0A0A0C]/95 sticky top-0 z-20 shadow-[0_0.5rem_1rem_rgba(0,0,0,0.2)]">
          <div className="flex items-center gap-4">
            {/* Connection Status */}
            <div
              className={`flex items-center gap-2 px-3 py-1.5 rounded-full  ${
                isDashboardConnected
                  ? "bg-gradient-to-r from-green-500/10 to-green-500/5 border border-green-500/20 shadow-[0_0_10px_rgba(74,222,128,0.1)]"
                  : "bg-gradient-to-r from-red-500/10 to-red-500/5 border border-red-500/20 shadow-[0_0_10px_rgba(248,113,113,0.1)]"
              }`}
            >
              <div className="relative flex items-center justify-center">
                <div
                  className={`w-2 h-2 rounded-full ${
                    isDashboardConnected ? "bg-green-400" : "bg-red-400"
                  }`}
                >
                  {/* Simple dot with no animations */}
                </div>
              </div>
              <span
                className={`text-xs font-medium tracking-wide ${
                  isDashboardConnected ? "text-green-300" : "text-red-300"
                }`}
              >
                {isDashboardConnected ? "Connected" : "Disconnected"}
              </span>
            </div>
          </div>

          {/* Right Section */}
          <div className="flex items-center gap-5">
            {/* Offline Toggle */}
            <div className="flex items-center gap-4">
              <button
                onClick={() => setShowOfflineDevices(!showOfflineDevices)}
                className="group flex items-center gap-3 px-3 py-1.5 rounded-full transition-all duration-500 ease-out hover:bg-[#1D1D1F]/40 border border-transparent hover:border-[#2D2D2F]/70"
                aria-pressed={showOfflineDevices}
                role="switch"
              >
                <div
                  className={`w-8 h-4 rounded-full flex items-center px-0.5 transition-all duration-500 ease-out ${
                    showOfflineDevices ? "bg-blue-500" : "bg-[#3D3D3F]"
                  }`}
                >
                  <div
                    className={`w-3 h-3 rounded-full bg-white shadow-md transform transition-all duration-500 ease-out ${
                      showOfflineDevices ? "translate-x-4" : "translate-x-0"
                    }`}
                  />
                </div>
                <span className="text-xs font-medium text-[#A1A1A6] group-hover:text-[#F5F5F7] transition-colors duration-300">
                  Show Offline Devices
                </span>
              </button>

              {/* Logs Toggle */}
              <button
                onClick={() => setIsEnabled(!isEnabled)}
                className="group flex items-center gap-3 px-3 py-1.5 rounded-full transition-all duration-500 ease-out hover:bg-[#1D1D1F]/40 border border-transparent hover:border-[#2D2D2F]/70"
                aria-pressed={isEnabled}
                role="switch"
              >
                <div
                  className={`w-8 h-4 rounded-full flex items-center px-0.5 transition-all duration-500 ease-out ${
                    isEnabled ? "bg-blue-500" : "bg-[#3D3D3F]"
                  }`}
                >
                  <div
                    className={`w-3 h-3 rounded-full bg-white shadow-md transform transition-all duration-500 ease-out ${
                      isEnabled ? "translate-x-4" : "translate-x-0"
                    }`}
                  />
                </div>
                <span className="text-xs font-medium text-[#A1A1A6] group-hover:text-[#F5F5F7] transition-colors duration-300">
                  Logs
                </span>
              </button>

              {/* Storage Controls */}
              <StorageControlsSection />
            </div>

            {/* Separator */}
            <div className="h-5 w-px bg-[#2D2D2F]/70"></div>

            {/* Device Selection */}
            <div className="flex-shrink-0">
              <DeviceSelection
                selectedDevice={targetDevice}
                setSelectedDevice={setTargetDevice}
                allDevices={filteredDevices}
              />
            </div>
          </div>
        </header>

        <main className="flex-1 overflow-y-auto p-6 pb-72 bg-gradient-to-b from-[#0A0A0C] to-[#121214]">
          <div className="px-2 max-w-3xl mx-auto">
            {/* Device count and stats */}
            {filteredDevices.length > 0 ? (
              <>
                <div className="mb-6 bg-[#1A1A1C] border border-[#2D2D2F]/60 rounded-2xl p-5 shadow-[0_0.5rem_1.5rem_rgba(0,0,0,0.25)] transition-all duration-500 ease-out hover:shadow-[0_0.5rem_2rem_rgba(0,0,0,0.35)]">
                  <div className="flex items-center justify-between">
                    <div className="flex items-center gap-3">
                      <div className="text-sm flex items-center gap-2 px-3.5 py-1.5 bg-[#0A0A0C]/80 rounded-full border border-[#2D2D2F]/50 shadow-inner">
                        <span className="text-blue-300 font-mono bg-blue-500/10 px-2 py-0.5 rounded-md border border-blue-500/20 text-xs">
                          {filteredDevices.length}
                        </span>
                        <span className="text-white font-medium">
                          {filteredDevices.length === 1 ? "device" : "devices"}
                        </span>
                      </div>

                      <div className="flex items-center gap-2 text-sm">
                        <div className="flex items-center gap-2 px-3.5 py-1.5 bg-green-900/20 rounded-full border border-green-900/30 shadow-inner">
                          <div className="w-1.5 h-1.5 rounded-full bg-green-500"></div>
                          <span className="text-green-300 font-medium">
                            {allDevices.filter((d) => d.isConnected).length}{" "}
                            online
                          </span>
                        </div>
                        <div className="flex items-center gap-2 px-3.5 py-1.5 bg-red-900/20 rounded-full border border-red-900/30 shadow-inner">
                          <div className="w-1.5 h-1.5 rounded-full bg-red-500"></div>
                          <span className="text-red-300 font-medium">
                            {allDevices.filter((d) => !d.isConnected).length}{" "}
                            offline
                          </span>
                        </div>
                      </div>
                    </div>

                    {targetDevice && (
                      <div className="flex items-center gap-3 px-4 py-2 bg-[#0A0A0C]/60 rounded-full border border-[#2D2D2F]/50 shadow-sm ">
                        <div className="flex items-center gap-2">
                          <div
                            className={`flex items-center gap-2 ${
                              targetDevice.deviceId === "All"
                                ? "text-blue-300"
                                : targetDevice.isConnected
                                ? "text-green-300"
                                : "text-red-300"
                            }`}
                          >
                            <div className="w-1.5 h-1.5 rounded-full bg-current"></div>
                            <span className="text-xs font-medium uppercase tracking-wider text-[#A1A1A6]">
                              Target
                            </span>
                          </div>
                          <span className="text-sm font-medium text-white">
                            {targetDevice.deviceId === "All"
                              ? "All Devices"
                              : targetDevice.deviceName}
                          </span>
                        </div>
                        {targetDevice.deviceId !== "All" &&
                          targetDevice.platform && (
                            <div
                              className={`flex items-center ${getPlatformColor(
                                targetDevice.platform
                              )}`}
                            >
                              <PlatformIcon platform={targetDevice.platform} />
                            </div>
                          )}
                      </div>
                    )}
                  </div>
                </div>

                {/* Device List */}
                <div className="space-y-5 transition-all duration-300">
                  {filteredDevices.map((device) => (
                    <UserInfo
                      key={device.id}
                      userData={device}
                      isTargeted={
                        targetDevice.deviceId === "All" ||
                        targetDevice.deviceId === device.deviceId
                      }
                    />
                  ))}
                </div>
              </>
            ) : (
              <NoDevicesConnected />
            )}
          </div>
        </main>

        {/* Console Panel */}
        <div className="fixed bottom-4 right-4 z-40">
          {isVisible ? (
            <div className="fixed inset-x-0 bottom-0 z-40 bg-[#0A0A0C] border-t border-[#2D2D2F]/50 shadow-2xl transition-all duration-500 ease-out transform animate-slideUpFade">
              <LogConsole
                onClose={() => setIsVisible(false)}
                allDevices={allDevices}
              />
            </div>
          ) : (
            <button
              onClick={() => setIsVisible(true)}
              className="flex items-center justify-center w-12 h-12 bg-[#1A1A1C] hover:bg-[#2D2D2F] text-[#F5F5F7] rounded-full shadow-[0_0.5rem_1rem_rgba(0,0,0,0.2)] border border-[#2D2D2F]/50 transition-all duration-500 ease-out hover:scale-110 hover:shadow-[0_0.5rem_1.5rem_rgba(59,130,246,0.2)]"
            >
              <svg
                className="w-5 h-5"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="2"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
                />
              </svg>
            </button>
          )}
        </div>

        <div className="fixed bottom-20 right-4 z-50">
          <ReactQueryDevtools
            initialIsOpen={false}
            position="bottom"
            buttonPosition="relative"
          />
        </div>
      </div>
    </div>
  );
};


================================================
FILE: src/components/external-dash/DeviceSelection.tsx
================================================
import React, { useEffect, useState, useRef } from "react";
import { User } from "./types/User";
import { PlatformIcon } from "./utils/platformUtils";

interface Props {
  selectedDevice: User;
  setSelectedDevice: (device: User) => void;
  allDevices?: User[];
}

interface DeviceOption {
  value: string;
  label: string;
  isOffline?: boolean;
  platform?: string;
}

export const DeviceSelection: React.FC<Props> = ({
  selectedDevice,
  setSelectedDevice,
  allDevices = [],
}: Props) => {
  const [isOpen, setIsOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  // Close dropdown when clicking outside
  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      if (ref.current && !ref.current.contains(event.target as Node)) {
        setIsOpen(false);
      }
    };

    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, []);

  // Generate options
  const generateOptions = (): DeviceOption[] => {
    if (allDevices.length === 0) {
      return [
        {
          value: "All",
          label: "No Devices",
        },
      ];
    }

    if (allDevices.length === 1) {
      const device = allDevices[0];
      return [
        {
          value: device.deviceId,
          label: device.deviceName,
          isOffline: !device.isConnected,
          platform: device.platform,
        },
      ];
    }

    // Multiple devices - add 'All' option
    return [
      {
        value: "All",
        label: "All Devices",
      },
      ...allDevices.map((device) => ({
        value: device.deviceId,
        label: device.deviceName,
        isOffline: !device.isConnected,
        platform: device.platform,
      })),
    ];
  };

  const deviceOptions = generateOptions();

  // If there are no devices, select "All"
  useEffect(() => {
    if (!selectedDevice || !selectedDevice.deviceId) {
      if (deviceOptions.length > 0) {
        const foundDevice = allDevices.find(
          (device) => device.deviceId === "All"
        );
        if (foundDevice) {
          setSelectedDevice(foundDevice);
        }
      }
    }

    // If the selected device is no longer in the device list, select "All"
    if (
      selectedDevice &&
      selectedDevice.deviceId !== "All" &&
      !allDevices.find((device) => device.deviceId === selectedDevice.deviceId)
    ) {
      const foundDevice = allDevices.find(
        (device) => device.deviceId === "All"
      );
      if (foundDevice) {
        setSelectedDevice(foundDevice);
      }
    }
  }, [allDevices, selectedDevice, setSelectedDevice, deviceOptions]);

  // Handle device selection
  const handleSelect = (deviceId: string) => {
    if (deviceId === "All") {
      setSelectedDevice({
        id: "all-devices",
        deviceId: "All",
        deviceName: "All Devices",
        isConnected: true,
        platform: undefined,
      });
    } else {
      const device = allDevices.find((d) => d.deviceId === deviceId);
      if (device) {
        setSelectedDevice(device);
      }
    }
    setIsOpen(false);
  };

  // Get selected device label
  const getSelectedDeviceLabel = (): string => {
    if (!selectedDevice || !selectedDevice.deviceId) {
      return deviceOptions[0]?.label || "All Devices";
    }

    return selectedDevice.deviceName || "Unknown Device";
  };

  const StatusDot: React.FC<{ isOffline?: boolean }> = ({ isOffline }) => (
    <span
      className={`w-1.5 h-1.5 rounded-full ${
        isOffline ? "bg-red-500" : "bg-green-500"
      }`}
    />
  );

  return (
    <div ref={ref} className="relative w-48 font-sf-pro">
      <button
        type="button"
        onClick={() => setIsOpen(!isOpen)}
        className={`flex items-center justify-between w-full px-4 py-2.5 text-sm font-medium transition-all duration-300 
        ${
          isOpen
            ? "text-white bg-[#2D2D2F] border-blue-500/40 ring-2 ring-blue-500/10 shadow-[0_0_12px_rgba(59,130,246,0.15)]"
            : "text-white bg-[#1A1A1C] hover:bg-[#2D2D2F] border-[#2D2D2F]"
        } border rounded-xl`}
        aria-haspopup="listbox"
        aria-expanded={isOpen}
      >
        <div className="flex items-center gap-2 truncate">
          {selectedDevice?.platform && selectedDevice.deviceId !== "All" ? (
            <>
              <StatusDot isOffline={!selectedDevice.isConnected} />
              <span className="flex items-center justify-center w-5 h-5 rounded-full bg-[#0A0A0C]">
                <PlatformIcon
                  platform={selectedDevice.platform}
                  className="w-3 h-3"
                />
              </span>
            </>
          ) : (
            <>
              <span className="w-1.5" /> {/* Spacer for alignment */}
              <span className="flex items-center justify-center w-5 h-5 rounded-full bg-[#0A0A0C]">
                <svg
                  className="w-3 h-3 text-blue-400"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
                  />
                </svg>
              </span>
            </>
          )}
          <span className="truncate">{getSelectedDeviceLabel()}</span>
        </div>
        <svg
          className={`w-4 h-4 ml-2 transition-transform duration-300 ${
            isOpen ? "rotate-180 text-blue-400" : "text-[#A1A1A6]"
          }`}
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M19 9l-7 7-7-7"
          />
        </svg>
      </button>

      {isOpen && (
        <div className="absolute z-50 w-full mt-2 animate-scaleIn">
          <div className="py-1 overflow-hidden bg-[#1A1A1C] rounded-xl shadow-lg border border-[#2D2D2F] ring-1 ring-black/5 shadow-[0_0.5rem_1rem_rgba(0,0,0,0.3)]">
            {/* Sticky header for All Devices */}
            {deviceOptions.length > 1 && deviceOptions[0].value === "All" && (
              <div className="sticky top-0 z-10 bg-[#1A1A1C] border-b border-[#2D2D2F]/70">
                <button
                  type="button"
                  className={`flex items-center w-full px-4 py-3 text-sm font-medium border-b border-[#2D2D2F] transition-colors duration-300 ${
                    selectedDevice?.deviceId === "All"
                      ? "bg-blue-900/20 text-blue-300"
                      : "text-white hover:bg-[#2D2D2F]"
                  }`}
                  onClick={() => handleSelect("All")}
                >
                  <span className="w-1.5" /> {/* Spacer for alignment */}
                  <span className="flex items-center justify-center w-5 h-5 mr-2 rounded-full bg-[#0A0A0C]">
                    <svg
                      className="w-3 h-3 text-blue-400"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="2"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
                      />
                    </svg>
                  </span>
                  All Devices
                  {selectedDevice?.deviceId === "All" && (
                    <svg
                      className="w-4 h-4 ml-auto text-blue-400"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="2"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        d="M5 13l4 4L19 7"
                      />
                    </svg>
                  )}
                </button>
              </div>
            )}

            {/* Individual devices */}
            <div className="max-h-56 overflow-y-auto p-1">
              {deviceOptions
                .filter((option) => option.value !== "All")
                .map((option, index) => (
                  <button
                    key={option.value}
                    type="button"
                    className={`flex items-center w-full px-3 py-2 text-sm rounded-lg my-0.5 transition-all duration-300 ${
                      selectedDevice?.deviceId === option.value
                        ? "bg-blue-500/10 text-blue-300 ring-1 ring-blue-500/20"
                        : option.isOffline
                        ? "text-gray-400 hover:bg-[#2D2D2F]/50"
                        : "text-white hover:bg-[#2D2D2F]/70"
                    }`}
                    onClick={() => handleSelect(option.value)}
                    style={{ animationDelay: `${index * 30}ms` }}
                  >
                    <div className="flex items-center gap-2 truncate">
                      <StatusDot isOffline={option.isOffline} />
                      {option.platform ? (
                        <span className="flex items-center justify-center w-5 h-5 rounded-full bg-[#0A0A0C]">
                          <PlatformIcon
                            platform={option.platform}
                            className="w-3 h-3"
                          />
                        </span>
                      ) : (
                        <span className="flex items-center justify-center w-5 h-5 rounded-full bg-[#0A0A0C]">
                          <svg
                            className="w-3 h-3 text-gray-400"
                            viewBox="0 0 24 24"
                            fill="none"
                            stroke="currentColor"
                            strokeWidth="2"
                          >
                            <path
                              strokeLinecap="round"
                              strokeLinejoin="round"
                              d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
                            />
                          </svg>
                        </span>
                      )}
                      <span className="truncate">{option.label}</span>

                      {/* Selected checkmark */}
                      {selectedDevice?.deviceId === option.value && (
                        <svg
                          className="w-4 h-4 ml-auto text-blue-400"
                          viewBox="0 0 24 24"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth="2"
                        >
                          <path
                            strokeLinecap="round"
                            strokeLinejoin="round"
                            d="M5 13l4 4L19 7"
                          />
                        </svg>
                      )}
                    </div>
                  </button>
                ))}
            </div>
          </div>
        </div>
      )}
    </div>
  );
};


================================================
FILE: src/components/external-dash/LogConsole.tsx
================================================
import React, { useEffect, useRef, useState } from "react";
import { useLogStore, LogEntry, LogLevel } from "./utils/logStore";
import { PlatformIcon } from "./utils/platformUtils";
import { User } from "./types/User";

// Get log level color
const getLogLevelColor = (level: LogLevel): string => {
  switch (level) {
    case "error":
      return "text-red-400";
    case "warn":
      return "text-yellow-400";
    case "debug":
      return "text-purple-400";
    case "info":
    default:
      return "text-blue-400";
  }
};

const formatTimestamp = (date: Date): string => {
  return date.toLocaleTimeString([], {
    hour12: false,
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  });
};

interface LogEntryItemProps {
  log: LogEntry;
}

const LogEntryItem: React.FC<LogEntryItemProps> = ({ log }) => {
  return (
    <div className="py-1 font-mono text-xs border-b border-gray-800 hover:bg-gray-800/30 transition-colors">
      <div className="flex items-start gap-2">
        {/* Timestamp */}
        <span className="text-gray-500 whitespace-nowrap min-w-[5rem] pl-2">
          {formatTimestamp(log.timestamp)}
        </span>

        {/* Log level */}
        <span
          className={`uppercase font-bold min-w-[3rem] ${getLogLevelColor(
            log.level
          )}`}
        >
          {log.level}
        </span>

        {/* Device info if available */}
        {log.platform && (
          <span className="flex items-center gap-1 text-gray-400 min-w-[8rem]">
            <PlatformIcon platform={log.platform} className="w-3 h-3" />
            <span className="truncate max-w-[7rem]">
              {log.deviceName || "Unknown"}
            </span>
          </span>
        )}

        {/* Log message */}
        <span className="text-gray-300 break-words flex-1">{log.message}</span>
      </div>
    </div>
  );
};

interface DeviceOption {
  value: string;
  label: string;
  disabled?: boolean;
  isOffline?: boolean;
  platform?: string;
}

interface LogConsoleProps {
  onClose: () => void;
  allDevices: User[];
}

export const LogConsole: React.FC<LogConsoleProps> = ({
  onClose,
  allDevices,
}) => {
  const logs = useLogStore((state: { logs: LogEntry[] }) => state.logs);
  const clearLogs = useLogStore(
    (state: { clearLogs: () => void }) => state.clearLogs
  );
  const [filter, setFilter] = useState<LogLevel | "all">("all");
  const [deviceFilter, setDeviceFilter] = useState<string>("all");

  // Auto-scroll functionality
  const scrollRef = useRef<HTMLDivElement>(null);
  const [autoScroll, setAutoScroll] = useState(true);

  // Resizable functionality
  const [height, setHeight] = useState(320); // Default height in pixels
  const resizableRef = useRef<HTMLDivElement>(null);
  const [isDragging, setIsDragging] = useState(false);

  // Calculate max height based on window height
  const calculateMaxHeight = () => {
    // Leave space for the header (approximately 64px) and some padding
    return window.innerHeight - 80;
  };

  const handleResizeStart = (e: React.MouseEvent) => {
    e.preventDefault(); // Prevent text selection
    if (e.button !== 0) return; // Only handle left mouse button

    setIsDragging(true);
    const startY = e.clientY;
    const startHeight = height;

    const handleResizeMove = (e: MouseEvent) => {
      const deltaY = startY - e.clientY;
      const maxHeight = calculateMaxHeight();
      const newHeight = Math.max(
        200,
        Math.min(maxHeight, startHeight + deltaY)
      );
      setHeight(newHeight);
    };

    const handleResizeEnd = () => {
      setIsDragging(false);
      document.removeEventListener("mousemove", handleResizeMove);
      document.removeEventListener("mouseup", handleResizeEnd);
      document.body.style.cursor = "default";
    };

    document.addEventListener("mousemove", handleResizeMove);
    document.addEventListener("mouseup", handleResizeEnd);
    document.body.style.cursor = "ns-resize";
  };

  // Update max height on window resize
  useEffect(() => {
    const handleResize = () => {
      const maxHeight = calculateMaxHeight();
      if (height > maxHeight) {
        setHeight(maxHeight);
      }
    };

    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, [height]);

  useEffect(() => {
    if (autoScroll && scrollRef.current) {
      scrollRef.current.scrollTop = 0;
    }
  }, [logs, autoScroll]);

  // Reset device filter if selected device is no longer available
  useEffect(() => {
    if (deviceFilter !== "all") {
      const deviceExists = allDevices.some(
        (device) => device.deviceId === deviceFilter
      );
      if (!deviceExists) {
        setDeviceFilter("all");
      }
    }
  }, [allDevices, deviceFilter]);

  // Filter logs based on level and device
  const filteredLogs = logs.filter((log: LogEntry) => {
    const matchesLevel = filter === "all" || log.level === filter;
    const matchesDevice =
      deviceFilter === "all" || log.deviceId === deviceFilter;
    return matchesLevel && matchesDevice;
  });

  // Generate device options based on available devices
  const deviceOptions: DeviceOption[] = (() => {
    if (allDevices?.length === 0) {
      return [{ value: "all", label: "All Devices" }];
    } else if (allDevices?.length === 1) {
      // Only one device, no need for "All" option
      const device = allDevices[0];
      return [
        {
          value: device.deviceId,
          label: device.deviceName || "Unknown Device",
          isOffline: !device.isConnected,
          platform: device.platform,
        },
      ];
    } else {
      // Multiple devices, include "All" option
      return [
        { value: "all", label: "All Devices" },
        ...allDevices.map((device) => ({
          value: device.deviceId,
          label: device.deviceName || "Unknown Device",
          isOffline: !device.isConnected,
          platform: device.platform,
        })),
      ];
    }
  })();

  const [isDeviceDropdownOpen, setIsDeviceDropdownOpen] = useState(false);
  const [isLevelDropdownOpen, setIsLevelDropdownOpen] = useState(false);
  const deviceDropdownRef = useRef<HTMLDivElement>(null);
  const levelDropdownRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      if (
        deviceDropdownRef.current &&
        !deviceDropdownRef.current.contains(event.target as Node)
      ) {
        setIsDeviceDropdownOpen(false);
      }
      if (
        levelDropdownRef.current &&
        !levelDropdownRef.current.contains(event.target as Node)
      ) {
        setIsLevelDropdownOpen(false);
      }
    };

    document.addEventListener("mousedown", handleClickOutside);
    return () => document.removeEventListener("mousedown", handleClickOutside);
  }, []);

  const levelOptions = [
    { value: "all", label: "All" },
    { value: "info", label: "Info" },
    { value: "warn", label: "Warn" },
    { value: "error", label: "Error" },
    { value: "debug", label: "Debug" },
  ];

  return (
    <>
      {/* Resize handle */}
      <div
        className="h-1 bg-transparent hover:bg-gray-600/50 cursor-ns-resize relative group"
        onMouseDown={handleResizeStart}
      >
        <div className="absolute inset-x-0 h-0.5 bottom-0 bg-gray-700/50 group-hover:bg-gray-500/50" />
      </div>

      {/* Header */}
      <div className="flex items-center justify-between px-4 py-2 border-b border-gray-700/50">
        <div className="flex items-center gap-2">
          <svg
            className="w-4 h-4 text-gray-400"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
            />
          </svg>
          <span className="text-sm font-medium text-gray-200">
            Console ({filteredLogs.length} logs)
          </span>
        </div>

        <div className="flex items-center gap-4">
          {/* Device Filter */}
          <div className="flex items-center gap-2 text-xs relative">
            <span className="text-gray-400">Device:</span>
            <div className="relative" ref={deviceDropdownRef}>
              <button
                onClick={() => setIsDeviceDropdownOpen(!isDeviceDropdownOpen)}
                className="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-gray-300 min-w-[8rem] flex items-center justify-between gap-2 select-none"
              >
                <div className="flex items-center gap-2">
                  <span>
                    {
                      deviceOptions.find((opt) => opt.value === deviceFilter)
                        ?.label
                    }
                  </span>
                  {deviceFilter !== "all" && (
                    <span className="text-gray-300">
                      <PlatformIcon
                        platform={
                          allDevices.find((d) => d.deviceId === deviceFilter)
                            ?.platform || ""
                        }
                      />
                    </span>
                  )}
                </div>
                <svg
                  className="w-4 h-4"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="M19 9l-7 7-7-7"
                  />
                </svg>
              </button>
              {isDeviceDropdownOpen && (
                <div className="absolute top-full left-0 mt-1 w-full bg-gray-800 border border-gray-700 rounded-md shadow-lg z-50">
                  {deviceOptions.map((option) => (
                    <button
                      key={option.value}
                      onClick={() => {
                        setDeviceFilter(option.value);
                        setIsDeviceDropdownOpen(false);
                      }}
                      className={`w-full px-2 py-1.5 text-left flex items-center justify-between gap-2 hover:bg-gray-700/50 ${
                        deviceFilter === option.value ? "bg-gray-700/30" : ""
                      } ${
                        option.isOffline ? "text-gray-500" : "text-gray-300"
                      } select-none`}
                    >
                      <div className="flex items-center gap-2">
                        {option.label}
                        {option.platform && (
                          <span
                            className={`${
                              option.isOffline
                                ? "text-gray-500"
                                : "text-gray-300"
                            }`}
                          >
                            <PlatformIcon platform={option.platform} />
                          </span>
                        )}
                      </div>
                      {option.value !== "all" && (
                        <div
                          className={`w-1.5 h-1.5 rounded-full ${
                            option.isOffline ? "bg-red-500" : "bg-green-500"
                          }`}
                        />
                      )}
                    </button>
                  ))}
                </div>
              )}
            </div>
          </div>

          {/* Log Level Filter */}
          <div className="flex items-center gap-2 text-xs">
            <span className="text-gray-400">Level:</span>
            <div className="relative" ref={levelDropdownRef}>
              <button
                onClick={() => setIsLevelDropdownOpen(!isLevelDropdownOpen)}
                className="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-gray-300 min-w-[6rem] flex items-center justify-between gap-2 select-none"
              >
                <span>
                  {levelOptions.find((opt) => opt.value === filter)?.label}
                </span>
                <svg
                  className="w-4 h-4"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="M19 9l-7 7-7-7"
                  />
                </svg>
              </button>
              {isLevelDropdownOpen && (
                <div className="absolute top-full left-0 mt-1 w-full bg-gray-800 border border-gray-700 rounded-md shadow-lg z-50">
                  {levelOptions.map((option) => (
                    <button
                      key={option.value}
                      onClick={() => {
                        setFilter(option.value as LogLevel | "all");
                        setIsLevelDropdownOpen(false);
                      }}
                      className={`w-full px-2 py-1.5 text-left hover:bg-gray-700/50 ${
                        filter === option.value ? "bg-gray-700/30" : ""
                      } text-gray-300 select-none`}
                    >
                      {option.label}
                    </button>
                  ))}
                </div>
              )}
            </div>
          </div>

          {/* Auto-scroll toggle */}
          <div className="flex items-center gap-2 text-xs">
            <span className="text-gray-400">Auto-scroll:</span>
            <button
              onClick={() => setAutoScroll(!autoScroll)}
              className={`w-10 h-5 rounded-full flex items-center transition-colors ${
                autoScroll ? "bg-blue-600" : "bg-gray-700"
              }`}
            >
              <span
                className={`w-4 h-4 rounded-full bg-white shadow-sm transform transition-transform ${
                  autoScroll ? "translate-x-5" : "translate-x-1"
                }`}
              />
            </button>
          </div>

          {/* Clear button */}
          <button
            onClick={clearLogs}
            className="text-xs px-3 py-1 bg-red-600/20 text-red-400 rounded hover:bg-red-600/30 transition-colors"
          >
            Clear
          </button>

          {/* Close button */}
          <button
            onClick={onClose}
            className="p-1 hover:bg-gray-800 rounded-md transition-colors duration-200"
          >
            <svg
              className="w-4 h-4 text-gray-400"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth="2"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                d="M19 9l-7 7-7-7"
              />
            </svg>
          </button>
        </div>
      </div>

      {/* Log entries container with dynamic height */}
      <div
        ref={scrollRef}
        style={{ height: `${height}px` }}
        className={`overflow-y-auto flex flex-col-reverse select-text ${
          isDragging ? "pointer-events-none" : ""
        }`}
        onKeyDown={(e) => {
          if ((e.ctrlKey || e.metaKey) && e.key === "a") {
            e.preventDefault();
            const selection = window.getSelection();
            const range = document.createRange();
            range.selectNodeContents(e.currentTarget);
            selection?.removeAllRanges();
            selection?.addRange(range);
          }
        }}
        tabIndex={0}
      >
        {filteredLogs.length > 0 ? (
          filteredLogs.map((log: LogEntry) => (
            <LogEntryItem key={log.id} log={log} />
          ))
        ) : (
          <div className="flex items-center justify-center h-full text-gray-500 italic">
            No logs to display
          </div>
        )}
      </div>
    </>
  );
};


================================================
FILE: src/components/external-dash/Main.tsx
================================================
import { useState } from "react";
import useConnectedUsers from "./_hooks/useConnectedUsers";
import { useSyncQueriesWeb } from "./useSyncQueriesWeb";
import { Dash } from "./Dash";
import { User } from "./types/User";

export default function Main() {
  const [targetDevice, setTargetDevice] = useState<User>({
    deviceId: "Please select a device",
    deviceName: "Please select a device",
    isConnected: false,
    id: "Please select a device",
  });
  const { allDevices, isDashboardConnected } = useConnectedUsers();
  useSyncQueriesWeb({ targetDevice, allDevices });

  return (
    <Dash
      allDevices={allDevices}
      isDashboardConnected={isDashboardConnected}
      targetDevice={targetDevice}
      setTargetDevice={setTargetDevice}
    />
  );
}


================================================
FILE: src/components/external-dash/NoDevicesConnected.tsx
================================================
import React, { useState } from "react";

export const NoDevicesConnected = () => {
  const [copiedText, setCopiedText] = useState<string | null>(null);

  const handleCopy = (text: string, label: string) => {
    navigator.clipboard.writeText(text);
    setCopiedText(label);
    setTimeout(() => setCopiedText(null), 2000);
  };

  return (
    <>
      <div className="flex flex-col items-center justify-center min-h-[calc(100vh-10rem)] text-center">
        <div className="bg-[#1A1A1C] rounded-3xl p-8 border border-[#2D2D2F]/50 max-w-md shadow-[0_1rem_3rem_rgba(0,0,0,0.3)] transition-shadow duration-700 ease-out hover:shadow-[0_1rem_3rem_rgba(59,130,246,0.1)] hover:scale-[1.01]">
          <svg
            className="w-16 h-16 text-[#9E9EA0] mx-auto mb-6 opacity-80"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="1.5"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 002.25-2.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v2.25A2.25 2.25 0 006 10.5zm0 9.75h2.25A2.25 2.25 0 0010.5 18v-2.25a2.25 2.25 0 00-2.25-2.25H6a2.25 2.25 0 00-2.25 2.25V18A2.25 2.25 0 006 20.25zm9.75-9.75H18a2.25 2.25 0 002.25-2.25V6A2.25 2.25 0 0018 3.75h-2.25A2.25 2.25 0 0013.5 6v2.25a2.25 2.25 0 002.25 2.25z"
            />
          </svg>
          <h3 className="text-xl font-medium text-white mb-3">
            No Devices Connected
          </h3>
          <p className="text-gray-100 text-sm leading-relaxed mb-6">
            Please ensure this app is running and then start or restart your
            devices to establish a connection.
          </p>
          <div className="text-sm text-gray-100 bg-[#26262A] rounded-xl p-5 border border-[#3D3D42] shadow-inner">
            <p className="font-semibold mb-3 text-white">
              Troubleshooting steps:
            </p>
            <ol className="list-decimal list-inside space-y-3 text-left">
              <li className="pl-1">Verify the app is running</li>
              <li className="pl-1">Restart your development devices</li>
              <li className="flex items-start gap-1.5 flex-wrap pl-1">
                <span>Please read the</span>
                <button
                  onClick={() =>
                    handleCopy(
                      "https://github.com/LovesWorking/rn-better-dev-tools",
                      "Documentation"
                    )
                  }
                  className="text-blue-300 hover:text-blue-200 hover:underline cursor-pointer inline-flex items-center gap-1 relative group transition-colors duration-300 font-medium"
                >
                  documentation
                  <svg
                    className="w-3 h-3"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
                    />
                  </svg>
                  {copiedText === "Documentation" && (
                    <span className="absolute -top-8 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-[#0A0A0C] text-xs text-white rounded-lg shadow-lg border border-[#2D2D2F]/50 animate-fadeIn">
                      Copied!
                    </span>
                  )}
                </button>
                <span>or</span>
                <button
                  onClick={() =>
                    handleCopy(
                      "https://github.com/LovesWorking/rn-better-dev-tools/issues",
                      "Issues"
                    )
                  }
                  className="text-blue-300 hover:text-blue-200 hover:underline cursor-pointer inline-flex items-center gap-1 relative group transition-colors duration-300 font-medium"
                >
                  create an issue
                  <svg
                    className="w-3 h-3"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
                    />
                  </svg>
                  {copiedText === "Issues" && (
                    <span className="absolute -top-8 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-[#0A0A0C] text-xs text-white rounded-lg shadow-lg border border-[#2D2D2F]/50 animate-fadeIn">
                      Copied!
                    </span>
                  )}
                </button>
                <span>for help</span>
              </li>
            </ol>
          </div>
        </div>
      </div>
    </>
  );
};


================================================
FILE: src/components/external-dash/UserInfo/DeviceSpecificationsSection.tsx
================================================
import React, { useState } from "react";
import { InfoRow } from "./InfoRow";

interface Props {
  extraDeviceInfo: Record<string, any>;
}

const formatValue = (value: any, key?: string): React.ReactElement => {
  // Special handling for NODE_ENV
  if (key === "NODE_ENV" && typeof value === "string") {
    const env = value.toLowerCase();
    let envStyles = "";

    switch (env) {
      case "development":
      case "dev":
        envStyles =
          "bg-green-900/80 text-green-300 shadow-[0_0_8px_rgba(74,222,128,0.1)]";
        break;
      case "production":
      case "prod":
        envStyles =
          "bg-blue-900/80 text-blue-300 shadow-[0_0_8px_rgba(59,130,246,0.1)]";
        break;
      case "staging":
      case "stage":
        envStyles =
          "bg-yellow-900/80 text-yellow-300 shadow-[0_0_8px_rgba(250,204,21,0.1)]";
        break;
      case "test":
      case "testing":
        envStyles =
          "bg-red-900/80 text-red-300 shadow-[0_0_8px_rgba(248,113,113,0.1)]";
        break;
      default:
        envStyles =
          "bg-purple-900/80 text-purple-300 shadow-[0_0_8px_rgba(147,51,234,0.1)]";
    }

    return (
      <span
        className={`inline-block px-3 py-1 text-xs font-medium rounded-full transition-all duration-500 ease-out break-words max-w-full ${envStyles}`}
      >
        {value}
      </span>
    );
  }

  // Check if it's a string boolean first
  if (
    typeof value === "string" &&
    (value.toLowerCase() === "true" || value.toLowerCase() === "false")
  ) {
    const boolValue = value.toLowerCase() === "true";
    return (
      <span
        className={`inline-block px-3 py-1 text-xs font-medium rounded-full transition-all duration-500 ease-out break-words max-w-full ${
          boolValue
            ? "bg-green-900/80 text-green-300 shadow-[0_0_8px_rgba(74,222,128,0.1)]"
            : "bg-red-900/80 text-red-300 shadow-[0_0_8px_rgba(248,113,113,0.1)]"
        }`}
      >
        {boolValue ? "true" : "false"}
      </span>
    );
  }

  if (typeof value === "boolean") {
    return (
      <span
        className={`inline-block px-3 py-1 text-xs font-medium rounded-full transition-all duration-500 ease-out break-words max-w-full ${
          value
            ? "bg-green-900/80 text-green-300 shadow-[0_0_8px_rgba(74,222,128,0.1)]"
            : "bg-red-900/80 text-red-300 shadow-[0_0_8px_rgba(248,113,113,0.1)]"
        }`}
      >
        {value ? "true" : "false"}
      </span>
    );
  }

  if (typeof value === "string") {
    return (
      <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-red-900/20 text-red-300 border border-red-900/30 shadow-inner break-words word-break-all max-w-full">
        {value}
      </span>
    );
  }

  if (typeof value === "number") {
    return (
      <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-blue-900/20 text-blue-300 border border-blue-900/30 shadow-inner break-words max-w-full">
        {value}
      </span>
    );
  }

  if (value === null) {
    return (
      <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-gray-900/20 text-gray-400 border border-gray-900/30 shadow-inner italic break-words max-w-full">
        null
      </span>
    );
  }

  if (value === undefined) {
    return (
      <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-gray-900/20 text-gray-400 border border-gray-900/30 shadow-inner italic break-words max-w-full">
        undefined
      </span>
    );
  }

  // For objects, arrays, etc.
  return (
    <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-yellow-900/20 text-yellow-300 border border-yellow-900/30 shadow-inner break-words word-break-all max-w-full">
      {JSON.stringify(value)}
    </span>
  );
};

export const DeviceSpecificationsSection: React.FC<Props> = ({
  extraDeviceInfo,
}) => {
  const hasDeviceInfo = Object.keys(extraDeviceInfo).length > 0;
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <>
      <div className="col-span-2 border-t border-[#2D2D2F]/70 my-3" />
      <div className="col-span-2 mb-2">
        <div
          className="flex items-center gap-1.5 text-[#F5F5F7] font-medium cursor-pointer hover:text-white transition-colors duration-200"
          onClick={() => setIsExpanded(!isExpanded)}
        >
          <svg
            className={`w-4 h-4 ${
              hasDeviceInfo ? "text-blue-400" : "text-amber-400"
            }`}
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d={
                hasDeviceInfo
                  ? "M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2z"
                  : "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
              }
            />
          </svg>
          <span>Custom Properties</span>
          <svg
            className={`w-4 h-4 ml-auto transition-transform duration-200 ${
              isExpanded ? "rotate-180" : ""
            }`}
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M19 9l-7 7-7-7"
            />
          </svg>
        </div>
      </div>

      {isExpanded && (
        <>
          {hasDeviceInfo ? (
            <>
              {Object.entries(extraDeviceInfo).map(([key, value]) => (
                <div
                  key={key}
                  className="col-span-2 grid grid-cols-2 gap-3 mb-2 min-w-0"
                >
                  <div className="text-[#A1A1A6] text-sm break-words overflow-hidden min-w-0">
                    <span className="font-mono">{key}</span>
                  </div>
                  <div className="text-sm break-words overflow-hidden font-mono min-w-0">
                    {formatValue(value, key)}
                  </div>
                </div>
              ))}
            </>
          ) : (
            <div className="col-span-2 text-xs text-[#A1A1A6]">
              <div className="mb-2">
                No custom properties available. Pass additional info via the{" "}
                <code className="px-1.5 py-0.5 bg-[#0A0A0C] rounded text-blue-300">
                  extraDeviceInfo
                </code>{" "}
                prop:
              </div>
              <code className="text-xs bg-[#0A0A0C]/70 rounded-lg p-3 font-mono text-[#F5F5F7] block w-full">
                extraDeviceInfo: {"{"}
                "Environment": "staging", "Version": "1.2.3", "Feature_Flag":
                true, ...
                {"}"}
              </code>
            </div>
          )}
        </>
      )}
    </>
  );
};


================================================
FILE: src/components/external-dash/UserInfo/EnvironmentVariablesSection.tsx
================================================
import React, { useState } from "react";
import { InfoRow } from "./InfoRow";

interface Props {
  envVariables: Record<string, any>;
}

const formatValue = (value: any, key?: string): React.ReactElement => {
  // Special handling for NODE_ENV
  if (key === "NODE_ENV" && typeof value === "string") {
    const env = value.toLowerCase();
    let envStyles = "";

    switch (env) {
      case "development":
      case "dev":
        envStyles =
          "bg-green-900/80 text-green-300 shadow-[0_0_8px_rgba(74,222,128,0.1)]";
        break;
      case "production":
      case "prod":
        envStyles =
          "bg-blue-900/80 text-blue-300 shadow-[0_0_8px_rgba(59,130,246,0.1)]";
        break;
      case "staging":
      case "stage":
        envStyles =
          "bg-yellow-900/80 text-yellow-300 shadow-[0_0_8px_rgba(250,204,21,0.1)]";
        break;
      case "test":
      case "testing":
        envStyles =
          "bg-red-900/80 text-red-300 shadow-[0_0_8px_rgba(248,113,113,0.1)]";
        break;
      default:
        envStyles =
          "bg-purple-900/80 text-purple-300 shadow-[0_0_8px_rgba(147,51,234,0.1)]";
    }

    return (
      <span
        className={`inline-block px-3 py-1 text-xs font-medium rounded-full transition-all duration-500 ease-out break-words max-w-full ${envStyles}`}
      >
        {value}
      </span>
    );
  }

  // Check if it's a string boolean first
  if (
    typeof value === "string" &&
    (value.toLowerCase() === "true" || value.toLowerCase() === "false")
  ) {
    const boolValue = value.toLowerCase() === "true";
    return (
      <span
        className={`inline-block px-3 py-1 text-xs font-medium rounded-full transition-all duration-500 ease-out break-words max-w-full ${
          boolValue
            ? "bg-green-900/80 text-green-300 shadow-[0_0_8px_rgba(74,222,128,0.1)]"
            : "bg-red-900/80 text-red-300 shadow-[0_0_8px_rgba(248,113,113,0.1)]"
        }`}
      >
        {boolValue ? "true" : "false"}
      </span>
    );
  }

  if (typeof value === "boolean") {
    return (
      <span
        className={`inline-block px-3 py-1 text-xs font-medium rounded-full transition-all duration-500 ease-out break-words max-w-full ${
          value
            ? "bg-green-900/80 text-green-300 shadow-[0_0_8px_rgba(74,222,128,0.1)]"
            : "bg-red-900/80 text-red-300 shadow-[0_0_8px_rgba(248,113,113,0.1)]"
        }`}
      >
        {value ? "true" : "false"}
      </span>
    );
  }

  if (typeof value === "string") {
    return (
      <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-red-900/20 text-red-300 border border-red-900/30 shadow-inner break-words word-break-all max-w-full">
        {value}
      </span>
    );
  }

  if (typeof value === "number") {
    return (
      <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-blue-900/20 text-blue-300 border border-blue-900/30 shadow-inner break-words max-w-full">
        {value}
      </span>
    );
  }

  if (value === null) {
    return (
      <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-gray-900/20 text-gray-400 border border-gray-900/30 shadow-inner italic break-words max-w-full">
        null
      </span>
    );
  }

  if (value === undefined) {
    return (
      <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-gray-900/20 text-gray-400 border border-gray-900/30 shadow-inner italic break-words max-w-full">
        undefined
      </span>
    );
  }

  // For objects, arrays, etc.
  return (
    <span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-yellow-900/20 text-yellow-300 border border-yellow-900/30 shadow-inner break-words word-break-all max-w-full">
      {JSON.stringify(value)}
    </span>
  );
};

export const EnvironmentVariablesSection: React.FC<Props> = ({
  envVariables,
}) => {
  const hasEnvVariables = Object.keys(envVariables).length > 0;
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <>
      <div className="col-span-2 border-t border-[#2D2D2F]/70 my-3" />
      <div className="col-span-2 mb-2">
        <div
          className="flex items-center gap-1.5 text-[#F5F5F7] font-medium cursor-pointer hover:text-white transition-colors duration-200"
          onClick={() => setIsExpanded(!isExpanded)}
        >
          <svg
            className={`w-4 h-4 ${
              hasEnvVariables ? "text-green-400" : "text-amber-400"
            }`}
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
            />
          </svg>
          <span>Environment Variables</span>
          <svg
            className={`w-4 h-4 ml-auto transition-transform duration-200 ${
              isExpanded ? "rotate-180" : ""
            }`}
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M19 9l-7 7-7-7"
            />
          </svg>
        </div>
      </div>

      {isExpanded && (
        <>
          {hasEnvVariables ? (
            <>
              {Object.entries(envVariables).map(([key, value]) => (
                <div
                  key={key}
                  className="col-span-2 grid grid-cols-2 gap-3 mb-2 min-w-0"
                >
                  <div className="text-[#A1A1A6] text-sm break-words overflow-hidden min-w-0">
                    <span className="font-mono">{key}</span>
                  </div>
                  <div className="text-sm break-words overflow-hidden font-mono min-w-0">
                    {formatValue(value, key)}
                  </div>
                </div>
              ))}
            </>
          ) : (
            <div className="col-span-2 text-xs text-[#A1A1A6]">
              <div className="mb-2">
                No environment variables available. Pass ENV variables via the{" "}
                <code className="px-1.5 py-0.5 bg-[#0A0A0C] rounded text-green-300">
                  envVariables
                </code>{" "}
                prop:
              </div>
              <code className="text-xs bg-[#0A0A0C]/70 rounded-lg p-3 font-mono text-[#F5F5F7] block w-full">
                envVariables: {"{"}
                "NODE_ENV": "development", "API_URL": "https://api.example.com",
                ...
                {"}"}
              </code>
            </div>
          )}
        </>
      )}
    </>
  );
};


================================================
FILE: src/components/external-dash/UserInfo/InfoRow.tsx
================================================
import React from "react";
import { PlatformIcon } from "../utils/platformUtils";

interface Props {
  label: string;
  value: string;
  monospace?: boolean;
  className?: string;
  labelClassName?: string;
}

export const InfoRow: React.FC<Props> = ({
  label,
  value,
  monospace,
  className = "text-[#F5F5F7]",
  labelClassName = "text-[#A1A1A6]",
}) => (
  <>
    <div className={`${labelClassName} font-medium antialiased`}>{label}:</div>
    <div
      className={`${className} ${
        monospace ? "font-mono" : ""
      } overflow-hidden text-ellipsis antialiased flex items-center gap-1.5`}
    >
      {label === "Platform" && <PlatformIcon platform={value} />}
      {value}
    </div>
  </>
);


================================================
FILE: src/components/external-dash/UserInfo/StorageControlsSection.tsx
================================================
import React, { useCallback, useMemo, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useStorageStore } from "../utils/storageStore";
import {
  StorageType,
  isStorageQuery,
  getStorageType,
} from "../utils/storageQueryKeys";

interface StorageControlsSectionProps {
  deviceId?: string; // Made optional for global usage
}

export const StorageControlsSection: React.FC<StorageControlsSectionProps> =
  React.memo(({ deviceId }) => {
    const queryClient = useQueryClient();
    const [isExpanded, setIsExpanded] = useState(false);

    // Get the entire store state to ensure reactivity
    const { enabledStorageTypes, setStorageTypeEnabled } = useStorageStore();

    const handleStorageToggle = useCallback(
      (storageType: StorageType) => {
        const isCurrentlyEnabled = enabledStorageTypes.has(storageType);
        const newEnabledState = !isCurrentlyEnabled;

        setStorageTypeEnabled(storageType, newEnabledState);

        if (!newEnabledState) {
          // Use requestAnimationFrame to defer heavy query operations
          // This prevents blocking the UI during transitions
          requestAnimationFrame(() => {
            const queryCache = queryClient.getQueryCache();
            const allQueries = queryCache.getAll();

            allQueries.forEach((query) => {
              if (
                isStorageQuery(query.queryKey) &&
                getStorageType(query.queryKey) === storageType
              ) {
                queryClient.removeQueries({ queryKey: query.queryKey });
              }
            });
          });
        }
      },
      [enabledStorageTypes, setStorageTypeEnabled, queryClient]
    );

    const storageTypes = useMemo(
      () => [
        {
          type: "mmkv" as StorageType,
          label: "MMKV",
          description: "High-performance key-value storage",
          icon: "⚡",
        },
        {
          type: "async" as StorageType,
          label: "AsyncStorage",
          description: "React Native async storage",
          icon: "📱",
        },
        {
          type: "secure" as StorageType,
          label: "SecureStorage",
          description: "Encrypted secure storage",
          icon: "🔒",
        },
      ],
      []
    );

    const enabledCount = enabledStorageTypes.size;
    const totalCount = storageTypes.length;

    // If deviceId is provided, render the old card-style layout for user info
    if (deviceId) {
      return (
        <>
          <div className="col-span-2 border-t border-[#2D2D2F]/70 my-3" />
          <div className="col-span-2 mb-2">
            <div
              className="flex items-center gap-1.5 text-[#F5F5F7] font-medium cursor-pointer hover:text-white transition-colors duration-200"
              onClick={() => setIsExpanded(!isExpanded)}
            >
              <svg
                className="w-4 h-4 text-blue-400"
                viewBox="0 0 24 24"
                fill="currentColor"
              >
                <path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z" />
              </svg>
              <span>Storage Query Controls</span>
              <span className="text-xs text-[#86868B] ml-2 font-mono bg-[#2D2D2F]/50 px-2 py-0.5 rounded">
                {enabledCount}/{totalCount} enabled
              </span>
              <svg
                className={`w-4 h-4 ml-auto transition-transform duration-200 ${
                  isExpanded ? "rotate-180" : ""
                }`}
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                  d="M19 9l-7 7-7-7"
                />
              </svg>
            </div>
          </div>

          {isExpanded && (
            <div className="col-span-2">
              <div className="space-y-2">
                {storageTypes.map(({ type, label, description, icon }) => {
                  const isEnabled = enabledStorageTypes.has(type);

                  return (
                    <button
                      key={type}
                      onClick={() => handleStorageToggle(type)}
                      className={`w-full p-4 rounded-xl border transition-all duration-300 ease-out ${
                        isEnabled
                          ? "bg-blue-500/10 border-blue-500/30 hover:bg-blue-500/15 hover:border-blue-500/40"
                          : "bg-red-500/10 border-red-500/30 hover:bg-red-500/15 hover:border-red-500/40"
                      }`}
                      aria-pressed={isEnabled}
                      role="switch"
                      type="button"
                      title={isEnabled ? `Disable ${label}` : `Enable ${label}`}
                    >
                      <div className="flex items-center justify-between">
                        <div className="flex items-center gap-3">
                          <div
                            className={`flex items-center justify-center w-10 h-10 rounded-xl transition-colors duration-300 ${
                              isEnabled
                                ? "bg-blue-500/20 text-blue-300"
                                : "bg-red-500/20 text-red-300"
                            }`}
                          >
                            <span className="text-xl">{icon}</span>
                          </div>
                          <div className="text-left">
                            <div
                              className={`font-medium transition-colors duration-300 ${
                                isEnabled ? "text-blue-200" : "text-red-200"
                              }`}
                            >
                              {label}
                            </div>
                            <div className="text-xs text-[#A1A1A6] leading-relaxed">
                              {description}
                            </div>
                          </div>
                        </div>

                        <div className="flex items-center">
                          <div
                            className={`w-12 h-6 rounded-full flex items-center px-1 transition-all duration-300 ease-out ${
                              isEnabled ? "bg-blue-500" : "bg-red-500"
                            }`}
                          >
                            <div
                              className={`w-4 h-4 rounded-full bg-white shadow-md transform transition-transform duration-300 ease-out ${
                                isEnabled ? "translate-x-6" : "translate-x-0"
                              }`}
                            />
                          </div>
                        </div>
                      </div>
                    </button>
                  );
                })}
              </div>

              <div className="mt-4 pt-4 border-t border-[#2D2D2F]/50">
                <div className="text-xs text-[#86868B] leading-relaxed">
                  <div className="flex items-start gap-2">
                    <svg
                      className="w-3 h-3 mt-0.5 text-blue-400 flex-shrink-0"
                      viewBox="0 0 24 24"
                      fill="currentColor"
                    >
                      <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" />
                    </svg>
                    <span>
                      Disabling a storage type will clear its queries from React
                      Query DevTools and filter out new queries. Re-enabling
                      will show new queries but won't restore previously cleared
                      ones.
                    </span>
                  </div>
                </div>
              </div>
            </div>
          )}
        </>
      );
    }

    // Global header layout - compact dropdown style
    return (
      <div className="relative">
        <button
          onClick={() => setIsExpanded(!isExpanded)}
          className="group flex items-center gap-2 px-3 py-1.5 rounded-full transition-all duration-500 ease-out hover:bg-[#1D1D1F]/40 border border-transparent hover:border-[#2D2D2F]/70"
        >
          <svg
            className="w-3 h-3 text-blue-400"
            viewBox="0 0 24 24"
            fill="currentColor"
          >
            <path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z" />
          </svg>
          <span className="text-xs font-medium text-[#A1A1A6] group-hover:text-[#F5F5F7] transition-colors duration-300">
            #storage
          </span>
          <span className="text-xs text-[#86868B] font-mono bg-[#2D2D2F]/50 px-1.5 py-0.5 rounded text-[10px]">
            {enabledCount}/{totalCount}
          </span>
          <svg
            className={`w-3 h-3 transition-transform duration-300 ${
              isExpanded ? "rotate-180" : ""
            }`}
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M19 9l-7 7-7-7"
            />
          </svg>
        </button>

        {isExpanded && (
          <div className="absolute top-full right-0 mt-2 w-80 bg-[#1A1A1C] border border-[#2D2D2F]/60 rounded-xl shadow-[0_0.5rem_1.5rem_rgba(0,0,0,0.25)] z-50">
            <div className="p-4">
              <div className="space-y-3">
                {storageTypes.map(({ type, label, description, icon }) => {
                  const isEnabled = enabledStorageTypes.has(type);

                  return (
                    <button
                      key={type}
                      onClick={() => handleStorageToggle(type)}
                      className={`w-full p-4 rounded-lg border transition-all duration-300 ease-out ${
                        isEnabled
                          ? "bg-blue-500/10 border-blue-500/30 hover:bg-blue-500/15 hover:border-blue-500/40"
                          : "bg-red-500/10 border-red-500/30 hover:bg-red-500/15 hover:border-red-500/40"
                      }`}
                      aria-pressed={isEnabled}
                      role="switch"
                      type="button"
                      title={isEnabled ? `Disable ${label}` : `Enable ${label}`}
                    >
                      <div className="flex items-center justify-between">
                        <div className="flex items-center gap-3">
                          <div
                            className={`flex items-center justify-center w-10 h-10 rounded-lg transition-colors duration-300 ${
                              isEnabled
                                ? "bg-blue-500/20 text-blue-300"
                                : "bg-red-500/20 text-red-300"
                            }`}
                          >
                            <span className="text-xl">{icon}</span>
                          </div>
                          <div className="text-left">
                            <div
                              className={`text-sm font-medium transition-colors duration-300 ${
                                isEnabled ? "text-blue-200" : "text-red-200"
                              }`}
                            >
                              {label}
                            </div>
                            <div className="text-xs text-[#A1A1A6] leading-relaxed">
                              {description}
                            </div>
                          </div>
                        </div>

                        <div className="flex items-center">
                          <div
                            className={`w-12 h-6 rounded-full flex items-center px-1 transition-all duration-300 ease-out ${
                              isEnabled ? "bg-blue-500" : "bg-red-500"
                            }`}
                          >
                            <div
                              className={`w-4 h-4 rounded-full bg-white shadow-md transform transition-transform duration-300 ease-out ${
                                isEnabled ? "translate-x-6" : "translate-x-0"
                              }`}
                            />
                          </div>
                        </div>
                      </div>
                    </button>
                  );
                })}
              </div>

              <div className="mt-4 pt-4 border-t border-[#2D2D2F]/50">
                <div className="text-xs text-[#86868B] leading-relaxed">
                  <div className="flex items-start gap-2">
                    <svg
                      className="w-3 h-3 mt-0.5 text-blue-400 flex-shrink-0"
                      viewBox="0 0 24 24"
                      fill="currentColor"
                    >
                      <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" />
                    </svg>
                    <span>
                      Global setting that affects storage queries in React Query
                      DevTools for all devices.
                    </span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        )}
      </div>
    );
  });


================================================
FILE: src/components/external-dash/UserInfo/TargetGlowEffect.tsx
================================================
import React from "react";

interface Props {
  isTargeted: boolean;
}

export const TargetGlowEffect: React.FC<Props> = ({ isTargeted }) => {
  if (!isTargeted) return null;

  return (
    <>
      {/* Extended glow effect - furthest back */}
      <div
        className="absolute -inset-[3px] bg-gradient-to-r from-red-500/10 via-violet-500/10 to-blue-500/10 rounded-2xl blur-xl animate-gradient opacity-70 transition-opacity duration-500 ease-out"
        aria-hidden="true"
      />

      {/* Outer glow effect */}
      <div
        className="absolute -inset-[2px] bg-gradient-to-r from-red-500/30 via-violet-500/30 to-blue-500/30 rounded-2xl blur-md animate-gradient transition-opacity duration-500 ease-out"
        aria-hidden="true"
      />

      {/* Primary glowing border */}
      <div
        className="absolute -inset-[1px] bg-gradient-to-r from-red-500/80 via-violet-500/80 to-blue-500/80 rounded-2xl opacity-90 animate-gradient transition-opacity duration-500 ease-out"
        aria-hidden="true"
      />
    </>
  );
};


================================================
FILE: src/components/external-dash/UserInfo/UserCardDetails.tsx
================================================
import React from "react";
import { User } from "../types/User";
import { InfoRow } from "./InfoRow";
import { EnvironmentVariablesSection } from "./EnvironmentVariablesSection";
import { DeviceSpecificationsSection } from "./DeviceSpecificationsSection";

interface Props {
  userData: User;
  isTargeted: boolean;
  envVariables: Record<string, any>;
  extraDeviceInfo: Record<string, any>;
}

export const UserCardDetails: React.FC<Props> = ({
  userData,
  isTargeted,
  envVariables,
  extraDeviceInfo,
}) => {
  const platform = userData.platform || "Unknown";
  const isConnected =
    userData.isConnected !== undefined ? userData.isConnected : true;
  const connectionStatusText = isConnected ? "Connected" : "Disconnected";

  return (
    <div
      className="grid grid-cols-2 gap-3 text-sm px-5 pb-5 animate-fadeIn border-t border-[#2D2D2F]/70 pt-4 select-text"
      onClick={(e) => e.stopPropagation()}
    >
      <InfoRow label="Socket ID" value={userData.id} monospace />

      {userData.deviceId && (
        <InfoRow label="Device ID" value={userData.deviceId} monospace />
      )}

      <InfoRow label="Platform" value={platform} />

      <InfoRow
        label="Connection Status"
        value={connectionStatusText}
        className={isConnected ? "text-green-400" : "text-red-400"}
      />

      <InfoRow
        label="Connection Type"
        value={
          userData.deviceId ? "Persistent Connection" : "Standard Connection"
        }
      />

      {isTargeted && (
        <InfoRow
          label="Target Status"
          value="Currently Targeted"
          className="text-blue-300"
          labelClassName="text-blue-400"
        />
      )}

      <EnvironmentVariablesSection envVariables={envVariables} />

      <DeviceSpecificationsSection extraDeviceInfo={extraDeviceInfo} />

      <div className="col-span-2 text-xs text-[#A1A1A6] mt-3 flex items-center justify-center space-x-2">
        <svg
          className="w-4 h-4"
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="2"
            d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
          />
        </svg>
        <span>Click header to collapse</span>
      </div>
    </div>
  );
};


================================================
FILE: src/components/external-dash/UserInfo/UserCardHeader.tsx
================================================
import React from "react";
import { User } from "../types/User";
import {
  PlatformIcon,
  getDisplayPlatform,
  getPlatformBgColor,
} from "../utils/platformUtils";

interface Props {
  userData: User;
  isTargeted: boolean;
  expanded: boolean;
  onToggleExpanded: () => void;
}

export const UserCardHeader: React.FC<Props> = ({
  userData,
  isTargeted,
  expanded,
  onToggleExpanded,
}) => {
  const platform = userData.platform || "Unknown";
  const displayPlatform = getDisplayPlatform(platform);
  const isConnected =
    userData.isConnected !== undefined ? userData.isConnected : true;
  const connectionStatusText = isConnected ? "Connected" : "Disconnected";

  return (
    <div
      className="flex justify-between items-center cursor-pointer group select-none p-5 w-full"
      onClick={(e) => {
        e.stopPropagation();
        onToggleExpanded();
      }}
    >
      <div className="flex items-center space-x-4">
        <div className="relative">
          <div
            className={`w-2 h-2 rounded-full transition-colors duration-500 ease-out
              ${isConnected ? "bg-green-500" : "bg-red-500"}
             `}
          >
            {isConnected && (
              <div className="absolute -inset-1 rounded-full bg-green-500/30 animate-pulse"></div>
            )}
          </div>
        </div>

        <h2
          className={`text-lg font-medium tracking-tight antialiased transition-all duration-500 ease-out
          ${
            isTargeted
              ? "text-blue-300 drop-shadow-[0_0_12px_rgba(59,130,246,0.3)]"
              : "text-[#F5F5F7]"
          }`}
        >
          {userData.deviceName}
        </h2>

        <span
          className={`px-3 py-1 text-xs font-medium rounded-full flex items-center gap-1.5 transition-all duration-500 ease-out
          ${getPlatformBgColor(platform)}
          ${
            isTargeted
              ? "ring-1 ring-blue-400/30 shadow-[0_0_8px_rgba(59,130,246,0.2)]"
              : ""
          }`}
        >
          <PlatformIcon platform={platform} />
          {displayPlatform}
        </span>
      </div>

      <div className="flex items-center space-x-4">
        <span
          className={`px-3 py-1 text-xs font-medium rounded-full transition-all duration-500 ease-out
            ${
              userData.deviceId
                ? isConnected
                  ? "bg-green-900/80 text-green-300 shadow-[0_0_8px_rgba(74,222,128,0.1)]"
                  : "bg-red-900/80 text-red-300 shadow-[0_0_8px_rgba(248,113,113,0.1)]"
                : "bg-yellow-900/80 text-yellow-300 shadow-[0_0_8px_rgba(250,204,21,0.1)]"
            }
          `}
        >
          {userData.deviceId ? connectionStatusText : "Legacy"}
        </span>

        <svg
          xmlns="http://www.w3.org/2000/svg"
          className={`h-4 w-4 transition-all duration-500 ease-out
            ${expanded ? "rotate-180" : ""}
            ${isTargeted ? "text-blue-400" : "text-[#A1A1A6]"}
            group-hover:text-[#F5F5F7]`}
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="2"
            d="M19 9l-7 7-7-7"
          />
        </svg>
      </div>
    </div>
  );
};


================================================
FILE: src/components/external-dash/UserInfo/index.ts
================================================
export { TargetGlowEffect } from "./TargetGlowEffect";
export { UserCardHeader } from "./UserCardHeader";
export { UserCardDetails } from "./UserCardDetails";
export { EnvironmentVariablesSection } from "./EnvironmentVariablesSection";
export { DeviceSpecificationsSection } from "./DeviceSpecificationsSection";
export { StorageControlsSection } from "./StorageControlsSection";
export { InfoRow } from "./InfoRow";


================================================
FILE: src/components/external-dash/UserInfo.tsx
================================================
import React, { useState } from "react";
import { User } from "./types/User";
import {
  TargetGlowEffect,
  UserCardHeader,
  UserCardDetails,
} from "./UserInfo/index";

interface Props {
  userData: User;
  isTargeted?: boolean;
}

export const UserInfo: React.FC<Props> = ({ userData, isTargeted = false }) => {
  const [expanded, setExpanded] = useState(false);

  // Parse extraDeviceInfo if it exists and is not null/undefined
  const extraDeviceInfo = (() => {
    try {
      return userData.extraDeviceInfo &&
        userData.extraDeviceInfo !== "undefined" &&
        userData.extraDeviceInfo.trim() !== ""
        ? JSON.parse(userData.extraDeviceInfo)
        : {};
    } catch (error) {
      console.warn("Failed to parse extraDeviceInfo:", error);
      return {};
    }
  })();

  // Parse envVariables if it exists and is not null/undefined
  const envVariables = (() => {
    try {
      return userData.envVariables &&
        userData.envVariables !== "undefined" &&
        userData.envVariables.trim() !== ""
        ? JSON.parse(userData.envVariables)
        : {};
    } catch (error) {
      console.warn("Failed to parse envVariables:", error);
      return {};
    }
  })();

  return (
    <div className="relative isolate w-full">
      <TargetGlowEffect isTargeted={isTargeted} />

      <div
        className={`relative bg-[#1A1A1C] transition-all duration-500 ease-out
          ${
            expanded
              ? "scale-[1.01] shadow-[0_0.75rem_2.5rem_rgba(0,0,0,0.25)]"
              : "scale-100 cursor-pointer shadow-[0_0.5rem_1.5rem_rgba(0,0,0,0.15)]"
          }
          border border-[#2D2D2F]/70 ${
            isTargeted ? "border-opacity-0" : "border-opacity-70"
          }
          rounded-2xl hover:shadow-[0_1rem_3rem_rgba(0,0,0,0.3)]`}
      >
        <UserCardHeader
          userData={userData}
          isTargeted={isTargeted}
          expanded={expanded}
          onToggleExpanded={() => setExpanded(!expanded)}
        />

        {expanded && (
          <UserCardDetails
            userData={userData}
            isTargeted={isTargeted}
            envVariables={envVariables}
            extraDeviceInfo={extraDeviceInfo}
          />
        )}
      </div>

      <div
        className={`absolute inset-0 -z-10 bg-gradient-to-b from-[#1A1A1C]/30 to-transparent rounded-2xl transition-opacity duration-500 ease-out
          ${expanded ? "opacity-100" : "opacity-0"}`}
        aria-hidden="true"
      />
    </div>
  );
};


================================================
FILE: src/components/external-dash/_hooks/useConnectedUsers.ts
================================================
import io, { Socket } from "socket.io-client";
import { useEffect, useState } from "react";
import { User } from "../types/User";

let socket = null as Socket | null; // Module-level variable to store the socket instance
export default function useConnectedUsers() {
  const socketURL = "http://localhost:42831";
  const [isDashboardConnected, setIsDashboardConnected] = useState(
    !!socket?.connected
  );
  const [allDevices, setAllDevices] = useState<User[]>([]);

  // Ensure we're properly identifying as a dashboard client with correct query params
  if (!socket) {
    // Include the "Dashboard" identifier explicitly in query params
    const enhancedQuery = {
      deviceName: "Dashboard",
    };

    console.log("[DASHBOARD] Initializing socket with query:", enhancedQuery);

    // Initialize the socket only if it hasn't been already initialized
    socket = io(socketURL, {
      autoConnect: false, // Initially prevent automatic connection
      query: enhancedQuery,
      reconnection: true,
      reconnectionAttempts: 10,
      reconnectionDelay: 1000,
    });
  }

  function connect() {
    if (!socket.connected) {
      console.log("[DASHBOARD] Connecting socket...");
      socket?.connect();
    } else {
      console.log("[DASHBOARD] Socket already connected:", socket.id);
    }
  }

  function disconnect() {
    console.log("[DASHBOARD] Disconnecting socket...");
    socket?.disconnect();
  }

  useEffect(() => {
    function onConnect() {
      console.log("[DASHBOARD] Socket connected with ID:", socket?.id);
      setIsDashboardConnected(true);
    }

    function onDisconnect() {
      console.log("[DASHBOARD] Socket disconnected");
      setIsDashboardConnected(false);
    }

    function onConnectError(error: Error) {
      console.error("[DASHBOARD] Connection error:", error.message);
    }

    // Make sure we're connected
    !socket.connected && connect();

    // Listen for all devices updates (including offline devices)
    socket.on("all-devices-update", (devices: User[]) => {
      console.log(
        "[DASHBOARD] Received all-devices-update:",
        devices.length,
        "devices"
      );
      setAllDevices(devices);
    });

    // Add listeners for connection events
    socket?.on("connect", onConnect);
    socket?.on("disconnect", onDisconnect);
    socket?.on("connect_error", onConnectError);

    // If already connected, log the ID
    if (socket.connected) {
      console.log(
        "[DASHBOARD] Socket already connected on mount with ID:",
        socket.id
      );
    }

    return () => {
      socket.off("all-devices-update");
      socket.off("connect");
      socket.off("disconnect");
      socket.off("connect_error");
      // Don't disconnect on cleanup - this would break the persistent connection
    };
  }, []);

  return {
    socket,
    connect,
    disconnect,
    isDashboardConnected,
    allDevices,
  };
}


================================================
FILE: src/components/external-dash/hooks/useDevToolsEventHandler.ts
================================================
import { useEffect, useRef } from "react";
import { onDevToolsEvent } from "../utils/devToolsEvents";
import type { DevToolsActionType } from "../utils/devToolsEvents";
import { useQueryClient } from "@tanstack/react-query";
import { Socket } from "socket.io-client";
import type { User } from "../types/User";
import type { QueryKey } from "@tanstack/react-query";
import { logger } from "../utils/logger";

interface UseDevToolsEventHandlerProps {
  isConnected?: boolean;
  socket?: Socket;
  selectedDevice?: User;
  sendQueryAction?: (
    socket: Socket,
    targetDevice: User,
    action: string,
    query: {
      queryHash: string;
      queryKey: QueryKey;
      state: { data: unknown };
    }
  ) => void;
}

const actionTypeToQueryAction: Record<DevToolsActionType, string> = {
  REFETCH: "ACTION-REFETCH",
  INVALIDATE: "ACTION-INVALIDATE",
  RESET: "ACTION-RESET",
  REMOVE: "ACTION-REMOVE",
  TRIGGER_ERROR: "ACTION-TRIGGER-ERROR",
  RESTORE_ERROR: "ACTION-RESTORE-ERROR",
  TRIGGER_LOADING: "ACTION-TRIGGER-LOADING",
  RESTORE_LOADING: "ACTION-RESTORE-LOADING",
  CLEAR_MUTATION_CACHE: "ACTION-CLEAR-MUTATION-CACHE",
  CLEAR_QUERY_CACHE: "ACTION-CLEAR-QUERY-CACHE",
};

export const useDevToolsEventHandler = ({
  isConnected,
  socket,
  selectedDevice,
  sendQueryAction,
}: UseDevToolsEventHandlerProps = {}) => {
  const queryClient = useQueryClient();
  const isReadyRef = useRef(false);

  // Track when all dependencies are ready
  useEffect(() => {
    isReadyRef.current = !!(
      isConnected &&
      socket &&
      selectedDevice &&
      sendQueryAction
    );
    logger.debug("🔍 DevToolsEventHandler Ready State:", {
      deviceId: selectedDevice?.deviceId,
      deviceName: selectedDevice?.deviceName,
      platform: selectedDevice?.platform,
    });
  }, [isConnected, socket, selectedDevice, sendQueryAction]);

  useEffect(() => {
    // Only set up the event handler if all dependencies are available
    if (!isReadyRef.current) {
      logger.debug(
        "🔄 Waiting for dependencies to be ready before setting up event handler",
        {
          deviceId: selectedDevice?.deviceId,
          deviceName: selectedDevice?.deviceName,
          platform: selectedDevice?.platform,
        }
      );
      return;
    }

    logger.debug(
      "🔄 Setting up dev tools event handler - all dependencies ready",
      {
        deviceId: selectedDevice?.deviceId,
        deviceName: selectedDevice?.deviceName,
        platform: selectedDevice?.platform,
      }
    );

    const cleanup = onDevToolsEvent(function (type, queryHash, metadata) {
      const actionName = type.toLowerCase().replace(/_/g, " ");
      logger.info(`🎯 Dev Tools Action Handled: ${actionName}`, {
        deviceId: selectedDevice?.deviceId,
        deviceName: selectedDevice?.deviceName,
        platform: selectedDevice?.platform,
      });

      // Convert DevTools action type to Query action type
      const queryActionType = actionTypeToQueryAction[type];
      if (!queryActionType) {
        logger.warn("⚠️ Unknown action type: " + type, {
          deviceId: selectedDevice?.deviceId,
          deviceName: selectedDevice?.deviceName,
          platform: selectedDevice?.platform,
        });
        return;
      }

      // Get the query from cache if we have a hash
      const query = queryHash
        ? queryClient.getQueryCache().get(queryHash)
        : undefined;
      if (
        !query &&
        type !== "CLEAR_QUERY_CACHE" &&
        type !== "CLEAR_MUTATION_CACHE"
      ) {
        logger.warn("⚠️ Query not found in cache: " + queryHash, {
          deviceId: selectedDevice?.deviceId,
          deviceName: selectedDevice?.deviceName,
          platform: selectedDevice?.platform,
        });
        return;
      }

      logger.info(
        `📤 Forwarding ${queryActionType} to device: ${
          selectedDevice.deviceName ?? "Unknown Device"
        }`,
        {
          deviceId: selectedDevice?.deviceId,
          deviceName: selectedDevice?.deviceName,
          platform: selectedDevice?.platform,
        }
      );

      // Special handling for cache clear actions which don't require a query
      if (type === "CLEAR_QUERY_CACHE" || type === "CLEAR_MUTATION_CACHE") {
        sendQueryAction(socket, selectedDevice, queryActionType, {
          queryHash: "",
          queryKey: undefined,
          state: { data: undefined },
        });
        return;
      }

      // For all other actions, ensure we have a valid query
      if (!query || !queryHash) {
        logger.warn("⚠️ Skipping action due to missing query or queryHash", {
          deviceId: selectedDevice?.deviceId,
          deviceName: selectedDevice?.deviceName,
          platform: selectedDevice?.platform,
        });
        return;
      }

      sendQueryAction(socket, selectedDevice, queryActionType, {
        queryHash: query.queryHash,
        queryKey: query.queryKey,
        state: { data: query.state.data },
      });
    });

    return () => {
      logger.debug("🧹 Cleaning up dev tools event handler", {
        deviceId: selectedDevice?.deviceId,
        deviceName: selectedDevice?.deviceName,
        platform: selectedDevice?.platform,
      });
      cleanup();
    };
  }, [isConnected, socket, selectedDevice, sendQueryAction, queryClient]);
};


================================================
FILE: src/components/external-dash/providers.tsx
================================================
// We can not useState or useRef in a server component, which is why we are
// extracting this part out into it's own file with 'use client' on top
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
interface Props {
  children: React.ReactNode;
}

export default function Providers({ children }: Props) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            queryFn: async ({ queryKey }) => {
              console.log("queryFn", queryKey);
              // Prevent refetch from throwing an error
              return Promise.resolve(null);
            },
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}


================================================
FILE: src/components/external-dash/shared/hydration.ts
================================================
import type {
  DefaultError,
  MutationOptions,
  QueryOptions,
  Query,
  QueryClient,
  Mutation,
} from "@tanstack/react-query";
import { QueryObserver } from "@tanstack/react-query";

import { DehydratedState, ObserverState } from "./types";
import { shouldFilterStorageQuery } from "../utils/storageQueryKeys";
import { useStorageStore } from "../utils/storageStore";

type TransformerFn = (data: any) => any;
function defaultTransformerFn(data: any): any {
  return data;
}

const mockQueryFn = () => {
  return Promise.resolve(null);
};

export function Hydrate(
  client: QueryClient,
  dehydratedState: DehydratedState,
  options?: HydrateOptions
): void {
  if (typeof dehydratedState !== "object" || dehydratedState === null) {
    console.log("dehydratedState is not an object or null");
    return;
  }
  const queryCache = client.getQueryCache();
  const mutationCache = client.getMutationCache();
  const deserializeData =
    options?.defaultOptions?.deserializeData ?? defaultTransformerFn;

  const dehydratedMutations = dehydratedState.mutations || [];
  const dehydratedQueries = dehydratedState.queries || [];

  // Get current storage preferences
  const enabledStorageTypes = useStorageStore.getState().enabledStorageTypes;

  // Sync mutations
  dehydratedMutations.forEach(({ state, ...mutationOptions }) => {
    const existingMutation = mutationCache.getAll().find(
      // @ts-expect-error -  mutation.options.mutationId does exist we add it in the dehydrated state
      (mutation) => mutation.options.mutationId === mutationOptions.mutationId
    );

    if (existingMutation) {
      existingMutation.state = state;
    } else {
      mutationCache.build(
        client,
        {
          ...client.getDefaultOptions().hydrate?.mutations,
          ...options?.defaultOptions?.mutations,
          ...mutationOptions,
        },
        state
      );
    }
  });

  // Hydrate queries - filter out disabled storage queries
  dehydratedQueries.forEach(
    ({ queryKey, state, queryHash, meta, promise, observers, gcTime }) => {
      // Filter out storage queries that are disabled
      if (shouldFilterStorageQuery(queryKey, enabledStorageTypes)) {
        console.log(`Filtering out disabled storage query:`, queryKey);
        return;
      }

      let query = queryCache.get(queryHash);
      const data =
        state.data === undefined ? state.data : deserializeData(state.data);
      // Do not hydrate if an existing query exists with newer data
      if (query) {
        if (
          query.state.dataUpdatedAt < state.dataUpdatedAt ||
          query.state.fetchStatus !== state.fetchStatus
        ) {
          query.setState({
            ...state,
            data,
          });
          query.setOptions({
            ...query.options,
            queryFn: mockQueryFn,
            retry: 0,
            gcTime: gcTime ?? 0,
          });
        }
      } else {
        // Restore query
        query = queryCache.build(
          client,
          {
            ...client.getDefaultOptions().hydrate?.queries,
            ...options?.defaultOptions?.queries,
            queryKey,
            queryHash,
            meta,
            queryFn: mockQueryFn,
            gcTime: gcTime ?? 0,
          },
          {
            ...state,
            data,
          }
        );
      }
      cleanUpObservers(query);
      recreateObserver(client, observers, query);

      if (promise) {
        // Note: `Promise.resolve` required cause
        // RSC transformed promises are not thenable
        const initialPromise = Promise.resolve(promise).then(deserializeData);

        // this doesn't actually fetch - it just creates a retryer
        // which will re-use the passed `initialPromise`
        void query.fetch(undefined, { initialPromise });
      }
    }
  );
  // @ts-expect-error - Refresh mutation state
  mutationCache.notify({ type: "observerResultsUpdated" });
}
// Clean up existing observers
function cleanUpObservers(query: Query) {
  const observers = query.observers;
  observers.forEach((observer) => {
    query.removeObserver(observer);
  });
}

function recreateObserver(
  queryClient: QueryClient,
  observers: ObserverState[],
  query: Query
) {
  observers.forEach((observerState) => {
    // Create a new options object without the unwanted properties
    const cleanedOptions = { ...observerState.options };
    // @ts-expect-error - This prevents infinite queries from being refetched in dev tools
    delete cleanedOptions?.initialPageParam;
    delete cleanedOptions?.behavior;
    // Replace the queryFn with a mock function to prevent errors when restoring error
    cleanedOptions.queryFn = mockQueryFn;
    const observer = new QueryObserver(queryClient, cleanedOptions);
    query.addObserver(observer);
  });
}

export interface DehydrateOptions {
  serializeData?: TransformerFn;
  shouldDehydrateMutation?: (mutation: Mutation) => boolean;
  shouldDehydrateQuery?: (query: Query) => boolean;
  shouldRedactErrors?: (error: unknown) => boolean;
}

export interface HydrateOptions {
  defaultOptions?: {
    deserializeData?: TransformerFn;
    queries?: QueryOptions;
    mutations?: MutationOptions<unknown, DefaultError, unknown, unknown>;
  };
}


================================================
FILE: src/components/external-dash/shared/types.ts
================================================
import {
  DefaultError,
  MutationMeta,
  MutationScope,
  MutationState,
  QueryKey,
  QueryMeta,
  QueryObserverOptions,
  QueryState,
  MutationKey,
} from "@tanstack/react-query";

// Define a simplified version of DehydratedState that both versions can work with
export interface SimpleDehydratedState {
  mutations: unknown[];
  queries: unknown[];
}

export interface SyncMessage {
  type: "dehydrated-state";
  state: DehydratedState;
  isOnlineManagerOnline: boolean;
  persistentDeviceId: string;
}

export interface DehydratedState {
  mutations: DehydratedMutation[];
  queries: DehydratedQuery[];
}

export interface DehydratedMutation {
  mutationId: number;
  mutationKey?: MutationKey;
  state: MutationState;
  meta?: MutationMeta;
  scope?: MutationScope;
}
export interface DehydratedQuery {
  queryHash: string;
  queryKey: QueryKey;
  state: QueryState;
  promise?: Promise<unknown>;
  meta?: QueryMeta;
  observers: ObserverState[];
  gcTime?: number;
}
export interface ObserverState<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey
> {
  queryHash: string;
  options: QueryObserverOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryData,
    TQueryKey
  >;
}


================================================
FILE: src/components/external-dash/types/ClientQuery.ts
================================================
export interface ClientQuery {
  deviceName: string;
}


================================================
FILE: src/components/external-dash/types/User.ts
================================================
export interface User {
  id: string;
  deviceName: string;
  deviceId: string; // Persisted device ID
  platform?: string; // Device platform (iOS, Android, Web)
  isConnected?: boolean; // Whether the device is currently connected
  extraDeviceInfo?: string; // json string of additional device information as key-value pairs
  envVariables?: string; // json string of environment variables from the mobile app
}


================================================
FILE: src/components/external-dash/useSyncQueriesWeb.ts
================================================
import {
  onlineManager,
  QueryClient,
  QueryKey,
  useQueryClient,
} from "@tanstack/react-query";
import { useEffect, useRef, useCallback } from "react";
import { Hydrate } from "./shared/hydration";
import { SyncMessage } from "./shared/types";
import useConnectedUsers from "./_hooks/useConnectedUsers";
import { User } from "./types/User";
import { Socket } from "socket.io-client";
import { logger, createDeviceLogger } from "./utils/logger";
import { useDevToolsEventHandler } from "./hooks/useDevToolsEventHandler";

/**
 * Query actions that can be performed on a query.
 * These actions are used to control query state on remote devices.
 */
type QueryActions =
  // Regular query actions
  | "ACTION-REFETCH" // Refetch a query without invalidating it
  | "ACTION-INVALIDATE" // Invalidate a query and trigger a refetch
  | "ACTION-RESET" // Reset a query to its initial state
  | "ACTION-REMOVE" // Remove a query from the cache
  | "ACTION-DATA-UPDATE" // Update a query's data manually
  // Error handling actions
  | "ACTION-TRIGGER-ERROR" // Manually trigger an error state
  | "ACTION-RESTORE-ERROR" // Restore from an error state
  // Loading state actions
  | "ACTION-TRIGGER-LOADING" // Manually trigger a loading state
  | "ACTION-RESTORE-LOADING" // Restore from a loading state
  | "success" // Internal success action
  | "ACTION-CLEAR-MUTATION-CACHE" // Clear the mutation cache
  | "ACTION-CLEAR-QUERY-CACHE"; // Clear the query cache

/**
 * Message structure for query actions sent from dashboard to devices
 */
export interface QueryActionMessage {
  queryHash: string; // Unique hash of the query
  queryKey: QueryKey; // Key array used to identify the query
  data: unknown; // Data payload (if applicable)
  action: QueryActions; // Action to perform
  deviceId: string; // Device ID to target
}

/**
 * Message structure for requesting initial state from devices
 */
export interface QueryRequestInitialStateMessage {
  targetDeviceId: string; // Device ID to request state from ('All' || device)
}

/**
 * Message structure for online manager actions sent from dashboard to devices
 */
export interface OnlineManagerMessage {
  action: "ACTION-ONLINE-MANAGER-ONLINE" | "ACTION-ONLINE-MANAGER-OFFLINE";
  targetDeviceId: string; // Device ID to target ('All' || device)
}

// --- Constants ---
const LOG_PREFIX = "[DASHBOARD]";
const INVALID_DEVICE_IDS = ["No devices available", "Please select a device"];

// Logger metadata types
interface BaseLogMetadata extends Partial<User> {
  queriesCount?: number;
  mutationsCount?: number;
  status?: "ONLINE" | "OFFLINE";
  connectionStatus?: "CONNECTED" | "DISCONNECTED";
  socketExists?: boolean;
  action?: QueryActions;
  queryHash?: string;
  queryKey?: QueryKey;
  validationError?: string;
  previousDevice?: {
    deviceName: string;
    deviceId: string;
  };
  newDevice?: {
    deviceName: string;
    deviceId: string;
    platform?: string;
  };
  // Additional fields for better debugging
  currentSelectedDeviceId?: string;
  currentSelectedDeviceName?: string;
  eventType?: string;
}

// --- Helper Functions ---

/** Checks if the provided device is valid for communication. */
const isValidDevice = (device: User | null): boolean => {
  return !!device && !INVALID_DEVICE_IDS.includes(device.deviceId);
};

/** Hydrates the query client with state received from a device */
const hydrateState = (
  queryClient: QueryClient,
  message: SyncMessage,
  deviceName: string
) => {
  const metadata: BaseLogMetadata = {
    deviceName,
    deviceId: message.persistentDeviceId,
    queriesCount: message.state.queries.length,
    mutationsCount: message.state.mutations.length,
  };
  logger.info(
    `${LOG_PREFIX} Hydrating state from device ${deviceName} `,
    metadata
  );

  Hydrate(queryClient, message.state, {
    defaultOptions: {
      queries: {
        staleTime: Infinity,
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
      } as any,
    },
  });
};

/** Sends a request for initial state to the target device. */
const requestInitialState = (socket: Socket, targetDevice: User) => {
  const deviceLogger = createDeviceLogger(targetDevice);
  deviceLogger.info(
    `${LOG_PREFIX} Requesting initial state from device: ${targetDevice.deviceName} (${targetDevice.deviceId})`
  );

  const message: QueryRequestInitialStateMessage = {
    targetDeviceId: targetDevice.deviceId,
  };
  socket.emit("request-initial-state", message);
};

/** Sends a query action message to the target device. */
const sendQueryAction = (
  socket: Socket,
  targetDevice: User,
  action: QueryActions,
  query: { queryHash: string; queryKey: QueryKey; state: { data: unknown } }
) => {
  const deviceLogger = createDeviceLogger(targetDevice);
  // Note: Since deviceLogger already has device context, we don't pass metadata as second param
  deviceLogger.info(
    `${LOG_PREFIX} Forwarding query action '${action}' to device: ${targetDevice.deviceName}`
  );

  const message: QueryActionMessage = {
    action,
    deviceId: targetDevice.deviceId,
    queryHash: query.queryHash,
    queryKey: query.queryKey,
    data: query.state.data,
  };
  socket.emit("query-action", message);
};

/** Sends an online status update message to the target device. */
const sendOnlineStatus = (
  socket: Socket,
  targetDevice: User,
  isOnline: boolean
) => {
  const deviceLogger = createDeviceLogger(targetDevice);
  const statusText = isOnline ? "ONLINE" : "OFFLINE";

  deviceLogger.info(
    `${LOG_PREFIX} Sending ${statusText} status update to device: ${targetDevice.deviceName}`
  );

  const message: OnlineManagerMessage = {
    action: isOnline
      ? "ACTION-ONLINE-MANAGER-ONLINE"
      : "ACTION-ONLINE-MANAGER-OFFLINE",
    targetDeviceId: targetDevice.deviceId,
  };
  socket.emit("online-manager", message);

  deviceLogger.debug(
    `${LOG_PREFIX} Online status message sent successfully to ${targetDevice.deviceName}`
  );
};

interface Props {
  targetDevice: User; // Currently selected device persistent ID
  allDevices: User[];
}
/**
 * Hook used by the dashboard to sync with and control device queries
 *
 * Handles:
 * - Requesting initial query state from devices
 * - Forwarding query actions to devices
 * - Processing query state updates from devices
 * - Tracking connected devices
 */
export function useSyncQueriesWeb({ targetDevice, allDevices }: Props) {
  const { socket } = useConnectedUsers();
  const isConnected = !!socket && socket.connected;

  const queryClient = useQueryClient();
  const selectedDeviceRef = useRef(targetDevice);

  // --- Callbacks ---

  // Callback to handle incoming query sync messages
  const handleQuerySync = useCallback(
    (message: SyncMessage) => {
      // Function that returns device from all devices based off deviceId
      function getDeviceFromDeviceId(deviceId: string) {
        return allDevices.find((device) => device.deviceId === deviceId);
      }

      const device = getDeviceFromDeviceId(message.persistentDeviceId);
      const deviceName = device?.deviceName || "Unknown Device";

      const metadata: BaseLogMetadata = {
        deviceName,
        deviceId: message.persistentDeviceId,
        platform: device?.platform,
        queriesCount: message.state.queries.length,
        mutationsCount: message.state.mutations.length,
      };
      logger.info(
        `${LOG_PREFIX} Received query sync from device: ${deviceName} `,
        metadata
      );

      if (message.type !== "dehydrated-state") {
        logger.warn(
          `${LOG_PREFIX} Received unexpected message type: ${message.type} from ${deviceName}`,
          metadata
        );
        return;
      }

      const currentSelectedDevice = selectedDeviceRef.current;
      const isFromSelectedDevice =
        currentSelectedDevice.deviceId === "All" ||
        message.persistentDeviceId === currentSelectedDevice.deviceId;

      if (isFromSelectedDevice) {
        if (message.isOnlineManagerOnline !== onlineManager.isOnline()) {
          const onlineMetadata: BaseLogMetadata = {
            status: message.isOnlineManagerOnline ? "ONLINE" : "OFFLINE",
            deviceName,
            deviceId: message.persistentDeviceId,
            platform: device?.platform,
          };
          const statusText = message.isOnlineManagerOnline
            ? "ONLINE"
            : "OFFLINE";
          logger.info(
            `${LOG_PREFIX} Syncing online status with device ${deviceName}: Setting dashboard to ${statusText}`,
            onlineMetadata
          );
          onlineManager.setOnline(message.isOnlineManagerOnline);
        }

        hydrateState(queryClient, message, deviceName);
      } else {
        const metadata: BaseLogMetadata = {
          deviceId: message.persistentDeviceId,
          deviceName,
          platform: device?.platform,
          currentSelectedDeviceId: currentSelectedDevice.deviceId,
          currentSelectedDeviceName: currentSelectedDevice.deviceName,
        };
        logger.debug(
          `${LOG_PREFIX} Ignoring sync from non-selected device: ${deviceName} (selected is ${currentSelectedDevice.deviceName})`,
          metadata
        );
      }
    },
    [queryClient, allDevices]
  );

  // --- Effects ---

  // Effect to handle device selection changes
  useEffect(() => {
    const previousDevice = selectedDeviceRef.current;
    selectedDeviceRef.current = targetDevice; // Update the ref immediately

    if (!isConnected || !socket) {
      const metadata: BaseLogMetadata = {
        status: isConnected ? "ONLINE" : "OFFLINE",
        deviceName: targetDevice.deviceName,
        deviceId: targetDevice.deviceId,
        platform: targetDevice.platform,
        connectionStatus: isConnected ? "CONNECTED" : "DISCONNECTED",
        socketExists: !!socket,
      };
      logger.warn(
        `${LOG_PREFIX} Device selection changed to ${targetDevice.deviceName} but connection unavailable`,
        metadata
      );
      return;
    }

    // Only log/clear/request if the device ID actually changed
    if (previousDevice.deviceId === targetDevice.deviceId) {
      return;
    }

    const metadata: BaseLogMetadata = {
      previousDevice: {
        deviceName: previousDevice.deviceName,
        deviceId: previousDevice.deviceId,
      },
      newDevice: {
        deviceName: targetDevice.deviceName,
        deviceId: targetDevice.deviceId,
        platform: targetDevice.platform,
      },
    };
    logger.info(
      `${LOG_PREFIX} Device selection changed from '${previousDevice.deviceName}' to '${targetDevice.deviceName}'`,
      metadata
    );

    if (isValidDevice(targetDevice)) {
      const stateMetadata: BaseLogMetadata = {
        deviceName: targetDevice.deviceName,
        deviceId: targetDevice.deviceId,
        platform: targetDevice.platform,
      };
      logger.info(
        `${LOG_PREFIX} Clearing dashboard query cache and requesting fresh state from '${targetDevice.deviceName}'`,
        stateMetadata
      );
      queryClient.clear();
      requestInitialState(socket, targetDevice);
    } else {
      const errorMetadata: BaseLogMetadata = {
        deviceName: targetDevice.deviceName,
        deviceId: targetDevice.deviceId,
        platform: targetDevice.platform,
        validationError: "Invalid device ID",
      };
      logger.warn(
        `${LOG_PREFIX} Invalid device selection '${targetDevice.deviceName}' - skipping state request`,
        errorMetadata
      );
    }
  }, [targetDevice, isConnected, socket, queryClient]);

  // Effect to set up the main query-sync listener
  useEffect(() => {
    if (!isConnected || !socket) {
      const metadata: BaseLogMetadata = {
        status: "OFFLINE",
        connectionStatus: isConnected ? "CONNECTED" : "DISCONNECTED",
        socketExists: !!socket,
      };
      logger.warn(
        `${LOG_PREFIX} Cannot setup query-sync listeners - no socket connection available`,
        metadata
      );
      return;
    }

    logger.info(
      `${LOG_PREFIX} Setting up query-sync event listener for incoming device updates`
    );
    socket.on("query-sync", handleQuerySync);

    return () => {
      logger.info(`${LOG_PREFIX} Cleaning up query-sync event listener`);
      socket.off("query-sync", handleQuerySync);
    };
  }, [isConnected, socket, handleQuerySync]);

  // Effect to handle online manager status synchronization
  useEffect(() => {
    if (!isConnected || !socket) return;

    const onlineManagerUnsubscribe = onlineManager.subscribe(
      (isOnline: boolean) => {
        const currentSelectedDevice = selectedDeviceRef.current;
        const statusText = isOnline ? "ONLINE" : "OFFLINE";
        const metadata: BaseLogMetadata = {
          status: statusText,
          deviceName: currentSelectedDevice?.deviceName,
          deviceId: currentSelectedDevice?.deviceId,
        };
        logger.info(
          `${LOG_PREFIX} Dashboard online status changed to ${statusText} - syncing with device`,
          metadata
        );

        if (isValidDevice(currentSelectedDevice)) {
          sendOnlineStatus(socket, currentSelectedDevice, isOnline);
        } else {
          const errorMetadata: BaseLogMetadata = {
            status: statusText,
            deviceId: currentSelectedDevice?.deviceId,
            deviceName: currentSelectedDevice?.deviceName,
            validationError: "No valid device selected",
          };
          logger.warn(
            `${LOG_PREFIX} Cannot sync ${statusText} status - no valid device selected`,
            errorMetadata
          );
        }
      }
    );

    return () => {
      logger.info(`${LOG_PREFIX} Cleaning up online manager subscription`);
      onlineManagerUnsubscribe();
    };
  }, [isConnected, socket]); // Depends on connection status and socket instance

  // Effect to handle query cache changes for manual updates
  useEffect(() => {
    if (!isConnected || !socket) return;

    const queryCache = queryClient.getQueryCache();
    logger.info(
      `${LOG_PREFIX} Setting up query cache listener for manual data updates`
    );

    const querySubscription = queryCache.subscribe((event) => {
      const currentSelectedDevice = selectedDeviceRef.current;

      if (!isValidDevice(currentSelectedDevice)) {
        const logMetadata: BaseLogMetadata = {
          eventType: event.type,
          deviceId: currentSelectedDevice?.deviceId,
          deviceName: currentSelectedDevice?.deviceName,
        };
        logger.debug(
          `${LOG_PREFIX} Query cache event ignored - no valid device selected`,
          logMetadata
        );
        return;
      }

      // Process query cache events
      if (event.type === "updated") {
        const actionType = event.action.type as QueryActions;
        const queryLogMetadata: BaseLogMetadata = {
          queryHash: event.query.queryHash,
          queryKey: event.query.queryKey,
          deviceName: currentSelectedDevice.deviceName,
          deviceId: currentSelectedDevice.deviceId,
          action: actionType,
        };

        // Handle manual data updates specifically
        if (actionType === "success") {
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-expect-error This 'manual' property might exist based on usage pattern
          if (event.action.manual) {
            logger.debug(
              `${LOG_PREFIX} Forwarding manual data update to device: ${currentSelectedDevice.deviceName}`,
              queryLogMetadata
            );
            sendQueryAction(
              socket,
              currentSelectedDevice,
              "ACTION-DATA-UPDATE",
              event.query
            );
          }
        }
      }
    });

    return () => {
      logger.info(`${LOG_PREFIX} Cleaning up query cache subscription`);
      querySubscription();
    };
  }, [isConnected, socket, queryClient]);

  // Memoize the action handler
  const handleQueryAction = useCallback(
    (socket: Socket, targetDevice: User, action: string, query: any) => {
      if (!isValidDevice(targetDevice)) {
        logger.debug(
          `${LOG_PREFIX} Query action ignored - no valid device selected`,
          {
            deviceId: targetDevice?.deviceId,
            deviceName: targetDevice?.deviceName,
          }
        );
        return;
      }

      const queryLogMetadata = {
        queryHash: query.queryHash,
        queryKey: query.queryKey,
        deviceName: targetDevice.deviceName,
        deviceId: targetDevice.deviceId,
        action,
      };

      logger.debug(
        `${LOG_PREFIX} Forwarding ${action} to device: ${targetDevice.deviceName}`,
        queryLogMetadata
      );

      sendQueryAction(socket, targetDevice, action as QueryActions, query);
    },
    []
  );

  // Debug log for dependencies
  useEffect(() => {
    console.log("🔍 SyncQueriesWeb Dependencies:", {
      isConnected,
      hasSocket: !!socket,
      targetDevice,
      handleQueryAction: !!handleQueryAction,
    });
  }, [isConnected, socket, targetDevice, handleQueryAction]);

  // Use the dev tools event handler for explicit actions
  useDevToolsEventHandler({
    isConnected,
    socket,
    selectedDevice: targetDevice,
    sendQueryAction: handleQueryAction,
  });

  // Return connection status (or potentially other state if needed later)
  return { isConnected };
}


================================================
FILE: src/components/external-dash/utils/devToolsEvents.ts
================================================
import { logger } from "./logger";

export type DevToolsActionType =
  | "REFETCH"
  | "INVALIDATE"
  | "RESET"
  | "REMOVE"
  | "TRIGGER_ERROR"
  | "RESTORE_ERROR"
  | "TRIGGER_LOADING"
  | "RESTORE_LOADING"
  | "CLEAR_MUTATION_CACHE"
  | "CLEAR_QUERY_CACHE";

interface DevToolsEventDetail {
  type: DevToolsActionType;
  queryHash?: string | undefined;
  metadata?: Record<string, unknown> | undefined;
}

export const DEV_TOOLS_EVENT = "@tanstack/query-devtools-event";

// For the external dev tools to use in their onClick handlers
export const sendDevToolsEvent = ({
  type,
  queryHash,
  metadata,
}: DevToolsEventDetail) => {
  logger.debug(
    `📤 Sending dev tools event - Type: ${type}, QueryHash: ${queryHash}`
  );
  const event = new CustomEvent<DevToolsEventDetail>(DEV_TOOLS_EVENT, {
    detail: {
      type,
      queryHash: queryHash ?? undefined,
      metadata: metadata ?? undefined,
    },
    bubbles: true,
    cancelable: true,
  });
  window.dispatchEvent(event);
  logger.debug("✅ Event dispatched successfully");
};

type DevToolsEventCallback = (
  type: DevToolsActionType,
  queryHash: string | undefined,
  metadata: Record<string, unknown> | undefined
) => void;

// For our app to listen to events
export const onDevToolsEvent = (callback: DevToolsEventCallback) => {
  logger.debug("🎧 Setting up dev tools event listener for " + DEV_TOOLS_EVENT);

  const handler = (event: CustomEvent<DevToolsEventDetail>) => {
    const { type, queryHash, metadata } = event.detail;
    logger.debug(
      `📥 Received dev tools event - Type: ${type}, QueryHash: ${queryHash}`
    );
    callback(type, queryHash ?? undefined, metadata ?? undefined);
  };

  window.addEventListener(DEV_TOOLS_EVENT, handler as EventListener);

  return () => {
    logger.debug("🛑 Removing dev tools event listener");
    window.removeEventListener(DEV_TOOLS_EVENT, handler as EventListener);
  };
};


================================================
FILE: src/components/external-dash/utils/logStore.ts
================================================
import { create } from "zustand";

export type LogLevel = "info" | "error" | "warn" | "debug";

export interface LogEntry {
  id: string;
  timestamp: Date;
  message: string;
  level: LogLevel;
  deviceId?: string;
  deviceName?: string;
  platform?: string;
}

interface LogState {
  logs: LogEntry[];
  addLog: (entry: Omit<LogEntry, "id" | "timestamp">) => void;
  clearLogs: () => void;
  maxLogs: number;
  setMaxLogs: (max: number) => void;
  isEnabled: boolean;
  setIsEnabled: (enabled: boolean) => void;
  isVisible: boolean;
  setIsVisible: (visible: boolean) => void;
}

export const useLogStore = create<LogState>((set) => ({
  logs: [] as LogEntry[],
  maxLogs: 500, // Default max logs to store
  isEnabled: true, // Default enabled
  isVisible: false, // Default hidden

  addLog: (entry: Omit<LogEntry, "id" | "timestamp">) =>
    set((state: LogState) => {
      // If logging is disabled, ignore the log
      if (!state.isEnabled) return state;

      const newLog: LogEntry = {
        id: crypto.randomUUID(),
        timestamp: new Date(),
        ...entry,
      };

      // Add new log and trim if exceeding max
      const newLogs = [newLog, ...state.logs];
      if (newLogs.length > state.maxLogs) {
        newLogs.length = state.maxLogs;
      }

      return { logs: newLogs };
    }),

  clearLogs: () => set({ logs: [] }),

  setMaxLogs: (max: number) =>
    set((state: LogState) => {
      // Trim logs if needed when reducing max
      const logs = state.logs.slice(0, max);
      return { maxLogs: max, logs };
    }),

  setIsEnabled: (enabled: boolean) => set({ isEnabled: enabled }),
  setIsVisible: (visible: boolean) => set({ isVisible: visible }),
}));

// Helper log functions
export const logInfo = (
  message: string,
  deviceInfo?: { deviceId?: string; deviceName?: string; platform?: string }
) => {
  useLogStore.getState().addLog({
    message,
    level: "info",
    ...deviceInfo,
  });
};

export const logError = (
  message: string,
  deviceInfo?: { deviceId?: string; deviceName?: string; platform?: string }
) => {
  useLogStore.getState().addLog({
    message,
    level: "error",
    ...deviceInfo,
  });
};

export const logWarn = (
  message: string,
  deviceInfo?: { deviceId?: string; deviceName?: string; platform?: string }
) => {
  useLogStore.getState().addLog({
    message,
    level: "warn",
    ...deviceInfo,
  });
};

export const logDebug = (
  message: string,
  deviceInfo?: { deviceId?: string; deviceName?: string; platform?: string }
) => {
  useLogStore.getState().addLog({
    message,
    level: "debug",
    ...deviceInfo,
  });
};


================================================
FILE: src/components/external-dash/utils/logger.ts
================================================
import { logInfo, logError, logWarn, logDebug } from "./logStore";
import { User } from "../types/User";

/**
 * A logger utility that both logs to the console and to our LogStore for display in the UI
 */
export const logger = {
  /**
   * Log informational message
   */
  info: (message: string, deviceInfo?: Partial<User>) => {
    console.info(message);
    logInfo(message, deviceInfo);
  },

  /**
   * Log warning message
   */
  warn: (message: string, deviceInfo?: Partial<User>) => {
    console.warn(message);
    logWarn(message, deviceInfo);
  },

  /**
   * Log error message
   */
  error: (message: string, deviceInfo?: Partial<User>) => {
    console.error(message);
    logError(message, deviceInfo);
  },

  /**
   * Log debug message
   */
  debug: (message: string, deviceInfo?: Partial<User>) => {
    console.debug(message);
    logDebug(message, deviceInfo);
  },

  /**
   * Regular log - maps to info level
   */
  log: (message: string, deviceInfo?: Partial<User>) => {
    console.log(message);
    logInfo(message, deviceInfo);
  },
};

/**
 * Create a logger instance pre-configured with a specific device
 */
export const createDeviceLogger = (device: User) => {
  const deviceInfo = {
    deviceId: device.deviceId,
    deviceName: device.deviceName,
    platform: device.platform,
  };

  return {
    info: (message: string) => logger.info(message, deviceInfo),
    warn: (message: string) => logger.warn(message, deviceInfo),
    error: (message: string) => logger.error(message, deviceInfo),
    debug: (message: string) => logger.debug(message, deviceInfo),
    log: (message: string) => logger.log(message, deviceInfo),
  };
};


================================================
FILE: src/components/external-dash/utils/platformUtils.tsx
================================================
import React from "react";

// --- 1. Extracted SVG Icon Components (SRP, KISS) ---

interface IconProps {
  className?: string;
  // Allow other SVG props like fill, viewBox etc., if needed later
  [key: string]: any; // Allow passing down arbitrary props
}

const defaultIconProps = {
  viewBox: "0 0 24 24",
  fill: "currentColor",
  className: "w-3 h-3", // Default size, can be overridden by props
};

const IosIcon: React.FC<IconProps> = (props) => (
  <svg {...defaultIconProps} {...props}>
    <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
  </svg>
);

const AndroidIcon: React.FC<IconProps> = (props) => (
  <svg {...defaultIconProps} {...props}>
    <path d="M16.61 15.15c-.46 0-.84-.37-.84-.83s.37-.83.84-.83.83.37.83.83-.37.83-.83.83m-9.22 0c-.46 0-.83-.37-.83-.83s.37-.83.83-.83.84.37.84.83-.37.83-.84.83m9.42-5.89l1.67-2.89c.09-.17.03-.38-.13-.47-.17-.09-.38-.03-.47.13l-1.69 2.93A9.973 9.973 0 0012 7.75c-1.89 0-3.63.52-5.19 1.37L5.12 6.19c-.09-.17-.3-.22-.47-.13-.17.09-.22.3-.13.47l1.67 2.89C3.44 11.15 1.62 14.56 1.62 18h20.76c0-3.44-1.82-6.85-4.57-8.74z" />
  </svg>
);

const WebIcon: React.FC<IconProps> = (props) => (
  <svg {...defaultIconProps} {...props}>
    <path d="M16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2m-5.15 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 01-4.33 3.56M14.34 14H9.66c-.1-.66-.16-1.32-.16-2 0-.68.06-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2M12 19.96c-.83-1.2-1.5-2.53-1.91-3.96h3.82c-.41 1.43-1.08 2.76-1.91 3.96M8 8H5.08A7.923 7.923 0 019.4 4.44C8.8 5.55 8.35 6.75 8 8m-2.92 8H8c.35 1.25.8 2.45 1.4 3.56A8.008 8.008 0 015.08 16m-.82-2C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2M12 4.03c.83 1.2 1.5 2.54 1.91 3.97h-3.82c.41-1.43 1.08-2.77 1.91-3.97M18.92 8h-2.95a15.65 15.65 0 00-1.38-3.56c1.84.63 3.37 1.9 4.33 3.56M12 2C6.47 2 2 6.5 2 12a10 10 0 0010 10 10 10 0 0010-10A10 10 0 0012 2z" />
  </svg>
);

// Updated Android TV Icon - Represents a modern Smart TV screen
const AndroidTvIcon: React.FC<IconProps> = (props) => (
  <svg {...defaultIconProps} {...props}>
    <path d="M21 17H3V5h18m0-2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
  </svg>
);

// Updated Apple TV Icon - Using the Apple logo
const AppleTvIcon: React.FC<IconProps> = (props) => (
  <svg {...defaultIconProps} {...props} viewBox="0 0 24 24">
    {/* Custom viewBox for Apple logo */}
    <path d="M18.71,19.5c-0.83,1.24-1.71,2.45-3.05,2.47c-1.34,0.03-1.77-0.79-3.29-0.79c-1.53,0-2,0.77-3.27,0.82 C7.79,22.55,6.79,21.28,5.95,20C4.25,17,2.94,12.45,4.7,9.39c0.87-1.52,2.43-2.48,4.12-2.51c1.28-0.02,2.5,0.87,3.29,0.87 c0.78,0,2.26-1.07,3.81-0.91c0.65,0.03,2.47,0.26,3.64,1.98c-0.09,0.06-2.17,1.28-2.15,3.81C17.44,15.67,20.06,16.68,20.09,16.69 C20.06,16.76,19.65,18.06,18.71,19.5z M13,3.5c0.73-0.83,1.94-1.46,2.94-1.5c0.13,1.17-0.34,2.35-1.04,3.19 C14.21,5.94,13.07,6.6,11.95,6.5C11.8,5.35,12.41,4.15,13,3.5z" />
  </svg>
);

// Default Icon (Generic Mobile Device)
const DefaultDeviceIcon: React.FC<IconProps> = (props) => (
  <svg {...defaultIconProps} {...props}>
    <path d="M17.25 18H6.75V4h10.5M14 21h-4v-1h4m2-19H8C6.34 1 5 2.34 5 4v16c0 1.66 1.34 3 3 3h8c1.66 0 3-1.34 3-3V4c0-1.66-1.34-3-3-3z" />
  </svg>
);

// --- 2. Central Platform Configuration (DRY, Convention over Config) ---

interface PlatformConfig {
  displayName: string;
  bgColor: string;
  textColor: string;
  IconComponent: React.FC<IconProps>;
}

const platformConfiguration: Record<string, PlatformConfig> = {
  ios: {
    displayName: "iOS",
    bgColor: "bg-blue-900/30 text-blue-200",
    textColor: "text-blue-300",
    IconComponent: IosIcon,
  },
  android: {
    displayName: "Android",
    bgColor: "bg-green-900/30 text-green-200",
    textColor: "text-green-300",
    IconComponent: AndroidIcon,
  },
  web: {
    displayName: "Web",
    bgColor: "bg-cyan-900/30 text-cyan-200",
    textColor: "text-cyan-300",
    IconComponent: WebIcon,
  },
  tv: {
    displayName: "Android TV",
    bgColor: "bg-green-900/30 text-green-200",
    textColor: "text-green-300",
    IconComponent: AndroidTvIcon,
  },
  tvos: {
    displayName: "Apple TV",
    bgColor: "bg-purple-900/30 text-purple-200",
    textColor: "text-purple-300",
    IconComponent: AppleTvIcon,
  },
  // Default configuration for unknown platforms
  default: {
    displayName: "Device",
    bgColor: "bg-[#1D1D1F]/60 text-[#F5F5F7]",
    textColor: "text-[#F5F5F7]",
    IconComponent: DefaultDeviceIcon,
  },
};

// Helper to get platform config safely
const getPlatformConfig = (
  platform: string | undefined | null
): PlatformConfig => {
  const normalizedPlatform = platform?.toLowerCase() || "";
  return (
    platformConfiguration[normalizedPlatform] || platformConfiguration.default
  );
};

// --- 3. Refactored PlatformIcon Component (Composition, KISS) ---

export const PlatformIcon: React.FC<{
  platform: string;
  className?: string;
}> = ({
  platform,
  className, // Allow overriding className
  ...rest // Pass any other props down
}) => {
  const config = getPlatformConfig(platform);
  const { IconComponent } = config;

  // Combine default className with passed className if provided
  const finalClassName = className ?? defaultIconProps.className;

  return <IconComponent className={finalClassName} {...rest} />;
};

/**
 * Gets the combined background and text color CSS classes for a platform.
 */
export const getPlatformColorClasses = (platform: string): string => {
  const config = getPlatformConfig(platform);
  return `${config.bgColor}`;
};

/**
 * Gets the text color CSS class for a platform.
 */
export const getPlatformTextColor = (platform: string): string => {
  const config = getPlatformConfig(platform);
  return config.textColor;
};

/**
 * Gets the background color CSS class for a platform.
 */
export const getPlatformBgColor = (platform: string): string => {
  const config = getPlatformConfig(platform);
  return config.bgColor;
};

/**
 * Gets the display-friendly name for a platform.
 */
export const getDisplayPlatform = (platform: string): string => {
  const config = getPlatformConfig(platform);
  // Return original platform string if it wasn't found and we used default,
  // unless the original platform string was empty/null.
  if (config === platformConfiguration.default && platform) {
    return platform;
  }
  return config.displayName;
};


================================================
FILE: src/components/external-dash/utils/storageQueryKeys.ts
================================================
/**
 * Centralized storage query keys for all storage hooks
 * This ensures consistency across MMKV, AsyncStorage, and SecureStorage hooks
 * and allows easy modification of the base storage key in one place
 */
export const storageQueryKeys = {
  /**
   * Base storage key - change this to update all storage-related queries
   */
  base: () => ["#storage"] as const,

  /**
   * MMKV storage query keys
   */
  mmkv: {
    root: () => [...storageQueryKeys.base(), "mmkv"] as const,
    key: (key: string) => [...storageQueryKeys.mmkv.root(), key] as const,
    all: () => [...storageQueryKeys.mmkv.root(), "all"] as const,
  },

  /**
   * AsyncStorage query keys
   */
  async: {
    root: () => [...storageQueryKeys.base(), "async"] as const,
    key: (key: string) => [...storageQueryKeys.async.root(), key] as const,
    all: () => [...storageQueryKeys.async.root(), "all"] as const,
  },

  /**
   * SecureStorage query keys
   */
  secure: {
    root: () => [...storageQueryKeys.base(), "secure"] as const,
    key: (key: string) => [...storageQueryKeys.secure.root(), key] as const,
    all: () => [...storageQueryKeys.secure.root(), "all"] as const,
  },
} as const;

/**
 * Storage types that can be enabled/disabled
 */
export type StorageType = "mmkv" | "async" | "secure";

/**
 * Check if a query key matches any of the storage patterns
 */
export function isStorageQuery(queryKey: readonly unknown[]): boolean {
  if (!Array.isArray(queryKey) || queryKey.length === 0) {
    return false;
  }

  return queryKey[0] === "#storage";
}

/**
 * Get the storage type from a query key
 */
export function getStorageType(
  queryKey: readonly unknown[]
): StorageType | null {
  if (!isStorageQuery(queryKey) || queryKey.length < 2) {
    return null;
  }

  const storageType = queryKey[1];
  if (
    storageType === "mmkv" ||
    storageType === "async" ||
    storageType === "secure"
  ) {
    return storageType;
  }

  return null;
}

/**
 * Check if a storage type should be filtered out based on enabled storage types
 */
export function shouldFilterStorageQuery(
  queryKey: readonly unknown[],
  enabledStorageTypes: Set<StorageType>
): boolean {
  const storageType = getStorageType(queryKey);
  if (storageType === null) {
    return false; // Not a storage query, don't filter
  }

  return !enabledStorageTypes.has(storageType);
}


================================================
FILE: src/components/external-dash/utils/storageStore.ts
================================================
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { StorageType } from "./storageQueryKeys";

interface StorageStore {
  enabledStorageTypes: Set<StorageType>;
  setStorageTypeEnabled: (storageType: StorageType, enabled: boolean) => void;
  isStorageTypeEnabled: (storageType: StorageType) => boolean;
  clearAllStorageTypes: () => void;
  enableAllStorageTypes: () => void;
}

export const useStorageStore = create<StorageStore>()(
  persist(
    (set, get) => ({
      // By default, all storage types are enabled
      enabledStorageTypes: new Set<StorageType>(["mmkv", "async", "secure"]),

      setStorageTypeEnabled: (storageType: StorageType, enabled: boolean) => {
        set((state) => {
          const newEnabledTypes = new Set(state.enabledStorageTypes);
          if (enabled) {
            newEnabledTypes.add(storageType);
          } else {
            newEnabledTypes.delete(storageType);
          }
          return { enabledStorageTypes: newEnabledTypes };
        });
      },

      isStorageTypeEnabled: (storageType: StorageType) => {
        return get().enabledStorageTypes.has(storageType);
      },

      clearAllStorageTypes: () => {
        set({ enabledStorageTypes: new Set() });
      },

      enableAllStorageTypes: () => {
        set({ enabledStorageTypes: new Set(["mmkv", "async", "secure"]) });
      },
    }),
    {
      name: "storage-preferences",
      // Transform Set to Array for storage
      partialize: (state) => ({
        ...state,
        enabledStorageTypes: Array.from(state.enabledStorageTypes),
      }),
      // Transform Array back to Set when loading
      onRehydrateStorage: () => (state) => {
        if (
          state &&
Download .txt
gitextract_s7vy5tt0/

├── .eslintignore
├── .eslintrc.json
├── .github/
│   └── workflows/
│       └── build.yml
├── .gitignore
├── .npmrc
├── DEVELOPMENT.md
├── GITHUB_RELEASE.md
├── README.md
├── RELEASE.md
├── assets/
│   └── icon.icns
├── auto-release.sh
├── build-and-pack.sh
├── build-macos.sh
├── config.js
├── copy-to-desktop.sh
├── entitlements.plist
├── forge.config.ts
├── forge.env.d.ts
├── index.html
├── package.json
├── postcss.config.js
├── release-version.sh
├── socket-client.js
├── src/
│   ├── auto-updater.ts
│   ├── components/
│   │   ├── App.tsx
│   │   └── external-dash/
│   │       ├── Dash.tsx
│   │       ├── DeviceSelection.tsx
│   │       ├── LogConsole.tsx
│   │       ├── Main.tsx
│   │       ├── NoDevicesConnected.tsx
│   │       ├── UserInfo/
│   │       │   ├── DeviceSpecificationsSection.tsx
│   │       │   ├── EnvironmentVariablesSection.tsx
│   │       │   ├── InfoRow.tsx
│   │       │   ├── StorageControlsSection.tsx
│   │       │   ├── TargetGlowEffect.tsx
│   │       │   ├── UserCardDetails.tsx
│   │       │   ├── UserCardHeader.tsx
│   │       │   └── index.ts
│   │       ├── UserInfo.tsx
│   │       ├── _hooks/
│   │       │   └── useConnectedUsers.ts
│   │       ├── hooks/
│   │       │   └── useDevToolsEventHandler.ts
│   │       ├── providers.tsx
│   │       ├── shared/
│   │       │   ├── hydration.ts
│   │       │   └── types.ts
│   │       ├── types/
│   │       │   ├── ClientQuery.ts
│   │       │   └── User.ts
│   │       ├── useSyncQueriesWeb.ts
│   │       └── utils/
│   │           ├── devToolsEvents.ts
│   │           ├── logStore.ts
│   │           ├── logger.ts
│   │           ├── platformUtils.tsx
│   │           ├── storageQueryKeys.ts
│   │           └── storageStore.ts
│   ├── config.ts
│   ├── index.css
│   ├── main.ts
│   ├── preload.ts
│   ├── react-query-external-sync/
│   │   ├── README.md
│   │   ├── User.ts
│   │   ├── hydration.ts
│   │   ├── index.ts
│   │   ├── platformUtils.ts
│   │   ├── types.ts
│   │   ├── useMySocket.ts
│   │   ├── useSyncQueries.ts
│   │   └── utils/
│   │       └── logger.ts
│   ├── renderer.ts
│   ├── renderer.tsx
│   ├── server/
│   │   └── socketHandle.ts
│   ├── server.ts
│   └── types.d.ts
├── tailwind.config.js
├── tanstack-query-devtools-5.74.7.tgz
├── tanstack-react-query-devtools-5.75.7.tgz
├── tsconfig.json
├── vite.main.config.ts
├── vite.preload.config.ts
└── vite.renderer.config.ts
Download .txt
SYMBOL INDEX (95 symbols across 37 files)

FILE: config.js
  constant SERVER_PORT (line 7) | const SERVER_PORT = 42831;
  constant CLIENT_URL (line 10) | const CLIENT_URL = `http://localhost:${SERVER_PORT}`;

FILE: socket-client.js
  function sendMessage (line 52) | function sendMessage(message) {

FILE: src/auto-updater.ts
  function setupAutoUpdater (line 8) | function setupAutoUpdater() {

FILE: src/components/external-dash/Dash.tsx
  type DashProps (line 87) | interface DashProps {

FILE: src/components/external-dash/DeviceSelection.tsx
  type Props (line 5) | interface Props {
  type DeviceOption (line 11) | interface DeviceOption {

FILE: src/components/external-dash/LogConsole.tsx
  type LogEntryItemProps (line 30) | interface LogEntryItemProps {
  type DeviceOption (line 69) | interface DeviceOption {
  type LogConsoleProps (line 77) | interface LogConsoleProps {

FILE: src/components/external-dash/Main.tsx
  function Main (line 7) | function Main() {

FILE: src/components/external-dash/UserInfo.tsx
  type Props (line 9) | interface Props {

FILE: src/components/external-dash/UserInfo/DeviceSpecificationsSection.tsx
  type Props (line 4) | interface Props {

FILE: src/components/external-dash/UserInfo/EnvironmentVariablesSection.tsx
  type Props (line 4) | interface Props {

FILE: src/components/external-dash/UserInfo/InfoRow.tsx
  type Props (line 4) | interface Props {

FILE: src/components/external-dash/UserInfo/StorageControlsSection.tsx
  type StorageControlsSectionProps (line 10) | interface StorageControlsSectionProps {

FILE: src/components/external-dash/UserInfo/TargetGlowEffect.tsx
  type Props (line 3) | interface Props {

FILE: src/components/external-dash/UserInfo/UserCardDetails.tsx
  type Props (line 7) | interface Props {

FILE: src/components/external-dash/UserInfo/UserCardHeader.tsx
  type Props (line 9) | interface Props {

FILE: src/components/external-dash/_hooks/useConnectedUsers.ts
  function useConnectedUsers (line 6) | function useConnectedUsers() {

FILE: src/components/external-dash/hooks/useDevToolsEventHandler.ts
  type UseDevToolsEventHandlerProps (line 10) | interface UseDevToolsEventHandlerProps {

FILE: src/components/external-dash/providers.tsx
  type Props (line 5) | interface Props {
  function Providers (line 9) | function Providers({ children }: Props) {

FILE: src/components/external-dash/shared/hydration.ts
  type TransformerFn (line 15) | type TransformerFn = (data: any) => any;
  function defaultTransformerFn (line 16) | function defaultTransformerFn(data: any): any {
  function Hydrate (line 24) | function Hydrate(
  function cleanUpObservers (line 132) | function cleanUpObservers(query: Query) {
  function recreateObserver (line 139) | function recreateObserver(
  type DehydrateOptions (line 157) | interface DehydrateOptions {
  type HydrateOptions (line 164) | interface HydrateOptions {

FILE: src/components/external-dash/shared/types.ts
  type SimpleDehydratedState (line 14) | interface SimpleDehydratedState {
  type SyncMessage (line 19) | interface SyncMessage {
  type DehydratedState (line 26) | interface DehydratedState {
  type DehydratedMutation (line 31) | interface DehydratedMutation {
  type DehydratedQuery (line 38) | interface DehydratedQuery {
  type ObserverState (line 47) | interface ObserverState<

FILE: src/components/external-dash/types/ClientQuery.ts
  type ClientQuery (line 1) | interface ClientQuery {

FILE: src/components/external-dash/types/User.ts
  type User (line 1) | interface User {

FILE: src/components/external-dash/useSyncQueriesWeb.ts
  type QueryActions (line 20) | type QueryActions =
  type QueryActionMessage (line 40) | interface QueryActionMessage {
  type QueryRequestInitialStateMessage (line 51) | interface QueryRequestInitialStateMessage {
  type OnlineManagerMessage (line 58) | interface OnlineManagerMessage {
  constant LOG_PREFIX (line 64) | const LOG_PREFIX = "[DASHBOARD]";
  constant INVALID_DEVICE_IDS (line 65) | const INVALID_DEVICE_IDS = ["No devices available", "Please select a dev...
  type BaseLogMetadata (line 68) | interface BaseLogMetadata extends Partial<User> {
  type Props (line 189) | interface Props {
  function useSyncQueriesWeb (line 202) | function useSyncQueriesWeb({ targetDevice, allDevices }: Props) {

FILE: src/components/external-dash/utils/devToolsEvents.ts
  type DevToolsActionType (line 3) | type DevToolsActionType =
  type DevToolsEventDetail (line 15) | interface DevToolsEventDetail {
  constant DEV_TOOLS_EVENT (line 21) | const DEV_TOOLS_EVENT = "@tanstack/query-devtools-event";
  type DevToolsEventCallback (line 45) | type DevToolsEventCallback = (

FILE: src/components/external-dash/utils/logStore.ts
  type LogLevel (line 3) | type LogLevel = "info" | "error" | "warn" | "debug";
  type LogEntry (line 5) | interface LogEntry {
  type LogState (line 15) | interface LogState {

FILE: src/components/external-dash/utils/platformUtils.tsx
  type IconProps (line 5) | interface IconProps {
  type PlatformConfig (line 59) | interface PlatformConfig {

FILE: src/components/external-dash/utils/storageQueryKeys.ts
  type StorageType (line 43) | type StorageType = "mmkv" | "async" | "secure";
  function isStorageQuery (line 48) | function isStorageQuery(queryKey: readonly unknown[]): boolean {
  function getStorageType (line 59) | function getStorageType(
  function shouldFilterStorageQuery (line 81) | function shouldFilterStorageQuery(

FILE: src/components/external-dash/utils/storageStore.ts
  type StorageStore (line 5) | interface StorageStore {

FILE: src/config.ts
  constant SERVER_PORT (line 6) | const SERVER_PORT = 42831;
  constant SOCKET_CONFIG (line 9) | const SOCKET_CONFIG = {
  constant CLIENT_URL (line 21) | const CLIENT_URL = `http://localhost:${SERVER_PORT}`;

FILE: src/react-query-external-sync/User.ts
  type User (line 1) | interface User {

FILE: src/react-query-external-sync/hydration.ts
  type TransformerFn (line 17) | type TransformerFn = (data: unknown) => unknown;
  function Dehydrate (line 19) | function Dehydrate(client: QueryClient): DehydratedState {
  type DehydrateOptions (line 32) | interface DehydrateOptions {
  type HydrateOptions (line 39) | interface HydrateOptions {
  function dehydrateMutation (line 47) | function dehydrateMutation(mutation: Mutation): DehydratedMutation {
  function dehydrateQuery (line 57) | function dehydrateQuery(query: Query): DehydratedQuery {

FILE: src/react-query-external-sync/platformUtils.ts
  type PlatformOS (line 10) | type PlatformOS =
  type StorageInterface (line 23) | interface StorageInterface {

FILE: src/react-query-external-sync/types.ts
  type SimpleDehydratedState (line 13) | interface SimpleDehydratedState {
  type SyncMessage (line 18) | interface SyncMessage {
  type DehydratedState (line 25) | interface DehydratedState {
  type DehydratedMutation (line 30) | interface DehydratedMutation {
  type DehydratedQuery (line 37) | interface DehydratedQuery {
  type ObserverState (line 46) | interface ObserverState<
  type User (line 63) | interface User {

FILE: src/react-query-external-sync/useMySocket.ts
  type Props (line 7) | interface Props {
  function useMySocket (line 38) | function useMySocket({

FILE: src/react-query-external-sync/useSyncQueries.ts
  type QueryActions (line 15) | type QueryActions =
  type QueryActionMessage (line 39) | interface QueryActionMessage {
  type OnlineManagerMessage (line 50) | interface OnlineManagerMessage {
  type ShouldProcessMessageProps (line 58) | interface ShouldProcessMessageProps {
  function shouldProcessMessage (line 62) | function shouldProcessMessage({
  function checkVersion (line 72) | function checkVersion(queryClient: QueryClient) {
  type useSyncQueriesExternalProps (line 92) | interface useSyncQueriesExternalProps {
  function useSyncQueriesExternal (line 122) | function useSyncQueriesExternal({

FILE: src/react-query-external-sync/utils/logger.ts
  type LogType (line 4) | type LogType = "log" | "warn" | "error";
  function log (line 11) | function log(

FILE: src/server/socketHandle.ts
  type DefaultEventsMap (line 4) | type DefaultEventsMap = Record<string, (...args: any[]) => void>;
  type Props (line 13) | interface Props {
  function socketHandle (line 36) | function socketHandle({ io }: Props) {
Condensed preview — 78 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (284K chars).
[
  {
    "path": ".eslintignore",
    "chars": 188,
    "preview": "# Dependencies\nnode_modules/\n\n# Build outputs\nbuild/\ndist/\n.next/\nout/\n\n# IDE Specific\n.idea/\n.vscode/\n\n# Specific files"
  },
  {
    "path": ".eslintrc.json",
    "chars": 677,
    "preview": "{\n  \"env\": {\n    \"browser\": true,\n    \"es6\": true,\n    \"node\": true\n  },\n  \"extends\": [\n    \"eslint:recommended\",\n    \"p"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 2909,
    "preview": "---\nname: Build/release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\n# Add permissions block\npermissions:\n  contents: write\n\njob"
  },
  {
    "path": ".gitignore",
    "chars": 1247,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
  },
  {
    "path": ".npmrc",
    "chars": 22,
    "preview": "node-linker = hoisted\n"
  },
  {
    "path": "DEVELOPMENT.md",
    "chars": 2761,
    "preview": "# Development Guide\n\nThis guide covers everything you need to know about developing React Native DevTools.\n\n## 🛠 Prerequ"
  },
  {
    "path": "GITHUB_RELEASE.md",
    "chars": 2891,
    "preview": "# GitHub Release Process\n\nThis guide explains how to release updates to GitHub and enable auto-updating for React Native"
  },
  {
    "path": "README.md",
    "chars": 17986,
    "preview": "# React Native DevTools\n\nEnhanced developer tools for React Native applications, supporting React Query DevTools and dev"
  },
  {
    "path": "RELEASE.md",
    "chars": 2307,
    "preview": "# Releasing React Native DevTools\n\nThis document outlines the process for creating a new release of React Native DevTool"
  },
  {
    "path": "assets/icon.icns",
    "chars": 236,
    "preview": "// This is a placeholder - you need to replace this with an actual .icns file\n// For now, you'll need to create or conve"
  },
  {
    "path": "auto-release.sh",
    "chars": 2807,
    "preview": "#!/bin/bash\n\n# Set error handling\nset -e\n\n# Colors for better logging\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1"
  },
  {
    "path": "build-and-pack.sh",
    "chars": 1539,
    "preview": "#!/bin/bash\n\n# Exit on any error\nset -e\n\n# Display what's happening\necho \"🚀 Building and packaging React Native DevTools"
  },
  {
    "path": "build-macos.sh",
    "chars": 1079,
    "preview": "#!/bin/bash\n\n# Exit on error\nset -e\n\necho \"=== Building React Native DevTools for macOS ===\"\n\n# Clean previous builds\nrm"
  },
  {
    "path": "config.js",
    "chars": 317,
    "preview": "/**\n * Socket.IO Test Client Config\n * CommonJS version for use with the test script\n */\n\n// Server configuration\nconst "
  },
  {
    "path": "copy-to-desktop.sh",
    "chars": 394,
    "preview": "#!/bin/bash\n\n# Get the version from package.json\nVERSION=$(node -p \"require('./package.json').version\")\n\n# Create deskto"
  },
  {
    "path": "entitlements.plist",
    "chars": 789,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "forge.config.ts",
    "chars": 3462,
    "preview": "import { config as dotenvConfig } from \"dotenv\";\nimport * as path from \"path\";\n\n// Load .env file from the project root\n"
  },
  {
    "path": "forge.env.d.ts",
    "chars": 69,
    "preview": "/// <reference types=\"@electron-forge/plugin-vite/forge-vite-env\" />\n"
  },
  {
    "path": "index.html",
    "chars": 225,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>React Native Dev Tools</title>\n  </head>\n  <body"
  },
  {
    "path": "package.json",
    "chars": 3102,
    "preview": "{\n  \"name\": \"rn-better-dev-tools\",\n  \"productName\": \"React Native DevTools\",\n  \"version\": \"1.1.0\",\n  \"description\": \"Enh"
  },
  {
    "path": "postcss.config.js",
    "chars": 83,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "release-version.sh",
    "chars": 5672,
    "preview": "#!/bin/bash\n\n# Exit on any error\nset -e\n\n# Colors for better output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;3"
  },
  {
    "path": "socket-client.js",
    "chars": 1707,
    "preview": "/**\n * Socket.IO Test Client\n *\n * This simple client connects to the Socket.IO server and allows\n * sending messages fr"
  },
  {
    "path": "src/auto-updater.ts",
    "chars": 1481,
    "preview": "import { autoUpdater, UpdateInfo, ProgressInfo } from \"electron-updater\";\nimport log from \"electron-log\";\n\n// Configure "
  },
  {
    "path": "src/components/App.tsx",
    "chars": 196,
    "preview": "import Providers from \"./external-dash/providers\";\nimport Main from \"./external-dash/Main\";\nexport const App: React.FC ="
  },
  {
    "path": "src/components/external-dash/Dash.tsx",
    "chars": 16187,
    "preview": "import React, { useEffect, useState } from \"react\";\nimport { User } from \"./types/User\";\nimport \"../../index.css\";\nimpor"
  },
  {
    "path": "src/components/external-dash/DeviceSelection.tsx",
    "chars": 11850,
    "preview": "import React, { useEffect, useState, useRef } from \"react\";\nimport { User } from \"./types/User\";\nimport { PlatformIcon }"
  },
  {
    "path": "src/components/external-dash/LogConsole.tsx",
    "chars": 16130,
    "preview": "import React, { useEffect, useRef, useState } from \"react\";\nimport { useLogStore, LogEntry, LogLevel } from \"./utils/log"
  },
  {
    "path": "src/components/external-dash/Main.tsx",
    "chars": 767,
    "preview": "import { useState } from \"react\";\nimport useConnectedUsers from \"./_hooks/useConnectedUsers\";\nimport { useSyncQueriesWeb"
  },
  {
    "path": "src/components/external-dash/NoDevicesConnected.tsx",
    "chars": 5248,
    "preview": "import React, { useState } from \"react\";\n\nexport const NoDevicesConnected = () => {\n  const [copiedText, setCopiedText] "
  },
  {
    "path": "src/components/external-dash/UserInfo/DeviceSpecificationsSection.tsx",
    "chars": 7000,
    "preview": "import React, { useState } from \"react\";\nimport { InfoRow } from \"./InfoRow\";\n\ninterface Props {\n  extraDeviceInfo: Reco"
  },
  {
    "path": "src/components/external-dash/UserInfo/EnvironmentVariablesSection.tsx",
    "chars": 6764,
    "preview": "import React, { useState } from \"react\";\nimport { InfoRow } from \"./InfoRow\";\n\ninterface Props {\n  envVariables: Record<"
  },
  {
    "path": "src/components/external-dash/UserInfo/InfoRow.tsx",
    "chars": 710,
    "preview": "import React from \"react\";\nimport { PlatformIcon } from \"../utils/platformUtils\";\n\ninterface Props {\n  label: string;\n  "
  },
  {
    "path": "src/components/external-dash/UserInfo/StorageControlsSection.tsx",
    "chars": 13512,
    "preview": "import React, { useCallback, useMemo, useState } from \"react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\ni"
  },
  {
    "path": "src/components/external-dash/UserInfo/TargetGlowEffect.tsx",
    "chars": 1045,
    "preview": "import React from \"react\";\n\ninterface Props {\n  isTargeted: boolean;\n}\n\nexport const TargetGlowEffect: React.FC<Props> ="
  },
  {
    "path": "src/components/external-dash/UserInfo/UserCardDetails.tsx",
    "chars": 2366,
    "preview": "import React from \"react\";\nimport { User } from \"../types/User\";\nimport { InfoRow } from \"./InfoRow\";\nimport { Environme"
  },
  {
    "path": "src/components/external-dash/UserInfo/UserCardHeader.tsx",
    "chars": 3325,
    "preview": "import React from \"react\";\nimport { User } from \"../types/User\";\nimport {\n  PlatformIcon,\n  getDisplayPlatform,\n  getPla"
  },
  {
    "path": "src/components/external-dash/UserInfo/index.ts",
    "chars": 417,
    "preview": "export { TargetGlowEffect } from \"./TargetGlowEffect\";\nexport { UserCardHeader } from \"./UserCardHeader\";\nexport { UserC"
  },
  {
    "path": "src/components/external-dash/UserInfo.tsx",
    "chars": 2498,
    "preview": "import React, { useState } from \"react\";\nimport { User } from \"./types/User\";\nimport {\n  TargetGlowEffect,\n  UserCardHea"
  },
  {
    "path": "src/components/external-dash/_hooks/useConnectedUsers.ts",
    "chars": 2916,
    "preview": "import io, { Socket } from \"socket.io-client\";\nimport { useEffect, useState } from \"react\";\nimport { User } from \"../typ"
  },
  {
    "path": "src/components/external-dash/hooks/useDevToolsEventHandler.ts",
    "chars": 5322,
    "preview": "import { useEffect, useRef } from \"react\";\nimport { onDevToolsEvent } from \"../utils/devToolsEvents\";\nimport type { DevT"
  },
  {
    "path": "src/components/external-dash/providers.tsx",
    "chars": 824,
    "preview": "// We can not useState or useRef in a server component, which is why we are\n// extracting this part out into it's own fi"
  },
  {
    "path": "src/components/external-dash/shared/hydration.ts",
    "chars": 5253,
    "preview": "import type {\n  DefaultError,\n  MutationOptions,\n  QueryOptions,\n  Query,\n  QueryClient,\n  Mutation,\n} from \"@tanstack/r"
  },
  {
    "path": "src/components/external-dash/shared/types.ts",
    "chars": 1289,
    "preview": "import {\n  DefaultError,\n  MutationMeta,\n  MutationScope,\n  MutationState,\n  QueryKey,\n  QueryMeta,\n  QueryObserverOptio"
  },
  {
    "path": "src/components/external-dash/types/ClientQuery.ts",
    "chars": 55,
    "preview": "export interface ClientQuery {\n  deviceName: string;\n}\n"
  },
  {
    "path": "src/components/external-dash/types/User.ts",
    "chars": 415,
    "preview": "export interface User {\n  id: string;\n  deviceName: string;\n  deviceId: string; // Persisted device ID\n  platform?: stri"
  },
  {
    "path": "src/components/external-dash/useSyncQueriesWeb.ts",
    "chars": 17391,
    "preview": "import {\n  onlineManager,\n  QueryClient,\n  QueryKey,\n  useQueryClient,\n} from \"@tanstack/react-query\";\nimport { useEffec"
  },
  {
    "path": "src/components/external-dash/utils/devToolsEvents.ts",
    "chars": 1909,
    "preview": "import { logger } from \"./logger\";\n\nexport type DevToolsActionType =\n  | \"REFETCH\"\n  | \"INVALIDATE\"\n  | \"RESET\"\n  | \"REM"
  },
  {
    "path": "src/components/external-dash/utils/logStore.ts",
    "chars": 2617,
    "preview": "import { create } from \"zustand\";\n\nexport type LogLevel = \"info\" | \"error\" | \"warn\" | \"debug\";\n\nexport interface LogEntr"
  },
  {
    "path": "src/components/external-dash/utils/logger.ts",
    "chars": 1667,
    "preview": "import { logInfo, logError, logWarn, logDebug } from \"./logStore\";\nimport { User } from \"../types/User\";\n\n/**\n * A logge"
  },
  {
    "path": "src/components/external-dash/utils/platformUtils.tsx",
    "chars": 6837,
    "preview": "import React from \"react\";\n\n// --- 1. Extracted SVG Icon Components (SRP, KISS) ---\n\ninterface IconProps {\n  className?:"
  },
  {
    "path": "src/components/external-dash/utils/storageQueryKeys.ts",
    "chars": 2355,
    "preview": "/**\n * Centralized storage query keys for all storage hooks\n * This ensures consistency across MMKV, AsyncStorage, and S"
  },
  {
    "path": "src/components/external-dash/utils/storageStore.ts",
    "chars": 1986,
    "preview": "import { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { StorageType } from \"./storageQu"
  },
  {
    "path": "src/config.ts",
    "chars": 489,
    "preview": "/**\n * Application configuration settings\n */\n\n// Server configuration\nexport const SERVER_PORT = 42831;\n\n// Socket.IO c"
  },
  {
    "path": "src/index.css",
    "chars": 3467,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* SF Pro Display font for Apple-like UI */\n@font-face {\n  f"
  },
  {
    "path": "src/main.ts",
    "chars": 2714,
    "preview": "import { app, BrowserWindow, dialog } from \"electron\";\nimport path from \"node:path\";\nimport started from \"electron-squir"
  },
  {
    "path": "src/preload.ts",
    "chars": 158,
    "preview": "// See the Electron documentation for details on how to use preload scripts:\n// https://www.electronjs.org/docs/latest/t"
  },
  {
    "path": "src/react-query-external-sync/README.md",
    "chars": 6856,
    "preview": "# React Query External Sync\n\nA powerful debugging tool for React Query state in any React-based application. Whether you"
  },
  {
    "path": "src/react-query-external-sync/User.ts",
    "chars": 362,
    "preview": "export interface User {\n  id: string;\n  deviceName: string;\n  deviceId?: string; // Optional for backward compatibility\n"
  },
  {
    "path": "src/react-query-external-sync/hydration.ts",
    "chars": 2069,
    "preview": "import type {\n  DefaultError,\n  Mutation,\n  MutationOptions,\n  Query,\n  QueryClient,\n  QueryFunction,\n  QueryOptions,\n} "
  },
  {
    "path": "src/react-query-external-sync/index.ts",
    "chars": 91,
    "preview": "// Export the main hook\nexport { useMySocket as useQuerySyncSocket } from \"./useMySocket\";\n"
  },
  {
    "path": "src/react-query-external-sync/platformUtils.ts",
    "chars": 3760,
    "preview": "/**\n * Platform and storage utilities that work across different environments (React Native, web, etc.)\n */\n\n// Types\n/*"
  },
  {
    "path": "src/react-query-external-sync/types.ts",
    "chars": 1704,
    "preview": "import {\n  DefaultError,\n  MutationKey,\n  MutationMeta,\n  MutationScope,\n  MutationState,\n  QueryKey,\n  QueryMeta,\n  Que"
  },
  {
    "path": "src/react-query-external-sync/useMySocket.ts",
    "chars": 7828,
    "preview": "import { useEffect, useRef, useState } from \"react\";\nimport { io as socketIO, Socket } from \"socket.io-client\";\n\nimport "
  },
  {
    "path": "src/react-query-external-sync/useSyncQueries.ts",
    "chars": 15486,
    "preview": "import { useEffect, useRef } from \"react\";\nimport type { QueryKey } from \"@tanstack/query-core\";\nimport { onlineManager,"
  },
  {
    "path": "src/react-query-external-sync/utils/logger.ts",
    "chars": 583,
    "preview": "/**\n * Log types supported by the logger\n */\nexport type LogType = \"log\" | \"warn\" | \"error\";\n\n/**\n * Helper function for"
  },
  {
    "path": "src/renderer.ts",
    "chars": 946,
    "preview": "/**\n * This file will automatically be loaded by vite and run in the \"renderer\" context.\n * To learn more about the diff"
  },
  {
    "path": "src/renderer.tsx",
    "chars": 385,
    "preview": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { App } from \"./components/App\";\nimport"
  },
  {
    "path": "src/server/socketHandle.ts",
    "chars": 19094,
    "preview": "import { Socket, Server as SocketIOServer } from \"socket.io\";\n// Replace the problematic import with a direct type\n// es"
  },
  {
    "path": "src/server.ts",
    "chars": 2353,
    "preview": "import express, { Request, Response } from \"express\";\nimport http from \"http\";\nimport { Server } from \"socket.io\";\nimpor"
  },
  {
    "path": "src/types.d.ts",
    "chars": 238,
    "preview": "declare module \"electron-squirrel-startup\" {\n  const value: boolean;\n  export default value;\n}\n\n// Vite environment vari"
  },
  {
    "path": "tailwind.config.js",
    "chars": 4862,
    "preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"]"
  },
  {
    "path": "tsconfig.json",
    "chars": 394,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"commonjs\",\n    \"allowJs\": true,\n    \"skipLibCheck\": true"
  },
  {
    "path": "vite.main.config.ts",
    "chars": 100,
    "preview": "import { defineConfig } from 'vite';\n\n// https://vitejs.dev/config\nexport default defineConfig({});\n"
  },
  {
    "path": "vite.preload.config.ts",
    "chars": 100,
    "preview": "import { defineConfig } from 'vite';\n\n// https://vitejs.dev/config\nexport default defineConfig({});\n"
  },
  {
    "path": "vite.renderer.config.ts",
    "chars": 165,
    "preview": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\n// https://vitejs.dev/config\nexport defa"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the LovesWorking/rn-better-dev-tools GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 78 files (260.7 KB), approximately 68.0k tokens, and a symbol index with 95 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!