diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f80b7cc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +*/node_modules +*/dist +data +.git +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bb37eaf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# ── Stage 1: Build client ─────────────────────────────────────────────────── +FROM node:20-alpine AS client-builder +WORKDIR /build + +COPY package.json ./ +COPY client/package.json ./client/ +COPY server/package.json ./server/ + +RUN npm install --workspace=client + +COPY client/ ./client/ +RUN npm run build --workspace=client + +# ── Stage 2: Build server ─────────────────────────────────────────────────── +FROM node:20-alpine AS server-builder +WORKDIR /build + +COPY package.json ./ +COPY server/package.json ./server/ + +RUN npm install --workspace=server + +COPY server/ ./server/ +RUN npm run build --workspace=server + +# ── Stage 3: Runtime ──────────────────────────────────────────────────────── +FROM node:20-alpine AS runtime +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 +ENV DATA_DIR=/data +ENV MAX_UPLOAD_MB=50 +ENV ADMIN_USERNAME=admin +ENV ADMIN_PASSWORD=codedump2024 +ENV JWT_SECRET=changeme-use-a-long-random-string + +# Install production deps only +COPY package.json ./ +COPY server/package.json ./server/ +RUN npm install --workspace=server --omit=dev + +# Copy built artifacts +COPY --from=server-builder /build/server/dist ./server/dist +COPY --from=client-builder /build/client/dist ./client/dist + +VOLUME ["/data"] +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \ + CMD wget -qO- http://localhost:3000/api/settings || exit 1 + +CMD ["node", "server/dist/index.js"] diff --git a/INSTALL.md b/INSTALL.md index 250df3a..8a3ded7 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,105 +1,185 @@ -# Install and Copy Guide +# Unraid Install Guide — CODEDUMP -Use this guide to copy the instruction suite into another repository without bloating the destination or confusing the agent. +## Prerequisites -## Recommended Install +- Unraid 6.10 or later +- Docker enabled (default on all modern Unraid installs) +- Community Applications plugin installed (recommended, not required) -Copy these items into the target repository root: +--- -- `AGENTS.md` -- `DEPLOYMENT-PROFILE.md` -- `SKILLS.md` -- `PROJECT-PROFILE-WORKBOOK.md` -- `ROUTING-EXAMPLES.md` -- `hubs/` -- `skills/` +## Option A — Community Applications (Easiest) -Optional: +> Use this if you have the CA plugin installed. -- `README.md` if you want human-facing explanation of the bundle in the destination repo +1. Open the **Apps** tab in the Unraid web UI. +2. Search for **CODEDUMP**. +3. Click **Install** and fill in the variables described in the [Variables](#variables) section below. +4. Click **Apply**. -## Fast Copy Workflow +--- -From this repository root, copy the suite into another repo root while excluding `.git`: +## Option B — Manual Docker Container (No CA Plugin) -```powershell -Copy-Item AGENTS.md,DEPLOYMENT-PROFILE.md,SKILLS.md,PROJECT-PROFILE-WORKBOOK.md,ROUTING-EXAMPLES.md -Destination -Copy-Item hubs -Destination -Recurse -Copy-Item skills -Destination -Recurse +### Step 1 — Open the Docker tab + +In the Unraid web UI, click **Docker** in the top navigation bar. + +### Step 2 — Add a new container + +Click **Add Container** at the bottom of the page. + +### Step 3 — Fill in Basic Settings + +| Field | Value | +|---|---| +| **Name** | `codedump` | +| **Repository** | `your-registry/codedump:latest` *(or build locally — see below)* | +| **Network Type** | `bridge` | +| **Console shell command** | `sh` | + +> **Building locally:** If you cloned this repo to Unraid, open a terminal and run: +> ```bash +> cd /path/to/ai-tools-dashboard +> docker build -t codedump:local . +> ``` +> Then set **Repository** to `codedump:local`. + +### Step 4 — Port Mapping + +Click **Add another Path, Port, Variable, Label or Device** → **Port**. + +| Field | Value | +|---|---| +| **Name** | `WebUI` | +| **Container Port** | `3000` | +| **Host Port** | `3000` *(or any open port on your Unraid server)* | +| **Connection Type** | `TCP` | + +### Step 5 — Volume (Persistent Data) + +Click **Add another Path, Port, Variable, Label or Device** → **Path**. + +| Field | Value | +|---|---| +| **Name** | `Data` | +| **Container Path** | `/data` | +| **Host Path** | `/mnt/user/appdata/codedump` | +| **Access Mode** | `Read/Write` | + +> This single volume stores the **SQLite database**, all **uploaded files**, and **user accounts**. +> Unraid creates the directory automatically on first run. + +### Step 6 — Environment Variables {#variables} + +For each variable, click **Add another Path, Port, Variable, Label or Device** → **Variable**. + +| Variable | Default | Required | Description | +|---|---|---|---| +| `PORT` | `3000` | No | Port the app listens on inside the container. Match to your Container Port above. | +| `DATA_DIR` | `/data` | No | Path inside the container for persistent data. Do not change unless you remapped the volume. | +| `MAX_UPLOAD_MB` | `50` | No | Maximum upload size in MB for documents and logos. | +| `ADMIN_USERNAME` | `admin` | **Yes** | Username for the bootstrap admin account. Set this before first launch. | +| `ADMIN_PASSWORD` | `codedump2024` | **Yes** | Password for the bootstrap admin account. **Change this to something strong.** | +| `JWT_SECRET` | *(insecure default)* | **Yes** | Secret used to sign login tokens. Set to a long random string (32+ chars). | + +> **Security note:** `ADMIN_PASSWORD` and `JWT_SECRET` must be changed from their defaults before exposing CODEDUMP to your network. The bootstrap admin is only created once — changing `ADMIN_PASSWORD` after first launch has no effect on the existing account (use the Admin → Users page instead). + +### Step 7 — WebUI Link (Optional but Recommended) + +| Field | Value | +|---|---| +| **WebUI** | `http://[IP]:[PORT:3000]` | + +This adds a clickable **WebUI** button on the Docker tab. + +### Step 8 — Apply + +Click **Apply**. Unraid will pull/build the image and start the container. + +--- + +## First Launch & Login + +1. Open `http://YOUR-UNRAID-IP:3000` in your browser. +2. You will be redirected to the **login page**. +3. Click **Admin Login** at the bottom of the login screen. +4. Enter the `ADMIN_USERNAME` and `ADMIN_PASSWORD` you set in the environment variables. +5. Once in, navigate to **Admin → Users** to create user accounts for your team. + +### User Account Types + +| Type | Login Method | Access | +|---|---|---| +| **Admin** | Username + Password | Full access including Settings and User Management | +| **User** | Select name → 4-digit PIN | Projects, Tools, Documents — all read/write | + +### Creating Users (Admin only) + +1. Click **Users** in the sidebar (Admin section). +2. Click **New User**. +3. Enter a username, select role **User**, and assign a **4-digit PIN**. +4. The user's name will appear on the login screen — they select it and enter their PIN. + +--- + +## Updating + +### Registry image: +1. Go to **Docker** tab → click the container icon → **Update** (or Force Update). +2. The `/data` volume is preserved — database, uploads, and user accounts are safe. + +### Locally built image: +```bash +cd /path/to/ai-tools-dashboard +git pull +docker build -t codedump:local . +``` +Then restart the container from the Unraid Docker tab. + +--- + +## Backup + +Everything lives in one directory: + +``` +/mnt/user/appdata/codedump/ +├── dashboard.db ← SQLite database (projects, tools, settings, users) +└── uploads/ ← Uploaded files (logos, markdown docs) ``` -If you also want the explanatory readme: +Back this up with Unraid's **Appdata Backup** plugin or any solution that covers `/mnt/user/appdata`. -```powershell -Copy-Item README.md -Destination +--- + +## Generating a Strong JWT_SECRET + +Run this in an Unraid terminal or any Linux shell: + +```bash +openssl rand -hex 32 ``` -## Prefill Workflow +Paste the output as the value of `JWT_SECRET`. -Before deployment: +--- -1. Fill out `PROJECT-PROFILE-WORKBOOK.md`. -2. Translate the answers into agent-facing defaults in `DEPLOYMENT-PROFILE.md`. -3. Keep the deployment profile concise so it can be read early without wasting context. -4. Copy the suite with the filled-in deployment profile included. +## Port Conflicts -At runtime: +Change **Host Port** to an unused port (e.g. `3100`) and update the WebUI link to match. -1. The agent reads `AGENTS.md`. -2. The agent reads `DEPLOYMENT-PROFILE.md` if it is filled in. -3. The agent reads `SKILLS.md`. -4. The agent opens the relevant hub and specialized skill files only as needed. +--- -## Minimal Install +## Troubleshooting -If the target repository wants the smallest useful setup, copy: +| Symptom | Fix | +|---|---| +| Stuck on login / "Session expired" | Clear browser localStorage and reload. | +| "No user accounts yet" on login | Log in as admin first, then create users via Admin → Users. | +| Admin can't log in after reinstall | If `/data` was wiped, bootstrap re-runs on next start using the current env vars. | +| Container exits immediately | Check Docker logs — usually a permissions issue on `/data`. | +| Can't upload files | Verify `MAX_UPLOAD_MB` and that the host path is writable. | +| Blank page / 404 | Wait 15 seconds after start, then refresh. | -- `AGENTS.md` -- `DEPLOYMENT-PROFILE.md` -- `SKILLS.md` -- `hubs/` -- only the skill files the team expects to use often - -Good minimal baseline: - -- `skills/software/repo-exploration.md` -- `skills/software/feature-implementation.md` -- `skills/software/test-strategy.md` -- `skills/software/code-review.md` -- `skills/debugging/bug-triage.md` -- `skills/debugging/debugging-workflow.md` -- `skills/documentation/technical-docs.md` -- `skills/ui-ux/ux-review.md` - -## How To Customize Safely - -- Keep `AGENTS.md` short and stable; put detail in hubs and skill files. -- Add repo-specific instructions near the top of `AGENTS.md` or in existing repo instruction files. -- Treat `DEPLOYMENT-PROFILE.md` as the canonical place for staged build, tool, environment, and workflow defaults. -- Prefer editing hub routing before adding more root-level rules. -- Add new skill files only when they introduce a distinct workflow, risk area, or deliverable. -- If two skills overlap heavily, merge them or make the routing distinction explicit. - -## Context Limit Guidance - -- Do not instruct the agent to load the entire `skills/` tree on every task. -- Keep the default path to one hub plus two to four specialized skills. -- Reserve four to six skills for large cross-functional tasks. -- Use the routing examples to confirm the suite still routes clearly after customization. - -## Updating The Bundle - -- Pull improvements from this source repository into destination repos periodically. -- Refill the workbook and refresh the deployment profile when your preferred defaults change materially. -- Review local customizations before overwriting shared files. -- If a destination repo has stronger local conventions, keep those and treat this suite as the fallback layer. - -## Suggested Verification After Copying - -Check that the destination repo can answer these questions clearly: - -1. Where does the agent start? `AGENTS.md` -2. Where does the agent get preloaded defaults? `DEPLOYMENT-PROFILE.md` -3. Where does the agent look for available skills? `SKILLS.md` -4. How does the agent decide what to read next? Deployment profile, hubs, and routing examples -5. Does the repo still prioritize local instructions over the generic bundle? It should +View container logs: **Docker tab** → click the container icon → **Logs**. diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..63ceef9 --- /dev/null +++ b/client/index.html @@ -0,0 +1,15 @@ + + + + + + CODEDUMP + + + + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..e398276 --- /dev/null +++ b/client/package.json @@ -0,0 +1,29 @@ +{ + "name": "ai-tools-dashboard-client", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.22.3", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0", + "lucide-react": "^0.364.0" + }, + "devDependencies": { + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^5.4.2", + "vite": "^5.2.6" + } +} diff --git a/client/postcss.config.js b/client/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/client/src/api/index.ts b/client/src/api/index.ts new file mode 100644 index 0000000..96f6dca --- /dev/null +++ b/client/src/api/index.ts @@ -0,0 +1,94 @@ +import type { Project, Tool, Settings } from '../types'; + +const BASE = '/api'; +const TOKEN_KEY = 'codedump_token'; + +function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY); +} + +async function req(path: string, options: RequestInit = {}): Promise { + const token = getToken(); + const headers: Record = { + ...(options.headers as Record || {}), + }; + if (token) headers['Authorization'] = `Bearer ${token}`; + if (options.body && typeof options.body === 'string') headers['Content-Type'] = 'application/json'; + + const res = await fetch(`${BASE}${path}`, { ...options, headers }); + + if (res.status === 401) { + // Token expired — clear storage and reload to login + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem('codedump_user'); + window.location.href = '/login'; + throw new Error('Session expired'); + } + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || res.statusText); + } + if (res.status === 204) return undefined as T; + return res.json(); +} + +// Auth +export const pinLogin = (pin: string) => + req<{ token: string; user: any }>('/auth/pin', { method: 'POST', body: JSON.stringify({ pin }) }); +export const adminLogin = (username: string, password: string) => + req<{ token: string; user: any }>('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }); + +// Users (admin only) +export const getUsers = () => req('/users'); +export const createUser = (data: any) => + req('/users', { method: 'POST', body: JSON.stringify(data) }); +export const updateUser = (id: string, data: any) => + req(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }); +export const deleteUser = (id: string) => req(`/users/${id}`, { method: 'DELETE' }); + +// Projects +export const getProjects = () => req('/projects'); +export const getProject = (id: string) => req(`/projects/${id}`); +export const createProject = (data: Partial) => + req('/projects', { method: 'POST', body: JSON.stringify(data) }); +export const updateProject = (id: string, data: Partial) => + req(`/projects/${id}`, { method: 'PUT', body: JSON.stringify(data) }); +export const deleteProject = (id: string) => req(`/projects/${id}`, { method: 'DELETE' }); + +// Tools +export const getTools = () => req('/tools'); +export const createTool = (data: Partial) => + req('/tools', { method: 'POST', body: JSON.stringify(data) }); +export const updateTool = (id: string, data: Partial) => + req(`/tools/${id}`, { method: 'PUT', body: JSON.stringify(data) }); +export const deleteTool = (id: string) => req(`/tools/${id}`, { method: 'DELETE' }); + +// Uploads +export const uploadDocument = (projectId: string, file: File) => { + const form = new FormData(); + form.append('file', file); + const token = getToken(); + return fetch(`${BASE}/uploads/projects/${projectId}`, { + method: 'POST', + headers: token ? { Authorization: `Bearer ${token}` } : {}, + body: form, + }).then((r) => r.json()); +}; +export const deleteDocument = (id: string) => req(`/uploads/documents/${id}`, { method: 'DELETE' }); +export const getFileUrl = (filename: string) => `${BASE}/uploads/${filename}`; + +// Settings +export const getSettings = () => req('/settings'); +export const updateSettings = (data: Partial) => + req('/settings', { method: 'PUT', body: JSON.stringify(data) }); +export const uploadLogo = (file: File) => { + const form = new FormData(); + form.append('logo', file); + const token = getToken(); + return fetch(`${BASE}/settings/logo`, { + method: 'POST', + headers: token ? { Authorization: `Bearer ${token}` } : {}, + body: form, + }).then((r) => r.json()); +}; diff --git a/client/src/components/layout/Layout.tsx b/client/src/components/layout/Layout.tsx new file mode 100644 index 0000000..196037e --- /dev/null +++ b/client/src/components/layout/Layout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from 'react-router-dom'; +import Sidebar from './Sidebar'; + +export default function Layout() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/client/src/components/layout/Sidebar.tsx b/client/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..de004a5 --- /dev/null +++ b/client/src/components/layout/Sidebar.tsx @@ -0,0 +1,129 @@ +import { NavLink, useNavigate } from 'react-router-dom'; +import { LayoutDashboard, FolderKanban, Wrench, Settings, Users, LogOut, Shield } from 'lucide-react'; +import { useSettings } from '../../hooks/useSettings'; +import { useAuth } from '../../hooks/useAuth'; + +const userLinks = [ + { to: '/', icon: LayoutDashboard, label: 'Dashboard' }, + { to: '/projects', icon: FolderKanban, label: 'Projects' }, + { to: '/tools', icon: Wrench, label: 'Tools' }, +]; + +const adminLinks = [ + { to: '/settings', icon: Settings, label: 'Settings' }, + { to: '/admin/users', icon: Users, label: 'Users' }, +]; + +export default function Sidebar() { + const { settings } = useSettings(); + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogout = () => { + logout(); + navigate('/login', { replace: true }); + }; + + const isAdmin = user?.role === 'admin'; + + return ( + + ); +} diff --git a/client/src/components/projects/ProjectCard.tsx b/client/src/components/projects/ProjectCard.tsx new file mode 100644 index 0000000..f072800 --- /dev/null +++ b/client/src/components/projects/ProjectCard.tsx @@ -0,0 +1,110 @@ +import { useNavigate } from 'react-router-dom'; +import { ExternalLink, FolderOpen, FileText, Percent } from 'lucide-react'; +import type { Project } from '../../types'; +import ProgressBar from '../ui/ProgressBar'; +import Badge from '../ui/Badge'; + +interface Props { + project: Project; +} + +export default function ProjectCard({ project }: Props) { + const navigate = useNavigate(); + + return ( +
navigate(`/projects/${project.id}`)} + className="group relative bg-card border border-border rounded-xl p-5 cursor-pointer hover:border-[var(--accent)]/40 hover:shadow-lg hover:shadow-black/20 transition-all duration-200 animate-fade-in" + > + {/* Accent top bar */} +
+ + {/* Header */} +
+
+
+ {project.is_new && New} + {project.status} +
+

{project.name}

+

{project.category}

+
+
+ +
+
+ + {/* Description */} + {project.description && ( +

{project.description}

+ )} + + {/* Progress */} +
+
+ + Completion + + {project.completion}% +
+ +
+ + {/* Footer */} + + + {/* Tags */} + {project.tags.length > 0 && ( +
+ {project.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ ); +} diff --git a/client/src/components/projects/ProjectForm.tsx b/client/src/components/projects/ProjectForm.tsx new file mode 100644 index 0000000..c9a5bd5 --- /dev/null +++ b/client/src/components/projects/ProjectForm.tsx @@ -0,0 +1,143 @@ +import { useState } from 'react'; +import type { Project } from '../../types'; +import Button from '../ui/Button'; + +const ACCENT_PRESETS = [ + '#6366f1', '#8b5cf6', '#ec4899', '#f97316', + '#10b981', '#06b6d4', '#eab308', '#ef4444', +]; + +const CATEGORIES = ['General', 'AI/ML', 'Automation', 'Data', 'DevOps', 'Frontend', 'Backend', 'Research', 'Other']; + +interface Props { + initial?: Partial; + onSubmit: (data: Partial) => Promise; + onCancel: () => void; +} + +export default function ProjectForm({ initial, onSubmit, onCancel }: Props) { + const [form, setForm] = useState({ + name: initial?.name || '', + description: initial?.description || '', + category: initial?.category || 'General', + status: initial?.status || 'active', + completion: initial?.completion ?? 0, + external_url: initial?.external_url || '', + drive_url: initial?.drive_url || '', + tags: initial?.tags?.join(', ') || '', + accent_color: initial?.accent_color || '#6366f1', + is_new: initial?.is_new ?? true, + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!form.name.trim()) { setError('Name is required'); return; } + setLoading(true); + setError(''); + try { + await onSubmit({ + ...form, + tags: form.tags.split(',').map((t) => t.trim()).filter(Boolean), + completion: Number(form.completion), + external_url: form.external_url || null, + drive_url: form.drive_url || null, + }); + } catch (err: any) { + setError(err.message || 'Something went wrong'); + setLoading(false); + } + }; + + const field = 'w-full bg-surface border border-border rounded-lg px-3 py-2 text-sm text-white placeholder:text-muted focus:outline-none focus:border-[var(--accent)] transition-colors'; + + return ( +
+ {error &&

{error}

} + +
+
+ + setForm({ ...form, name: e.target.value })} placeholder="My AI Project" /> +
+ +
+ + +
+ +
+ + +
+ +
+ +