From 9dcc0868de535927bfde7b429ced05875c0cc4e2 Mon Sep 17 00:00:00 2001 From: drjonesxxx Date: Sun, 14 Sep 2025 13:09:39 -0700 Subject: [PATCH] Initial commit --- .gitignore | 24 +++++ App.tsx | 89 ++++++++++++++++++ README.md | 59 ++++++++++++ components/AddServiceModal.tsx | 130 ++++++++++++++++++++++++++ components/Header.tsx | 44 +++++++++ components/ScanInfoModal.tsx | 166 +++++++++++++++++++++++++++++++++ components/ServiceCard.tsx | 50 ++++++++++ constants.ts | 48 ++++++++++ hooks/useLocalStorage.ts | 49 ++++++++++ index.html | 43 +++++++++ index.tsx | 16 ++++ metadata.json | 5 + package.json | 22 +++++ server/package.json | 21 +++++ server/server.js | 92 ++++++++++++++++++ services/geminiService.ts | 42 +++++++++ setup.sh | 0 tsconfig.json | 29 ++++++ types.ts | 10 ++ vite.config.ts | 17 ++++ 20 files changed, 956 insertions(+) create mode 100644 .gitignore create mode 100644 App.tsx create mode 100644 README.md create mode 100644 components/AddServiceModal.tsx create mode 100644 components/Header.tsx create mode 100644 components/ScanInfoModal.tsx create mode 100644 components/ServiceCard.tsx create mode 100644 constants.ts create mode 100644 hooks/useLocalStorage.ts create mode 100644 index.html create mode 100644 index.tsx create mode 100644 metadata.json create mode 100644 package.json create mode 100644 server/package.json create mode 100644 server/server.js create mode 100644 services/geminiService.ts create mode 100644 setup.sh create mode 100644 tsconfig.json create mode 100644 types.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..f630663 --- /dev/null +++ b/App.tsx @@ -0,0 +1,89 @@ +import React, { useState, useCallback } from 'react'; +import { type Service } from './types'; +import { useLocalStorage } from './hooks/useLocalStorage'; +import { PRECONFIGURED_SERVICES } from './constants'; +import ServiceCard from './components/ServiceCard'; +import AddServiceModal from './components/AddServiceModal'; +import Header from './components/Header'; +import ScanInfoModal from './components/ScanInfoModal'; +import { Plus } from 'lucide-react'; +import { Globe } from 'lucide-react'; + +const App: React.FC = () => { + const [services, setServices] = useLocalStorage('dashboard-services', PRECONFIGURED_SERVICES); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isScanInfoModalOpen, setIsScanInfoModalOpen] = useState(false); + + const addService = useCallback((newService: Omit) => { + setServices(prevServices => [ + ...prevServices, + { ...newService, id: Date.now().toString() } + ]); + }, [setServices]); + + const addDiscoveredServices = useCallback((discoveredServices: Omit[]) => { + setServices(prevServices => { + const existingUrls = new Set(prevServices.map(s => s.url)); + const newServices = discoveredServices + .filter(ds => !existingUrls.has(ds.url)) + .map(ds => ({ + ...ds, + id: `${ds.name}-${ds.url}-${Math.random()}`, // Create a unique ID + icon: Globe, // Default icon for discovered services + })); + + return [...prevServices, ...newServices]; + }); + }, [setServices]); + + const deleteService = useCallback((id: string) => { + setServices(prevServices => prevServices.filter(service => service.id !== id)); + }, [setServices]); + + return ( +
+
setIsModalOpen(true)} + onScanNetwork={() => setIsScanInfoModalOpen(true)} + /> + +
+ {services.length > 0 ? ( +
+ {services.map(service => ( + + ))} +
+ ) : ( +
+

Your Dashboard is Empty

+

Click the button below to start adding your local services.

+ +
+ )} +
+ + {isModalOpen && ( + setIsModalOpen(false)} + onAddService={addService} + /> + )} + + {isScanInfoModalOpen && ( + setIsScanInfoModalOpen(false)} + onScanComplete={addDiscoveredServices} + /> + )} +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e58c2e --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Local Network Dashboard + +A beautiful, self-hosted dashboard to organize and access all your local network services. + +This project consists of two parts: +1. **Frontend:** A React-based dashboard that runs in your browser. +2. **Backend:** A Node.js server that you run locally to perform network scans. + +--- + +## Running the Application + +### 1. Prerequisites + +Before you start, you need to have **Nmap** installed on the computer where you will run the backend server. Nmap is a powerful network scanning tool that the backend uses to discover services. + +- **macOS (using Homebrew):** + ```bash + brew install nmap + ``` +- **Debian/Ubuntu:** + ```bash + sudo apt-get update && sudo apt-get install nmap + ``` +- **Windows:** + Download the installer from the [official Nmap website](https://nmap.org/download.html). + +You also need [Node.js](https://nodejs.org/) installed to run the backend server. + +### 2. Running the Backend Server + +The backend server is responsible for scanning your network and finding services. + +1. **Navigate to the `server` directory:** + ```bash + cd server + ``` + +2. **Install dependencies:** + ```bash + npm install + ``` + +3. **Start the server:** + ```bash + node server.js + ``` + +The server will start and listen on port 3001. You should see a message `Backend server running on http://localhost:3001`. Keep this terminal window open. + +### 3. Running the Frontend + +The frontend is the visual dashboard you interact with. Simply open the `index.html` file in your web browser. + +- Once the dashboard is open, click the "Scan Network" button. +- Enter your network range (e.g., `192.168.1.0/24`). +- Click "Start Scan". + +The frontend will contact your running backend server, which will perform the nmap scan and return the discovered services to be automatically added to your dashboard. diff --git a/components/AddServiceModal.tsx b/components/AddServiceModal.tsx new file mode 100644 index 0000000..f2c2012 --- /dev/null +++ b/components/AddServiceModal.tsx @@ -0,0 +1,130 @@ + +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { type Service } from '../types'; +import { generateServiceDescription } from '../services/geminiService'; +import { Globe, Loader2 } from 'lucide-react'; + +interface AddServiceModalProps { + onClose: () => void; + onAddService: (service: Omit) => void; +} + +const AddServiceModal: React.FC = ({ onClose, onAddService }) => { + const [name, setName] = useState(''); + const [url, setUrl] = useState(''); + const [description, setDescription] = useState(''); + const [isFetchingDesc, setIsFetchingDesc] = useState(false); + const modalRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + onClose(); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [onClose]); + + const handleFetchDescription = useCallback(async () => { + if (name.trim().length > 2 && process.env.API_KEY) { + setIsFetchingDesc(true); + const desc = await generateServiceDescription(name); + setDescription(desc); + setIsFetchingDesc(false); + } + }, [name]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name && url) { + onAddService({ + name, + url, + description: description || `A local service at ${url}`, + icon: Globe, + }); + onClose(); + } + }; + + return ( +
+
+

Add a New Service

+
+
+
+ + setName(e.target.value)} + onBlur={handleFetchDescription} + className="mt-1 block w-full bg-gray-900 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-white focus:outline-none focus:ring-teal-500 focus:border-teal-500 sm:text-sm" + placeholder="e.g., Home Assistant" + required + /> +
+
+ + setUrl(e.target.value)} + className="mt-1 block w-full bg-gray-900 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-white focus:outline-none focus:ring-teal-500 focus:border-teal-500 sm:text-sm" + placeholder="http://192.168.1.100:8123" + required + /> +
+
+ +
+