From 8b8da5584ca2d6874de9f7b7f615b43226954f50 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 21 Apr 2026 11:23:52 +0200 Subject: [PATCH 01/10] feat: add logout button to sidebar --- frontend/src/components/panels/Sidebar.tsx | 12 +++++++++- .../panels/__tests__/Sidebar.test.tsx | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/panels/Sidebar.tsx b/frontend/src/components/panels/Sidebar.tsx index bf382d4..e614da5 100644 --- a/frontend/src/components/panels/Sidebar.tsx +++ b/frontend/src/components/panels/Sidebar.tsx @@ -1,8 +1,9 @@ import { useState, useCallback, useEffect, useRef } from 'react' -import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2, Square, Eye, Settings, StopCircle, X } from 'lucide-react' +import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2, Square, Eye, Settings, StopCircle, X, LogOut } from 'lucide-react' import { Logo } from '@/components/ui/Logo' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useCanvasStore } from '@/stores/canvasStore' +import { useAuthStore } from '@/stores/authStore' import { scanApi, settingsApi } from '@/api/client' import { toast } from 'sonner' import { useLatestRelease } from '@/hooks/useLatestRelease' @@ -43,6 +44,7 @@ interface SidebarProps { export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved, forceView, highlightPendingId }: SidebarProps) { const [_collapsed, setCollapsed] = useState(false) const [_activeView, setActiveView] = useState('canvas') + const logout = useAuthStore((s) => s.logout) // When forceView is set, override local state without useEffect const collapsed = forceView ? false : _collapsed @@ -152,6 +154,14 @@ export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeAppro onClick={() => setActiveView((v) => v === 'settings' ? 'canvas' : 'settings')} /> )} + {!STANDALONE && ( + + )} {!collapsed && } diff --git a/frontend/src/components/panels/__tests__/Sidebar.test.tsx b/frontend/src/components/panels/__tests__/Sidebar.test.tsx index a63d0ed..4853225 100644 --- a/frontend/src/components/panels/__tests__/Sidebar.test.tsx +++ b/frontend/src/components/panels/__tests__/Sidebar.test.tsx @@ -2,12 +2,14 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { Sidebar } from '../Sidebar' import { useCanvasStore } from '@/stores/canvasStore' +import { useAuthStore } from '@/stores/authStore' import type { Node } from '@xyflow/react' import type { NodeData } from '@/types' // ── Mocks ──────────────────────────────────────────────────────────────────── vi.mock('@/stores/canvasStore') +vi.mock('@/stores/authStore') const mockBulkApprove = vi.fn() const mockBulkHide = vi.fn() @@ -60,6 +62,7 @@ const makeNode = (id: string, status: NodeData['status'], type: NodeData['type'] }) const mockToggleHideIp = vi.fn() +const mockLogout = vi.fn() function mockStore(overrides: Partial> = {}) { vi.mocked(useCanvasStore).mockReturnValue({ @@ -73,6 +76,12 @@ function mockStore(overrides: Partial> = {}) { } as ReturnType) } +function mockAuth() { + vi.mocked(useAuthStore).mockImplementation((selector: (s: { logout: () => void }) => unknown) => + selector({ logout: mockLogout }) as ReturnType + ) +} + const defaultProps = { onAddNode: vi.fn(), onAddGroupRect: vi.fn(), @@ -86,6 +95,7 @@ const defaultProps = { describe('Sidebar', () => { beforeEach(() => { mockStore() + mockAuth() vi.clearAllMocks() }) @@ -267,6 +277,19 @@ describe('Sidebar', () => { fireEvent.click(screen.getByRole('button', { name: 'Settings' })) expect(screen.queryByText('Status check interval (s)')).not.toBeInTheDocument() }) + + // ── Logout ───────────────────────────────────────────────────────────────── + + it('shows Logout button in normal mode', () => { + render() + expect(screen.getByText('Logout')).toBeInTheDocument() + }) + + it('calls logout when Logout is clicked', () => { + render() + fireEvent.click(screen.getByText('Logout')) + expect(mockLogout).toHaveBeenCalledOnce() + }) }) // ── PendingDevicesPanel — bulk select ───────────────────────────────────────── @@ -298,6 +321,7 @@ const DEVICE_B = { describe('PendingDevicesPanel — bulk select', () => { beforeEach(() => { mockStore() + mockAuth() vi.clearAllMocks() mockBulkApprove.mockResolvedValue({ data: { approved: 2, node_ids: ['n1', 'n2'], device_ids: ['dev-a', 'dev-b'], skipped: 0 }, From 110592f89e53152d2114ebcdf377e16692f92062 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 21 Apr 2026 11:28:55 +0200 Subject: [PATCH 02/10] fix: checkbox onChange anti-pattern in PendingDevicesPanel --- frontend/src/components/panels/Sidebar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/panels/Sidebar.tsx b/frontend/src/components/panels/Sidebar.tsx index e614da5..c7b5d73 100644 --- a/frontend/src/components/panels/Sidebar.tsx +++ b/frontend/src/components/panels/Sidebar.tsx @@ -395,8 +395,7 @@ function PendingDevicesPanel({ onNodeApproved, highlightId }: { onNodeApproved: toggleCheck(d.id, e)} - onChange={() => {}} + onChange={(e) => { e.stopPropagation(); toggleCheck(d.id, e as unknown as React.MouseEvent) }} className="w-3 h-3 accent-[#00d4ff] cursor-pointer shrink-0" /> {title} From 88554ef9527958ccffce4026e81a09e9f47c47cc Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 21 Apr 2026 11:37:43 +0200 Subject: [PATCH 03/10] fix: stop click propagation on pending device checkbox to prevent modal opening --- frontend/src/components/panels/Sidebar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/panels/Sidebar.tsx b/frontend/src/components/panels/Sidebar.tsx index c7b5d73..af75140 100644 --- a/frontend/src/components/panels/Sidebar.tsx +++ b/frontend/src/components/panels/Sidebar.tsx @@ -395,6 +395,7 @@ function PendingDevicesPanel({ onNodeApproved, highlightId }: { onNodeApproved: e.stopPropagation()} onChange={(e) => { e.stopPropagation(); toggleCheck(d.id, e as unknown as React.MouseEvent) }} className="w-3 h-3 accent-[#00d4ff] cursor-pointer shrink-0" /> From 3deb750441043a1653a41ff4de258a6d6957eb6c Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 21 Apr 2026 11:43:15 +0200 Subject: [PATCH 04/10] fix: NaN guard on settings interval input and validate release URL scheme --- frontend/src/components/panels/Sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/panels/Sidebar.tsx b/frontend/src/components/panels/Sidebar.tsx index af75140..322b8ad 100644 --- a/frontend/src/components/panels/Sidebar.tsx +++ b/frontend/src/components/panels/Sidebar.tsx @@ -637,7 +637,7 @@ function SettingsPanel() { min={10} max={3600} value={interval} - onChange={(e) => setIntervalValue(Number(e.target.value))} + onChange={(e) => { const v = Number(e.target.value); if (!isNaN(v)) setIntervalValue(v) }} className="w-24 px-2 py-1 rounded-md text-xs font-mono bg-[#0d1117] border border-border text-foreground focus:outline-none focus:border-[#00d4ff]" /> seconds @@ -674,7 +674,7 @@ function VersionBadge() { {hasUpdate && latest && ( Date: Tue, 21 Apr 2026 11:52:43 +0200 Subject: [PATCH 05/10] bump: version 1.10.2 --- VERSION | 2 +- frontend/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 4dae298..5ad2491 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.10.1 +1.10.2 diff --git a/frontend/package.json b/frontend/package.json index 8bf55e8..c445b74 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "1.10.1", + "version": "1.10.2", "type": "module", "scripts": { "dev": "vite", From c9cd6a08fcaa1baca86a443f0a19d279fa742ca4 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 21 Apr 2026 12:23:50 +0200 Subject: [PATCH 06/10] add CONTRIBUTING.md --- CONTRIBUTING.md | 276 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0b0fa90 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,276 @@ +# Contributing to Homelable + +Thanks for taking the time to contribute! This document covers everything you need to get started. + +--- + +## Table of Contents + +- [Ways to Contribute](#ways-to-contribute) +- [Reporting Bugs](#reporting-bugs) +- [Suggesting Features](#suggesting-features) +- [Development Setup](#development-setup) +- [Project Structure](#project-structure) +- [Coding Standards](#coding-standards) +- [Testing](#testing) +- [Submitting a Pull Request](#submitting-a-pull-request) +- [Commit Message Format](#commit-message-format) + +--- + +## Ways to Contribute + +- Report bugs or unexpected behavior +- Suggest new features or improvements +- Fix open issues (check the [issue tracker](https://github.com/Pouzor/homelable/issues)) +- Improve documentation +- Add service signatures to `service_signatures.json` + +--- + +## Reporting Bugs + +Before opening an issue, search existing ones to avoid duplicates. + +When filing a bug, include: + +- **Homelable version** (visible in the sidebar bottom-left) +- **Deployment method** (Docker Compose, Proxmox LXC, source) +- **Steps to reproduce** +- **Expected vs actual behavior** +- **Relevant logs** (`docker compose logs backend` / `docker compose logs frontend`) +- **Browser console errors** if it's a UI issue + +--- + +## Suggesting Features + +Open an issue with the `enhancement` label. Describe: + +- The problem you're trying to solve +- Your proposed solution +- Any alternatives you considered + +For large changes, discuss first before writing code — it avoids wasted effort. + +--- + +## Development Setup + +### Prerequisites + +- **Node.js 20+** and **npm** +- **Python 3.11–3.13** (3.14 not yet supported by all dependencies) +- **nmap** installed on your system (required for scanner) +- **Docker + Docker Compose** (optional, for full-stack testing) + +### 1. Clone the repo + +```bash +git clone https://github.com/Pouzor/homelable.git +cd homelable +``` + +### 2. Backend + +```bash +cd backend +python3.13 -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt + +# Copy and configure environment +cp .env.example .env # edit AUTH_PASSWORD_HASH, SECRET_KEY, etc. + +# Start the backend (auto-reloads on change) +uvicorn app.main:app --reload --port 8000 +``` + +API docs available at `http://localhost:8000/docs`. + +### 3. Frontend + +```bash +cd frontend +npm install +npm run dev # http://localhost:5173 +``` + +Vite proxies `/api` to `localhost:8000` — the backend must be running. + +### 4. Verify tooling + +```bash +./scripts/verify-tooling.sh +``` + +--- + +## Project Structure + +``` +homelable/ +├── frontend/src/ +│ ├── components/ +│ │ ├── canvas/ # React Flow canvas, custom nodes & edges +│ │ ├── panels/ # Sidebar, detail panel, toolbar +│ │ ├── modals/ # Add/edit node, scan config, pending devices +│ │ └── ui/ # Shadcn/ui base components +│ ├── stores/ # Zustand state (canvas, auth, scan) +│ ├── hooks/ # Custom React hooks +│ ├── types/ # TypeScript interfaces & enums +│ ├── api/ # Axios client & typed endpoints +│ └── utils/ # Layout, export, color helpers +│ +├── backend/app/ +│ ├── api/routes/ # FastAPI route handlers +│ ├── services/ # Scanner, status checker, canvas service +│ ├── db/ # SQLAlchemy models, Alembic migrations +│ ├── schemas/ # Pydantic request/response schemas +│ └── core/ # Config, JWT, scheduler +│ +├── docker/ # Nginx configs +├── scripts/ # LXC bootstrap, dev helpers +└── mcp/ # MCP server (AI integration) +``` + +--- + +## Coding Standards + +### General + +- No untested code merged — every feature or fix must include tests +- Keep changes focused — one concern per PR + +### Frontend (TypeScript + React) + +- Strict TypeScript — no `any`, no type assertions unless truly necessary +- React Flow node domain fields go in `node.data`, never on the node root +- State management via Zustand stores — no prop drilling beyond 2 levels +- Styling via TailwindCSS utility classes — follow the existing [design system](#design-system) +- Run before committing: + ```bash + cd frontend + npm run lint + npm run typecheck + npm test + ``` + +### Backend (Python + FastAPI) + +- Python 3.11+ syntax +- Pydantic v2 schemas for all request/response types +- SQLAlchemy async sessions — never block the event loop +- Scanner logic runs in a background thread — never in an async route directly +- All schema changes via Alembic migrations — never modify tables directly +- Run before committing: + ```bash + cd backend + source .venv/bin/activate + ruff check . + pytest + ``` + +### Design System + +| Token | Value | +|---|---| +| Background | `#0d1117` | +| Surface | `#161b22` | +| Card | `#21262d` | +| Accent cyan | `#00d4ff` | +| Online | `#39d353` | +| Offline | `#f85149` | +| Pending | `#e3b341` | +| Font (UI) | Inter | +| Font (IPs/ports) | JetBrains Mono | + +--- + +## Testing + +Tests run automatically via a pre-commit hook when frontend or backend files are staged. + +### Frontend + +```bash +cd frontend +npm test # run all tests +npm run test:coverage # with coverage report +``` + +Test files live in `__tests__/` next to their module, named `*.test.ts(x)`. + +**What to test:** Zustand store actions, utility functions, non-trivial component logic. + +### Backend + +```bash +cd backend +source .venv/bin/activate +pytest # run all tests +pytest -v tests/test_nodes.py # single file +``` + +Test files live in `backend/tests/test_*.py`. + +**What to test:** all API routes (happy path + error cases), auth flows, service logic. + +Use the `client` and `headers` fixtures from `conftest.py` — they provide an in-memory SQLite database so tests are isolated and fast. + +--- + +## Submitting a Pull Request + +1. **Fork** the repo and create a branch from `main`: + ```bash + git checkout -b feat/my-feature + ``` + +2. **Make your changes** — include tests. + +3. **Run the full test suite** (frontend + backend) and make sure everything passes. + +4. **Open a PR** against `main`: + - Use a clear title (see commit format below) + - Describe what changed and why + - Reference any related issues (`Closes #123`) + - Include screenshots for UI changes + +5. Keep the PR focused — one feature or fix per PR. Large refactors should be discussed in an issue first. + +--- + +## Commit Message Format + +Follow [Conventional Commits](https://www.conventionalcommits.org/): + +``` +: + +[optional body] +``` + +| Type | When to use | +|---|---| +| `feat` | New feature | +| `fix` | Bug fix | +| `docs` | Documentation only | +| `refactor` | Code change with no behavior change | +| `test` | Adding or fixing tests | +| `chore` | Build, deps, tooling | + +**Examples:** +``` +feat: add logout button to sidebar +fix: stop click propagation on pending device checkbox +docs: add CONTRIBUTING.md +``` + + +--- + +## Questions? + +Open a [GitHub Discussion](https://github.com/Pouzor/homelable/discussions) or drop a comment on a relevant issue. From 03e1e32af141ea596dc871fedfec9e6fcb45e90f Mon Sep 17 00:00:00 2001 From: Remy Date: Tue, 21 Apr 2026 12:29:18 +0200 Subject: [PATCH 07/10] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1b2685a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Remy Jardinet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 0204a7ddeb12412551b0f45a6422a17c00247f97 Mon Sep 17 00:00:00 2001 From: Brett Ferrante <83841899+findthelorax@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:35:53 -0400 Subject: [PATCH 08/10] Update Docker image references to use repository owner --- .github/workflows/docker-publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 600ae67..fe0a4e8 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -15,13 +15,13 @@ jobs: strategy: matrix: include: - - image: ghcr.io/pouzor/homelable-backend + - image: ghcr.io/${{ github.repository_owner }}/homelable-backend dockerfile: Dockerfile.backend build_args: "" - - image: ghcr.io/pouzor/homelable-frontend + - image: ghcr.io/${{ github.repository_owner }}/homelable-frontend dockerfile: Dockerfile.frontend build_args: "" - - image: ghcr.io/pouzor/homelable-frontend-standalone + - image: ghcr.io/${{ github.repository_owner }}/homelable-frontend-standalone dockerfile: Dockerfile.frontend build_args: "VITE_STANDALONE=true" From feb05a1df5b4851b73abef5b03c7c00c7b9caed4 Mon Sep 17 00:00:00 2001 From: findthelorax Date: Tue, 21 Apr 2026 09:53:09 -0400 Subject: [PATCH 09/10] updated node modal with consistent casing for type and check method following standard conventions --- frontend/src/components/modals/NodeModal.tsx | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/modals/NodeModal.tsx b/frontend/src/components/modals/NodeModal.tsx index 5caa1cd..bcb61d0 100644 --- a/frontend/src/components/modals/NodeModal.tsx +++ b/frontend/src/components/modals/NodeModal.tsx @@ -1,4 +1,4 @@ -import { createElement, useState } from 'react' +import { createElement, Fragment, useState } from 'react' import { RotateCcw, ChevronDown } from 'lucide-react' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' @@ -18,6 +18,17 @@ const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [ const CHECK_METHODS: CheckMethod[] = ['none', 'ping', 'http', 'https', 'tcp', 'ssh', 'prometheus', 'health'] +const CHECK_METHOD_LABELS: Record = { + none: 'None', + ping: 'Ping', + http: 'HTTP', + https: 'HTTPS', + tcp: 'TCP', + ssh: 'SSH', + prometheus: 'Prometheus', + health: 'Health', +} + const DEFAULT_DATA: Partial = { type: 'server', label: '', @@ -76,13 +87,13 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' @@ -221,11 +232,11 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' From a8ca4f1ccaff62d495297dac464a78483f8b3f48 Mon Sep 17 00:00:00 2001 From: findthelorax Date: Tue, 21 Apr 2026 09:55:46 -0400 Subject: [PATCH 10/10] revert: restore workflow to upstream version --- .github/workflows/docker-publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index fe0a4e8..600ae67 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -15,13 +15,13 @@ jobs: strategy: matrix: include: - - image: ghcr.io/${{ github.repository_owner }}/homelable-backend + - image: ghcr.io/pouzor/homelable-backend dockerfile: Dockerfile.backend build_args: "" - - image: ghcr.io/${{ github.repository_owner }}/homelable-frontend + - image: ghcr.io/pouzor/homelable-frontend dockerfile: Dockerfile.frontend build_args: "" - - image: ghcr.io/${{ github.repository_owner }}/homelable-frontend-standalone + - image: ghcr.io/pouzor/homelable-frontend-standalone dockerfile: Dockerfile.frontend build_args: "VITE_STANDALONE=true"