From e47f32b4e93815e7cc84969fbfa98ddacd9d9a9a Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Wed, 17 Dec 2025 06:39:28 -0500 Subject: [PATCH 001/179] Rewrite: Next.js 16 with AI-powered chat interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete rewrite of the inventory system with: - Next.js 16 App Router + TypeScript - MongoDB/Mongoose data layer - NextAuth with Google OAuth (JWT sessions) - OpenAI-powered chat with multi-agent architecture: - Router agent delegates to specialists - Module agent for storage setup - Inventory agent for item management - Search agent for finding items - Storage modules with dimension templates - Item tracking with flexible parameters and units - Token tracking with compression thresholds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 560 ++ IMPLEMENTATION_PLAN.md | 189 + inventory-system/.env.example | 21 - inventory-system/.gitignore | 74 - inventory-system/README.md | 406 - inventory-system/backend/Dockerfile | 21 - inventory-system/backend/app/__init__.py | 36 - inventory-system/backend/app/models.py | 212 - inventory-system/backend/app/routes/items.py | 213 - .../backend/app/routes/locations.py | 101 - inventory-system/backend/app/routes/main.py | 26 - .../backend/app/routes/modules.py | 275 - inventory-system/backend/app/routes/search.py | 48 - inventory-system/backend/requirements.txt | 6 - inventory-system/backend/run.py | 30 - inventory-system/create_sample_data.py | 237 - inventory-system/docker-compose.yml | 46 - inventory-system/docs/ARCHITECTURE.md | 578 -- inventory-system/docs/DEPLOY.md | 548 -- inventory-system/docs/DEPLOYMENT_SUMMARY.md | 312 - inventory-system/docs/FILE_INDEX.md | 494 -- .../docs/GETTING_STARTED_CHECKLIST.md | 237 - inventory-system/docs/INDEX.md | 457 - inventory-system/docs/PROJECT_OVERVIEW.md | 665 -- inventory-system/docs/PROJECT_SUMMARY.md | 486 -- inventory-system/docs/QUICKSTART.md | 279 - inventory-system/docs/QUICK_REFERENCE.md | 534 -- inventory-system/docs/ROADMAP.md | 1065 --- inventory-system/docs/START_HERE.md | 456 - inventory-system/docs/TESTING_CHECKLIST.md | 763 -- inventory-system/docs/VERSION.md | 249 - .../frontend/static/css/style.css | 829 -- inventory-system/frontend/static/js/main.js | 77 - inventory-system/frontend/templates/base.html | 52 - .../frontend/templates/index.html | 102 - .../frontend/templates/items/form.html | 93 - .../frontend/templates/items/list.html | 65 - .../frontend/templates/items/view.html | 107 - .../frontend/templates/levels/form.html | 62 - .../frontend/templates/levels/view.html | 81 - .../frontend/templates/locations/form.html | 55 - .../frontend/templates/locations/list.html | 71 - .../frontend/templates/locations/view.html | 90 - .../frontend/templates/modules/form.html | 44 - .../frontend/templates/modules/list.html | 49 - .../frontend/templates/modules/view.html | 74 - .../frontend/templates/search/results.html | 64 - inventory-system/nginx.conf | 26 - specification/agents.md | 729 ++ specification/ai-engine.md | 274 + specification/architecture.md | 169 + specification/authentication.md | 351 + specification/data-models.md | 269 + specification/frontend.md | 431 + specification/sessions.md | 420 + web/.gitignore | 41 + web/.nvmrc | 1 + web/README.md | 36 + web/app/(protected)/chat/page.tsx | 13 + web/app/(protected)/layout.tsx | 24 + web/app/(protected)/sessions/page.tsx | 26 + web/app/(protected)/settings/agents/page.tsx | 31 + web/app/(protected)/settings/page.tsx | 55 + web/app/(protected)/settings/tools/page.tsx | 32 + web/app/api/agents/[name]/route.ts | 87 + web/app/api/agents/route.ts | 66 + web/app/api/auth/[...nextauth]/route.ts | 3 + web/app/api/chat/route.ts | 101 + web/app/api/sessions/[id]/compress/route.ts | 90 + web/app/api/sessions/[id]/route.ts | 100 + web/app/api/sessions/route.ts | 71 + web/app/api/tools/[name]/route.ts | 50 + web/app/api/tools/route.ts | 25 + web/app/auth/error/page.tsx | 54 + web/app/auth/signin/page.tsx | 56 + web/app/favicon.ico | Bin 0 -> 25931 bytes web/app/globals.css | 18 + web/app/layout.tsx | 35 + web/app/page.tsx | 117 + web/app/providers.tsx | 26 + web/components/chat/ChatContainer.tsx | 182 + web/components/chat/ChatInput.tsx | 123 + web/components/chat/ChatMessage.tsx | 93 + web/components/chat/ContextIndicator.tsx | 46 + web/components/chat/index.ts | 4 + web/components/layout/Header.tsx | 18 + web/components/layout/Sidebar.tsx | 88 + web/components/layout/UserMenu.tsx | 69 + web/components/layout/index.ts | 3 + web/eslint.config.mjs | 18 + web/lib/agentRunner.ts | 206 + web/lib/auth.config.ts | 48 + web/lib/auth.ts | 28 + web/lib/contextManager.ts | 63 + web/lib/mongodb-client.ts | 31 + web/lib/mongodb.ts | 54 + web/lib/openai.ts | 14 + web/lib/pathValidator.ts | 196 + web/lib/seeds/agents.ts | 279 + web/lib/seeds/index.ts | 67 + web/lib/seeds/parameters.ts | 176 + web/lib/seeds/templates.ts | 57 + web/lib/seeds/tools.ts | 458 + web/lib/seeds/units.ts | 89 + web/lib/toolHandlers.ts | 156 + web/middleware.ts | 14 + web/models/Agent.ts | 70 + web/models/DimensionTemplate.ts | 40 + web/models/Item.ts | 58 + web/models/ParameterKey.ts | 29 + web/models/Session.ts | 102 + web/models/StorageModule.ts | 46 + web/models/Tool.ts | 76 + web/models/Unit.ts | 29 + web/models/User.ts | 45 + web/models/index.ts | 19 + web/next.config.ts | 15 + web/package-lock.json | 7655 +++++++++++++++++ web/package.json | 33 + web/postcss.config.mjs | 7 + web/public/file.svg | 1 + web/public/globe.svg | 1 + web/public/next.svg | 1 + web/public/vercel.svg | 1 + web/public/window.svg | 1 + web/repositories/agentRepository.ts | 159 + web/repositories/index.ts | 8 + web/repositories/itemRepository.ts | 188 + web/repositories/moduleRepository.ts | 155 + web/repositories/parameterKeyRepository.ts | 72 + web/repositories/sessionRepository.ts | 259 + web/repositories/templateRepository.ts | 103 + web/repositories/toolRepository.ts | 174 + web/repositories/unitRepository.ts | 72 + web/tsconfig.json | 34 + web/types/next-auth.d.ts | 10 + 136 files changed, 16593 insertions(+), 10816 deletions(-) create mode 100644 AGENTS.md create mode 100644 IMPLEMENTATION_PLAN.md delete mode 100644 inventory-system/.env.example delete mode 100644 inventory-system/.gitignore delete mode 100644 inventory-system/README.md delete mode 100644 inventory-system/backend/Dockerfile delete mode 100644 inventory-system/backend/app/__init__.py delete mode 100644 inventory-system/backend/app/models.py delete mode 100644 inventory-system/backend/app/routes/items.py delete mode 100644 inventory-system/backend/app/routes/locations.py delete mode 100644 inventory-system/backend/app/routes/main.py delete mode 100644 inventory-system/backend/app/routes/modules.py delete mode 100644 inventory-system/backend/app/routes/search.py delete mode 100644 inventory-system/backend/requirements.txt delete mode 100644 inventory-system/backend/run.py delete mode 100644 inventory-system/create_sample_data.py delete mode 100644 inventory-system/docker-compose.yml delete mode 100644 inventory-system/docs/ARCHITECTURE.md delete mode 100644 inventory-system/docs/DEPLOY.md delete mode 100644 inventory-system/docs/DEPLOYMENT_SUMMARY.md delete mode 100644 inventory-system/docs/FILE_INDEX.md delete mode 100644 inventory-system/docs/GETTING_STARTED_CHECKLIST.md delete mode 100644 inventory-system/docs/INDEX.md delete mode 100644 inventory-system/docs/PROJECT_OVERVIEW.md delete mode 100644 inventory-system/docs/PROJECT_SUMMARY.md delete mode 100644 inventory-system/docs/QUICKSTART.md delete mode 100644 inventory-system/docs/QUICK_REFERENCE.md delete mode 100644 inventory-system/docs/ROADMAP.md delete mode 100644 inventory-system/docs/START_HERE.md delete mode 100644 inventory-system/docs/TESTING_CHECKLIST.md delete mode 100644 inventory-system/docs/VERSION.md delete mode 100644 inventory-system/frontend/static/css/style.css delete mode 100644 inventory-system/frontend/static/js/main.js delete mode 100644 inventory-system/frontend/templates/base.html delete mode 100644 inventory-system/frontend/templates/index.html delete mode 100644 inventory-system/frontend/templates/items/form.html delete mode 100644 inventory-system/frontend/templates/items/list.html delete mode 100644 inventory-system/frontend/templates/items/view.html delete mode 100644 inventory-system/frontend/templates/levels/form.html delete mode 100644 inventory-system/frontend/templates/levels/view.html delete mode 100644 inventory-system/frontend/templates/locations/form.html delete mode 100644 inventory-system/frontend/templates/locations/list.html delete mode 100644 inventory-system/frontend/templates/locations/view.html delete mode 100644 inventory-system/frontend/templates/modules/form.html delete mode 100644 inventory-system/frontend/templates/modules/list.html delete mode 100644 inventory-system/frontend/templates/modules/view.html delete mode 100644 inventory-system/frontend/templates/search/results.html delete mode 100644 inventory-system/nginx.conf create mode 100644 specification/agents.md create mode 100644 specification/ai-engine.md create mode 100644 specification/architecture.md create mode 100644 specification/authentication.md create mode 100644 specification/data-models.md create mode 100644 specification/frontend.md create mode 100644 specification/sessions.md create mode 100644 web/.gitignore create mode 100644 web/.nvmrc create mode 100644 web/README.md create mode 100644 web/app/(protected)/chat/page.tsx create mode 100644 web/app/(protected)/layout.tsx create mode 100644 web/app/(protected)/sessions/page.tsx create mode 100644 web/app/(protected)/settings/agents/page.tsx create mode 100644 web/app/(protected)/settings/page.tsx create mode 100644 web/app/(protected)/settings/tools/page.tsx create mode 100644 web/app/api/agents/[name]/route.ts create mode 100644 web/app/api/agents/route.ts create mode 100644 web/app/api/auth/[...nextauth]/route.ts create mode 100644 web/app/api/chat/route.ts create mode 100644 web/app/api/sessions/[id]/compress/route.ts create mode 100644 web/app/api/sessions/[id]/route.ts create mode 100644 web/app/api/sessions/route.ts create mode 100644 web/app/api/tools/[name]/route.ts create mode 100644 web/app/api/tools/route.ts create mode 100644 web/app/auth/error/page.tsx create mode 100644 web/app/auth/signin/page.tsx create mode 100644 web/app/favicon.ico create mode 100644 web/app/globals.css create mode 100644 web/app/layout.tsx create mode 100644 web/app/page.tsx create mode 100644 web/app/providers.tsx create mode 100644 web/components/chat/ChatContainer.tsx create mode 100644 web/components/chat/ChatInput.tsx create mode 100644 web/components/chat/ChatMessage.tsx create mode 100644 web/components/chat/ContextIndicator.tsx create mode 100644 web/components/chat/index.ts create mode 100644 web/components/layout/Header.tsx create mode 100644 web/components/layout/Sidebar.tsx create mode 100644 web/components/layout/UserMenu.tsx create mode 100644 web/components/layout/index.ts create mode 100644 web/eslint.config.mjs create mode 100644 web/lib/agentRunner.ts create mode 100644 web/lib/auth.config.ts create mode 100644 web/lib/auth.ts create mode 100644 web/lib/contextManager.ts create mode 100644 web/lib/mongodb-client.ts create mode 100644 web/lib/mongodb.ts create mode 100644 web/lib/openai.ts create mode 100644 web/lib/pathValidator.ts create mode 100644 web/lib/seeds/agents.ts create mode 100644 web/lib/seeds/index.ts create mode 100644 web/lib/seeds/parameters.ts create mode 100644 web/lib/seeds/templates.ts create mode 100644 web/lib/seeds/tools.ts create mode 100644 web/lib/seeds/units.ts create mode 100644 web/lib/toolHandlers.ts create mode 100644 web/middleware.ts create mode 100644 web/models/Agent.ts create mode 100644 web/models/DimensionTemplate.ts create mode 100644 web/models/Item.ts create mode 100644 web/models/ParameterKey.ts create mode 100644 web/models/Session.ts create mode 100644 web/models/StorageModule.ts create mode 100644 web/models/Tool.ts create mode 100644 web/models/Unit.ts create mode 100644 web/models/User.ts create mode 100644 web/models/index.ts create mode 100644 web/next.config.ts create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/postcss.config.mjs create mode 100644 web/public/file.svg create mode 100644 web/public/globe.svg create mode 100644 web/public/next.svg create mode 100644 web/public/vercel.svg create mode 100644 web/public/window.svg create mode 100644 web/repositories/agentRepository.ts create mode 100644 web/repositories/index.ts create mode 100644 web/repositories/itemRepository.ts create mode 100644 web/repositories/moduleRepository.ts create mode 100644 web/repositories/parameterKeyRepository.ts create mode 100644 web/repositories/sessionRepository.ts create mode 100644 web/repositories/templateRepository.ts create mode 100644 web/repositories/toolRepository.ts create mode 100644 web/repositories/unitRepository.ts create mode 100644 web/tsconfig.json create mode 100644 web/types/next-auth.d.ts diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4dffe28 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,560 @@ +# WhereTF - Development Guidelines + +This document describes the patterns, conventions, and guidelines for developing the WhereTF application. + +## Project Overview + +WhereTF is an AI-powered workshop inventory management system. Users describe items through voice, text, or images via a chat interface. The AI identifies items, assigns parameters, and stores location information. + +See the [specification/](./specification/) folder for detailed documentation: +- [architecture.md](./specification/architecture.md) - System overview +- [data-models.md](./specification/data-models.md) - MongoDB schemas +- [agents.md](./specification/agents.md) - AI agent definitions +- [ai-engine.md](./specification/ai-engine.md) - Agent runner +- [sessions.md](./specification/sessions.md) - Session management +- [authentication.md](./specification/authentication.md) - NextAuth setup +- [frontend.md](./specification/frontend.md) - UI specification + +--- + +## Data Access Pattern + +We use a **three-layer architecture** for data access: + +``` +┌─────────────────────────────────────────┐ +│ API Routes (thin) │ ← Request/response handling only +├─────────────────────────────────────────┤ +│ Repository Layer │ ← Business logic, validation +├─────────────────────────────────────────┤ +│ Schema/Model Layer │ ← Mongoose schemas, DB structure +└─────────────────────────────────────────┘ +``` + +### 1. Schema Layer (`/models`) + +Mongoose schemas define the data structure. **No business logic here.** + +```javascript +// models/Item.js +import mongoose from 'mongoose'; + +const parameterValueSchema = new mongoose.Schema({ + key: { type: String, required: true, lowercase: true }, + value: { type: String, required: true }, + unit: { type: String, lowercase: true }, +}, { _id: false }); + +const itemSchema = new mongoose.Schema({ + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + name: { type: String, required: true }, + description: String, + parameters: [parameterValueSchema], + location: { type: String, required: true, unique: true, index: true } +}, { timestamps: true }); + +itemSchema.index({ name: 'text', description: 'text' }); +itemSchema.index({ 'parameters.key': 1, 'parameters.value': 1 }); + +export default mongoose.models.Item || mongoose.model('Item', itemSchema); +``` + +### 2. Repository Layer (`/repositories`) + +All business logic lives here. Repositories handle: +- Validation +- Authorization (user scoping) +- Complex queries +- Data transformation +- Error handling + +```javascript +// repositories/itemRepository.js +import Item from '@/models/Item'; +import { validatePath } from '@/lib/pathValidator'; + +export async function create({ userId, name, description, parameters, location }) { + // Validate location path + const pathValidation = await validatePath(location, userId); + if (!pathValidation.valid) { + throw new Error(`Invalid location: ${pathValidation.error}`); + } + + // Check for duplicate location + const existing = await Item.findOne({ user: userId, location }); + if (existing) { + throw new Error(`Location ${location} already has an item: ${existing.name}`); + } + + // Create item + const item = await Item.create({ + user: userId, + name, + description, + parameters, + location + }); + + return item; +} + +export async function search({ userId, query, location, parameters }) { + const filter = { user: userId }; + + // Text search + if (query) { + filter.$text = { $search: query }; + } + + // Location prefix search + if (location) { + filter.location = { $regex: `^${location}` }; + } + + // Parameter filters + if (parameters?.length) { + filter.$and = parameters.map(p => ({ + parameters: { $elemMatch: { key: p.key, value: p.value } } + })); + } + + return Item.find(filter).sort({ updatedAt: -1 }); +} + +export async function update({ userId, location, updates }) { + const item = await Item.findOne({ user: userId, location }); + if (!item) { + throw new Error(`No item found at ${location}`); + } + + Object.assign(item, updates); + await item.save(); + + return item; +} + +export async function remove({ userId, location }) { + const result = await Item.deleteOne({ user: userId, location }); + if (result.deletedCount === 0) { + throw new Error(`No item found at ${location}`); + } + return { success: true }; +} +``` + +### 3. API Layer (`/app/api`) + +API routes are **thin**. They only handle: +- Request parsing +- Authentication check +- Calling repository methods +- Response formatting + +**No business logic in API routes.** + +```javascript +// app/api/items/route.js +import { auth } from '@/lib/auth'; +import * as itemRepo from '@/repositories/itemRepository'; + +export async function GET(request) { + const session = await auth(); + if (!session) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const query = searchParams.get('query'); + const location = searchParams.get('location'); + + try { + const items = await itemRepo.search({ + userId: session.user.id, + query, + location + }); + return Response.json({ items }); + } catch (error) { + return Response.json({ error: error.message }, { status: 400 }); + } +} + +export async function POST(request) { + const session = await auth(); + if (!session) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + + try { + const item = await itemRepo.create({ + userId: session.user.id, + ...body + }); + return Response.json({ item }, { status: 201 }); + } catch (error) { + return Response.json({ error: error.message }, { status: 400 }); + } +} +``` + +--- + +## Directory Structure + +``` +wheretf/ +├── AGENTS.md # This file +├── specification/ # Design docs +│ +├── app/ # Next.js App Router +│ ├── layout.js +│ ├── page.js # Landing page +│ ├── globals.css +│ ├── providers.js # Client providers +│ │ +│ ├── api/ # API routes (thin) +│ │ ├── auth/[...nextauth]/ +│ │ ├── chat/ +│ │ ├── sessions/ +│ │ ├── agents/ +│ │ ├── tools/ +│ │ ├── items/ +│ │ └── modules/ +│ │ +│ ├── auth/ # Auth pages +│ │ ├── signin/ +│ │ └── error/ +│ │ +│ └── (protected)/ # Authenticated routes +│ ├── layout.js +│ ├── chat/ +│ ├── sessions/ +│ └── settings/ +│ +├── components/ # React components +│ ├── layout/ +│ ├── chat/ +│ ├── agents/ +│ └── sessions/ +│ +├── models/ # Mongoose schemas +│ ├── User.js +│ ├── Session.js +│ ├── Item.js +│ ├── StorageModule.js +│ ├── DimensionTemplate.js +│ ├── ParameterKey.js +│ ├── Unit.js +│ ├── Agent.js +│ └── Tool.js +│ +├── repositories/ # Business logic +│ ├── itemRepository.js +│ ├── moduleRepository.js +│ ├── templateRepository.js +│ ├── parameterRepository.js +│ ├── unitRepository.js +│ ├── sessionRepository.js +│ ├── agentRepository.js +│ └── toolRepository.js +│ +├── lib/ # Shared utilities +│ ├── auth.js # NextAuth config +│ ├── mongodb.js # DB connection +│ ├── openai.js # OpenAI client +│ ├── agentRunner.js # Agent execution +│ ├── toolHandlers.js # Tool -> repository mapping +│ ├── contextManager.js # Token tracking +│ ├── pathValidator.js # Location path validation +│ └── seeds/ # Seed data +│ ├── index.js +│ ├── agents.js +│ ├── tools.js +│ ├── parameters.js +│ └── units.js +│ +└── hooks/ # React hooks + ├── useChat.js + ├── useAgents.js + ├── useSessions.js + └── useTools.js +``` + +--- + +## Coding Conventions + +### General + +- **TypeScript optional** - Start with JavaScript, add TypeScript later if needed +- **ES Modules** - Use `import/export`, not `require` +- **Async/await** - Prefer over `.then()` chains +- **Early returns** - Fail fast, reduce nesting + +### Naming + +| Type | Convention | Example | +|------|------------|---------| +| Files (components) | PascalCase | `MessageList.jsx` | +| Files (utilities) | camelCase | `agentRunner.js` | +| Files (models) | PascalCase | `StorageModule.js` | +| Variables | camelCase | `userId`, `itemCount` | +| Constants | UPPER_SNAKE | `MAX_TOKENS`, `WARNING_THRESHOLD` | +| React components | PascalCase | `ChatContainer` | +| Functions | camelCase | `createItem`, `validatePath` | +| Database fields | camelCase | `createdAt`, `isSystem` | + +### Repository Pattern + +All repository methods receive an object with named parameters: + +```javascript +// Good +export async function create({ userId, name, location }) { ... } + +// Bad +export async function create(userId, name, location) { ... } +``` + +All repository methods that access user data **must** include `userId` for scoping: + +```javascript +// Good - always scope by user +const items = await Item.find({ user: userId, ... }); + +// Bad - no user scoping +const items = await Item.find({ location }); // Security issue! +``` + +### Error Handling + +Repositories throw errors. API routes catch and format them: + +```javascript +// Repository - throws +export async function create({ userId, name }) { + if (!name) { + throw new Error('Name is required'); + } + // ... +} + +// API route - catches +try { + const item = await itemRepo.create({ ... }); + return Response.json({ item }); +} catch (error) { + return Response.json({ error: error.message }, { status: 400 }); +} +``` + +### API Responses + +Consistent response format: + +```javascript +// Success +{ items: [...] } +{ item: {...} } +{ session: {...} } + +// Error +{ error: "Error message" } + +// With metadata +{ + items: [...], + pagination: { page: 1, total: 50 } +} +``` + +--- + +## Frontend Patterns + +### Server vs Client Components + +- **Server Components** (default) - Data fetching, no interactivity +- **Client Components** (`'use client'`) - Interactivity, hooks, browser APIs + +```javascript +// Server component (default) - fetches data +// app/(protected)/sessions/page.js +import { auth } from '@/lib/auth'; +import { SessionList } from '@/components/sessions/SessionList'; + +export default async function SessionsPage() { + const session = await auth(); + // Can fetch data directly here + return ; +} + +// Client component - handles interaction +// components/sessions/SessionList.jsx +'use client'; + +import { useSessions } from '@/hooks/useSessions'; + +export function SessionList({ userId }) { + const { sessions, deleteSession } = useSessions(userId); + // Can use hooks, handle clicks, etc. +} +``` + +### Data Fetching + +Use React Query (or SWR) for client-side data fetching: + +```javascript +// hooks/useSessions.js +'use client'; + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + +export function useSessions() { + const queryClient = useQueryClient(); + + const { data: sessions, isLoading } = useQuery({ + queryKey: ['sessions'], + queryFn: () => fetch('/api/sessions').then(r => r.json()) + }); + + const deleteMutation = useMutation({ + mutationFn: (id) => fetch(`/api/sessions/${id}`, { method: 'DELETE' }), + onSuccess: () => queryClient.invalidateQueries(['sessions']) + }); + + return { + sessions: sessions?.sessions ?? [], + isLoading, + deleteSession: deleteMutation.mutate + }; +} +``` + +### Component Structure + +Keep components focused. Split when they get too large: + +```javascript +// ChatContainer.jsx - orchestrates +export function ChatContainer({ sessionId }) { + const { messages, sendMessage, context } = useChat(sessionId); + + return ( +
+ + + +
+ ); +} + +// MessageList.jsx - displays messages +export function MessageList({ messages }) { + return ( +
+ {messages.map(msg => ( + + ))} +
+ ); +} + +// Message.jsx - single message +export function Message({ message }) { + // ... +} +``` + +--- + +## Seed Data + +Default agents and tools are defined in code and seeded on first run. + +### When to Seed + +- On first authenticated request (check if user has agents) +- Via explicit setup endpoint +- On application startup (for global tools) + +### Seed Strategy + +```javascript +// lib/seeds/index.js +export async function seedDefaults(userId) { + // Global data (no userId) + await seedTools(); + + // Per-user data + await seedAgents(userId); + await seedParameterKeys(userId); + await seedUnits(userId); +} + +// Use $setOnInsert to only create if not exists +await Agent.findOneAndUpdate( + { user: userId, name: agent.name }, + { $setOnInsert: { ...agent, user: userId } }, + { upsert: true } +); +``` + +--- + +## Testing + +(To be expanded) + +- Unit tests for repository methods +- Integration tests for API routes +- E2E tests for critical user flows + +--- + +## Environment Variables + +```bash +# .env.local + +# NextAuth +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=your-secret-key + +# Google OAuth +GOOGLE_CLIENT_ID=your-client-id +GOOGLE_CLIENT_SECRET=your-client-secret + +# MongoDB +MONGODB_URI=mongodb://localhost:27017/wheretf + +# OpenAI +OPENAI_API_KEY=your-openai-key +``` + +--- + +## Common Tasks + +### Adding a New Model + +1. Create schema in `/models/NewModel.js` +2. Create repository in `/repositories/newModelRepository.js` +3. Create API routes in `/app/api/new-model/` +4. Add to seed data if needed + +### Adding a New Tool + +1. Add tool definition to `/lib/seeds/tools.js` +2. Add handler mapping in `/lib/toolHandlers.js` +3. Implement handler in appropriate repository +4. Assign to relevant agents + +### Adding a New Agent + +1. Add agent definition to `/lib/seeds/agents.js` +2. Add `runNewAgent` tool if it should be callable from router +3. Assign appropriate tools to the agent diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..139844f --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,189 @@ +# WhereTF Implementation Plan + +## Phase 1: Project Setup + +- [x] Initialize Next.js project with App Router +- [x] Install core dependencies + - [x] mongoose + - [x] next-auth + @auth/mongodb-adapter + - [x] openai + - [x] tailwindcss + - [x] @tanstack/react-query +- [x] Set up environment variables (.env.local.example) +- [x] Configure Tailwind CSS +- [x] Set up MongoDB connection (lib/mongodb.ts) +- [x] Create base layout and providers + +## Phase 2: Authentication + +- [x] Configure NextAuth with Google provider (lib/auth.ts) +- [x] Create User model (extends NextAuth default) +- [x] Set up auth API routes (app/api/auth/[...nextauth]) +- [x] Create auth middleware for protected routes +- [x] Build sign-in page (app/auth/signin) +- [x] Build error page (app/auth/error) +- [x] Add SessionProvider to root layout (in providers.tsx) +- [x] Test auth flow end-to-end (build passes) + +## Phase 3: Data Layer - Models & Repositories + +### Core Models +- [x] ParameterKey model + repository +- [x] Unit model + repository +- [x] DimensionTemplate model + repository +- [x] StorageModule model + repository +- [x] Item model + repository + +### AI/Session Models +- [x] Agent model + repository +- [x] Tool model + repository +- [x] Session model + repository + +### Seed Data +- [x] Create seed data definitions (lib/seeds/) + - [x] Default parameter keys (length, voltage, material, etc.) + - [x] Default units (mm, in, V, ohm, etc.) + - [x] Default tools (all tool definitions) + - [x] Default agents (router, module, inventory, search) +- [x] Create seed runner function +- [ ] Hook seed to first authenticated request or startup + +## Phase 4: Frontend - Layout & Navigation + +- [x] Create protected layout with sidebar (app/(protected)/layout.tsx) +- [x] Build Sidebar component +- [x] Build Header component +- [x] Build UserMenu component +- [x] Create settings pages (agents, tools) +- [x] Build landing page (app/page.tsx) +- [x] Build placeholder pages (chat, sessions, settings) + +## Phase 5: Agent Management UI + API + +### API Endpoints +- [x] GET /api/agents - List agents (with auto-seed) +- [x] POST /api/agents - Create agent +- [x] GET /api/agents/[name] - Get agent +- [x] PATCH /api/agents/[name] - Update agent +- [x] DELETE /api/agents/[name] - Delete agent +- [ ] POST /api/agents/[name]/reset - Reset to default + +### API Endpoints - Tools +- [x] GET /api/tools - List tools (with auto-seed) +- [x] PATCH /api/tools/[name] - Toggle active + +### Frontend +- [ ] Agent list page (app/(protected)/settings/agents/page.js) +- [ ] AgentList component +- [ ] AgentCard component +- [ ] Agent editor page (app/(protected)/settings/agents/[name]/page.js) +- [ ] AgentEditor component +- [ ] ToolSelector component +- [ ] New agent page (app/(protected)/settings/agents/new/page.js) +- [ ] Tool list page (app/(protected)/settings/tools/page.js) +- [ ] useAgents hook +- [ ] useTools hook + +## Phase 6: Session Management UI + API + +### API Endpoints +- [ ] GET /api/sessions - List sessions +- [ ] POST /api/sessions - Create session +- [ ] GET /api/sessions/[id] - Get session with messages +- [ ] DELETE /api/sessions/[id] - Delete session +- [ ] POST /api/sessions/[id]/compress - Compress session + +### Frontend +- [ ] Sessions page (app/(protected)/sessions/page.js) +- [ ] SessionList component +- [ ] SessionCard component +- [ ] CompressDialog component +- [ ] DeleteDialog component +- [ ] useSessions hook + +## Phase 7: Chat UI + +- [ ] Chat page (app/(protected)/chat/page.js) +- [ ] Chat with session page (app/(protected)/chat/[sessionId]/page.js) +- [ ] ChatContainer component +- [ ] MessageList component +- [ ] Message component (user + assistant variants) +- [ ] MessageInput component +- [ ] ImageUpload component (drag & drop, paste, file picker) +- [ ] VoiceInput component (Web Speech API) +- [ ] ContextIndicator component +- [ ] AgentBadge component +- [ ] ToolCallDisplay component (collapsible) +- [ ] useChat hook + +## Phase 8: AI Engine + +### Core Engine +- [ ] OpenAI client setup (lib/openai.js) +- [ ] Tool handler mapping (lib/toolHandlers.js) +- [ ] Agent runner (lib/agentRunner.js) + - [ ] executeAgent function + - [ ] Tool call processing loop + - [ ] Recursive agent invocation + - [ ] Multimodal message building + +### Context Management +- [ ] Token estimation (lib/contextManager.js) +- [ ] Context tracking on message add +- [ ] Compression function +- [ ] Warning/critical threshold logic + +### Chat API +- [ ] POST /api/chat - Send message, get AI response + - [ ] Load session + - [ ] Run agent + - [ ] Track tokens + - [ ] Return response with context status + +## Phase 9: Inventory Management (AI-driven) + +These are handled by AI tools, but we need the underlying APIs for direct access: + +### API Endpoints (optional, for debugging/admin) +- [ ] GET /api/items - List/search items +- [ ] GET /api/modules - List modules +- [ ] GET /api/templates - List templates + +### Path Validation +- [ ] validatePath utility (lib/pathValidator.js) + +## Phase 10: Polish & Testing + +- [ ] Error handling throughout +- [ ] Loading states +- [ ] Empty states +- [ ] Mobile responsiveness +- [ ] Dark mode support +- [ ] Manual testing of all flows + - [ ] Sign in / sign out + - [ ] Create storage module via chat + - [ ] Add items via chat + - [ ] Search items via chat + - [ ] Edit agent instructions + - [ ] Session compression + +## Phase 11: Deployment + +- [ ] Create Dockerfile +- [ ] Create docker-compose.yml (with MongoDB) +- [ ] Environment variable documentation +- [ ] Production build test +- [ ] Deploy to hosting platform + +--- + +## Current Status + +**Phase:** 5 - Agent Management UI + API +**Last Updated:** 2025-12-15 + +## Notes + +- Keep API routes thin - all business logic in repositories +- Test each phase before moving to next +- Commit frequently with descriptive messages diff --git a/inventory-system/.env.example b/inventory-system/.env.example deleted file mode 100644 index 9984914..0000000 --- a/inventory-system/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -# Inventory System Environment Configuration -# Copy this file to .env and customize as needed - -# Database Configuration -DATABASE_URL=postgresql://inventoryuser:inventorypass@postgres:5432/inventory -POSTGRES_USER=inventoryuser -POSTGRES_PASSWORD=inventorypass -POSTGRES_DB=inventory - -# Flask Configuration -FLASK_ENV=development -FLASK_APP=app -SECRET_KEY=dev-secret-key-change-in-production - -# Application Port -PORT=5000 - -# Production Settings (uncomment for production) -# FLASK_ENV=production -# SECRET_KEY=your-secure-random-key-here -# DATABASE_URL=postgresql://user:password@host:5432/dbname diff --git a/inventory-system/.gitignore b/inventory-system/.gitignore deleted file mode 100644 index 77fe08c..0000000 --- a/inventory-system/.gitignore +++ /dev/null @@ -1,74 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# Virtual Environment -venv/ -env/ -ENV/ - -# IDEs -.vscode/ -.idea/ -.claude/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db -*:Zone.Identifier - -# Environment variables -.env -.env.local - -# Database -data/ -*.sql -*.db -*.sqlite3 - -# Logs -*.log -logs/ - -# Docker -.docker/ - -# Backup files -backup*.sql -backup*.tar.gz - -# Flask -instance/ -.webassets-cache - -# Testing -.pytest_cache/ -.coverage -htmlcov/ - -# Temporary files -*.tmp -temp/ -tmp/ diff --git a/inventory-system/README.md b/inventory-system/README.md deleted file mode 100644 index 97ab486..0000000 --- a/inventory-system/README.md +++ /dev/null @@ -1,406 +0,0 @@ -# 🏠 Homelab Inventory System - -A comprehensive inventory management system for homelab, makerspace, and workshop environments. Track thousands of items across organized storage modules with natural language search and AI-powered capabilities. - -## 🚀 Phase 1: Foundation (Current) - -This is **Phase 1** of an 8-phase development roadmap. The current release includes: - -- ✅ Complete storage hierarchy (Modules → Levels → Locations) -- ✅ Full CRUD operations for items, modules, levels, and locations -- ✅ Web UI with responsive design -- ✅ Basic keyword search -- ✅ Location visualization (grid view) -- ✅ Docker deployment ready -- ✅ PostgreSQL backend with proper relationships - -### Coming in Future Phases - -- 🔜 **Phase 2**: Smart location suggestions -- 🔜 **Phase 3**: Duplicate detection -- 🔜 **Phase 4**: Semantic search with AI embeddings -- 🔜 **Phase 5**: CLI interface -- 🔜 **Phase 6**: Voice interface -- 🔜 **Phase 7**: Advanced AI features -- 🔜 **Phase 8**: Production polish & mobile optimization - -## 📋 Prerequisites - -- Docker and Docker Compose -- Git (for cloning) -- 2GB RAM minimum -- 10GB disk space - -## 🏃 Quick Start - -### 1. Clone or Extract the Project - -If you have the project as files: -```bash -cd inventory-system -``` - -### 2. Start the System - -```bash -docker-compose up -d -``` - -This will: -- Start PostgreSQL database -- Build and start the Flask backend -- Start nginx reverse proxy - -### 3. Access the Application - -Open your browser and navigate to: -``` -http://localhost:8080 -``` - -### 4. Stop the System - -```bash -docker-compose down -``` - -To stop and remove all data: -```bash -docker-compose down -v -``` - -## 📚 Usage Guide - -### First Steps - -1. **Create a Module**: A module is a storage unit (cabinet, shelving unit, etc.) - - Navigate to "Modules" → "Add Module" - - Example: Name it "Zeus" or "Main Workbench" - -2. **Add Levels**: Levels are drawers, shelves, or compartments within a module - - View your module → "Add Level" - - Specify grid dimensions (rows × columns) - - Example: 4 rows × 6 columns creates locations A1-A6, B1-B6, etc. - -3. **Add Items**: Store your inventory items - - Navigate to "Items" → "Add Item" - - Provide a natural language description - - Optionally assign a storage location - - Example: "Pan head phillips screw, 3/4 inch long, #8, mild steel" - -4. **Search**: Find items quickly - - Use the search bar to find items by name, description, or tags - - View item locations on the results page - -### Storage Hierarchy - -``` -Module (e.g., "Zeus", "Muse") -├── Level 1 (e.g., drawer, shelf) -│ ├── Location A1 -│ ├── Location A2 -│ └── ... -├── Level 2 -│ ├── Location A1 -│ └── ... -└── ... -``` - -### Example: Adding a Screw - -1. Go to "Items" → "Add Item" -2. Fill in: - - **Name**: "Phillips Pan Head #8 Screw" - - **Description**: "Pan head phillips screw, 3/4 inch long, #8 diameter, mild steel" - - **Category**: "Fasteners" - - **Item Type**: "solid" - - **Quantity**: "100" - - **Unit**: "pieces" - - **Tags**: "screw, phillips, pan head, #8, fastener" - - **Location**: "Muse:4:A3" (Module: Muse, Level: 4, Location: A3) -3. Click "Create Item" - -### Location Types - -The system supports different location types for various storage needs: - -- **general**: Standard bins -- **small_box**: For tiny components (SMD parts, small hardware) -- **medium_bin**: Standard drawer compartments -- **large_bin**: Bulk storage -- **liquid_container**: For paints, solvents, coatings -- **smd_container**: Specialized for surface-mount components - -## 🗂️ Database Schema - -``` -modules -├── id -├── name (unique) -├── description -└── location_description - -levels -├── id -├── module_id → modules.id -├── level_number -├── rows -└── columns - -locations -├── id -├── level_id → levels.id -├── row -├── column -├── location_type -└── dimensions (width, height, depth) - -items -├── id -├── name -├── description -├── category -├── quantity -├── metadata (JSON) -└── tags - -item_locations (many-to-many) -├── item_id → items.id -├── location_id → locations.id -└── quantity -``` - -## 🔧 Configuration - -### Environment Variables - -Create a `.env` file in the project root: - -```env -# Database -DATABASE_URL=postgresql://inventoryuser:inventorypass@postgres:5432/inventory - -# Flask -FLASK_ENV=development -SECRET_KEY=your-secret-key-here - -# Port -PORT=5000 -``` - -### Changing Ports - -Edit `docker-compose.yml`: - -```yaml -services: - nginx: - ports: - - "8080:80" # Change 8080 to your preferred port -``` - -## 🐛 Troubleshooting - -### Database Connection Issues - -```bash -# Check if PostgreSQL is running -docker-compose ps - -# View PostgreSQL logs -docker-compose logs postgres - -# Restart PostgreSQL -docker-compose restart postgres -``` - -### Application Won't Start - -```bash -# View backend logs -docker-compose logs backend - -# Rebuild containers -docker-compose up --build - -# Reset everything -docker-compose down -v -docker-compose up --build -``` - -### Port Already in Use - -If port 8080 is in use: - -```bash -# Find what's using the port -lsof -i :8080 - -# Or change the port in docker-compose.yml -``` - -## 📊 API Endpoints - -The system provides REST API endpoints for programmatic access: - -### Modules -- `GET /modules/api/modules` - List all modules -- `GET /modules/api/modules/` - Get module details -- `GET /modules/api/modules//levels` - List module levels - -### Locations -- `GET /locations/api/locations` - List locations (with filters) -- `GET /locations/api/locations/` - Get location details - -### Items -- `GET /items/api/items` - List items (with search) -- `GET /items/api/items/` - Get item details - -### Search -- `GET /search/api?q=query` - Search items - -Example: -```bash -curl http://localhost:8080/items/api/items?search=screw -``` - -## 🚢 Deployment Options - -### Option 1: Local VPS/Server -```bash -# Clone and run -git clone -cd inventory-system -docker-compose up -d -``` - -### Option 2: Proxmox Container -1. Create an LXC container (Ubuntu 22.04+) -2. Install Docker and Docker Compose -3. Clone and run as above - -### Option 3: Jetson Nano -1. Install Docker on Jetson -2. Clone the repository -3. Run with docker-compose - -### Production Considerations - -For production deployment: - -1. **Change default passwords** in `docker-compose.yml` -2. **Set a secure SECRET_KEY** in environment variables -3. **Enable HTTPS** with Let's Encrypt -4. **Set up backups** for the PostgreSQL data volume -5. **Configure firewall rules** -6. **Use production WSGI server** (Gunicorn instead of Flask dev server) - -## 💾 Backup and Restore - -### Backup Database - -```bash -docker-compose exec postgres pg_dump -U inventoryuser inventory > backup.sql -``` - -### Restore Database - -```bash -docker-compose exec -T postgres psql -U inventoryuser inventory < backup.sql -``` - -### Backup Data Directory - -```bash -tar -czf backup-$(date +%Y%m%d).tar.gz data/ -``` - -## 🛠️ Development - -### Running Without Docker - -```bash -# Install PostgreSQL locally -# Create database 'inventory' - -# Install Python dependencies -cd backend -pip install -r requirements.txt - -# Set environment variable -export DATABASE_URL="postgresql://user:pass@localhost:5432/inventory" - -# Run application -python run.py -``` - -Access at http://localhost:5000 - -### Project Structure - -``` -inventory-system/ -├── backend/ -│ ├── app/ -│ │ ├── models.py # Database models -│ │ ├── routes/ # Route handlers -│ │ │ ├── main.py -│ │ │ ├── items.py -│ │ │ ├── modules.py -│ │ │ ├── locations.py -│ │ │ └── search.py -│ │ └── __init__.py -│ ├── requirements.txt -│ ├── Dockerfile -│ └── run.py -├── frontend/ -│ ├── templates/ # Jinja2 templates -│ └── static/ -│ ├── css/ -│ └── js/ -├── docker-compose.yml -├── nginx.conf -└── README.md -``` - -## 📖 Next Steps - -After getting comfortable with Phase 1: - -1. Add your first 50-100 items -2. Organize them into modules and levels -3. Test the search functionality -4. Provide feedback on what features you need most - -## 🐛 Known Limitations (Phase 1) - -- No AI-powered semantic search yet (coming in Phase 4) -- No duplicate detection (coming in Phase 3) -- No location suggestions (coming in Phase 2) -- No CLI or voice interface (coming in Phases 5-6) -- Basic keyword search only -- No user authentication (single-user system for now) - -## 🤝 Support - -For issues, questions, or feature requests, please open an issue in the project repository. - -## 📝 License - -[Your License Here] - -## 🎯 Roadmap - -- [x] Phase 1: Foundation (Current) -- [ ] Phase 2: Smart Location Management -- [ ] Phase 3: Duplicate Detection -- [ ] Phase 4: Semantic Search -- [ ] Phase 5: CLI Interface -- [ ] Phase 6: Voice Interface -- [ ] Phase 7: Advanced AI Features -- [ ] Phase 8: Production Polish - ---- - -**Version**: 1.0.0 (Phase 1) -**Last Updated**: 2024 diff --git a/inventory-system/backend/Dockerfile b/inventory-system/backend/Dockerfile deleted file mode 100644 index 5a8b491..0000000 --- a/inventory-system/backend/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM python:3.11-slim - -WORKDIR /app - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - gcc \ - postgresql-client \ - && rm -rf /var/lib/apt/lists/* - -# Copy requirements first for better caching -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Copy application code -COPY . . - -# Expose port -EXPOSE 5000 - -CMD ["python", "run.py"] diff --git a/inventory-system/backend/app/__init__.py b/inventory-system/backend/app/__init__.py deleted file mode 100644 index 34a5886..0000000 --- a/inventory-system/backend/app/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -import os -from flask import Flask -from flask_migrate import Migrate -from app.models import db - - -def create_app(): - app = Flask(__name__, - template_folder='../frontend/templates', - static_folder='../frontend/static') - - # Configuration - app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv( - 'DATABASE_URL', - 'postgresql://inventoryuser:inventorypass@localhost:5432/inventory' - ) - app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False - app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') - - # Initialize extensions - db.init_app(app) - migrate = Migrate(app, db) - - # Register blueprints - from app.routes import main, items, locations, modules, search - app.register_blueprint(main.bp) - app.register_blueprint(items.bp, url_prefix='/items') - app.register_blueprint(locations.bp, url_prefix='/locations') - app.register_blueprint(modules.bp, url_prefix='/modules') - app.register_blueprint(search.bp, url_prefix='/search') - - # Create tables - with app.app_context(): - db.create_all() - - return app diff --git a/inventory-system/backend/app/models.py b/inventory-system/backend/app/models.py deleted file mode 100644 index ab5036b..0000000 --- a/inventory-system/backend/app/models.py +++ /dev/null @@ -1,212 +0,0 @@ -from datetime import datetime -from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import JSON - -db = SQLAlchemy() - - -class Module(db.Model): - """Storage modules - the big units (cabinets, shelving units, etc.)""" - __tablename__ = 'modules' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), unique=True, nullable=False) - description = db.Column(db.Text) - location_description = db.Column(db.String(200)) # Physical location in lab/shop - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # Relationships - levels = db.relationship('Level', back_populates='module', cascade='all, delete-orphan') - - def __repr__(self): - return f'' - - def to_dict(self): - return { - 'id': self.id, - 'name': self.name, - 'description': self.description, - 'location_description': self.location_description, - 'level_count': len(self.levels), - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'updated_at': self.updated_at.isoformat() if self.updated_at else None, - } - - -class Level(db.Model): - """Levels within modules - drawers, shelves, compartments""" - __tablename__ = 'levels' - - id = db.Column(db.Integer, primary_key=True) - module_id = db.Column(db.Integer, db.ForeignKey('modules.id'), nullable=False) - level_number = db.Column(db.Integer, nullable=False) # 1, 2, 3, etc. - name = db.Column(db.String(100)) # Optional custom name - rows = db.Column(db.Integer, default=1) # Number of rows in grid - columns = db.Column(db.Integer, default=1) # Number of columns in grid - description = db.Column(db.Text) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # Relationships - module = db.relationship('Module', back_populates='levels') - locations = db.relationship('Location', back_populates='level', cascade='all, delete-orphan') - - # Unique constraint: one level number per module - __table_args__ = ( - db.UniqueConstraint('module_id', 'level_number', name='unique_module_level'), - ) - - def __repr__(self): - return f'' - - def to_dict(self): - return { - 'id': self.id, - 'module_id': self.module_id, - 'module_name': self.module.name if self.module else None, - 'level_number': self.level_number, - 'name': self.name, - 'rows': self.rows, - 'columns': self.columns, - 'description': self.description, - 'location_count': len(self.locations), - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'updated_at': self.updated_at.isoformat() if self.updated_at else None, - } - - -class Location(db.Model): - """Individual storage locations (bins) within levels""" - __tablename__ = 'locations' - - id = db.Column(db.Integer, primary_key=True) - level_id = db.Column(db.Integer, db.ForeignKey('levels.id'), nullable=False) - row = db.Column(db.String(10), nullable=False) # A, B, C or 1, 2, 3 - column = db.Column(db.String(10), nullable=False) # 1, 2, 3 or A, B, C - - # Location characteristics - location_type = db.Column(db.String(50), default='general') # small_box, medium_bin, large_bin, liquid_container, etc. - width_mm = db.Column(db.Float) - height_mm = db.Column(db.Float) - depth_mm = db.Column(db.Float) - notes = db.Column(db.Text) - - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # Relationships - level = db.relationship('Level', back_populates='locations') - item_locations = db.relationship('ItemLocation', back_populates='location', cascade='all, delete-orphan') - - # Unique constraint: one location per row/col in a level - __table_args__ = ( - db.UniqueConstraint('level_id', 'row', 'column', name='unique_level_position'), - ) - - def __repr__(self): - return f'' - - def full_address(self): - """Returns full address like 'Zeus:3:B4'""" - if self.level and self.level.module: - return f"{self.level.module.name}:{self.level.level_number}:{self.row}{self.column}" - return f"{self.row}{self.column}" - - def to_dict(self): - return { - 'id': self.id, - 'level_id': self.level_id, - 'module_name': self.level.module.name if self.level and self.level.module else None, - 'level_number': self.level.level_number if self.level else None, - 'row': self.row, - 'column': self.column, - 'full_address': self.full_address(), - 'location_type': self.location_type, - 'dimensions': { - 'width_mm': self.width_mm, - 'height_mm': self.height_mm, - 'depth_mm': self.depth_mm, - } if self.width_mm or self.height_mm or self.depth_mm else None, - 'notes': self.notes, - 'item_count': len(self.item_locations), - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'updated_at': self.updated_at.isoformat() if self.updated_at else None, - } - - -class Item(db.Model): - """Inventory items""" - __tablename__ = 'items' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(200), nullable=False) - description = db.Column(db.Text, nullable=False) # Natural language description - category = db.Column(db.String(100)) # electronics, fasteners, tools, paints, etc. - - # Structured metadata (parsed from description or manually entered) - item_metadata = db.Column(JSON) # Flexible storage for specs, dimensions, etc. - - # Item characteristics - item_type = db.Column(db.String(50)) # solid, liquid, smd_component, bulk, etc. - notes = db.Column(db.Text) - tags = db.Column(db.String(500)) # Comma-separated tags - - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # Relationships - item_locations = db.relationship('ItemLocation', back_populates='item', cascade='all, delete-orphan') - - def __repr__(self): - return f'' - - def to_dict(self): - return { - 'id': self.id, - 'name': self.name, - 'description': self.description, - 'category': self.category, - 'item_metadata': self.item_metadata, - 'item_type': self.item_type, - 'notes': self.notes, - 'tags': self.tags.split(',') if self.tags else [], - 'locations': [il.to_dict() for il in self.item_locations], - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'updated_at': self.updated_at.isoformat() if self.updated_at else None, - } - - -class ItemLocation(db.Model): - """Many-to-many relationship between items and locations""" - __tablename__ = 'item_locations' - - id = db.Column(db.Integer, primary_key=True) - item_id = db.Column(db.Integer, db.ForeignKey('items.id'), nullable=False) - location_id = db.Column(db.Integer, db.ForeignKey('locations.id'), nullable=False) - notes = db.Column(db.Text) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # Relationships - item = db.relationship('Item', back_populates='item_locations') - location = db.relationship('Location', back_populates='item_locations') - - # Unique constraint: one item per location - __table_args__ = ( - db.UniqueConstraint('item_id', 'location_id', name='unique_item_location'), - ) - - def __repr__(self): - return f'' - - def to_dict(self): - return { - 'id': self.id, - 'item_id': self.item_id, - 'location_id': self.location_id, - 'notes': self.notes, - 'location': self.location.to_dict() if self.location else None, - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'updated_at': self.updated_at.isoformat() if self.updated_at else None, - } diff --git a/inventory-system/backend/app/routes/items.py b/inventory-system/backend/app/routes/items.py deleted file mode 100644 index 4a7401e..0000000 --- a/inventory-system/backend/app/routes/items.py +++ /dev/null @@ -1,213 +0,0 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify -from app.models import db, Item, ItemLocation, Location, Level, Module - -bp = Blueprint('items', __name__) - - -@bp.route('/') -def list_items(): - """List all items""" - # Get filter parameters - category = request.args.get('category') - search = request.args.get('search') - - query = Item.query - - if category: - query = query.filter(Item.category == category) - - if search: - search_term = f"%{search}%" - query = query.filter( - db.or_( - Item.name.ilike(search_term), - Item.description.ilike(search_term), - Item.tags.ilike(search_term) - ) - ) - - items = query.order_by(Item.name).all() - - # Get unique categories for filter dropdown - categories = db.session.query(Item.category).distinct().all() - categories = [c[0] for c in categories if c[0]] - - return render_template('items/list.html', items=items, categories=categories) - - -@bp.route('/new', methods=['GET', 'POST']) -def new_item(): - """Create a new item""" - if request.method == 'POST': - name = request.form.get('name') - description = request.form.get('description') - category = request.form.get('category') - item_type = request.form.get('item_type') - tags = request.form.get('tags') - notes = request.form.get('notes') - - # Location information - location_id = request.form.get('location_id', type=int) - - if not name or not description: - flash('Name and description are required', 'error') - return render_template('items/form.html', modules=Module.query.all()) - - item = Item( - name=name, - description=description, - category=category, - item_type=item_type, - tags=tags, - notes=notes - ) - - db.session.add(item) - db.session.flush() # Get the item ID - - # Add location if provided - if location_id: - item_location = ItemLocation( - item_id=item.id, - location_id=location_id - ) - db.session.add(item_location) - - db.session.commit() - - flash(f'Item "{name}" created successfully', 'success') - return redirect(url_for('items.view_item', item_id=item.id)) - - # For GET request, load modules for location selection - modules = Module.query.order_by(Module.name).all() - return render_template('items/form.html', modules=modules) - - -@bp.route('/') -def view_item(item_id): - """View item details""" - item = Item.query.get_or_404(item_id) - modules = Module.query.order_by(Module.name).all() - return render_template('items/view.html', item=item, modules=modules) - - -@bp.route('//edit', methods=['GET', 'POST']) -def edit_item(item_id): - """Edit an item""" - item = Item.query.get_or_404(item_id) - - if request.method == 'POST': - item.name = request.form.get('name') - item.description = request.form.get('description') - item.category = request.form.get('category') - item.item_type = request.form.get('item_type') - item.tags = request.form.get('tags') - item.notes = request.form.get('notes') - - if not item.name or not item.description: - flash('Name and description are required', 'error') - return render_template('items/form.html', item=item, modules=Module.query.all()) - - db.session.commit() - - flash(f'Item "{item.name}" updated successfully', 'success') - return redirect(url_for('items.view_item', item_id=item.id)) - - modules = Module.query.order_by(Module.name).all() - return render_template('items/form.html', item=item, modules=modules) - - -@bp.route('//delete', methods=['POST']) -def delete_item(item_id): - """Delete an item""" - item = Item.query.get_or_404(item_id) - name = item.name - - db.session.delete(item) - db.session.commit() - - flash(f'Item "{name}" deleted successfully', 'success') - return redirect(url_for('items.list_items')) - - -@bp.route('//locations/add', methods=['POST']) -def add_location(item_id): - """Add a location to an item""" - item = Item.query.get_or_404(item_id) - - location_id = request.form.get('location_id', type=int) - notes = request.form.get('notes') - - if not location_id: - flash('Location is required', 'error') - return redirect(url_for('items.view_item', item_id=item_id)) - - # Check if this item-location combination already exists - existing = ItemLocation.query.filter_by(item_id=item_id, location_id=location_id).first() - if existing: - flash('Item is already stored at this location', 'error') - return redirect(url_for('items.view_item', item_id=item_id)) - - item_location = ItemLocation( - item_id=item_id, - location_id=location_id, - notes=notes - ) - - db.session.add(item_location) - db.session.commit() - - location = Location.query.get(location_id) - flash(f'Location {location.full_address()} added successfully', 'success') - return redirect(url_for('items.view_item', item_id=item_id)) - - -@bp.route('//locations//remove', methods=['POST']) -def remove_location(item_id, item_location_id): - """Remove a location from an item""" - item_location = ItemLocation.query.get_or_404(item_location_id) - - if item_location.item_id != item_id: - flash('Invalid item-location combination', 'error') - return redirect(url_for('items.view_item', item_id=item_id)) - - location_address = item_location.location.full_address() - - db.session.delete(item_location) - db.session.commit() - - flash(f'Location {location_address} removed successfully', 'success') - return redirect(url_for('items.view_item', item_id=item_id)) - - -# API endpoints - -@bp.route('/api/items', methods=['GET']) -def api_list_items(): - """API endpoint to list items""" - search = request.args.get('search') - category = request.args.get('category') - - query = Item.query - - if search: - search_term = f"%{search}%" - query = query.filter( - db.or_( - Item.name.ilike(search_term), - Item.description.ilike(search_term) - ) - ) - - if category: - query = query.filter(Item.category == category) - - items = query.order_by(Item.name).limit(50).all() - return jsonify([i.to_dict() for i in items]) - - -@bp.route('/api/items/', methods=['GET']) -def api_get_item(item_id): - """API endpoint to get a single item""" - item = Item.query.get_or_404(item_id) - return jsonify(item.to_dict()) diff --git a/inventory-system/backend/app/routes/locations.py b/inventory-system/backend/app/routes/locations.py deleted file mode 100644 index 00cfccb..0000000 --- a/inventory-system/backend/app/routes/locations.py +++ /dev/null @@ -1,101 +0,0 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify -from app.models import db, Location, Level, Item, ItemLocation - -bp = Blueprint('locations', __name__) - - -@bp.route('/') -def list_locations(): - """List all locations""" - # Get filter parameters - module_id = request.args.get('module_id', type=int) - level_id = request.args.get('level_id', type=int) - location_type = request.args.get('location_type') - occupied = request.args.get('occupied') # 'yes', 'no', or None - - query = Location.query.join(Level) - - if level_id: - query = query.filter(Location.level_id == level_id) - elif module_id: - query = query.filter(Level.module_id == module_id) - - if location_type: - query = query.filter(Location.location_type == location_type) - - if occupied == 'yes': - query = query.join(ItemLocation).distinct() - elif occupied == 'no': - query = query.outerjoin(ItemLocation).filter(ItemLocation.id == None) - - locations = query.order_by(Level.module_id, Level.level_number, Location.row, Location.column).all() - - # Get unique location types for filter dropdown - location_types = db.session.query(Location.location_type).distinct().all() - location_types = [lt[0] for lt in location_types if lt[0]] - - return render_template('locations/list.html', - locations=locations, - location_types=location_types) - - -@bp.route('/') -def view_location(location_id): - """View location details and items stored there""" - location = Location.query.get_or_404(location_id) - item_locations = ItemLocation.query.filter_by(location_id=location_id).all() - - return render_template('locations/view.html', - location=location, - item_locations=item_locations) - - -@bp.route('//edit', methods=['GET', 'POST']) -def edit_location(location_id): - """Edit location properties""" - location = Location.query.get_or_404(location_id) - - if request.method == 'POST': - location.location_type = request.form.get('location_type', 'general') - location.width_mm = request.form.get('width_mm', type=float) - location.height_mm = request.form.get('height_mm', type=float) - location.depth_mm = request.form.get('depth_mm', type=float) - location.notes = request.form.get('notes') - - db.session.commit() - - flash(f'Location {location.full_address()} updated successfully', 'success') - return redirect(url_for('locations.view_location', location_id=location.id)) - - return render_template('locations/form.html', location=location) - - -# API endpoints - -@bp.route('/api/locations', methods=['GET']) -def api_list_locations(): - """API endpoint to list locations with filters""" - level_id = request.args.get('level_id', type=int) - location_type = request.args.get('location_type') - available = request.args.get('available', type=bool) - - query = Location.query - - if level_id: - query = query.filter(Location.level_id == level_id) - - if location_type: - query = query.filter(Location.location_type == location_type) - - if available: - query = query.outerjoin(ItemLocation).filter(ItemLocation.id == None) - - locations = query.all() - return jsonify([l.to_dict() for l in locations]) - - -@bp.route('/api/locations/', methods=['GET']) -def api_get_location(location_id): - """API endpoint to get a single location""" - location = Location.query.get_or_404(location_id) - return jsonify(location.to_dict()) diff --git a/inventory-system/backend/app/routes/main.py b/inventory-system/backend/app/routes/main.py deleted file mode 100644 index 555441c..0000000 --- a/inventory-system/backend/app/routes/main.py +++ /dev/null @@ -1,26 +0,0 @@ -from flask import Blueprint, render_template -from app.models import Module, Level, Location, Item - -bp = Blueprint('main', __name__) - - -@bp.route('/') -def index(): - """Main dashboard""" - stats = { - 'modules': Module.query.count(), - 'levels': Level.query.count(), - 'locations': Location.query.count(), - 'items': Item.query.count(), - } - - recent_items = Item.query.order_by(Item.created_at.desc()).limit(10).all() - modules = Module.query.order_by(Module.name).all() - - return render_template('index.html', stats=stats, recent_items=recent_items, modules=modules) - - -@bp.route('/about') -def about(): - """About page""" - return render_template('about.html') diff --git a/inventory-system/backend/app/routes/modules.py b/inventory-system/backend/app/routes/modules.py deleted file mode 100644 index 28b5f93..0000000 --- a/inventory-system/backend/app/routes/modules.py +++ /dev/null @@ -1,275 +0,0 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify -from app.models import db, Module, Level, Location - -bp = Blueprint('modules', __name__) - - -@bp.route('/') -def list_modules(): - """List all modules""" - modules = Module.query.order_by(Module.name).all() - return render_template('modules/list.html', modules=modules) - - -@bp.route('/new', methods=['GET', 'POST']) -def new_module(): - """Create a new module""" - if request.method == 'POST': - name = request.form.get('name') - description = request.form.get('description') - location_description = request.form.get('location_description') - - if not name: - flash('Name is required', 'error') - return render_template('modules/form.html') - - # Check if name already exists - existing = Module.query.filter_by(name=name).first() - if existing: - flash(f'Module with name "{name}" already exists', 'error') - return render_template('modules/form.html') - - module = Module( - name=name, - description=description, - location_description=location_description - ) - - db.session.add(module) - db.session.commit() - - flash(f'Module "{name}" created successfully', 'success') - return redirect(url_for('modules.view_module', module_id=module.id)) - - return render_template('modules/form.html') - - -@bp.route('/') -def view_module(module_id): - """View module details and its levels""" - module = Module.query.get_or_404(module_id) - levels = Level.query.filter_by(module_id=module_id).order_by(Level.level_number).all() - return render_template('modules/view.html', module=module, levels=levels) - - -@bp.route('//edit', methods=['GET', 'POST']) -def edit_module(module_id): - """Edit a module""" - module = Module.query.get_or_404(module_id) - - if request.method == 'POST': - name = request.form.get('name') - description = request.form.get('description') - location_description = request.form.get('location_description') - - if not name: - flash('Name is required', 'error') - return render_template('modules/form.html', module=module) - - # Check if name already exists (excluding current module) - existing = Module.query.filter(Module.name == name, Module.id != module_id).first() - if existing: - flash(f'Module with name "{name}" already exists', 'error') - return render_template('modules/form.html', module=module) - - module.name = name - module.description = description - module.location_description = location_description - - db.session.commit() - - flash(f'Module "{name}" updated successfully', 'success') - return redirect(url_for('modules.view_module', module_id=module.id)) - - return render_template('modules/form.html', module=module) - - -@bp.route('//delete', methods=['POST']) -def delete_module(module_id): - """Delete a module""" - module = Module.query.get_or_404(module_id) - name = module.name - - db.session.delete(module) - db.session.commit() - - flash(f'Module "{name}" deleted successfully', 'success') - return redirect(url_for('modules.list_modules')) - - -@bp.route('//levels/new', methods=['GET', 'POST']) -def new_level(module_id): - """Add a new level to a module""" - module = Module.query.get_or_404(module_id) - - if request.method == 'POST': - level_number = request.form.get('level_number', type=int) - name = request.form.get('name') - rows = request.form.get('rows', type=int, default=1) - columns = request.form.get('columns', type=int, default=1) - description = request.form.get('description') - - if not level_number: - flash('Level number is required', 'error') - return render_template('levels/form.html', module=module) - - # Check if level number already exists for this module - existing = Level.query.filter_by(module_id=module_id, level_number=level_number).first() - if existing: - flash(f'Level {level_number} already exists in this module', 'error') - return render_template('levels/form.html', module=module) - - level = Level( - module_id=module_id, - level_number=level_number, - name=name, - rows=rows, - columns=columns, - description=description - ) - - db.session.add(level) - db.session.commit() - - # Create locations for this level - create_locations_for_level(level) - - flash(f'Level {level_number} created with {rows}x{columns} locations', 'success') - return redirect(url_for('modules.view_module', module_id=module_id)) - - return render_template('levels/form.html', module=module) - - -@bp.route('/levels/') -def view_level(level_id): - """View level details with its locations""" - level = Level.query.get_or_404(level_id) - - # Organize locations by row and column - locations = Location.query.filter_by(level_id=level_id).all() - location_grid = {} - for loc in locations: - if loc.row not in location_grid: - location_grid[loc.row] = {} - location_grid[loc.row][loc.column] = loc - - return render_template('levels/view.html', level=level, location_grid=location_grid, chr=chr) - - -@bp.route('/levels//edit', methods=['GET', 'POST']) -def edit_level(level_id): - """Edit a level""" - level = Level.query.get_or_404(level_id) - module = level.module - - if request.method == 'POST': - level_number = request.form.get('level_number', type=int) - name = request.form.get('name') - rows = request.form.get('rows', type=int, default=1) - columns = request.form.get('columns', type=int, default=1) - description = request.form.get('description') - - if not level_number: - flash('Level number is required', 'error') - return render_template('levels/form.html', module=module, level=level) - - # Check if level number already exists (excluding current level) - existing = Level.query.filter( - Level.module_id == level.module_id, - Level.level_number == level_number, - Level.id != level_id - ).first() - if existing: - flash(f'Level {level_number} already exists in this module', 'error') - return render_template('levels/form.html', module=module, level=level) - - # Check if grid size changed - old_rows = level.rows - old_columns = level.columns - - level.level_number = level_number - level.name = name - level.rows = rows - level.columns = columns - level.description = description - - db.session.commit() - - # If grid size changed, recreate locations - if old_rows != rows or old_columns != columns: - # Delete old locations (cascade will handle item_locations) - Location.query.filter_by(level_id=level_id).delete() - db.session.commit() - - # Create new locations - create_locations_for_level(level) - flash(f'Level updated and locations regenerated ({rows}x{columns})', 'success') - else: - flash(f'Level {level_number} updated successfully', 'success') - - return redirect(url_for('modules.view_level', level_id=level.id)) - - return render_template('levels/form.html', module=module, level=level) - - -@bp.route('/levels//delete', methods=['POST']) -def delete_level(level_id): - """Delete a level""" - level = Level.query.get_or_404(level_id) - module_id = level.module_id - level_num = level.level_number - - db.session.delete(level) - db.session.commit() - - flash(f'Level {level_num} deleted successfully', 'success') - return redirect(url_for('modules.view_module', module_id=module_id)) - - -def create_locations_for_level(level): - """Helper function to create locations for a level based on its grid""" - rows = level.rows - columns = level.columns - - # Generate row labels (A, B, C... or 1, 2, 3...) - row_labels = [chr(65 + i) for i in range(rows)] if rows <= 26 else [str(i+1) for i in range(rows)] - - # Generate column labels (1, 2, 3...) - col_labels = [str(i+1) for i in range(columns)] - - locations = [] - for row_label in row_labels: - for col_label in col_labels: - location = Location( - level_id=level.id, - row=row_label, - column=col_label, - location_type='general' - ) - locations.append(location) - - db.session.add_all(locations) - db.session.commit() - - -# API endpoints for AJAX requests - -@bp.route('/api/modules', methods=['GET']) -def api_list_modules(): - """API endpoint to list all modules""" - modules = Module.query.order_by(Module.name).all() - return jsonify([m.to_dict() for m in modules]) - - -@bp.route('/api/modules/', methods=['GET']) -def api_get_module(module_id): - """API endpoint to get a single module""" - module = Module.query.get_or_404(module_id) - return jsonify(module.to_dict()) - - -@bp.route('/api/modules//levels', methods=['GET']) -def api_list_levels(module_id): - """API endpoint to list levels for a module""" - levels = Level.query.filter_by(module_id=module_id).order_by(Level.level_number).all() - return jsonify([l.to_dict() for l in levels]) diff --git a/inventory-system/backend/app/routes/search.py b/inventory-system/backend/app/routes/search.py deleted file mode 100644 index dc4e08a..0000000 --- a/inventory-system/backend/app/routes/search.py +++ /dev/null @@ -1,48 +0,0 @@ -from flask import Blueprint, render_template, request, jsonify -from app.models import db, Item - -bp = Blueprint('search', __name__) - - -@bp.route('/') -def search(): - """Search page""" - query = request.args.get('q', '') - results = [] - - if query: - search_term = f"%{query}%" - results = Item.query.filter( - db.or_( - Item.name.ilike(search_term), - Item.description.ilike(search_term), - Item.tags.ilike(search_term), - Item.notes.ilike(search_term) - ) - ).order_by(Item.name).all() - - return render_template('search/results.html', query=query, results=results) - - -@bp.route('/api', methods=['GET']) -def api_search(): - """API endpoint for search""" - query = request.args.get('q', '') - - if not query: - return jsonify({'results': []}) - - search_term = f"%{query}%" - items = Item.query.filter( - db.or_( - Item.name.ilike(search_term), - Item.description.ilike(search_term), - Item.tags.ilike(search_term) - ) - ).order_by(Item.name).limit(20).all() - - return jsonify({ - 'query': query, - 'count': len(items), - 'results': [i.to_dict() for i in items] - }) diff --git a/inventory-system/backend/requirements.txt b/inventory-system/backend/requirements.txt deleted file mode 100644 index 21394b0..0000000 --- a/inventory-system/backend/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Flask==3.0.0 -Flask-SQLAlchemy==3.1.1 -Flask-Migrate==4.0.5 -psycopg2-binary==2.9.9 -python-dotenv==1.0.0 -sqlalchemy==2.0.23 diff --git a/inventory-system/backend/run.py b/inventory-system/backend/run.py deleted file mode 100644 index c962b68..0000000 --- a/inventory-system/backend/run.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -""" -Inventory System - Flask Application Runner -Phase 1: Foundation -""" - -import os -from app import create_app -from app.models import db - -app = create_app() - -if __name__ == '__main__': - with app.app_context(): - # Create all database tables - db.create_all() - print("Database tables created successfully!") - - # Run the application - port = int(os.getenv('PORT', 5000)) - debug = os.getenv('FLASK_ENV') == 'development' - - print(f"\n{'='*60}") - print("🏠 Homelab Inventory System - Phase 1: Foundation") - print(f"{'='*60}") - print(f"Starting Flask server on http://0.0.0.0:{port}") - print(f"Debug mode: {debug}") - print(f"{'='*60}\n") - - app.run(host='0.0.0.0', port=port, debug=debug) diff --git a/inventory-system/create_sample_data.py b/inventory-system/create_sample_data.py deleted file mode 100644 index 086afb2..0000000 --- a/inventory-system/create_sample_data.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to populate the inventory system with sample data -Run this after the system is deployed to see it in action -""" - -import requests -import time - -BASE_URL = "http://localhost:8080" - -def wait_for_server(): - """Wait for the server to be ready""" - print("Waiting for server to be ready...") - max_attempts = 30 - for i in range(max_attempts): - try: - response = requests.get(BASE_URL) - if response.status_code == 200: - print("✓ Server is ready!") - return True - except: - pass - time.sleep(1) - print("✗ Server did not start in time") - return False - -def create_sample_data(): - """Create sample modules, levels, and items""" - - print("\n" + "="*60) - print("Creating Sample Data for Inventory System") - print("="*60 + "\n") - - # Sample modules - modules = [ - { - "name": "Zeus", - "description": "Main electronics and component storage", - "location_description": "North wall, workshop" - }, - { - "name": "Muse", - "description": "Fasteners and hardware storage", - "location_description": "East wall, near workbench" - }, - { - "name": "Apollo", - "description": "Tools and equipment", - "location_description": "Tool wall, west side" - } - ] - - created_modules = [] - - # Create modules - print("Creating modules...") - for module_data in modules: - try: - response = requests.post(f"{BASE_URL}/modules/new", data=module_data, allow_redirects=False) - if response.status_code in [200, 302]: - print(f" ✓ Created module: {module_data['name']}") - created_modules.append(module_data['name']) - else: - print(f" ✗ Failed to create module: {module_data['name']}") - except Exception as e: - print(f" ✗ Error creating module {module_data['name']}: {e}") - - # Get module IDs - try: - response = requests.get(f"{BASE_URL}/modules/api/modules") - modules_list = response.json() - module_map = {m['name']: m['id'] for m in modules_list} - except: - print("✗ Failed to get module list") - return - - # Sample levels for each module - levels_data = { - "Zeus": [ - {"level_number": 1, "name": "Top Drawer", "rows": 4, "columns": 6, "description": "Small components"}, - {"level_number": 2, "name": "Middle Drawer", "rows": 3, "columns": 4, "description": "Medium bins"}, - {"level_number": 3, "name": "Bottom Drawer", "rows": 2, "columns": 3, "description": "Large storage"} - ], - "Muse": [ - {"level_number": 1, "rows": 5, "columns": 8, "description": "Metric fasteners"}, - {"level_number": 2, "rows": 5, "columns": 8, "description": "Imperial fasteners"}, - {"level_number": 3, "rows": 4, "columns": 6, "description": "Specialty hardware"} - ], - "Apollo": [ - {"level_number": 1, "rows": 2, "columns": 4, "description": "Hand tools"}, - {"level_number": 2, "rows": 2, "columns": 3, "description": "Power tools"} - ] - } - - print("\nCreating levels...") - for module_name, levels in levels_data.items(): - if module_name not in module_map: - continue - - module_id = module_map[module_name] - for level_data in levels: - try: - response = requests.post(f"{BASE_URL}/modules/{module_id}/levels/new", data=level_data, allow_redirects=False) - if response.status_code in [200, 302]: - print(f" ✓ Created {module_name} Level {level_data['level_number']}") - else: - print(f" ✗ Failed to create level {level_data['level_number']} in {module_name}") - except Exception as e: - print(f" ✗ Error creating level: {e}") - - # Sample items - items_data = [ - { - "name": "M6 Hex Bolts", - "description": "Hex head bolt, M6 diameter, 50mm long, zinc plated, metric thread", - "category": "Fasteners", - "item_type": "solid", - "quantity": 100, - "unit": "pieces", - "tags": "bolt, metric, m6, hex, zinc, fastener" - }, - { - "name": "1kΩ Resistors", - "description": "1/4 watt carbon film resistor, 1000 ohm, 5% tolerance, through-hole", - "category": "Electronics", - "item_type": "solid", - "quantity": 200, - "unit": "pieces", - "tags": "resistor, 1k, 1000ohm, carbon film, electronics" - }, - { - "name": "Arduino Uno R3", - "description": "Arduino Uno R3 development board, ATmega328P microcontroller, USB interface", - "category": "Electronics", - "item_type": "solid", - "quantity": 5, - "unit": "pieces", - "tags": "arduino, uno, microcontroller, development board" - }, - { - "name": "#8 Wood Screws", - "description": "Phillips pan head wood screw, #8 size, 3/4 inch long, zinc plated", - "category": "Fasteners", - "item_type": "solid", - "quantity": 250, - "unit": "pieces", - "tags": "screw, wood screw, phillips, pan head, #8" - }, - { - "name": "SMD Capacitors 0.1µF", - "description": "Ceramic capacitor, 0.1 microfarad, 0805 package, 50V rating", - "category": "Electronics", - "item_type": "smd_component", - "quantity": 500, - "unit": "pieces", - "tags": "capacitor, smd, 0805, ceramic, 0.1uf" - }, - { - "name": "Red Spray Paint", - "description": "Rust-Oleum 2X Ultra Cover Paint+Primer, gloss red, 12 oz aerosol", - "category": "Paints & Coatings", - "item_type": "liquid", - "quantity": 3, - "unit": "cans", - "tags": "paint, spray paint, red, rust-oleum" - }, - { - "name": "Phillips Screwdriver", - "description": "Phillips head screwdriver, #2 size, 6 inch shaft, cushion grip handle", - "category": "Tools", - "item_type": "tool", - "quantity": 2, - "unit": "pieces", - "tags": "screwdriver, phillips, hand tool" - }, - { - "name": "M3 Standoffs", - "description": "Aluminum standoff, M3 thread, 10mm length, hex body", - "category": "Hardware", - "item_type": "solid", - "quantity": 50, - "unit": "pieces", - "tags": "standoff, m3, metric, aluminum, spacer" - }, - { - "name": "LED 5mm Red", - "description": "Light emitting diode, 5mm diameter, red, 2V forward voltage, 20mA", - "category": "Electronics", - "item_type": "solid", - "quantity": 100, - "unit": "pieces", - "tags": "led, red, 5mm, light, electronics" - }, - { - "name": "Zip Ties 6 inch", - "description": "Nylon cable tie, 6 inch length, 40 lb tensile strength, black", - "category": "Hardware", - "item_type": "bulk", - "quantity": 500, - "unit": "pieces", - "tags": "zip tie, cable tie, nylon, fastener" - } - ] - - print("\nCreating items...") - for item_data in items_data: - try: - response = requests.post(f"{BASE_URL}/items/new", data=item_data, allow_redirects=False) - if response.status_code in [200, 302]: - print(f" ✓ Created item: {item_data['name']}") - else: - print(f" ✗ Failed to create item: {item_data['name']}") - except Exception as e: - print(f" ✗ Error creating item {item_data['name']}: {e}") - - print("\n" + "="*60) - print("Sample data creation complete!") - print("="*60) - print(f"\nYou can now access the system at: {BASE_URL}") - print("\nTry these features:") - print(" • Browse Modules to see the storage hierarchy") - print(" • View Items to see the inventory") - print(" • Use Search to find items by keyword") - print(" • Click on levels to see the location grid") - print("\n") - -if __name__ == "__main__": - print("\n🏠 Homelab Inventory System - Sample Data Generator\n") - - if wait_for_server(): - time.sleep(2) # Give it a moment to fully initialize - create_sample_data() - else: - print("\nMake sure the system is running:") - print(" docker-compose up -d") - print("\nThen run this script again.") diff --git a/inventory-system/docker-compose.yml b/inventory-system/docker-compose.yml deleted file mode 100644 index 8d9689d..0000000 --- a/inventory-system/docker-compose.yml +++ /dev/null @@ -1,46 +0,0 @@ -version: '3.8' - -services: - postgres: - image: postgres:15-alpine - container_name: inventory-db - environment: - POSTGRES_DB: inventory - POSTGRES_USER: inventoryuser - POSTGRES_PASSWORD: inventorypass - volumes: - - ./data/postgres:/var/lib/postgresql/data - ports: - - "5432:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U inventoryuser -d inventory"] - interval: 10s - timeout: 5s - retries: 5 - - backend: - build: ./backend - container_name: inventory-backend - environment: - DATABASE_URL: postgresql://inventoryuser:inventorypass@postgres:5432/inventory - FLASK_ENV: development - FLASK_APP: app - volumes: - - ./backend:/app - - ./frontend:/app/frontend - ports: - - "5000:5000" - depends_on: - postgres: - condition: service_healthy - command: python run.py - - nginx: - image: nginx:alpine - container_name: inventory-nginx - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - ports: - - "8080:80" - depends_on: - - backend diff --git a/inventory-system/docs/ARCHITECTURE.md b/inventory-system/docs/ARCHITECTURE.md deleted file mode 100644 index 1ede231..0000000 --- a/inventory-system/docs/ARCHITECTURE.md +++ /dev/null @@ -1,578 +0,0 @@ -# Technical Architecture - Phase 1 - -## System Overview - -The Homelab Inventory System is a full-stack web application designed for managing thousands of inventory items across organized storage locations. - -## Architecture Diagram - -``` -┌─────────────────────────────────────────────────────────┐ -│ Web Browser │ -│ (User Interface) │ -└────────────────────┬────────────────────────────────────┘ - │ HTTP/HTTPS - │ Port 8080 -┌────────────────────▼────────────────────────────────────┐ -│ NGINX │ -│ (Reverse Proxy) │ -└────────────────────┬────────────────────────────────────┘ - │ - │ Proxy Pass - │ Port 5000 -┌────────────────────▼────────────────────────────────────┐ -│ Flask Application │ -│ (Python Backend) │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ Routes Layer │ │ -│ │ - main.py (Dashboard) │ │ -│ │ - items.py (Item CRUD) │ │ -│ │ - modules.py (Storage CRUD) │ │ -│ │ - locations.py (Location management) │ │ -│ │ - search.py (Search functionality) │ │ -│ └──────────────────┬──────────────────────────────┘ │ -│ │ │ -│ ┌──────────────────▼──────────────────────────────┐ │ -│ │ SQLAlchemy ORM │ │ -│ │ - models.py (Database models) │ │ -│ │ - Relationships & constraints │ │ -│ └──────────────────┬──────────────────────────────┘ │ -└────────────────────┬┴───────────────────────────────────┘ - │ - │ TCP/IP - │ Port 5432 -┌────────────────────▼────────────────────────────────────┐ -│ PostgreSQL Database │ -│ │ -│ Tables: │ -│ - modules │ -│ - levels │ -│ - locations │ -│ - items │ -│ - item_locations (junction table) │ -│ │ -│ Persistent Volume: ./data/postgres │ -└─────────────────────────────────────────────────────────┘ -``` - -## Technology Stack - -### Frontend -- **HTML5**: Semantic markup -- **CSS3**: Custom styling with CSS variables -- **JavaScript (ES6+)**: Client-side interactivity -- **Jinja2**: Server-side templating - -### Backend -- **Python 3.11+**: Programming language -- **Flask 3.0**: Web framework -- **SQLAlchemy 2.0**: ORM -- **Flask-Migrate 4.0**: Database migrations -- **psycopg2**: PostgreSQL adapter - -### Database -- **PostgreSQL 15**: Primary data store -- **Relations**: Foreign keys with cascade -- **Constraints**: Unique, not null, check constraints -- **JSON fields**: For flexible metadata storage - -### Infrastructure -- **Docker**: Containerization -- **Docker Compose**: Multi-container orchestration -- **nginx**: Reverse proxy and static file serving -- **Ubuntu 24**: Base OS for containers - -## Database Schema - -### Tables - -#### modules -```sql -CREATE TABLE modules ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) UNIQUE NOT NULL, - description TEXT, - location_description VARCHAR(200), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -#### levels -```sql -CREATE TABLE levels ( - id SERIAL PRIMARY KEY, - module_id INTEGER REFERENCES modules(id) ON DELETE CASCADE, - level_number INTEGER NOT NULL, - name VARCHAR(100), - rows INTEGER DEFAULT 1, - columns INTEGER DEFAULT 1, - description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(module_id, level_number) -); -``` - -#### locations -```sql -CREATE TABLE locations ( - id SERIAL PRIMARY KEY, - level_id INTEGER REFERENCES levels(id) ON DELETE CASCADE, - row VARCHAR(10) NOT NULL, - column VARCHAR(10) NOT NULL, - location_type VARCHAR(50) DEFAULT 'general', - width_mm FLOAT, - height_mm FLOAT, - depth_mm FLOAT, - notes TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(level_id, row, column) -); -``` - -#### items -```sql -CREATE TABLE items ( - id SERIAL PRIMARY KEY, - name VARCHAR(200) NOT NULL, - description TEXT NOT NULL, - category VARCHAR(100), - metadata JSON, - quantity INTEGER DEFAULT 1, - unit VARCHAR(20), - min_quantity INTEGER, - item_type VARCHAR(50), - notes TEXT, - tags VARCHAR(500), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -#### item_locations -```sql -CREATE TABLE item_locations ( - id SERIAL PRIMARY KEY, - item_id INTEGER REFERENCES items(id) ON DELETE CASCADE, - location_id INTEGER REFERENCES locations(id) ON DELETE CASCADE, - quantity INTEGER DEFAULT 1, - notes TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(item_id, location_id) -); -``` - -### Relationships - -``` -modules (1) ──< (N) levels -levels (1) ──< (N) locations -items (N) >──< (N) locations [via item_locations] -``` - -## API Endpoints - -### Web UI Routes - -#### Dashboard -- `GET /` - Main dashboard - -#### Modules -- `GET /modules/` - List modules -- `GET /modules/new` - Module creation form -- `POST /modules/new` - Create module -- `GET /modules/` - View module -- `GET /modules//edit` - Edit form -- `POST /modules//edit` - Update module -- `POST /modules//delete` - Delete module - -#### Levels -- `GET /modules//levels/new` - Level creation form -- `POST /modules//levels/new` - Create level -- `GET /modules/levels/` - View level -- `GET /modules/levels//edit` - Edit form -- `POST /modules/levels//edit` - Update level -- `POST /modules/levels//delete` - Delete level - -#### Items -- `GET /items/` - List items -- `GET /items/new` - Item creation form -- `POST /items/new` - Create item -- `GET /items/` - View item -- `GET /items//edit` - Edit form -- `POST /items//edit` - Update item -- `POST /items//delete` - Delete item -- `POST /items//locations/add` - Add location -- `POST /items//locations//remove` - Remove location - -#### Locations -- `GET /locations/` - List locations (with filters) -- `GET /locations/` - View location -- `GET /locations//edit` - Edit form -- `POST /locations//edit` - Update location - -#### Search -- `GET /search/?q=` - Search page - -### REST API Routes - -#### Modules -- `GET /modules/api/modules` - List all modules (JSON) -- `GET /modules/api/modules/` - Get module (JSON) -- `GET /modules/api/modules//levels` - List levels (JSON) - -#### Locations -- `GET /locations/api/locations` - List locations (JSON) - - Query params: `level_id`, `location_type`, `available` -- `GET /locations/api/locations/` - Get location (JSON) - -#### Items -- `GET /items/api/items` - List items (JSON) - - Query params: `search`, `category` -- `GET /items/api/items/` - Get item (JSON) - -#### Search -- `GET /search/api?q=` - Search items (JSON) - -## Data Flow - -### Creating an Item with Location - -``` -1. User fills form - └─> POST /items/new - -2. Flask route handler (items.py) - ├─> Validate input - ├─> Create Item object - ├─> db.session.add(item) - ├─> db.session.flush() # Get item.id - ├─> Create ItemLocation object - ├─> db.session.add(item_location) - └─> db.session.commit() - -3. Database - ├─> INSERT INTO items - └─> INSERT INTO item_locations - -4. Redirect to item view page -``` - -### Searching for Items - -``` -1. User enters search query - └─> GET /search/?q=M6+bolt - -2. Flask route handler (search.py) - ├─> Extract query parameter - ├─> Build SQL ILIKE query - │ WHERE name ILIKE '%M6%bolt%' - │ OR description ILIKE '%M6%bolt%' - │ OR tags ILIKE '%M6%bolt%' - └─> Execute query via SQLAlchemy - -3. Database - └─> Return matching rows - -4. Render results template - └─> Show items with locations -``` - -### Viewing Location Grid - -``` -1. User clicks level - └─> GET /modules/levels/ - -2. Flask route handler (modules.py) - ├─> Query level by ID - ├─> Query all locations for level - ├─> Organize into grid structure - │ grid[row][column] = location - └─> Pass to template - -3. Template (levels/view.html) - ├─> Iterate rows - ├─> Iterate columns - ├─> Render table cell - │ ├─> Show location address - │ ├─> Show item count - │ └─> Apply CSS class (occupied/empty) - └─> Generate interactive grid - -4. Browser - └─> Render visual grid with colors -``` - -## Security Considerations - -### Phase 1 (Current) -- ✅ SQL injection protection (SQLAlchemy ORM) -- ✅ CSRF protection via Flask -- ✅ Input validation -- ⚠️ No authentication (single-user) -- ⚠️ No HTTPS (development only) -- ⚠️ Default database password - -### Future Phases -- 🔜 User authentication -- 🔜 Role-based access control -- 🔜 HTTPS/TLS -- 🔜 Session management -- 🔜 API rate limiting -- 🔜 Audit logging - -## Performance Characteristics - -### Current (Phase 1) -- **Database**: Single PostgreSQL instance -- **Queries**: Non-optimized, works well up to ~10k items -- **Indexing**: Primary keys only -- **Caching**: None -- **Concurrent users**: 1-5 recommended - -### Future Optimizations -- Database indexing on frequently queried fields -- Query optimization with eager loading -- Redis caching layer -- Connection pooling -- CDN for static assets - -## Deployment Configurations - -### Development -```yaml -services: - postgres: - # Development with data persistence - backend: - FLASK_ENV: development - # Auto-reload enabled - nginx: - # Basic proxy only -``` - -### Production (Recommended Changes) -```yaml -services: - postgres: - # Strong password - # Backup volumes - # Resource limits - backend: - FLASK_ENV: production - # Gunicorn/uWSGI - # Multiple workers - nginx: - # SSL/TLS certificates - # Gzip compression - # Rate limiting -``` - -## Monitoring & Logging - -### Current Logging -- Flask request logs (stdout) -- PostgreSQL logs (Docker logs) -- nginx access logs (Docker logs) - -### View Logs -```bash -docker-compose logs backend -docker-compose logs postgres -docker-compose logs nginx -``` - -### Future Monitoring -- Application performance monitoring (APM) -- Error tracking (Sentry) -- Metrics (Prometheus) -- Dashboards (Grafana) - -## Scalability Path - -### Current Limits -- Single server deployment -- ~10,000 items perform well -- ~1,000 locations per level max -- Single database instance - -### Future Scaling -**Horizontal:** -- Load balancer → Multiple Flask instances -- Read replicas for PostgreSQL -- Redis for session storage - -**Vertical:** -- More RAM for larger datasets -- SSD for database performance -- CPU for concurrent users - -## Extensibility Points - -### Adding Features -1. **New routes**: Add to `app/routes/` -2. **New models**: Extend `app/models.py` -3. **New templates**: Add to `frontend/templates/` -4. **New API endpoints**: Follow existing pattern - -### Plugin Architecture (Future) -- Custom location types -- Custom item attributes -- Export formats -- Integration hooks - -## Development Workflow - -### Adding a New Feature - -1. **Model changes** - ```python - # app/models.py - class NewTable(db.Model): - # Define schema - ``` - -2. **Route handler** - ```python - # app/routes/new_feature.py - @bp.route('/new') - def new_feature(): - # Logic here - ``` - -3. **Template** - ```html - - {% extends "base.html" %} - ``` - -4. **Register blueprint** - ```python - # app/__init__.py - app.register_blueprint(new_feature.bp) - ``` - -### Testing Changes -```bash -docker-compose restart backend -docker-compose logs -f backend -``` - -## Migration Path to Phase 2+ - -### Phase 2 Additions -- Location suggestion service -- Size/type constraint checking -- Visual location picker - -### Phase 3 Additions -- Duplicate detection service -- Fuzzy matching algorithm -- Similarity scoring - -### Phase 4 Additions -- Sentence transformer model -- Embedding generation service -- Vector similarity search -- pgvector extension - -### Phase 5 Additions -- CLI tool (`invctl`) -- Batch operations -- CSV import/export - -### Phase 6 Additions -- Voice service (Whisper/Vosk) -- Wake word detection (Porcupine) -- TTS service -- Audio interface - -## Technical Debt - -### Known Issues -- No database migrations setup (add Flask-Migrate migrations) -- No automated tests -- No CI/CD pipeline -- Limited error handling -- No rate limiting - -### Future Improvements -- Add pytest test suite -- Set up GitHub Actions -- Improve error messages -- Add request validation with marshmallow -- Implement caching strategy - -## Dependencies - -### Python Packages -``` -Flask==3.0.0 # Web framework -Flask-SQLAlchemy==3.1.1 # ORM -Flask-Migrate==4.0.5 # Migrations -psycopg2-binary==2.9.9 # PostgreSQL driver -python-dotenv==1.0.0 # Environment variables -sqlalchemy==2.0.23 # Database toolkit -``` - -### System Requirements -- Python 3.11+ -- PostgreSQL 15+ -- Docker 20.10+ -- Docker Compose 2.0+ - -## Configuration Files - -### docker-compose.yml -Defines all services and their relationships - -### nginx.conf -Reverse proxy configuration - -### .env -Environment variables (DATABASE_URL, SECRET_KEY, etc.) - -## File Structure -``` -inventory-system/ -├── backend/ -│ ├── app/ -│ │ ├── __init__.py # App factory -│ │ ├── models.py # Database models -│ │ ├── routes/ # Route handlers -│ │ │ ├── main.py -│ │ │ ├── items.py -│ │ │ ├── modules.py -│ │ │ ├── locations.py -│ │ │ └── search.py -│ │ └── services/ # Business logic (future) -│ ├── requirements.txt -│ ├── Dockerfile -│ └── run.py -├── frontend/ -│ ├── templates/ -│ │ ├── base.html -│ │ ├── index.html -│ │ └── [feature]/ -│ └── static/ -│ ├── css/style.css -│ └── js/main.js -├── data/ # Docker volumes -│ └── postgres/ -├── docker-compose.yml -├── nginx.conf -└── README.md -``` - ---- - -This architecture is designed to be: -- **Modular**: Easy to add features -- **Maintainable**: Clear separation of concerns -- **Scalable**: Can grow with your needs -- **Extensible**: Plugin-friendly design diff --git a/inventory-system/docs/DEPLOY.md b/inventory-system/docs/DEPLOY.md deleted file mode 100644 index 3d9929b..0000000 --- a/inventory-system/docs/DEPLOY.md +++ /dev/null @@ -1,548 +0,0 @@ -# 🚀 Homelab Inventory System - Complete Deployment Guide - -## What You Have - -A **complete Phase 1 Inventory System** ready to deploy! This is a working system you can start using immediately. - -### ✅ Included Features (Phase 1) - -- Full storage hierarchy (Modules → Levels → Locations) -- Web-based UI for managing inventory -- PostgreSQL database for reliable storage -- Docker deployment (runs anywhere) -- Basic search functionality -- Item tracking with quantities -- Location management with grid visualization -- Many-to-many item-location relationships - -### 🔜 Coming Next (Future Phases) - -- Phase 2: Smart location suggestions -- Phase 3: Duplicate detection -- Phase 4: AI semantic search -- Phase 5: CLI interface -- Phase 6: Voice interface -- Phase 7: Advanced AI features -- Phase 8: Production polish - ---- - -## Quick Start (5 Minutes) - -### Prerequisites - -- Docker & Docker Compose installed -- 2GB RAM available -- 10GB disk space -- Ports 8080, 5432, 5000 available - -### Deployment Steps - -```bash -# 1. Extract the inventory-system folder to your server/workstation -cd inventory-system - -# 2. Start the system -docker-compose up -d - -# 3. Wait 30-60 seconds for containers to start - -# 4. Open your browser -http://localhost:8080 - -# 5. (Optional) Load sample data -python3 create_sample_data.py -``` - -**That's it!** You now have a working inventory system. - ---- - -## Deployment Options - -### Option 1: Local Workstation/Server - -Perfect for testing and personal use. - -```bash -cd inventory-system -docker-compose up -d -``` - -Access at: `http://localhost:8080` - -### Option 2: VPS (Cloud Server) - -Deploy to DigitalOcean, Linode, AWS, etc. - -```bash -# SSH into your VPS -ssh user@your-vps-ip - -# Install Docker & Docker Compose (if not installed) -curl -fsSL https://get.docker.com | sh -sudo usermod -aG docker $USER -sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose -sudo chmod +x /usr/local/bin/docker-compose - -# Deploy system -cd inventory-system -docker-compose up -d - -# Configure firewall (if needed) -sudo ufw allow 8080/tcp -``` - -Access at: `http://your-vps-ip:8080` - -### Option 3: Proxmox LXC Container - -Great for homelab deployments. - -```bash -# 1. Create Ubuntu 22.04 LXC container in Proxmox -# 2. Install Docker in the container -apt update && apt install -y docker.io docker-compose - -# 3. Copy inventory-system folder to container -# 4. Deploy -cd inventory-system -docker-compose up -d -``` - -Access at: `http://container-ip:8080` - -### Option 4: Jetson Nano (Edge AI Device) - -For local AI inference capabilities (Phase 4+). - -```bash -# Docker should already be installed on Jetson -cd inventory-system -docker-compose up -d -``` - -Access at: `http://jetson-ip:8080` - ---- - -## First-Time Setup - -### 1. Create Your Storage Modules - -Modules are your physical storage units (cabinets, shelving, drawers). - -**Example Modules:** -- `Zeus` - Electronics cabinet -- `Muse` - Fasteners organizer -- `Apollo` - Tool chest -- `Workshop-Main` - Main workbench storage - -### 2. Add Levels to Modules - -Levels are drawers, shelves, or compartments within modules. - -**Example for "Zeus" module:** -- Level 1: Top drawer (4 rows × 6 columns) = 24 bins -- Level 2: Middle drawer (3 rows × 4 columns) = 12 bins -- Level 3: Bottom drawer (2 rows × 3 columns) = 6 bins - -The system automatically creates locations (A1, A2, B1, B2, etc.) based on your grid. - -### 3. Add Items - -Items are your actual inventory pieces. - -**Good Description Examples:** -- "Pan head phillips screw, #8 size, 3/4 inch long, mild steel, zinc plated" -- "Ceramic capacitor, 0.1 microfarad, 0805 package, 50V rating" -- "M6 hex bolt, 50mm long, zinc plated, metric thread" -- "Arduino Uno R3 development board with ATmega328P" - -**Location Format:** `ModuleName:LevelNumber:RowCol` -- Example: `Zeus:1:A3` means Module "Zeus", Level 1, Location A3 - -### 4. Use the System - -- **Browse:** Navigate Modules → Levels → Locations to see what's where -- **Search:** Find items by keyword in name, description, or tags -- **Edit:** Update quantities, add/remove locations, change descriptions -- **Organize:** Set location types (small_box, liquid_container, etc.) - ---- - -## Sample Data (Optional) - -Want to see the system in action immediately? - -```bash -# Make sure the system is running first -docker-compose ps - -# Run the sample data script -python3 create_sample_data.py -``` - -This creates: -- 3 storage modules (Zeus, Muse, Apollo) -- Multiple levels per module -- 10 sample items (electronics, fasteners, tools, paint) - -You can delete this sample data later or use it as a template. - ---- - -## Accessing from Other Devices - -### On Your Local Network - -1. Find your server's IP: - ```bash - hostname -I - # or - ip addr show - ``` - -2. Access from any device on the network: - ``` - http://192.168.1.100:8080 (use your actual IP) - ``` - -### Over the Internet (VPS only) - -If deployed on a VPS with a public IP: -``` -http://your-public-ip:8080 -``` - -⚠️ **Security Note:** For internet-facing deployments: -- Change default database password -- Set up HTTPS with Let's Encrypt -- Use a reverse proxy (nginx/Caddy) -- Consider authentication (Phase 8) - ---- - -## Container Management - -### View Status -```bash -docker-compose ps -``` - -### View Logs -```bash -# All containers -docker-compose logs -f - -# Specific container -docker-compose logs -f backend -docker-compose logs -f postgres -docker-compose logs -f nginx -``` - -### Restart Containers -```bash -# Restart all -docker-compose restart - -# Restart specific -docker-compose restart backend -``` - -### Stop System (Keep Data) -```bash -docker-compose stop -``` - -### Start System -```bash -docker-compose start -``` - -### Shutdown Completely (Keep Data) -```bash -docker-compose down -``` - -### Nuclear Option (Delete Everything) -```bash -docker-compose down -v -# This deletes the database volume! -``` - ---- - -## Data Backup - -### Backup Database - -```bash -# Backup to SQL file -docker-compose exec postgres pg_dump -U inventoryuser inventory > backup_$(date +%Y%m%d).sql - -# Backup entire data directory -tar -czf backup_$(date +%Y%m%d).tar.gz data/ -``` - -### Restore Database - -```bash -# Restore from SQL file -docker-compose exec -T postgres psql -U inventoryuser inventory < backup_20241026.sql -``` - -### Automated Backups (Cron) - -```bash -# Add to crontab -crontab -e - -# Backup daily at 2 AM -0 2 * * * cd /path/to/inventory-system && docker-compose exec postgres pg_dump -U inventoryuser inventory > /backups/inventory_$(date +\%Y\%m\%d).sql -``` - ---- - -## Troubleshooting - -### Port Already in Use - -Edit `docker-compose.yml`: -```yaml -nginx: - ports: - - "8081:80" # Change 8080 to 8081 -``` - -Then: `docker-compose down && docker-compose up -d` - -### Database Connection Failed - -```bash -# Check if PostgreSQL is running -docker-compose ps - -# Restart PostgreSQL -docker-compose restart postgres - -# View logs -docker-compose logs postgres -``` - -### Web UI Not Loading - -```bash -# Check nginx logs -docker-compose logs nginx - -# Restart nginx -docker-compose restart nginx - -# Rebuild containers -docker-compose up --build -d -``` - -### Backend Errors - -```bash -# View backend logs -docker-compose logs backend - -# Common issue: Database not ready -# Solution: Wait 30 seconds after starting, or restart backend -docker-compose restart backend -``` - -### Reset Everything - -```bash -# Nuclear option - starts fresh -docker-compose down -v -docker-compose up -d -``` - ---- - -## Performance Tuning - -### For Better Performance: - -1. **Increase Docker Memory:** - - Docker Desktop → Settings → Resources → Memory - - Allocate at least 4GB for larger inventories - -2. **Use SSD Storage:** - - Ensure `data/` directory is on an SSD - -3. **PostgreSQL Tuning:** - Edit `docker-compose.yml` and add: - ```yaml - postgres: - command: postgres -c shared_buffers=256MB -c max_connections=100 - ``` - ---- - -## Security Checklist - -For production/internet-facing deployments: - -- [ ] Change default PostgreSQL password in `docker-compose.yml` -- [ ] Set secure `SECRET_KEY` in environment variables -- [ ] Don't expose PostgreSQL port (5432) to the internet -- [ ] Use HTTPS with SSL certificate -- [ ] Set up firewall rules -- [ ] Enable automated backups -- [ ] Update containers regularly: `docker-compose pull && docker-compose up -d` - ---- - -## Next Steps - -### Immediate (Phase 1): -1. ✅ Deploy system and verify it works -2. ✅ Create your first module -3. ✅ Add 10-20 items to test -4. ✅ Experiment with search -5. ✅ Set up daily backups - -### Soon (Phase 2-3): -1. Add smart location suggestions -2. Implement duplicate detection -3. Parse item specifications automatically - -### Future (Phase 4-6): -1. Add AI semantic search -2. Build CLI interface -3. Create voice interface - ---- - -## Getting Help - -### Documentation -- `README.md` - Comprehensive documentation -- `QUICKSTART.md` - 5-minute deployment guide -- `ARCHITECTURE.md` - Technical architecture details -- `PROJECT_SUMMARY.md` - Full project overview - -### Common Questions - -**Q: Can I run this on a Raspberry Pi?** -A: Yes! Use Docker on Raspberry Pi OS. ARM architecture is supported. - -**Q: How many items can it handle?** -A: Tested with 10,000+ items. PostgreSQL can handle millions. - -**Q: Can I import existing inventory from CSV?** -A: Not yet (Phase 5), but you can write a Python script using the API. - -**Q: Does it work offline?** -A: Yes! It's completely self-hosted. No internet required. - -**Q: Can multiple people use it?** -A: Yes, but no user accounts yet (Phase 8). Everyone shares the same view. - ---- - -## Success Criteria - -You'll know Phase 1 is working when you can: - -- ✅ Access the web UI at http://localhost:8080 -- ✅ Create modules, levels, and locations -- ✅ Add items with descriptions -- ✅ Search for items and find them -- ✅ View the location grid -- ✅ See item counts per location -- ✅ Edit and delete items - ---- - -## What's Different from Other Inventory Systems? - -1. **Storage-first design:** Models your actual physical storage -2. **Natural language:** Describe items how you think about them -3. **Flexible locations:** Items can be in multiple places -4. **Future AI integration:** Ready for semantic search (Phase 4) -5. **Voice control ready:** Architecture supports voice UI (Phase 6) -6. **Open source:** Customize it however you want - ---- - -## Files in This Package - -``` -inventory-system/ -├── README.md # Full documentation -├── QUICKSTART.md # 5-minute quick start -├── ARCHITECTURE.md # Technical details -├── PROJECT_SUMMARY.md # Project overview -├── docker-compose.yml # Docker orchestration -├── nginx.conf # Web server config -├── create_sample_data.py # Sample data generator -├── backend/ # Flask application -│ ├── app/ -│ │ ├── models.py # Database models -│ │ ├── routes/ # API endpoints -│ │ └── __init__.py -│ ├── requirements.txt # Python dependencies -│ ├── Dockerfile -│ └── run.py -└── frontend/ # Web UI - ├── templates/ # HTML templates - └── static/ # CSS/JS assets -``` - ---- - -## Version Info - -- **Version:** 1.0.0 -- **Phase:** 1 (Foundation) -- **Date:** October 2024 -- **Status:** ✅ Production Ready (for Phase 1 features) - ---- - -## Quick Command Reference - -```bash -# Deploy -docker-compose up -d - -# Status -docker-compose ps - -# Logs -docker-compose logs -f - -# Restart -docker-compose restart - -# Stop -docker-compose down - -# Backup -docker-compose exec postgres pg_dump -U inventoryuser inventory > backup.sql - -# Load sample data -python3 create_sample_data.py - -# Access -http://localhost:8080 -``` - ---- - -## Ready to Deploy? - -1. Extract the `inventory-system` folder -2. Run `docker-compose up -d` -3. Open `http://localhost:8080` -4. Start organizing! 🎉 - -**Questions?** Check the README.md or open an issue. - -**Happy organizing!** 🏠🔧📦 diff --git a/inventory-system/docs/DEPLOYMENT_SUMMARY.md b/inventory-system/docs/DEPLOYMENT_SUMMARY.md deleted file mode 100644 index 7a63819..0000000 --- a/inventory-system/docs/DEPLOYMENT_SUMMARY.md +++ /dev/null @@ -1,312 +0,0 @@ -# 🎉 Your Homelab Inventory System is Ready! - -## What You Have - -I've created a complete, working **Phase 1** inventory management system with: - -### ✅ Core Features -- **Full storage hierarchy**: Modules → Levels → Locations (with row/column addressing) -- **Complete CRUD operations**: Add, view, edit, delete items, modules, levels -- **Web interface**: Clean, responsive UI that works on desktop and mobile -- **PostgreSQL backend**: Professional database with proper relationships -- **Docker deployment**: One command to start everything -- **RESTful API**: Programmatic access to all data -- **Search functionality**: Find items by name, description, or tags - -### 📁 Project Structure -``` -inventory-system/ -├── README.md # Complete documentation -├── QUICKSTART.md # 5-minute deployment guide -├── docker-compose.yml # Docker orchestration -├── nginx.conf # Reverse proxy config -├── .env.example # Environment template -├── create_sample_data.py # Demo data generator -├── backend/ -│ ├── app/ -│ │ ├── models.py # Database schema -│ │ ├── routes/ # All API endpoints -│ │ └── __init__.py # Flask app -│ ├── requirements.txt -│ ├── Dockerfile -│ └── run.py -└── frontend/ - ├── templates/ # HTML templates - │ ├── base.html - │ ├── index.html - │ ├── modules/ - │ ├── levels/ - │ ├── items/ - │ ├── locations/ - │ └── search/ - └── static/ - ├── css/style.css # Complete styling - └── js/main.js # Client-side logic -``` - -## 🚀 Quick Start (3 Steps) - -### 1. Navigate to the Project -```bash -cd inventory-system -``` - -### 2. Start the System -```bash -docker-compose up -d -``` - -### 3. Open Your Browser -``` -http://localhost:8080 -``` - -That's it! The system is running. - -## 📚 What to Do Next - -### Option A: Try It Empty -1. Open http://localhost:8080 -2. Click "Modules" → "Add Module" -3. Create your first storage module -4. Add levels and start organizing! - -### Option B: Load Sample Data -```bash -# First, install requests if needed -pip install requests - -# Then run the sample data generator -python create_sample_data.py -``` - -This creates: -- 3 storage modules (Zeus, Muse, Apollo) -- Multiple levels with different grid layouts -- 10 sample items across various categories - -## 🎯 Real-World Usage Example - -Let's say you have a cabinet called "Zeus" with 3 drawers: - -1. **Create Module "Zeus"** - - Modules → Add Module - - Name: Zeus - - Description: Main component storage - - Location: North wall - -2. **Add Drawer (Level 1)** - - Click Zeus → Add Level - - Level Number: 1 - - Rows: 4, Columns: 6 - - This creates locations A1-A6, B1-B6, C1-C6, D1-D6 - -3. **Add an Item** - - Items → Add Item - - Name: "M6 Bolts" - - Description: "Hex head bolt, M6 diameter, 50mm long, zinc plated" - - Category: Fasteners - - Quantity: 100, Unit: pieces - - Location: Zeus:1:A3 (Module Zeus, Level 1, Location A3) - -4. **Find It Later** - - Search → "M6" or "bolt" - - Results show the item with location Zeus:1:A3 - - Click to see exact position in the grid - -## 📊 Database Schema - -The system uses a properly normalized PostgreSQL schema: - -``` -Module (e.g., Zeus) - └── Level 1 (4x6 grid) - ├── Location A1 → [Item: M6 Bolts (qty: 100)] - ├── Location A2 → [Empty] - ├── Location A3 → [Item: Resistors (qty: 200)] - └── ... - └── Level 2 (3x4 grid) - └── ... -``` - -## 🔄 Future Phases (Coming Soon) - -**Phase 2: Smart Locations** (Week 3) -- System suggests where to put items -- Location constraints by type/size -- Visual location maps - -**Phase 3: Duplicate Detection** (Week 4) -- Warns about similar items -- Helps avoid redundant storage - -**Phase 4: AI Search** (Week 5-6) -- Natural language queries -- "Find me a long metric bolt around M6" -- Semantic similarity matching - -**Phase 5: CLI** (Week 7) -- Command-line interface -- Batch operations -- Power user features - -**Phase 6: Voice** (Week 8-9) -- "Hey Inventory, where are my M6 bolts?" -- Hands-free workshop operation - -**Phase 7: Advanced AI** (Week 10-11) -- Usage analytics -- Smart reorganization suggestions -- Alternative part recommendations - -**Phase 8: Production** (Week 12+) -- Mobile optimization -- Multi-user support -- QR codes & barcodes - -## 🛠️ Customization - -### Change Ports -Edit `docker-compose.yml`: -```yaml -nginx: - ports: - - "8080:80" # Change 8080 to your preference -``` - -### Change Database Password -Edit `docker-compose.yml`: -```yaml -environment: - POSTGRES_PASSWORD: your-secure-password -``` - -### Add More Location Types -Edit locations in the web UI and choose from: -- general, small_box, medium_bin, large_bin -- liquid_container, smd_container, bulk_storage -- Or add your own custom types! - -## 💾 Backup Your Data - -### Quick Backup -```bash -docker-compose exec postgres pg_dump -U inventoryuser inventory > backup.sql -``` - -### Restore -```bash -docker-compose exec -T postgres psql -U inventoryuser inventory < backup.sql -``` - -## 🐛 Troubleshooting - -**Containers won't start?** -```bash -docker-compose logs backend -docker-compose logs postgres -``` - -**Port already in use?** -Change the port in docker-compose.yml or: -```bash -lsof -i :8080 # Find what's using it -``` - -**Want to start fresh?** -```bash -docker-compose down -v # ⚠️ Deletes all data! -docker-compose up -d -``` - -## 📖 Documentation - -- **README.md**: Complete documentation -- **QUICKSTART.md**: 5-minute setup guide -- **Code comments**: Every file is well-documented -- **API endpoints**: Check README for REST API details - -## 🎓 Learning Path - -1. ✅ **Week 1-2**: Use Phase 1, add 50-100 items -2. 🔄 **Week 3**: Deploy Phase 2 with location suggestions -3. 🔄 **Week 4**: Add duplicate detection -4. 🔄 **Week 5-6**: Enable semantic search -5. 🔄 **Week 7+**: CLI and voice interfaces - -## 🌟 Key Features - -### Storage Hierarchy -- **Modules**: Physical storage units (cabinets, shelves) -- **Levels**: Drawers, compartments within modules -- **Locations**: Individual bins with row/col addressing -- **Items**: Your actual inventory - -### Location Types -Different location types for different needs: -- Small boxes for tiny SMD components -- Medium bins for fasteners -- Large bins for bulk storage -- Liquid containers for paints/chemicals - -### Flexible Search -- Search by item name -- Search by description -- Search by tags -- Filter by category -- Filter by location - -### Visual Grid -- See occupied vs. empty locations -- Click any location to view contents -- Color-coded status indicators - -## 🔐 Security Notes - -For production: -1. Change PostgreSQL password -2. Set secure SECRET_KEY -3. Enable HTTPS -4. Set up firewall rules -5. Configure backups - -## 🤝 Support - -Read the documentation: -- README.md for detailed info -- QUICKSTART.md for quick help - -Check the logs: -```bash -docker-compose logs -``` - -## 📈 Performance - -Current Phase 1 handles: -- ✅ Thousands of items -- ✅ Hundreds of locations -- ✅ Multiple concurrent users -- ✅ Fast keyword search - -Future phases will add: -- AI-powered semantic search -- Voice recognition -- More advanced features - -## 🎉 You're All Set! - -Your inventory system is ready to use. Start by: - -1. Opening http://localhost:8080 -2. Creating your first module -3. Adding some items -4. Testing the search - -**Enjoy organizing your workshop!** 🛠️ - ---- - -**Need Help?** Check README.md and QUICKSTART.md for detailed guides. - -**Ready for More?** Once comfortable with Phase 1, we'll add smart location suggestions, duplicate detection, and AI search! diff --git a/inventory-system/docs/FILE_INDEX.md b/inventory-system/docs/FILE_INDEX.md deleted file mode 100644 index 18beab1..0000000 --- a/inventory-system/docs/FILE_INDEX.md +++ /dev/null @@ -1,494 +0,0 @@ -# 📁 Complete File Index - -## Project Statistics - -- **Total Files**: 36 source files -- **Lines of Code**: 3,158 (Python, HTML, CSS, JavaScript) -- **Documentation**: 7 comprehensive guides -- **Total Size**: ~147KB (excluding Docker volumes) - -## 📚 Documentation Files (7 files) - -### Primary Guides -1. **README.md** (400+ lines) - - Complete user and developer guide - - Prerequisites and installation - - Usage examples - - API documentation - - Troubleshooting - - Backup procedures - -2. **QUICKSTART.md** (300+ lines) - - 5-minute deployment guide - - First-time setup walkthrough - - Common tasks - - Quick troubleshooting - -3. **DEPLOYMENT_SUMMARY.md** (250+ lines) - - What's included overview - - Quick start steps - - Real-world usage examples - - Future phases roadmap - - Customization guide - -4. **ARCHITECTURE.md** (600+ lines) - - Technical architecture - - System diagrams - - Database schema with SQL - - API endpoint documentation - - Security considerations - - Performance characteristics - -5. **PROJECT_SUMMARY.md** (500+ lines) - - Complete project overview - - Deliverables summary - - Technical achievements - - Future roadmap - - Success metrics - -### Additional Guides -6. **GETTING_STARTED_CHECKLIST.md** (200+ lines) - - Step-by-step checklist - - Testing procedures - - Daily usage guide - - Maintenance tasks - -7. **VERSION.md** (200+ lines) - - Version information - - Feature changelog - - Phase roadmap - - Compatibility matrix - - Dependencies - -## 🐍 Python Backend Files (8 files) - -### Application Core -1. **backend/app/__init__.py** (40 lines) - - Flask application factory - - Extension initialization - - Blueprint registration - - Database creation - -2. **backend/app/models.py** (350 lines) - - Module model - - Level model - - Location model - - Item model - - ItemLocation junction model - - Relationships and constraints - - Helper methods - -### Route Handlers -3. **backend/app/routes/main.py** (30 lines) - - Dashboard route - - Statistics aggregation - - About page - -4. **backend/app/routes/modules.py** (280 lines) - - Module CRUD operations - - Level CRUD operations - - Location grid generation - - API endpoints for modules - -5. **backend/app/routes/items.py** (200 lines) - - Item CRUD operations - - Location assignment - - Multi-location support - - API endpoints for items - -6. **backend/app/routes/locations.py** (100 lines) - - Location listing with filters - - Location details - - Location editing - - API endpoints for locations - -7. **backend/app/routes/search.py** (50 lines) - - Keyword search - - Multi-field search (name, description, tags) - - API search endpoint - -### Application Runner -8. **backend/run.py** (30 lines) - - Development server starter - - Database initialization - - Configuration loading - -## 🎨 Frontend Template Files (15 files) - -### Base Templates -1. **frontend/templates/base.html** (60 lines) - - Page structure - - Navigation menu - - Flash message display - - Footer - -2. **frontend/templates/index.html** (80 lines) - - Dashboard layout - - Statistics cards - - Quick actions - - Recent items table - -### Module Templates -3. **frontend/templates/modules/list.html** (50 lines) - - Module listing - - Module cards - - Empty state - -4. **frontend/templates/modules/view.html** (60 lines) - - Module details - - Level listing - - Statistics - -5. **frontend/templates/modules/form.html** (50 lines) - - Create/edit module form - - Validation - - Delete option - -### Level Templates -6. **frontend/templates/levels/form.html** (60 lines) - - Create/edit level form - - Grid configuration - - Warning messages - -7. **frontend/templates/levels/view.html** (80 lines) - - Location grid display - - Interactive cells - - Legend - -### Item Templates -8. **frontend/templates/items/list.html** (70 lines) - - Item listing table - - Filter form - - Search integration - -9. **frontend/templates/items/view.html** (90 lines) - - Item details - - Location display - - Add/remove locations - -10. **frontend/templates/items/form.html** (120 lines) - - Create/edit item form - - Category selection - - Location picker - - Tag input - -### Location Templates -11. **frontend/templates/locations/list.html** (70 lines) - - Location listing - - Filter options - - Status indicators - -12. **frontend/templates/locations/view.html** (70 lines) - - Location details - - Items stored - - Breadcrumb navigation - -13. **frontend/templates/locations/form.html** (60 lines) - - Edit location properties - - Type selection - - Dimension inputs - -### Search Templates -14. **frontend/templates/search/results.html** (60 lines) - - Search form - - Results table - - Empty state - -## 🎨 Styling & Scripts (2 files) - -1. **frontend/static/css/style.css** (1,100 lines) - - Complete design system - - CSS custom properties - - Responsive layouts - - Component styles - - Grid system - - Forms - - Tables - - Cards - - Navigation - - Utilities - -2. **frontend/static/js/main.js** (100 lines) - - Flash message auto-hide - - Confirm dialogs - - Dynamic location selector - - Form enhancements - -## 🐋 Infrastructure Files (4 files) - -1. **docker-compose.yml** (50 lines) - - Multi-container orchestration - - PostgreSQL service - - Flask backend service - - nginx proxy service - - Volume definitions - - Health checks - -2. **backend/Dockerfile** (20 lines) - - Python base image - - System dependencies - - Package installation - - Application setup - -3. **nginx.conf** (25 lines) - - Reverse proxy configuration - - Upstream backend - - Static file serving - - Headers - -4. **backend/requirements.txt** (6 lines) - - Flask 3.0 - - SQLAlchemy 2.0 - - psycopg2 - - Flask-Migrate - - python-dotenv - -## 🔧 Configuration Files (2 files) - -1. **.env.example** (15 lines) - - Database configuration - - Flask settings - - Secret keys - - Port configuration - -2. **.gitignore** (50 lines) - - Python artifacts - - Virtual environments - - Database files - - IDE files - - Environment files - - Logs - -## 🧪 Utility Scripts (1 file) - -1. **create_sample_data.py** (200 lines) - - Sample data generator - - Creates 3 modules - - Creates multiple levels - - Creates 10 sample items - - Server health check - - Progress reporting - -## 📊 File Breakdown by Type - -### By Language -``` -Python: ~1,200 lines (8 files) -HTML: ~1,100 lines (15 files) -CSS: ~1,100 lines (1 file) -JavaScript: ~100 lines (1 file) -Configuration: ~100 lines (4 files) -Documentation: ~2,500 lines (7 files) -``` - -### By Purpose -``` -Core Application: ~1,500 lines (Python backend + models) -User Interface: ~2,200 lines (HTML templates + CSS + JS) -Infrastructure: ~100 lines (Docker, nginx) -Documentation: ~2,500 lines (7 guides) -Configuration/Utils: ~300 lines (config, scripts) -``` - -## 🗂️ Directory Structure - -``` -inventory-system/ -│ -├── Documentation (7 files) -│ ├── README.md -│ ├── QUICKSTART.md -│ ├── DEPLOYMENT_SUMMARY.md -│ ├── ARCHITECTURE.md -│ ├── PROJECT_SUMMARY.md -│ ├── GETTING_STARTED_CHECKLIST.md -│ └── VERSION.md -│ -├── Configuration (4 files) -│ ├── docker-compose.yml -│ ├── nginx.conf -│ ├── .env.example -│ └── .gitignore -│ -├── Utilities (1 file) -│ └── create_sample_data.py -│ -├── backend/ -│ ├── App Core (2 files) -│ │ ├── app/__init__.py -│ │ └── app/models.py -│ │ -│ ├── Routes (5 files) -│ │ ├── app/routes/main.py -│ │ ├── app/routes/modules.py -│ │ ├── app/routes/items.py -│ │ ├── app/routes/locations.py -│ │ └── app/routes/search.py -│ │ -│ ├── Configuration (2 files) -│ │ ├── requirements.txt -│ │ └── Dockerfile -│ │ -│ └── Runner (1 file) -│ └── run.py -│ -└── frontend/ - ├── static/ - │ ├── css/ - │ │ └── style.css (1 file) - │ └── js/ - │ └── main.js (1 file) - │ - └── templates/ - ├── Base (2 files) - │ ├── base.html - │ └── index.html - │ - ├── modules/ (3 files) - │ ├── list.html - │ ├── view.html - │ └── form.html - │ - ├── levels/ (2 files) - │ ├── view.html - │ └── form.html - │ - ├── items/ (3 files) - │ ├── list.html - │ ├── view.html - │ └── form.html - │ - ├── locations/ (3 files) - │ ├── list.html - │ ├── view.html - │ └── form.html - │ - └── search/ (1 file) - └── results.html -``` - -## 📈 Code Quality Metrics - -### Python Code -- **Modularity**: Excellent (blueprints, models separated) -- **Documentation**: Comprehensive (docstrings, comments) -- **Error Handling**: Good (try-except, validation) -- **Code Style**: PEP 8 compliant -- **Complexity**: Low to medium - -### HTML/CSS -- **Semantic HTML**: Yes (HTML5 elements) -- **Accessibility**: Good (labels, alt text) -- **Responsive**: Yes (mobile-friendly) -- **Maintainability**: Excellent (CSS variables, consistent naming) -- **Browser Support**: Modern browsers - -### JavaScript -- **Modern Syntax**: ES6+ -- **Vanilla JS**: No framework dependencies -- **Progressive Enhancement**: Yes -- **Error Handling**: Basic - -## 🔍 Key Features by File - -### Database Models (models.py) -- 5 models with relationships -- Cascade deletes -- Unique constraints -- JSON metadata support -- Helper methods (to_dict, full_address) - -### Route Handlers -- **main.py**: Dashboard with statistics -- **modules.py**: Full module/level CRUD + API -- **items.py**: Item management with multi-location -- **locations.py**: Location filtering and viewing -- **search.py**: Keyword search with wildcards - -### Templates -- **base.html**: Navigation, flash messages, structure -- **index.html**: Dashboard with quick actions -- **Module views**: List, detail, form patterns -- **Item views**: Complex forms with location picker -- **Location views**: Grid visualization -- **Search**: Results with filtering - -### Styling (style.css) -- CSS custom properties for theming -- Responsive grid system -- Component library (cards, tables, forms) -- Interactive grid cells -- Alert system -- Mobile-friendly - -## 🎯 Most Important Files - -### For Users -1. **QUICKSTART.md** - Start here -2. **README.md** - Complete guide -3. **docker-compose.yml** - One-command deploy - -### For Developers -1. **models.py** - Database schema -2. **ARCHITECTURE.md** - System design -3. **modules.py** - Example route patterns - -### For Operators -1. **docker-compose.yml** - Deployment config -2. **.env.example** - Configuration template -3. **QUICKSTART.md** - Troubleshooting - -## 📝 Lines of Code by Component - -``` -Database Models: ~350 lines -Route Handlers: ~660 lines -Templates: ~1,100 lines -Styling (CSS): ~1,100 lines -JavaScript: ~100 lines -Configuration: ~100 lines -Documentation: ~2,500 lines -Utilities: ~200 lines -─────────────────────────────── -Total: ~6,110 lines -``` - -## 🎉 Completeness Check - -### Documentation ✅ -- [x] User guide (README) -- [x] Quick start -- [x] Architecture docs -- [x] Deployment guide -- [x] Version info -- [x] Checklist -- [x] Project summary - -### Code ✅ -- [x] Database models -- [x] All routes -- [x] All templates -- [x] Complete styling -- [x] JavaScript utilities -- [x] Sample data script - -### Infrastructure ✅ -- [x] Docker Compose -- [x] Dockerfiles -- [x] nginx config -- [x] Environment template - -### Quality ✅ -- [x] Code comments -- [x] Docstrings -- [x] Error handling -- [x] Input validation -- [x] Responsive design - ---- - -**Total Project Scope**: 36 files, 6,100+ lines, 7 comprehensive guides - -**Status**: Phase 1 Complete ✅ - -**Next**: Deploy and use, then proceed to Phase 2! diff --git a/inventory-system/docs/GETTING_STARTED_CHECKLIST.md b/inventory-system/docs/GETTING_STARTED_CHECKLIST.md deleted file mode 100644 index 80dfedc..0000000 --- a/inventory-system/docs/GETTING_STARTED_CHECKLIST.md +++ /dev/null @@ -1,237 +0,0 @@ -# ✅ Getting Started Checklist - -Use this checklist to deploy and start using your inventory system. - -## Pre-Deployment Checks - -- [ ] Docker installed (`docker --version`) -- [ ] Docker Compose installed (`docker-compose --version`) -- [ ] At least 2GB RAM available -- [ ] At least 10GB disk space available -- [ ] Ports 8080, 5432, 5000 are not in use - -## Deployment Steps - -- [ ] Navigate to project directory: `cd inventory-system` -- [ ] Review QUICKSTART.md -- [ ] Start the system: `docker-compose up -d` -- [ ] Wait 30-60 seconds for containers to start -- [ ] Verify all containers running: `docker-compose ps` -- [ ] Access web interface: http://localhost:8080 - -## First-Time Setup - -- [ ] Dashboard loads successfully -- [ ] Create first module (e.g., "Zeus") -- [ ] Add first level to module (e.g., 4x6 grid) -- [ ] View level to see location grid -- [ ] Create first item -- [ ] Assign item to a location -- [ ] Test search functionality - -## Optional: Load Sample Data - -- [ ] Install Python requests: `pip install requests` -- [ ] Run sample data script: `python create_sample_data.py` -- [ ] Verify data loaded in web interface -- [ ] Explore sample modules (Zeus, Muse, Apollo) -- [ ] View sample items and locations - -## Testing Checklist - -### Module Management -- [ ] Create a module -- [ ] Edit module details -- [ ] View module with levels -- [ ] Delete a test module - -### Level Management -- [ ] Add level to module -- [ ] Configure grid size (rows x columns) -- [ ] View location grid -- [ ] Edit level configuration -- [ ] Delete a test level - -### Item Management -- [ ] Create an item -- [ ] Add description and tags -- [ ] Assign to location -- [ ] View item details -- [ ] Edit item information -- [ ] Add second location to item -- [ ] Remove location from item -- [ ] Delete a test item - -### Location Management -- [ ] View locations list -- [ ] Filter locations (occupied/empty) -- [ ] View location details -- [ ] Edit location properties -- [ ] Set location type -- [ ] Add dimensions to location - -### Search & Discovery -- [ ] Search by item name -- [ ] Search by description keyword -- [ ] Search by tag -- [ ] View search results -- [ ] Click through to item details -- [ ] Navigate to item location - -## Real Data Migration - -- [ ] Plan storage module structure -- [ ] Create all physical modules -- [ ] Add levels with accurate grids -- [ ] Start with high-value items -- [ ] Add 10 items -- [ ] Add 50 items -- [ ] Add 100 items -- [ ] Label physical locations (optional) -- [ ] Test workflow in real usage - -## Daily Usage - -- [ ] Add new items as acquired -- [ ] Update quantities when used -- [ ] Search when looking for items -- [ ] Keep descriptions accurate -- [ ] Use tags consistently -- [ ] Note location changes - -## Maintenance - -- [ ] Review dashboard statistics weekly -- [ ] Backup database monthly: `docker-compose exec postgres pg_dump -U inventoryuser inventory > backup.sql` -- [ ] Check disk space -- [ ] Review logs if issues: `docker-compose logs` -- [ ] Update items that moved -- [ ] Archive or delete obsolete items - -## Troubleshooting - -If something doesn't work: - -- [ ] Check all containers running: `docker-compose ps` -- [ ] View backend logs: `docker-compose logs backend` -- [ ] View database logs: `docker-compose logs postgres` -- [ ] View nginx logs: `docker-compose logs nginx` -- [ ] Restart services: `docker-compose restart` -- [ ] Check QUICKSTART.md troubleshooting section -- [ ] Check README.md for detailed help - -## Performance Checks - -After adding significant data: - -- [ ] Search responds in < 1 second -- [ ] Pages load quickly -- [ ] No database errors in logs -- [ ] Disk space sufficient -- [ ] Container memory usage acceptable - -## Security Hardening (Production) - -If exposing to network: - -- [ ] Change PostgreSQL password in docker-compose.yml -- [ ] Set secure SECRET_KEY in .env -- [ ] Configure firewall rules -- [ ] Set up HTTPS with nginx + Let's Encrypt -- [ ] Restrict database port (5432) access -- [ ] Set up automated backups -- [ ] Enable audit logging -- [ ] Review nginx access logs - -## Ready for Phase 2? - -Before adding Phase 2 features: - -- [ ] 50+ items in system -- [ ] Comfortable with web interface -- [ ] Storage hierarchy makes sense -- [ ] Search works well -- [ ] Ready for smart location suggestions -- [ ] Ready for duplicate detection -- [ ] Identified pain points to improve - -## Phase 2 Preparation - -- [ ] Document desired location suggestion behavior -- [ ] Note which items are hard to place -- [ ] Identify duplicate items manually -- [ ] List location types needed -- [ ] Consider physical labeling strategy - -## Success Indicators - -You're successfully using the system when: - -- [ ] You find items without looking physically -- [ ] You know where everything is -- [ ] You avoid buying duplicates -- [ ] Adding items is quick and easy -- [ ] Search saves you time -- [ ] Workshop feels more organized -- [ ] You trust the system data - -## Next Steps - -- [ ] Use system for 1 week -- [ ] Add 100+ items -- [ ] Identify features you need most -- [ ] Decide which phase to deploy next -- [ ] Consider CLI for power users -- [ ] Plan for voice interface -- [ ] Think about semantic search use cases - ---- - -## Quick Commands Reference - -```bash -# Start system -docker-compose up -d - -# Stop system -docker-compose stop - -# Restart system -docker-compose restart - -# View logs -docker-compose logs -f - -# Stop and remove (keeps data) -docker-compose down - -# Nuclear option (deletes ALL data) -docker-compose down -v - -# Backup database -docker-compose exec postgres pg_dump -U inventoryuser inventory > backup.sql - -# Restore database -docker-compose exec -T postgres psql -U inventoryuser inventory < backup.sql - -# Check status -docker-compose ps - -# Load sample data -python create_sample_data.py -``` - ---- - -**Date Started**: _________________ - -**Date Completed Setup**: _________________ - -**First Real Item Added**: _________________ - -**Items in System**: _______ (update weekly) - -**Notes**: -_________________________________________ -_________________________________________ -_________________________________________ diff --git a/inventory-system/docs/INDEX.md b/inventory-system/docs/INDEX.md deleted file mode 100644 index 48f0981..0000000 --- a/inventory-system/docs/INDEX.md +++ /dev/null @@ -1,457 +0,0 @@ -# 📚 Documentation Index - -Welcome! This guide helps you navigate all the documentation for the Homelab Inventory System. - ---- - -## 🚀 Start Here - -### New User? Read These First: - -1. **[PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md)** ⭐ START HERE - - What is this system? - - What can it do? - - Quick start guide - - **Time: 10 minutes** - -2. **[QUICKSTART.md](QUICKSTART.md)** - - Deploy in 5 minutes - - First-time setup - - Load sample data - - **Time: 5 minutes** - -3. **[QUICK_REFERENCE.md](QUICK_REFERENCE.md)** - - Essential commands - - Common workflows - - Troubleshooting tips - - **Time: 5 minutes to read, keep for reference** - ---- - -## 📖 Complete Documentation - -### Full Guides - -**[README.md](inventory-system/README.md)** - Complete Documentation -- Full feature list -- Detailed usage guide -- API documentation -- Development info -- **Time: 30 minutes, reference document** - -**[DEPLOY.md](DEPLOY.md)** - Deployment Guide -- All deployment options -- VPS, Proxmox, Jetson setup -- Production hardening -- Security checklist -- **Time: 20 minutes** - -**[ROADMAP.md](ROADMAP.md)** - Development Roadmap -- Complete 8-phase plan -- Feature timeline -- Technical details -- Phase dependencies -- **Time: 30 minutes** - -**[TESTING_CHECKLIST.md](TESTING_CHECKLIST.md)** - Testing Guide -- Verification procedures -- Test all features -- Troubleshooting tests -- Success criteria -- **Time: 1 hour to complete all tests** - ---- - -## 🗺️ Navigation by Task - -### I Want to... - -#### Get Started -→ Read: **PROJECT_OVERVIEW.md** -→ Then: **QUICKSTART.md** -→ Deploy and test! - -#### Deploy the System -→ Read: **QUICKSTART.md** (simple) or **DEPLOY.md** (comprehensive) -→ Run: `docker-compose up -d` -→ Verify: **TESTING_CHECKLIST.md** - -#### Learn Daily Operations -→ Read: **QUICK_REFERENCE.md** -→ Bookmark for daily use -→ Print and keep near workstation - -#### Understand Features -→ Read: **README.md** (complete docs) -→ Check: **PROJECT_OVERVIEW.md** (summary) -→ Try: Sample data - -#### Plan Future Phases -→ Read: **ROADMAP.md** -→ Understand phase dependencies -→ Choose next features - -#### Troubleshoot Issues -→ Check: **QUICK_REFERENCE.md** (common issues) -→ Try: **TESTING_CHECKLIST.md** (verify setup) -→ Review: Logs with `docker-compose logs` - -#### Deploy to Production -→ Read: **DEPLOY.md** (full guide) -→ Follow: Security checklist -→ Set up: Automated backups - -#### Test Everything -→ Use: **TESTING_CHECKLIST.md** -→ Verify: All features work -→ Document: Any issues - ---- - -## 📋 Document Purpose Guide - -| Document | Purpose | When to Use | Time | -|----------|---------|-------------|------| -| PROJECT_OVERVIEW.md | Big picture overview | Starting out | 10 min | -| QUICKSTART.md | Fast deployment | Want it running now | 5 min | -| README.md | Complete reference | Need detailed info | 30 min | -| DEPLOY.md | Production deployment | Serious deployment | 20 min | -| ROADMAP.md | Future planning | Curious about phases | 30 min | -| QUICK_REFERENCE.md | Daily cheat sheet | Using the system | 5 min | -| TESTING_CHECKLIST.md | Verification | After deployment | 60 min | - ---- - -## 🎯 Reading Paths - -### Path 1: Quick Start (15 minutes) -1. PROJECT_OVERVIEW.md (10 min) -2. QUICKSTART.md (5 min) -3. Deploy and test - -**Best for:** Getting started fast - -### Path 2: Comprehensive (90 minutes) -1. PROJECT_OVERVIEW.md (10 min) -2. README.md (30 min) -3. DEPLOY.md (20 min) -4. QUICKSTART.md (5 min) -5. Deploy -6. TESTING_CHECKLIST.md (60 min) -7. QUICK_REFERENCE.md (5 min) - -**Best for:** Thorough understanding - -### Path 3: Production Deploy (60 minutes) -1. PROJECT_OVERVIEW.md (10 min) -2. DEPLOY.md (20 min) -3. Deploy with production settings -4. TESTING_CHECKLIST.md (60 min) -5. Set up backups -6. QUICK_REFERENCE.md (5 min) - -**Best for:** Production deployment - -### Path 4: Developer (120 minutes) -1. README.md (30 min) -2. Review code in inventory-system/ -3. ROADMAP.md (30 min) -4. Deploy and test -5. TESTING_CHECKLIST.md (60 min) -6. Plan customizations - -**Best for:** Extending the system - ---- - -## 📂 File Structure - -``` -/ -├── PROJECT_OVERVIEW.md ⭐ Start here! -├── QUICKSTART.md Fast deployment -├── README.md In inventory-system/ -├── DEPLOY.md Deployment guide -├── ROADMAP.md Future plans -├── QUICK_REFERENCE.md Daily cheat sheet -├── TESTING_CHECKLIST.md Verification -└── inventory-system/ The actual system - ├── docker-compose.yml - ├── backend/ - ├── frontend/ - └── create_sample_data.py -``` - ---- - -## 🆘 Help! I Need... - -### To deploy quickly -→ **QUICKSTART.md** - -### To understand what this is -→ **PROJECT_OVERVIEW.md** - -### Detailed information -→ **README.md** in inventory-system/ - -### Production deployment -→ **DEPLOY.md** - -### Daily commands -→ **QUICK_REFERENCE.md** - -### Future features -→ **ROADMAP.md** - -### To verify it works -→ **TESTING_CHECKLIST.md** - -### Troubleshooting -→ **QUICK_REFERENCE.md** then **README.md** - ---- - -## 💡 Tips for Reading - -### For Beginners -- Start with PROJECT_OVERVIEW.md -- Don't try to read everything at once -- Deploy using QUICKSTART.md -- Keep QUICK_REFERENCE.md handy -- Come back to other docs as needed - -### For Advanced Users -- Skim PROJECT_OVERVIEW.md -- Jump straight to deployment -- Reference README.md for details -- Check ROADMAP.md for future features -- Use TESTING_CHECKLIST.md thoroughly - -### For Production -- Read DEPLOY.md carefully -- Follow security checklist -- Complete TESTING_CHECKLIST.md -- Set up automated backups -- Keep QUICK_REFERENCE.md accessible - ---- - -## 🔖 Bookmarks - -Print or bookmark these for quick access: - -### Daily Use -- QUICK_REFERENCE.md (commands) -- TESTING_CHECKLIST.md (troubleshooting section) - -### Occasional Reference -- README.md (complete docs) -- DEPLOY.md (production tips) - -### Planning -- ROADMAP.md (feature planning) -- PROJECT_OVERVIEW.md (big picture) - ---- - -## 📝 Documentation Map - -``` - PROJECT_OVERVIEW.md - | - [Quick Summary] - | - +---------+----------+ - | | - QUICKSTART.md README.md - [Fast Deploy] [Complete Docs] - | | - +-------- + ---------+ - | - DEPLOY.md - [Production Guide] - | - | - TESTING_CHECKLIST.md - [Verify] - | - | - QUICK_REFERENCE.md - [Daily Use] - | - | - ROADMAP.md - [Future Plans] -``` - ---- - -## ✅ Checklist: Have You Read? - -Before deploying: -- [ ] PROJECT_OVERVIEW.md -- [ ] QUICKSTART.md or DEPLOY.md - -After deploying: -- [ ] TESTING_CHECKLIST.md (at least critical tests) -- [ ] QUICK_REFERENCE.md (for daily operations) - -For production: -- [ ] DEPLOY.md (security section) -- [ ] TESTING_CHECKLIST.md (complete) - -Optional but recommended: -- [ ] README.md (comprehensive reference) -- [ ] ROADMAP.md (understand future) - ---- - -## 🎓 Learning Progression - -### Week 1: Getting Started -- Read: PROJECT_OVERVIEW.md -- Deploy: Using QUICKSTART.md -- Test: Basic tests from TESTING_CHECKLIST.md -- Use: Add first 20 items - -### Week 2: Daily Operations -- Master: QUICK_REFERENCE.md -- Complete: TESTING_CHECKLIST.md -- Organize: Add more items -- Refine: Storage organization - -### Week 3: Advanced -- Read: Complete README.md -- Explore: API endpoints -- Review: ROADMAP.md -- Plan: Next phase needs - -### Week 4+: Mastery -- Optimize: Storage layout -- Automate: Backup scripts -- Customize: Add features -- Prepare: For Phase 2 - ---- - -## 📞 Still Lost? - -### Read This Order: -1. PROJECT_OVERVIEW.md (the big picture) -2. QUICKSTART.md (get it running) -3. Use the system for a day -4. QUICK_REFERENCE.md (daily operations) -5. Come back to other docs as needed - -### Common Mistakes: -- ❌ Trying to read everything first -- ❌ Skipping PROJECT_OVERVIEW.md -- ❌ Not testing after deployment -- ❌ Forgetting QUICK_REFERENCE.md - -### Best Approach: -- ✅ Read PROJECT_OVERVIEW.md -- ✅ Deploy with QUICKSTART.md -- ✅ Load sample data -- ✅ Use the system -- ✅ Reference docs as needed - ---- - -## 🗂️ Documentation Stats - -| Document | Pages | Read Time | Update Frequency | -|----------|-------|-----------|------------------| -| PROJECT_OVERVIEW.md | ~8 | 10 min | Each phase | -| QUICKSTART.md | ~4 | 5 min | Rarely | -| README.md | ~15 | 30 min | Each phase | -| DEPLOY.md | ~12 | 20 min | Each phase | -| ROADMAP.md | ~20 | 30 min | Each phase | -| QUICK_REFERENCE.md | ~8 | 5 min | As needed | -| TESTING_CHECKLIST.md | ~12 | 60 min | Each phase | - ---- - -## 🎯 Quick Decision Tree - -**Just want to try it?** -→ PROJECT_OVERVIEW.md + QUICKSTART.md - -**Need to deploy for real?** -→ DEPLOY.md + TESTING_CHECKLIST.md - -**Want all the details?** -→ README.md - -**Daily operations?** -→ QUICK_REFERENCE.md - -**Planning future?** -→ ROADMAP.md - -**Something broken?** -→ QUICK_REFERENCE.md troubleshooting section - ---- - -## 🏁 Final Recommendations - -### Absolute Minimum -Must read: -1. PROJECT_OVERVIEW.md -2. QUICKSTART.md - -### Recommended -Also read: -3. QUICK_REFERENCE.md -4. README.md (skim) - -### Complete -Read all documents in this order: -1. PROJECT_OVERVIEW.md -2. QUICKSTART.md -3. Deploy system -4. TESTING_CHECKLIST.md -5. QUICK_REFERENCE.md -6. README.md -7. DEPLOY.md (if production) -8. ROADMAP.md (for planning) - ---- - -## 📚 Additional Resources - -### In the Code -- `inventory-system/backend/app/models.py` - Database schema -- `inventory-system/backend/app/routes/` - API endpoints -- `inventory-system/docker-compose.yml` - Container config - -### Generated by System -- Docker logs: `docker-compose logs` -- Database: Connect with psql -- Backups: Created in `data/` directory - ---- - -## 🎊 Ready to Start? - -**Recommended first steps:** - -1. Read PROJECT_OVERVIEW.md (10 minutes) -2. Read QUICKSTART.md (5 minutes) -3. Deploy: `docker-compose up -d` (1 minute) -4. Load sample data: `python3 create_sample_data.py` (1 minute) -5. Explore at http://localhost:8080 (as long as you want!) - -**Total time to working system: ~20 minutes** - ---- - -**Questions?** Check the relevant document above or the troubleshooting sections. - -**Ready?** Start with [PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md)! - ---- - -*Documentation Index - Version 1.0.0 - October 2024* diff --git a/inventory-system/docs/PROJECT_OVERVIEW.md b/inventory-system/docs/PROJECT_OVERVIEW.md deleted file mode 100644 index 765068b..0000000 --- a/inventory-system/docs/PROJECT_OVERVIEW.md +++ /dev/null @@ -1,665 +0,0 @@ -# 🏠 Homelab Inventory System - Complete Package - -## What Is This? - -A comprehensive, AI-ready inventory management system designed specifically for homelabs, makerspaces, and workshops. Track thousands of items (electronics, fasteners, tools, paints, etc.) across organized storage with natural language descriptions and future AI capabilities. - -**Current Status: Phase 1 Complete ✅** - -This is a **fully functional, production-ready system** you can deploy and start using immediately. - ---- - -## 📦 Package Contents - -This package includes everything needed to run your inventory system: - -### Core System Files -``` -inventory-system/ -├── docker-compose.yml # Orchestration config -├── nginx.conf # Web server config -├── backend/ # Flask application -│ ├── app/ -│ │ ├── models.py # Database models -│ │ ├── routes/ # API endpoints -│ │ └── __init__.py # App initialization -│ ├── requirements.txt # Python dependencies -│ ├── Dockerfile # Container definition -│ └── run.py # Application entry point -└── frontend/ # Web UI - ├── templates/ # HTML templates - └── static/ # CSS/JS assets -``` - -### Documentation Files -- `README.md` - Complete user and technical documentation -- `QUICKSTART.md` - 5-minute deployment guide -- `DEPLOY.md` - Comprehensive deployment guide -- `ROADMAP.md` - 8-phase development plan -- `QUICK_REFERENCE.md` - Daily operations cheat sheet -- `TESTING_CHECKLIST.md` - Verification procedures -- `create_sample_data.py` - Sample data generator - ---- - -## 🚀 Quick Start (3 Steps) - -### 1. Prerequisites -- Docker & Docker Compose -- 2GB RAM, 10GB disk -- Ports 5000, 5432, 8080 available - -### 2. Deploy -```bash -cd inventory-system -docker-compose up -d -``` - -### 3. Access -Open browser: `http://localhost:8080` - -**Done!** You now have a working inventory system. - ---- - -## ✨ What You Get (Phase 1) - -### Core Features -- ✅ **Storage Hierarchy**: Modules → Levels → Locations -- ✅ **Full CRUD**: Create, read, update, delete everything -- ✅ **Web UI**: Clean, responsive interface -- ✅ **Search**: Find items by keyword -- ✅ **Grid Visualization**: See your storage layout -- ✅ **Flexible Locations**: Items can be in multiple places -- ✅ **Quantity Tracking**: Know what you have -- ✅ **Categorization**: Organize by type -- ✅ **Tagging**: Cross-reference items -- ✅ **REST API**: Programmatic access -- ✅ **Docker Deployment**: Runs anywhere - -### Technology Stack -- **Backend**: Python 3.11+ with Flask -- **Database**: PostgreSQL 15 -- **ORM**: SQLAlchemy -- **Frontend**: Jinja2 templates -- **Deployment**: Docker Compose -- **Web Server**: nginx - -### Data Model -``` -Module (Storage Unit) - └── Level (Drawer/Shelf) - └── Location (Bin/Position) - └── Items (Inventory) - -Example: -Zeus (cabinet) - └── Level 1 (top drawer) - └── Location A3 - └── M6 Bolts (100 pcs) -``` - ---- - -## 🔮 What's Coming (Future Phases) - -This is just the beginning! Here's what's planned: - -### Phase 2: Smart Location Management (Week 3) -- System suggests where to store items -- Location compatibility checking -- Space utilization tracking -- Auto-organization hints - -### Phase 3: Duplicate Detection (Week 4) -- Automatic duplicate detection -- Parse item specifications -- Warn before creating duplicates -- Suggest consolidation - -### Phase 4: Semantic Search (Weeks 5-6) -- AI-powered natural language search -- "Find long metric bolts" actually works -- Similarity-based results -- BERT/Transformer models - -### Phase 5: CLI Interface (Week 7) -- Command-line tool (`invctl`) -- Scriptable operations -- Batch import/export -- Interactive shell - -### Phase 6: Voice Interface (Weeks 8-9) -- Wake word activation -- Hands-free operation -- Voice commands -- Workshop-ready - -### Phase 7: Advanced AI (Weeks 10-11) -- Usage analytics -- Smart recommendations -- Auto-categorization -- Predictive restocking - -### Phase 8: Production Polish (Week 12+) -- Multi-user support -- Mobile optimization -- QR code/barcode scanning -- Professional features - -**Total Development Time: ~3 months for complete system** - ---- - -## 📚 Documentation Guide - -### Getting Started -1. **Start here**: `QUICKSTART.md` - Get running in 5 minutes -2. **Then read**: `README.md` - Full documentation -3. **For deployment**: `DEPLOY.md` - Comprehensive guide - -### Daily Use -- **Quick Reference**: `QUICK_REFERENCE.md` - Commands and tips -- **Troubleshooting**: `README.md` - Common issues section - -### Planning -- **Roadmap**: `ROADMAP.md` - Future features and timeline -- **Testing**: `TESTING_CHECKLIST.md` - Verify everything works - -### Development -- **Architecture**: Models and relationships in code -- **API Docs**: README.md → API Endpoints section - ---- - -## 🎯 Use Cases - -Perfect for: -- **Homelabs**: Track server parts, cables, tools -- **Makerspaces**: Manage shared inventory -- **Workshops**: Organize fasteners, materials, tools -- **Electronics**: Store components, modules, equipment -- **General**: Any organized storage needs - -### Example Inventories - -**Electronics Lab:** -``` -Zeus Module (Component Cabinet) -├── Level 1: Resistors (5×8 grid) -├── Level 2: Capacitors (4×6 grid) -├── Level 3: ICs (3×4 grid) -└── Level 4: Modules (2×3 grid) -``` - -**Workshop:** -``` -Muse Module (Fastener Organizer) -├── Level 1: Metric screws (8×10 grid) -├── Level 2: Metric bolts (6×8 grid) -├── Level 3: Imperial screws (8×10 grid) -└── Level 4: Imperial bolts (6×8 grid) -``` - -**Maker Space:** -``` -Apollo Module (Tool Storage) -├── Level 1: Hand tools -├── Level 2: Power tools -└── Level 3: Measuring instruments -``` - ---- - -## 💡 Key Concepts - -### Modules -Physical storage units (cabinets, shelving, toolboxes). Name them memorably! - -**Examples:** -- Zeus (Greek god theme) -- Cabinet-1 (Simple numbering) -- Electronics-Main (Descriptive) - -### Levels -Drawers, shelves, or compartments within modules. Each has a row×column grid. - -**Example:** -- Level 1: 4 rows × 6 columns = 24 storage bins - -### Locations -Individual storage positions (bins, compartments). Auto-created from grid. - -**Addressing:** `Module:Level:RowCol` -- `Zeus:1:A3` = Module "Zeus", Level 1, Location A3 - -### Items -Your actual inventory with natural language descriptions. - -**Good Description:** -> "Hex head bolt, M6 diameter, 50mm long, zinc plated, metric coarse thread" - -**Bad Description:** -> "Bolt" - -### Tags -Comma-separated keywords for better searching. - -**Example:** -> `bolt, metric, m6, hex, zinc, fastener` - ---- - -## 🔧 Common Workflows - -### First-Time Setup -1. Create your first module (e.g., "Zeus") -2. Add levels to the module (e.g., 3 drawers) -3. Define grid for each level (e.g., 4×6) -4. Start adding items! - -### Adding an Item -1. Items → Add Item -2. Name: "M6 Bolts" -3. Description: Natural language (be detailed!) -4. Category: "Fasteners" -5. Location: "Zeus:1:A3" -6. Quantity: 100 -7. Tags: "bolt, m6, metric, hex" -8. Create! - -### Finding an Item -**Option 1:** Search -- Search → Type "M6" -- Click result - -**Option 2:** Browse -- Modules → Zeus -- Level 1 -- Location A3 -- See all items - -### Moving Items -1. Find item -2. Edit -3. Update location -4. Adjust quantities -5. Save - ---- - -## 📊 Sample Data - -Want to see it in action immediately? - -```bash -python3 create_sample_data.py -``` - -Creates: -- 3 modules (Zeus, Muse, Apollo) -- Multiple levels per module -- 10 realistic items -- Various categories - -Perfect for testing and learning! - ---- - -## 🌐 Deployment Options - -### Option 1: Local Development -Perfect for testing. -```bash -cd inventory-system -docker-compose up -d -``` -Access: `http://localhost:8080` - -### Option 2: VPS/Cloud -Deploy to DigitalOcean, AWS, etc. -```bash -# Install Docker on VPS -# Copy inventory-system folder -docker-compose up -d -``` -Access: `http://your-server-ip:8080` - -### Option 3: Proxmox LXC -Great for homelabs. -- Create Ubuntu container -- Install Docker -- Deploy system - -### Option 4: Jetson Nano -For edge AI (Phase 4+). -- Docker pre-installed -- GPU support for AI -- Deploy normally - ---- - -## 🔐 Security - -### Default Settings (Development) -- ⚠️ Default PostgreSQL password -- ⚠️ No HTTPS -- ⚠️ No authentication -- ⚠️ All ports exposed - -**Fine for:** Local/home network use - -### Production Hardening (Do This!) -- ✅ Change database password -- ✅ Set secure SECRET_KEY -- ✅ Enable HTTPS -- ✅ Configure firewall -- ✅ Restrict port access -- ✅ Regular backups - -See `DEPLOY.md` for details. - ---- - -## 💾 Backup & Recovery - -### Quick Backup -```bash -# Database -docker-compose exec postgres pg_dump -U inventoryuser inventory > backup.sql - -# Everything -tar -czf backup.tar.gz data/ -``` - -### Quick Restore -```bash -docker-compose exec -T postgres psql -U inventoryuser inventory < backup.sql -``` - -### Automated Backups -Set up cron job for daily backups (see `DEPLOY.md`). - ---- - -## 🐛 Troubleshooting - -### Common Issues - -**Problem:** Can't access UI -**Fix:** `docker-compose logs nginx` - -**Problem:** Items not saving -**Fix:** `docker-compose logs backend` - -**Problem:** Port in use -**Fix:** Change port in `docker-compose.yml` - -**Problem:** Database connection failed -**Fix:** `docker-compose restart postgres` - -See `QUICK_REFERENCE.md` for full troubleshooting guide. - ---- - -## 📈 Performance - -### Current Capacity (Phase 1) -- **Items**: 10,000+ easily -- **Concurrent Users**: 1-5 recommended -- **Storage**: ~100MB per 1000 items -- **Search**: Fast (< 1 second) - -### Optimization (Phase 7+) -- Redis caching -- Database indexing -- Background jobs -- CDN for assets - ---- - -## 🤝 Support & Community - -### Getting Help -1. Check `QUICK_REFERENCE.md` -2. Review `README.md` -3. Check logs: `docker-compose logs` -4. Try sample data -5. Reset system: `docker-compose down -v && docker-compose up -d` - -### Reporting Issues -Include: -- Phase version (currently Phase 1) -- Error messages -- Steps to reproduce -- Docker logs - -### Feature Requests -Check `ROADMAP.md` first - it might be planned! - ---- - -## 📝 Version Information - -### Current Release -- **Version**: 1.0.0 -- **Phase**: 1 (Foundation) -- **Status**: ✅ Production Ready -- **Date**: October 2024 - -### Compatibility -- **Docker**: 20.10+ -- **Docker Compose**: 1.29+ -- **PostgreSQL**: 15 -- **Python**: 3.11+ -- **Browsers**: Chrome, Firefox, Safari, Edge (latest) - ---- - -## 🎓 Learning Path - -### Beginner -1. Deploy system (5 minutes) -2. Load sample data -3. Browse through modules -4. Try searching -5. Add your first item - -### Intermediate -1. Create your storage modules -2. Add 50-100 real items -3. Organize by category -4. Use tags effectively -5. Set up backups - -### Advanced -1. Use API endpoints -2. Write custom scripts -3. Optimize location layout -4. Plan for Phase 2+ -5. Customize system - ---- - -## 🏁 Success Checklist - -You'll know Phase 1 is working when: - -- [x] System starts without errors -- [x] Web UI is accessible -- [x] Can create modules and levels -- [x] Can add and find items -- [x] Search returns correct results -- [x] Location grid displays -- [x] Items can be in multiple locations -- [x] Data persists after restart -- [x] Backup/restore works -- [x] Sample data loads successfully - ---- - -## 🎯 Next Steps - -### Immediate (Today) -1. ✅ Extract package -2. ✅ Run `docker-compose up -d` -3. ✅ Access `http://localhost:8080` -4. ✅ Load sample data -5. ✅ Explore the system - -### This Week -1. ✅ Create your storage modules -2. ✅ Add 20-50 real items -3. ✅ Test search functionality -4. ✅ Set up daily backups -5. ✅ Read full documentation - -### This Month -1. ✅ Add majority of inventory -2. ✅ Refine organization -3. ✅ Provide feedback -4. ✅ Plan Phase 2 needs -5. ✅ Enjoy organized storage! - ---- - -## 💪 What Makes This Special? - -### Compared to Other Solutions - -**vs. Spreadsheets:** -- ✅ Better organization -- ✅ Faster search -- ✅ Location visualization -- ✅ Relationship tracking -- ✅ Future AI capabilities - -**vs. Commercial Systems:** -- ✅ Self-hosted (your data) -- ✅ Unlimited items -- ✅ No subscription fees -- ✅ Customizable -- ✅ Privacy-focused - -**vs. Basic Database:** -- ✅ User-friendly UI -- ✅ Storage hierarchy built-in -- ✅ Natural language support -- ✅ Easy deployment -- ✅ Regular backups - ---- - -## 🔬 Technical Highlights - -### Architecture -- **Design Pattern**: MVC (Model-View-Controller) -- **Database**: Relational (PostgreSQL) -- **ORM**: SQLAlchemy (prevents SQL injection) -- **Frontend**: Server-side rendering (fast, simple) -- **Deployment**: Containerized (portable) - -### Code Quality -- ✅ Proper foreign keys -- ✅ Cascade deletes -- ✅ Unique constraints -- ✅ Proper indexes -- ✅ Clean models -- ✅ RESTful API - -### Extensibility -Ready for: -- AI integration (Phase 4) -- Voice control (Phase 6) -- Mobile apps (Phase 8) -- Custom features -- Third-party integrations - ---- - -## 📞 Contact & Support - -### Documentation -All docs included in this package: -- README.md -- QUICKSTART.md -- DEPLOY.md -- ROADMAP.md -- QUICK_REFERENCE.md -- TESTING_CHECKLIST.md - -### Community -- Open issues for bugs -- Suggest features -- Share your setup -- Help others - -### Professional Support -For commercial deployments or customization, contact information would go here. - ---- - -## 📜 License - -[Your license here - recommend MIT or GPL] - ---- - -## 🙏 Credits - -Built with: -- Flask (Web framework) -- PostgreSQL (Database) -- SQLAlchemy (ORM) -- Docker (Containerization) -- nginx (Web server) - -Inspired by: -- Real homelab needs -- Maker community -- Electronic hobbyists -- Workshop organization challenges - ---- - -## 🌟 Final Words - -This inventory system is designed to grow with you: -- **Phase 1 (Now)**: Solid foundation -- **Phase 4**: AI-powered search -- **Phase 6**: Voice control -- **Phase 8**: Professional features - -But even Phase 1 is **fully usable** for real inventory management! - -**Start simple, expand as needed.** - ---- - -## Quick Links - -- [5-Minute Start](QUICKSTART.md) -- [Full Documentation](README.md) -- [Deployment Guide](DEPLOY.md) -- [Complete Roadmap](ROADMAP.md) -- [Quick Reference](QUICK_REFERENCE.md) -- [Testing Guide](TESTING_CHECKLIST.md) - ---- - -## Ready to Deploy? - -```bash -cd inventory-system -docker-compose up -d -# Wait 30 seconds -open http://localhost:8080 -# Start organizing! 🎉 -``` - ---- - -**Happy organizing! Your homelab will never be the same.** 🏠🔧📦✨ - -*Version 1.0.0 - Phase 1 Complete - October 2024* diff --git a/inventory-system/docs/PROJECT_SUMMARY.md b/inventory-system/docs/PROJECT_SUMMARY.md deleted file mode 100644 index 0da2fc1..0000000 --- a/inventory-system/docs/PROJECT_SUMMARY.md +++ /dev/null @@ -1,486 +0,0 @@ -# 🎉 Project Complete: Homelab Inventory System - Phase 1 - -## What Has Been Built - -I've created a **complete, production-ready Phase 1** inventory management system for your homelab/makerspace. The system is fully functional, tested, and ready to deploy. - -### 📊 Project Statistics - -- **Total Files**: 30+ source files -- **Lines of Code**: ~5,000+ lines -- **Project Size**: 147KB (excluding Docker images) -- **Time to Deploy**: 3 minutes -- **Documentation**: 4 comprehensive guides - -### ✅ Delivered Features - -#### Core Functionality -- ✅ **Complete storage hierarchy**: Modules → Levels → Locations -- ✅ **Full CRUD operations**: Create, Read, Update, Delete for all entities -- ✅ **Web interface**: Clean, responsive, mobile-friendly UI -- ✅ **Search system**: Keyword-based search across all fields -- ✅ **Visual location grids**: Interactive row/column displays -- ✅ **RESTful API**: JSON endpoints for programmatic access -- ✅ **PostgreSQL backend**: Properly normalized database schema -- ✅ **Docker deployment**: One-command deployment with Docker Compose - -#### Database Schema -- ✅ 5 core tables with proper relationships -- ✅ Foreign key constraints with cascade deletes -- ✅ Unique constraints preventing duplicates -- ✅ JSON metadata fields for flexibility -- ✅ Timestamp tracking for all records - -#### User Interface -- ✅ Dashboard with statistics -- ✅ Module management (add/edit/delete/view) -- ✅ Level management with grid configuration -- ✅ Location management with types and dimensions -- ✅ Item management with categories and tags -- ✅ Search interface with filtering -- ✅ Responsive design (desktop/tablet/mobile) - -#### Technical Features -- ✅ SQLAlchemy ORM with relationships -- ✅ Flask blueprints for modular routes -- ✅ Jinja2 templating with inheritance -- ✅ Custom CSS with design system -- ✅ Client-side JavaScript for interactivity -- ✅ nginx reverse proxy -- ✅ Environment variable configuration - -### 📁 Project Structure - -``` -inventory-system/ -├── README.md # 400+ line comprehensive guide -├── QUICKSTART.md # 5-minute deployment guide -├── DEPLOYMENT_SUMMARY.md # What's included & next steps -├── ARCHITECTURE.md # Technical deep dive -├── docker-compose.yml # Multi-container orchestration -├── nginx.conf # Reverse proxy config -├── .env.example # Configuration template -├── .gitignore # Version control exclusions -├── create_sample_data.py # Demo data generator -│ -├── backend/ # Python Flask application -│ ├── app/ -│ │ ├── __init__.py # App factory pattern -│ │ ├── models.py # 5 database models, 300+ lines -│ │ └── routes/ # Modular route handlers -│ │ ├── main.py # Dashboard routes -│ │ ├── items.py # Item CRUD + API -│ │ ├── modules.py # Module & level management -│ │ ├── locations.py # Location management -│ │ └── search.py # Search functionality -│ ├── requirements.txt # Python dependencies -│ ├── Dockerfile # Backend container -│ └── run.py # Application runner -│ -└── frontend/ # Web interface - ├── templates/ - │ ├── base.html # Base template with nav - │ ├── index.html # Dashboard - │ ├── modules/ - │ │ ├── list.html # Module listing - │ │ ├── view.html # Module details - │ │ └── form.html # Add/edit module - │ ├── levels/ - │ │ ├── view.html # Level grid view - │ │ └── form.html # Add/edit level - │ ├── items/ - │ │ ├── list.html # Item listing - │ │ ├── view.html # Item details - │ │ └── form.html # Add/edit item - │ ├── locations/ - │ │ ├── list.html # Location listing - │ │ ├── view.html # Location details - │ │ └── form.html # Edit location - │ └── search/ - │ └── results.html # Search results - └── static/ - ├── css/ - │ └── style.css # 1000+ lines, complete styling - └── js/ - └── main.js # Client-side logic -``` - -## 🚀 How to Use This Project - -### Immediate Steps - -1. **Extract the project** - ```bash - # The project is in: inventory-system/ - cd inventory-system - ``` - -2. **Read the documentation** - - Start with `QUICKSTART.md` (5-minute guide) - - Review `README.md` for complete documentation - - Check `DEPLOYMENT_SUMMARY.md` for overview - -3. **Deploy the system** - ```bash - docker-compose up -d - ``` - -4. **Access the web interface** - - Open browser: http://localhost:8080 - - Create your first module - - Add some items - - Test the search - -5. **Optional: Load sample data** - ```bash - pip install requests - python create_sample_data.py - ``` - -### What You Can Do Right Now - -#### Organize Your Workshop -1. Create modules for each physical storage unit -2. Add levels (drawers, shelves) to each module -3. Configure grid layouts (rows × columns) -4. Start adding your inventory items -5. Assign items to specific locations - -#### Example Workflow -``` -Create Module "Zeus" - → Add Level 1 (4×6 grid = 24 locations) - → Add Level 2 (3×4 grid = 12 locations) - → Total: 36 storage locations - -Add Items: - → "M6 Bolts" at Zeus:1:A3 - → "Resistors 1kΩ" at Zeus:1:B2 - → "Arduino Uno" at Zeus:2:A1 - -Search: - → Type "M6" → Find bolts at Zeus:1:A3 - → Type "arduino" → Find board at Zeus:2:A1 -``` - -## 📚 Documentation Provided - -### 1. README.md (Complete Guide) -- Prerequisites & installation -- Usage guide with examples -- Database schema documentation -- API endpoint reference -- Troubleshooting guide -- Backup/restore procedures -- Development setup -- Known limitations - -### 2. QUICKSTART.md (5-Minute Setup) -- Rapid deployment steps -- First-time setup walkthrough -- Common tasks guide -- Troubleshooting basics -- Access from other devices - -### 3. DEPLOYMENT_SUMMARY.md (Overview) -- What's included -- Quick start (3 steps) -- Real-world usage example -- Future phases roadmap -- Customization options -- Key features summary - -### 4. ARCHITECTURE.md (Technical Deep Dive) -- System architecture diagram -- Technology stack details -- Database schema with SQL -- API endpoint documentation -- Data flow diagrams -- Security considerations -- Performance characteristics -- Scalability path -- Development workflow - -## 🎯 What Makes This Special - -### Production Quality -- **Professional code structure**: Modular, maintainable, extensible -- **Proper database design**: Normalized schema with relationships -- **Complete error handling**: Flash messages, validation, constraints -- **Responsive UI**: Works on desktop, tablet, and mobile -- **Docker deployment**: Consistent across all platforms -- **Comprehensive docs**: Everything you need to know - -### Real-World Ready -- **Tested hierarchy**: Modules → Levels → Locations (proven structure) -- **Flexible storage**: Different location types for different items -- **Tag system**: Multiple ways to categorize and find items -- **Natural descriptions**: Store items as you describe them -- **Visual grids**: See your storage layout at a glance -- **Quick search**: Find items instantly by any keyword - -### Built for Growth -- **Phase 1 foundation**: Solid base for future features -- **Clean architecture**: Easy to add AI features later -- **API-first design**: CLI and voice can plug in easily -- **Extensible models**: JSON metadata for custom fields -- **Modular routes**: Add new features without breaking existing - -## 🔮 Future Roadmap - -### Phase 2: Smart Locations (Week 3) -- System suggests where to put items -- Location constraint checking -- Visual location picker - -### Phase 3: Duplicate Detection (Week 4) -- Warns about similar items -- Fuzzy matching algorithm -- Merge suggestions - -### Phase 4: AI Search (Week 5-6) -- Natural language queries -- Semantic similarity matching -- "Find me a long metric bolt" works - -### Phase 5: CLI (Week 7) -- Command-line interface -- Batch operations -- Power user features - -### Phase 6: Voice (Week 8-9) -- Wake word activation -- Voice queries -- Hands-free operation - -### Phase 7: Advanced AI (Week 10-11) -- Usage analytics -- Smart reorganization -- Alternative suggestions - -### Phase 8: Production (Week 12+) -- Multi-user support -- Mobile optimization -- QR codes & barcodes - -## 💡 Key Insights from Design - -### Storage Hierarchy -The three-level hierarchy (Module → Level → Location) perfectly matches physical storage: -- **Modules**: Cabinets, shelving units, storage areas -- **Levels**: Drawers, shelves, compartments -- **Locations**: Individual bins with row/col addresses - -This maps naturally to how people organize workshops. - -### Flexible Descriptions -Using natural language descriptions instead of rigid fields: -- Users describe items as they think about them -- No need to learn a specific format -- Tags provide additional structure -- Future AI will understand these descriptions - -### Location Types -Different storage needs different container types: -- Small boxes for SMD components -- Medium bins for fasteners -- Large bins for bulk items -- Liquid containers for paints -- This flexibility is key for real-world use - -### Many-to-Many Relationships -Items can be in multiple locations: -- Split quantities across bins -- Track partially used items -- Move items without losing history -- Essential for real inventory management - -## 🛠️ Technical Achievements - -### Database Design -- Properly normalized (3NF) -- Foreign keys with cascade -- Unique constraints prevent duplicates -- JSON fields for extensibility -- Timestamps for audit trail - -### Backend Architecture -- Flask blueprints for modularity -- SQLAlchemy ORM for type safety -- Separation of concerns (routes/models/services) -- RESTful API alongside web UI -- Environment-based configuration - -### Frontend Design -- Semantic HTML5 -- CSS custom properties (design system) -- Responsive grid layouts -- Progressive enhancement -- Accessible (WCAG considerations) - -### DevOps -- Docker multi-stage builds -- Compose for orchestration -- Volume mounts for persistence -- nginx for production-ready serving -- Environment variable configuration - -## 📊 Performance Characteristics - -### Current Capacity -- **Items**: Tested with 10,000+ items -- **Locations**: 1,000+ per level -- **Modules**: Unlimited -- **Search**: Sub-second for 10k items -- **Concurrent users**: 1-5 recommended - -### Resource Usage -- **RAM**: ~500MB total (all containers) -- **Disk**: ~147KB code + PostgreSQL data -- **CPU**: Minimal (Flask development server) -- **Network**: Local only (can expose) - -### Future Scaling -- Add indexing for 100k+ items -- Redis caching for heavy load -- Gunicorn workers for concurrency -- Read replicas for search queries - -## 🔐 Security Status - -### Current (Development) -- ✅ SQL injection protection (ORM) -- ✅ Input validation -- ⚠️ No authentication (single-user) -- ⚠️ HTTP only (no TLS) -- ⚠️ Default passwords - -### Production Checklist -- [ ] Change PostgreSQL password -- [ ] Set secure SECRET_KEY -- [ ] Enable HTTPS -- [ ] Add authentication -- [ ] Configure firewall -- [ ] Set up backups -- [ ] Enable audit logging - -## 🎓 Learning Resources Included - -### For Users -- QUICKSTART.md: Get running in 5 minutes -- README.md: Complete user guide -- Sample data script: See it in action -- In-app examples: Module/item creation - -### For Developers -- ARCHITECTURE.md: System design -- Code comments: Every file documented -- Modular structure: Easy to understand -- API examples: Integration guidance - -### For Deployers -- Docker Compose: Production-ready -- Environment variables: Easy config -- Backup scripts: Data protection -- Troubleshooting: Common issues solved - -## ✨ What Makes This Production-Ready - -1. **Complete Feature Set**: Everything needed for Phase 1 -2. **Professional Code**: Clean, documented, maintainable -3. **Comprehensive Docs**: 4 guides covering all aspects -4. **Docker Deployment**: Works everywhere, consistently -5. **Database Design**: Proper schema with relationships -6. **Error Handling**: User-friendly messages -7. **Responsive UI**: Works on all devices -8. **RESTful API**: Ready for integration -9. **Sample Data**: Quick demonstration -10. **Future-Proof**: Ready for AI features - -## 🚦 Next Steps - -### Immediate (Today) -1. ✅ Extract and review the project -2. ✅ Read QUICKSTART.md -3. ✅ Deploy with docker-compose -4. ✅ Load sample data -5. ✅ Explore the interface - -### Short-Term (This Week) -1. ✅ Add your real storage modules -2. ✅ Configure levels and grids -3. ✅ Start adding inventory items -4. ✅ Test search functionality -5. ✅ Customize location types - -### Medium-Term (Next Week) -1. 🔄 Add 50-100 real items -2. 🔄 Refine your organization -3. 🔄 Use it daily in your workflow -4. 🔄 Identify pain points -5. 🔄 Prepare for Phase 2 - -### Long-Term (Next Month) -1. 🔮 Deploy Phase 2 (smart locations) -2. 🔮 Add Phase 3 (duplicate detection) -3. 🔮 Enable Phase 4 (AI search) -4. 🔮 Build Phase 5 (CLI) -5. 🔮 Implement Phase 6 (voice) - -## 🎉 Success Metrics - -You'll know the system is working when: -- ✅ You can find any item in seconds -- ✅ You know exactly where everything is stored -- ✅ You stop buying duplicate parts -- ✅ Your workshop feels organized -- ✅ You save time on projects - -## 📞 Support Resources - -### Documentation -- README.md: Complete reference -- QUICKSTART.md: Quick help -- ARCHITECTURE.md: Technical details -- Code comments: Implementation notes - -### Troubleshooting -- Check Docker logs: `docker-compose logs` -- Verify containers: `docker-compose ps` -- Restart services: `docker-compose restart` -- Reset database: `docker-compose down -v` - -### Community -- Open issues on GitHub -- Share your setup -- Contribute improvements -- Request features - -## 🏆 Project Summary - -**What**: Complete inventory management system -**For**: Homelab/makerspace environments -**Features**: Storage hierarchy, search, web UI, API -**Phase**: 1 of 8 (Foundation - Complete) -**Status**: Production-ready, fully tested -**Documentation**: Comprehensive (4 guides) -**Deployment**: One command with Docker -**Next**: Use it, then add AI features - ---- - -## 🎯 Bottom Line - -You now have a **professional, production-ready inventory system** that: -- Works out of the box -- Handles thousands of items -- Provides web and API access -- Includes complete documentation -- Ready for AI features -- Deployable anywhere - -**Start organizing your workshop today!** 🛠️ - -The foundation is solid. The future phases will make it even more powerful with AI-powered search, voice control, and smart features. But right now, you have everything you need to manage your inventory effectively. - -**Happy organizing!** 🎉 diff --git a/inventory-system/docs/QUICKSTART.md b/inventory-system/docs/QUICKSTART.md deleted file mode 100644 index 06f499f..0000000 --- a/inventory-system/docs/QUICKSTART.md +++ /dev/null @@ -1,279 +0,0 @@ -# Quick Deployment Guide - -## Prerequisites Check - -Before starting, ensure you have: -- [ ] Docker installed (`docker --version`) -- [ ] Docker Compose installed (`docker-compose --version`) -- [ ] At least 2GB free RAM -- [ ] At least 10GB free disk space -- [ ] Ports 8080, 5432, and 5000 available - -## 5-Minute Deployment - -### Step 1: Navigate to Project Directory -```bash -cd inventory-system -``` - -### Step 2: Start the System -```bash -docker-compose up -d -``` - -Wait for containers to start (usually 30-60 seconds). - -### Step 3: Verify Deployment -```bash -docker-compose ps -``` - -You should see three containers running: -- `inventory-db` (postgres) -- `inventory-backend` (flask) -- `inventory-nginx` (nginx) - -### Step 4: Access the Application - -Open your web browser and go to: -``` -http://localhost:8080 -``` - -You should see the Inventory System dashboard! - -## First-Time Setup - -### 1. Create Your First Module - -Click "Modules" → "Add Module" - -Example: -- **Name**: Zeus -- **Description**: Main component storage cabinet -- **Physical Location**: North wall, workshop - -Click "Create Module" - -### 2. Add Levels to Your Module - -Click on your module name → "Add Level" - -Example: -- **Level Number**: 1 -- **Name**: Top Drawer -- **Rows**: 4 -- **Columns**: 6 -- **Description**: Small components and fasteners - -Click "Create Level" - -This creates a 4×6 grid of locations (A1-A6, B1-B6, C1-C6, D1-D6) - -### 3. Add Your First Item - -Click "Items" → "Add Item" - -Example: -- **Name**: M6 Bolts -- **Description**: Hex head bolt, M6 diameter, 50mm long, zinc plated, metric -- **Category**: Fasteners -- **Item Type**: solid -- **Quantity**: 100 -- **Unit**: pieces -- **Tags**: bolt, metric, m6, hex, zinc -- **Location**: Zeus:1:A1 - -Click "Create Item" - -### 4. Test Search - -Click "Search" and type "M6" or "bolt" - -You should see your item in the results! - -## Common Tasks - -### Viewing Storage Hierarchy - -1. Click "Modules" to see all storage units -2. Click a module name to see its levels -3. Click a level to see the location grid -4. Click a location to see what's stored there - -### Adding Items to Existing Locations - -1. Click "Items" → "Add Item" -2. Fill in item details -3. Select location from dropdown -4. Submit - -Or: - -1. Find the item you want to update -2. Click "Edit" on the item page -3. Add a new location - -### Searching for Items - -1. Click "Search" in the navigation -2. Type keywords (name, description, tags) -3. View results with locations - -### Editing Location Properties - -1. Navigate to the location (Modules → Module → Level → Location) -2. Click "Edit" -3. Set location type (small_box, medium_bin, etc.) -4. Add dimensions if needed -5. Save - -## Stopping the System - -### Temporary Stop (keeps data) -```bash -docker-compose stop -``` - -### Start Again -```bash -docker-compose start -``` - -### Complete Shutdown (keeps data) -```bash -docker-compose down -``` - -### Nuclear Option (deletes ALL data) -```bash -docker-compose down -v -``` - -⚠️ **Warning**: The `-v` flag deletes the database volume. Only use if you want to start fresh! - -## Troubleshooting - -### Container Won't Start - -```bash -# Check logs -docker-compose logs backend - -# Common issue: Port already in use -# Solution: Change port in docker-compose.yml or stop conflicting service -``` - -### Can't Connect to Database - -```bash -# Check if PostgreSQL is healthy -docker-compose ps - -# Restart PostgreSQL -docker-compose restart postgres - -# Check PostgreSQL logs -docker-compose logs postgres -``` - -### Web UI Not Loading - -```bash -# Check if nginx is running -docker-compose ps - -# Check nginx logs -docker-compose logs nginx - -# Restart nginx -docker-compose restart nginx -``` - -### Port 8080 Already in Use - -Edit `docker-compose.yml` and change the nginx port: -```yaml -nginx: - ports: - - "8081:80" # Changed from 8080 to 8081 -``` - -Then restart: -```bash -docker-compose down -docker-compose up -d -``` - -## Accessing from Other Devices - -To access from other computers on your network: - -1. Find your server's IP address: - ```bash - hostname -I - ``` - -2. On other devices, open browser to: - ``` - http://YOUR_SERVER_IP:8080 - ``` - -Example: `http://192.168.1.100:8080` - -## Backup Your Data - -### Quick Backup -```bash -# Backup database -docker-compose exec postgres pg_dump -U inventoryuser inventory > backup.sql - -# Backup everything -tar -czf inventory-backup-$(date +%Y%m%d).tar.gz data/ -``` - -### Restore from Backup -```bash -# Restore database -docker-compose exec -T postgres psql -U inventoryuser inventory < backup.sql -``` - -## Performance Tips - -### For Better Performance: - -1. **Add more RAM** to Docker (Docker Desktop → Resources) -2. **Use SSD** for the data directory -3. **Limit concurrent users** (single-user recommended for Phase 1) - -## Security Notes - -⚠️ **Important for Production**: - -1. Change default PostgreSQL password in `docker-compose.yml` -2. Set a secure SECRET_KEY in environment variables -3. Don't expose PostgreSQL port (5432) to the network -4. Use HTTPS with a reverse proxy -5. Set up regular backups - -## Getting Help - -If you encounter issues: - -1. Check the logs: `docker-compose logs` -2. Verify all containers are running: `docker-compose ps` -3. Try restarting: `docker-compose restart` -4. Check the main README.md for detailed documentation -5. Open an issue on GitHub with error logs - -## Next Steps - -Once you're comfortable with the basics: - -1. ✅ Add more modules for different storage areas -2. ✅ Organize items into categories -3. ✅ Use tags for better searchability -4. ✅ Set up location types for different bin sizes -5. ✅ Experiment with the grid layout for different storage configs - -Enjoy your new inventory system! 🎉 diff --git a/inventory-system/docs/QUICK_REFERENCE.md b/inventory-system/docs/QUICK_REFERENCE.md deleted file mode 100644 index e257b35..0000000 --- a/inventory-system/docs/QUICK_REFERENCE.md +++ /dev/null @@ -1,534 +0,0 @@ -# 📋 Quick Reference Card - -## Essential Commands - -### Starting & Stopping -```bash -# Start system -docker-compose up -d - -# Stop system (keep data) -docker-compose stop - -# Restart system -docker-compose restart - -# Shutdown completely (keep data) -docker-compose down - -# Nuclear option (DELETE ALL DATA) -docker-compose down -v -``` - -### Status Checks -```bash -# Check if containers are running -docker-compose ps - -# View logs (all services) -docker-compose logs -f - -# View specific service logs -docker-compose logs -f backend -docker-compose logs -f postgres -docker-compose logs -f nginx -``` - -### Backup & Restore -```bash -# Backup database -docker-compose exec postgres pg_dump -U inventoryuser inventory > backup_$(date +%Y%m%d).sql - -# Backup entire data directory -tar -czf backup_$(date +%Y%m%d).tar.gz data/ - -# Restore database -docker-compose exec -T postgres psql -U inventoryuser inventory < backup_20241026.sql -``` - -### Troubleshooting -```bash -# Restart a stuck container -docker-compose restart backend - -# Rebuild containers -docker-compose up --build -d - -# Check container health -docker-compose ps - -# Reset everything -docker-compose down -v && docker-compose up -d -``` - ---- - -## Web Interface - -### Access -- Local: `http://localhost:8080` -- Network: `http://YOUR_IP:8080` - -### Navigation -- **Dashboard** → Overview of inventory -- **Modules** → Manage storage units -- **Items** → Browse/add inventory -- **Search** → Find items - -### Adding Items Flow -1. Click "Items" → "Add Item" -2. Fill in name and description -3. Select category and type -4. Enter quantity -5. Choose/create location -6. Add tags (comma-separated) -7. Click "Create Item" - -### Location Format -``` -ModuleName:LevelNumber:RowCol - -Examples: -Zeus:1:A3 → Module "Zeus", Level 1, Location A3 -Muse:2:B5 → Module "Muse", Level 2, Location B5 -Apollo:3:C2 → Module "Apollo", Level 3, Location C2 -``` - ---- - -## Common Workflows - -### Workflow 1: New Storage Module -``` -1. Modules → Add Module - - Name: Zeus - - Description: Electronics cabinet - - Location: North wall - -2. Click module → Add Level - - Level: 1 - - Rows: 4 - - Columns: 6 - - Creates 24 locations (A1-D6) - -3. Repeat for other levels -``` - -### Workflow 2: Adding First Item -``` -1. Items → Add Item -2. Name: M6 Bolts -3. Description: Hex head bolt, M6 diameter, 50mm long, zinc plated -4. Category: Fasteners -5. Type: solid -6. Quantity: 100 -7. Unit: pieces -8. Location: Zeus:1:A3 -9. Tags: bolt, metric, m6, hex -10. Create Item -``` - -### Workflow 3: Finding Items -``` -Option A: Browse -1. Modules → Zeus -2. Level 1 -3. Click location (e.g., A3) -4. See all items in that location - -Option B: Search -1. Search → Type "M6" -2. View results -3. Click item for details & location -``` - -### Workflow 4: Moving Items -``` -1. Find item (search or browse) -2. Click item name -3. Click "Edit" -4. Change location -5. Update quantity at each location -6. Save -``` - -### Workflow 5: Organizing -``` -1. Create module for category (e.g., "Electronics") -2. Add levels for subcategories - - Level 1: Resistors - - Level 2: Capacitors - - Level 3: ICs -3. Set up grid for each level -4. Move items to appropriate locations -5. Use tags for cross-referencing -``` - ---- - -## Tips & Best Practices - -### Item Descriptions -✅ **Good:** -- "Pan head phillips screw, #8 size, 3/4 inch long, mild steel, zinc plated" -- "Ceramic capacitor, 0.1 microfarad, 0805 package, 50V rating, X7R dielectric" -- "M6 hex bolt, 50mm length, zinc plated, metric coarse thread" - -❌ **Bad:** -- "Screw" -- "Capacitor" -- "Bolt" - -### Tagging Strategy -``` -Use multiple tags for findability: -- Material: steel, aluminum, plastic, ceramic -- Size: m6, #8, 0805, 1/4-inch -- Type: screw, bolt, resistor, capacitor -- Finish: zinc, stainless, black-oxide -- Category: fastener, electronics, tool -``` - -### Module Naming -``` -✅ Descriptive & Memorable: -- Zeus (main electronics) -- Muse (fasteners) -- Apollo (tools) -- Workshop-Main -- Garage-Cabinet-1 - -❌ Generic: -- Cabinet1 -- Storage2 -- Box3 -``` - -### Level Organization -``` -Top to Bottom or Most to Least Used: - -Level 1: Frequently accessed items -Level 2: Moderate use -Level 3: Occasional use -Level 4: Rarely used / archive - -Or by size: -Level 1: Small components -Level 2: Medium parts -Level 3: Large items -Level 4: Bulk storage -``` - ---- - -## Location Types - -| Type | Best For | Examples | -|------|----------|----------| -| `small_box` | Tiny components | SMD parts, small screws | -| `medium_bin` | Standard parts | Resistors, bolts, LEDs | -| `large_bin` | Bigger items | Tools, wire spools | -| `liquid_container` | Liquids/chemicals | Paints, solvents, oils | -| `smd_container` | Surface-mount | 0402, 0603, 0805 parts | -| `bulk_bin` | Loose items | Zip ties, cable, wire | -| `tool_holder` | Tools | Screwdrivers, pliers | -| `general` | Default | Anything | - ---- - -## Item Types - -| Type | Description | Examples | -|------|-------------|----------| -| `solid` | Standard parts | Bolts, resistors, brackets | -| `liquid` | Liquids/coatings | Paint, glue, solvents | -| `smd_component` | Surface-mount | 0805 caps, SOT-23 transistors | -| `bulk` | Loose/bulk items | Wire, zip ties, sandpaper | -| `tool` | Tools/equipment | Screwdrivers, meters, crimpers | -| `consumable` | Used up over time | Solder, flux, tape | - ---- - -## API Quick Reference - -### List Items -```bash -curl http://localhost:8080/items/api/items -``` - -### Search Items -```bash -curl http://localhost:8080/items/api/items?search=M6 -``` - -### Get Item Details -```bash -curl http://localhost:8080/items/api/items/42 -``` - -### List Modules -```bash -curl http://localhost:8080/modules/api/modules -``` - -### List Locations -```bash -curl http://localhost:8080/locations/api/locations -``` - ---- - -## Keyboard Shortcuts (Web UI) - -| Key | Action | -|-----|--------| -| `/` | Focus search | -| `Ctrl+K` | Quick search | -| `Escape` | Close modals | - -*(More shortcuts coming in future phases)* - ---- - -## Maintenance Schedule - -### Daily -- Use the system! -- Add items as you acquire them - -### Weekly -- Review uncategorized items -- Update quantities as needed -- Check for low stock - -### Monthly -- Backup database -- Review organization -- Archive old logs (Phase 8) - -### Quarterly -- Deep clean/reorganize -- Review location efficiency -- Update documentation - ---- - -## When Things Go Wrong - -### Problem: Can't access web UI - -**Check:** -```bash -# Are containers running? -docker-compose ps - -# Check nginx logs -docker-compose logs nginx - -# Restart nginx -docker-compose restart nginx -``` - -### Problem: Items not saving - -**Check:** -```bash -# Check backend logs -docker-compose logs backend - -# Check database -docker-compose exec postgres psql -U inventoryuser inventory -c "SELECT COUNT(*) FROM items;" - -# Restart backend -docker-compose restart backend -``` - -### Problem: Search not working - -**Check:** -```bash -# Check backend logs -docker-compose logs backend - -# Try searching via API -curl "http://localhost:8080/items/api/items?search=test" -``` - -### Problem: Database connection failed - -**Fix:** -```bash -# Restart PostgreSQL -docker-compose restart postgres - -# Wait 10 seconds, then restart backend -sleep 10 -docker-compose restart backend -``` - -### Problem: Port already in use - -**Fix:** -Edit `docker-compose.yml`: -```yaml -nginx: - ports: - - "8081:80" # Change 8080 to 8081 -``` - -Then: -```bash -docker-compose down && docker-compose up -d -``` - -### Problem: Out of disk space - -**Fix:** -```bash -# Remove old Docker images -docker system prune -a - -# Remove old backups -rm old_backup_*.sql - -# Clean up logs -docker-compose logs --tail=100 > recent_logs.txt -# (Then manually clean up old logs) -``` - ---- - -## Performance Tips - -1. **Add more RAM** to Docker (Settings → Resources) -2. **Use SSD** for data directory -3. **Regular backups** prevent data loss -4. **Limit concurrent users** (Phase 1 is single-user optimized) -5. **Index frequently searched fields** (automatically done) - ---- - -## Security Checklist - -For production deployments: - -- [ ] Changed default PostgreSQL password -- [ ] Set secure `SECRET_KEY` -- [ ] PostgreSQL port not exposed to internet -- [ ] Using HTTPS (Let's Encrypt) -- [ ] Firewall configured -- [ ] Regular backups enabled -- [ ] Updates applied regularly -- [ ] Monitoring enabled (Phase 8) - ---- - -## Sample Data - -Want to test with realistic data? - -```bash -python3 create_sample_data.py -``` - -This adds: -- 3 modules (Zeus, Muse, Apollo) -- Multiple levels per module -- 10 sample items - -Delete sample data later: -```sql -docker-compose exec postgres psql -U inventoryuser inventory -DELETE FROM items WHERE name LIKE '%sample%'; -``` - ---- - -## Getting Help - -1. **Check logs:** `docker-compose logs` -2. **Review README.md** for detailed docs -3. **Check ROADMAP.md** for future features -4. **Try sample data** to verify setup -5. **Ask for help** (open issue, forum, etc.) - ---- - -## Phase 1 Limitations - -What's NOT included yet (coming in future phases): - -- ❌ AI semantic search (Phase 4) -- ❌ Duplicate detection (Phase 3) -- ❌ Location suggestions (Phase 2) -- ❌ CLI interface (Phase 5) -- ❌ Voice control (Phase 6) -- ❌ User authentication (Phase 8) -- ❌ Mobile app (Phase 8) -- ❌ Barcode scanning (Phase 8) - -But you CAN: -- ✅ Track unlimited items -- ✅ Organize in modules/levels/locations -- ✅ Search by keyword -- ✅ View location grids -- ✅ Export/backup data -- ✅ Access from any device on network - ---- - -## Next Steps - -1. **Deploy:** `docker-compose up -d` -2. **Create first module:** Your main storage unit -3. **Add 10-20 items:** Get familiar with the system -4. **Experiment:** Try different organizations -5. **Provide feedback:** What works? What's missing? - ---- - -## Emergency Recovery - -If everything breaks: - -```bash -# 1. Backup current database (if possible) -docker-compose exec postgres pg_dump -U inventoryuser inventory > emergency_backup.sql - -# 2. Stop everything -docker-compose down - -# 3. Fresh start (DELETES DATA) -docker-compose down -v -docker-compose up -d - -# 4. Restore from backup -docker-compose exec -T postgres psql -U inventoryuser inventory < emergency_backup.sql -``` - ---- - -## Version Info - -- **Version:** 1.0.0 (Phase 1) -- **Status:** Production Ready -- **Last Updated:** October 2024 - ---- - -**Print this card and keep it near your workstation!** 📌 - ---- - -## Quick URL Reference - -| Service | URL | -|---------|-----| -| Web UI | http://localhost:8080 | -| API Docs | http://localhost:8080/api (Phase 8) | -| Health Check | http://localhost:8080/health | - ---- - -**Happy organizing! 🏠🔧📦** diff --git a/inventory-system/docs/ROADMAP.md b/inventory-system/docs/ROADMAP.md deleted file mode 100644 index 287662f..0000000 --- a/inventory-system/docs/ROADMAP.md +++ /dev/null @@ -1,1065 +0,0 @@ -# 🗺️ Homelab Inventory System - Complete Roadmap - -## Overview - -This document outlines the complete 8-phase development plan. **Phase 1 is complete and deployable now.** Each subsequent phase builds on the previous ones, following the HIIL (Hardware-In-the-Loop) principle where you can deploy and test at each stage. - ---- - -## ✅ Phase 1: Foundation [COMPLETE] - -**Status:** ✅ Deployed and Ready -**Timeline:** Weeks 1-2 -**Complexity:** Basic - -### What's Working Now: - -- [x] Complete database schema with proper relationships -- [x] Docker deployment (PostgreSQL + Flask + nginx) -- [x] Storage hierarchy: Modules → Levels → Locations -- [x] Full CRUD operations via web UI -- [x] Items with natural language descriptions -- [x] Basic keyword search -- [x] Location grid visualization -- [x] Many-to-many item-location relationships -- [x] Quantity tracking -- [x] Category and tag support -- [x] Sample data generator - -### Technology Stack: -- Python 3.11+ with Flask -- PostgreSQL 15 -- SQLAlchemy ORM -- Jinja2 templates -- Docker Compose - -### Deliverables: -1. ✅ Working web application -2. ✅ Database with migrations -3. ✅ Docker deployment configuration -4. ✅ User documentation -5. ✅ Sample data for testing - -### Test Milestone: -✅ Add 50-100 items and navigate the storage hierarchy - ---- - -## 🔜 Phase 2: Smart Location Management - -**Status:** 🚧 Next Up -**Timeline:** Week 3 -**Complexity:** Intermediate - -### Goals: -- System suggests optimal storage locations -- Location compatibility checking -- Space utilization tracking -- Smart reorganization hints - -### Features to Build: - -1. **Location Profiles** - - Define location dimensions and types - - Set compatibility rules (e.g., no liquids in electronics drawer) - - Track occupied vs. available space - -2. **Location Suggestion Algorithm** - ```python - def suggest_location(item): - # Find empty locations matching item type - # Prioritize locations near similar items - # Consider accessibility and frequency of use - # Return ranked suggestions - ``` - -3. **Enhanced UI** - - "Suggest location" button when adding items - - Visual location map with availability heatmap - - Filter locations by type/size/availability - - Show capacity utilization per location - -4. **Location Types** - - Extend existing types: small_box, medium_bin, large_bin - - Add: liquid_container, smd_box, bulk_bin, tool_holder - - Custom dimensions for each type - - Visual indicators in UI - -### Implementation Plan: - -**Week 3, Day 1-2: Backend** -```bash -# Add to backend/app/services/location_suggestion.py -- LocationMatcher class -- CompatibilityChecker class -- SuggestionEngine class -``` - -**Week 3, Day 3-4: UI** -```bash -# Enhance templates -- Add suggestion button to item form -- Create location picker with suggestions -- Add location type configuration page -``` - -**Week 3, Day 5: Testing** -- Test with various item types -- Verify suggestions make sense -- Check edge cases (no available locations, etc.) - -### API Endpoints: -``` -GET /locations/api/suggest?item_type=liquid&size=large -POST /locations/api//configure (set dimensions, type) -GET /locations/api/availability -``` - -### Test Milestone: -Add 100 items using location suggestions, verify they make sense - -### Database Changes: -```sql --- Add to locations table -ALTER TABLE locations ADD COLUMN max_weight_kg FLOAT; -ALTER TABLE locations ADD COLUMN is_temperature_controlled BOOLEAN; -ALTER TABLE locations ADD COLUMN compatible_item_types TEXT[]; - --- Add utilization tracking -CREATE TABLE location_utilization ( - location_id INTEGER, - used_percentage FLOAT, - last_updated TIMESTAMP -); -``` - ---- - -## 🔜 Phase 3: Duplicate Detection - -**Status:** 📋 Planned -**Timeline:** Week 4 -**Complexity:** Intermediate - -### Goals: -- Detect when adding duplicate/similar items -- Parse common specifications automatically -- Warn before creating near-duplicates -- Suggest merging or consolidating - -### Features to Build: - -1. **Pattern Recognition** - - Parse bolt/screw specifications (M6, #8, etc.) - - Extract resistor values (1kΩ, 10k, etc.) - - Identify capacitor specifications - - Recognize standard tool sizes - -2. **Similarity Detection** - ```python - def find_similar_items(new_item_description): - # Extract key specifications - # Search existing items - # Calculate similarity scores - # Return potential duplicates with locations - ``` - -3. **Smart Warnings** - - Show similar items when adding new item - - Display differences between items - - Option to update existing item instead - - Suggest consolidating locations - -4. **Specification Extraction** - - Automatically populate metadata fields - - Standardize formats (e.g., "1/4 inch" → "6.35mm") - - Create searchable tags from specs - -### Implementation Plan: - -**Week 4, Day 1-2: Parser Library** -```python -# backend/app/services/spec_parser.py -class SpecificationParser: - def parse_fastener(description) - def parse_resistor(description) - def parse_capacitor(description) - def extract_dimensions(description) - def standardize_units(value, unit) -``` - -**Week 4, Day 3-4: Duplicate Detection** -```python -# backend/app/services/duplicate_detection.py -class DuplicateDetector: - def find_similar(description) - def calculate_similarity(item1, item2) - def suggest_merge(items) -``` - -**Week 4, Day 5: UI Integration** -- Add warning dialog when duplicates found -- Show comparison view -- Add "merge items" functionality - -### Regex Patterns to Implement: -```python -# Fasteners -r'M(\d+)(?:x(\d+))?' # M6x50 or M6 -r'#(\d+)(?:\s*x\s*([0-9/]+))?' # #8 x 3/4 -r'(\d+/\d+)\s*inch' # 3/4 inch - -# Electronics -r'(\d+\.?\d*)\s*([kMΩ]?Ω)' # 1kΩ, 10Ω -r'(\d+\.?\d*)\s*([μnp]?F)' # 0.1μF, 100nF -r'(\d{4})\s*(?:package)?' # 0805, 1206 - -# Tools -r'(\d+)\s*mm' # 10mm -r'(\d+/\d+)\s*inch' # 1/4 inch -``` - -### Test Milestone: -Add intentional duplicates and verify detection works - ---- - -## 🤖 Phase 4: Semantic Search Foundation - -**Status:** 📋 Planned -**Timeline:** Weeks 5-6 -**Complexity:** Advanced - -### Goals: -- Natural language queries work well -- Find items by concept, not just keywords -- "M6 metric bolt" finds "M6 hex head bolt, 50mm" -- Ranked results by relevance - -### Features to Build: - -1. **Embedding Generation** - ```python - from sentence_transformers import SentenceTransformer - - model = SentenceTransformer('all-MiniLM-L6-v2') - - def generate_embedding(description): - return model.encode(description) - ``` - -2. **PostgreSQL Setup** - ```sql - -- Install pgvector extension - CREATE EXTENSION IF NOT EXISTS vector; - - -- Add embedding column to items - ALTER TABLE items ADD COLUMN embedding vector(384); - - -- Create index for fast similarity search - CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops); - ``` - -3. **Semantic Search API** - ```python - def semantic_search(query, limit=10): - query_embedding = generate_embedding(query) - # Cosine similarity search - results = db.session.query(Item).order_by( - Item.embedding.cosine_distance(query_embedding) - ).limit(limit) - return results - ``` - -4. **Enhanced Search UI** - - Natural language search bar - - Results with relevance scores - - "Similar items" suggestions - - Search history - -### Implementation Plan: - -**Week 5, Day 1: Setup** -- Install sentence-transformers -- Add pgvector to PostgreSQL -- Test embedding generation - -**Week 5, Day 2-3: Batch Processing** -```python -# Generate embeddings for all existing items -def backfill_embeddings(): - items = Item.query.all() - for item in items: - embedding = generate_embedding(item.description) - item.embedding = embedding - db.session.commit() -``` - -**Week 5, Day 4-5: Search API** -```python -# backend/app/routes/search.py -@bp.route('/api/semantic-search') -def semantic_search(): - query = request.args.get('q') - results = semantic_search_service.search(query) - return jsonify([r.to_dict() for r in results]) -``` - -**Week 6, Day 1-3: UI Integration** -- Replace/enhance existing search -- Add relevance scores -- Show similar items sidebar - -**Week 6, Day 4-5: Testing & Tuning** -- Test with various queries -- Compare with keyword search -- Fine-tune similarity thresholds - -### Model Options: -```python -# Fast & efficient (default) -'all-MiniLM-L6-v2' # 384 dimensions, 120MB - -# Better accuracy -'all-mpnet-base-v2' # 768 dimensions, 420MB - -# Domain-specific (if we fine-tune later) -'custom-inventory-model' # Trained on our data -``` - -### Test Milestone: -Query "long metric bolt M6" and get relevant results ranked properly - ---- - -## 💻 Phase 5: CLI Interface - -**Status:** 📋 Planned -**Timeline:** Week 7 -**Complexity:** Intermediate - -### Goals: -- Terminal access for power users -- Scriptable inventory management -- Fast bulk operations -- Import/export capabilities - -### Features to Build: - -1. **Command-Line Tool: `invctl`** - ```bash - invctl add "M6 bolt, 50mm, zinc" --location Zeus:1:A3 - invctl search "resistor 1k" - invctl list --module Zeus --level 1 - invctl update item 42 --quantity 200 - invctl delete item 42 - invctl export --format csv > inventory.csv - invctl import inventory.csv - ``` - -2. **Interactive Mode** - ```bash - invctl shell - > add "New item description" - > search "bolt" - > exit - ``` - -3. **Batch Operations** - ```bash - # Import from CSV - invctl import fasteners.csv --module Muse --level 2 - - # Export filtered items - invctl export --category Electronics --format json - - # Bulk update - invctl bulk-update --tag resistor --field category --value "Electronics/Resistors" - ``` - -4. **Tab Completion** - ```bash - invctl add "..." --location - # Shows: Zeus, Muse, Apollo - - invctl add "..." --location Zeus: - # Shows: 1, 2, 3 - ``` - -### Implementation Plan: - -**Week 7, Day 1-2: Core CLI** -```python -# cli/invctl.py -import click -import requests - -@click.group() -def cli(): - """Inventory Control CLI""" - pass - -@cli.command() -@click.argument('description') -@click.option('--location') -@click.option('--quantity', default=1) -def add(description, location, quantity): - """Add a new item""" - # Parse location (Module:Level:RowCol) - # POST to API - # Show confirmation -``` - -**Week 7, Day 3: Interactive Shell** -```python -# Use prompt_toolkit for better UX -from prompt_toolkit import PromptSession -from prompt_toolkit.completion import WordCompleter - -def interactive_shell(): - session = PromptSession() - while True: - command = session.prompt('invctl> ') - # Parse and execute -``` - -**Week 7, Day 4: Import/Export** -```python -# CSV format -# name,description,category,quantity,location -# M6 Bolts,Hex head...,Fasteners,100,Zeus:1:A3 - -@cli.command() -@click.argument('file') -def import_csv(file): - # Read CSV - # Parse each line - # POST to API - # Show progress bar -``` - -**Week 7, Day 5: Packaging** -```bash -# Setup.py for easy installation -pip install -e . -# Now 'invctl' is in PATH -``` - -### CLI Structure: -``` -cli/ -├── invctl.py # Main CLI entry point -├── commands/ -│ ├── add.py -│ ├── search.py -│ ├── list.py -│ ├── import_export.py -│ └── interactive.py -├── utils/ -│ ├── api_client.py # Requests wrapper -│ ├── formatters.py # Pretty printing -│ └── validators.py # Input validation -└── setup.py -``` - -### Test Milestone: -Manage inventory from terminal only for a full day - ---- - -## 🎤 Phase 6: Voice Interface - -**Status:** 📋 Planned -**Timeline:** Weeks 8-9 -**Complexity:** Advanced - -### Goals: -- Hands-free operation in workshop -- Natural voice commands -- Offline speech recognition -- Voice confirmations - -### Features to Build: - -1. **Wake Word Detection** - ```python - import pvporcupine - - # Offline wake word: "Hey Inventory" - porcupine = pvporcupine.create( - keywords=['jarvis'] # Or custom trained - ) - ``` - -2. **Speech Recognition** - ```python - # Option 1: Vosk (offline, fast) - from vosk import Model, KaldiRecognizer - - # Option 2: Whisper (better accuracy) - import whisper - model = whisper.load_model("base") - ``` - -3. **Voice Commands** - ```python - # Example interactions: - "Hey Inventory" - > "Listening..." - - "Add new item: M6 bolt, 50 millimeters long, to Zeus level 2 position A3" - > "Adding M6 bolt, 50mm to Zeus:2:A3. Is this correct?" - - "Yes" - > "Item added successfully. Anything else?" - - "Find metric bolts" - > "I found 3 items: M6 bolts in Zeus:2:A3, M8 bolts in Muse:1:B2..." - ``` - -4. **Text-to-Speech Responses** - ```python - import pyttsx3 - - engine = pyttsx3.init() - engine.say("Item added successfully") - engine.runAndWait() - ``` - -### Implementation Plan: - -**Week 8, Day 1-2: Wake Word** -```python -# voice/wake_word.py -class WakeWordDetector: - def __init__(self): - self.porcupine = pvporcupine.create( - keywords=['jarvis'] - ) - - def listen(self): - # Listen for wake word - # Return True when detected -``` - -**Week 8, Day 3-4: Speech-to-Text** -```python -# voice/stt.py -class SpeechRecognizer: - def __init__(self): - self.model = vosk.Model("model") - - def transcribe(self, audio): - # Convert audio to text - return text -``` - -**Week 8, Day 5 - Week 9, Day 2: Command Parsing** -```python -# voice/parser.py -class VoiceCommandParser: - def parse(self, text): - # Identify intent (add, search, update, delete) - # Extract entities (item desc, location, quantity) - # Return structured command - - # Example: - # "Add M6 bolt to Zeus level 2 A3" - # → {'action': 'add', 'item': 'M6 bolt', - # 'location': 'Zeus:2:A3'} -``` - -**Week 9, Day 3-4: Integration** -```python -# voice/voice_interface.py -class VoiceInterface: - def __init__(self): - self.wake_word = WakeWordDetector() - self.stt = SpeechRecognizer() - self.tts = TextToSpeech() - self.parser = VoiceCommandParser() - self.api = APIClient() - - def run(self): - while True: - if self.wake_word.detected(): - self.process_command() -``` - -**Week 9, Day 5: Testing** -- Test in noisy workshop environment -- Verify accuracy with different accents -- Test edge cases (similar sounding words) - -### Hardware Options: - -**Option 1: Raspberry Pi Station** -- Raspberry Pi 4 (2GB+) -- USB microphone -- Speaker -- Runs voice interface as service -- Connects to main server API - -**Option 2: USB Microphone + Server** -- Connect mic to server running inventory system -- Voice interface runs on same machine - -**Option 3: Jetson Nano** -- Run everything on Jetson -- GPU acceleration for Whisper -- Better accuracy with AI models - -### Test Milestone: -Add and search for 20 items using only voice - ---- - -## 🧠 Phase 7: Advanced AI Features - -**Status:** 📋 Planned -**Timeline:** Weeks 10-11 -**Complexity:** Advanced - -### Goals: -- Smarter recommendations -- Predictive organization -- Usage pattern analysis -- Automated categorization - -### Features to Build: - -1. **Fine-Tuned Semantic Model** - ```python - # Train on your actual inventory descriptions - from sentence_transformers import SentenceTransformer, InputExample - - # Create training data from your inventory - train_examples = [ - InputExample(texts=['M6 bolt', 'M6 hex bolt'], label=0.9), - InputExample(texts=['M6 bolt', 'resistor'], label=0.1), - # ... more examples - ] - - # Fine-tune - model = SentenceTransformer('all-MiniLM-L6-v2') - model.fit(train_examples) - ``` - -2. **Smart Categorization** - ```python - def auto_categorize(item_description): - # Use zero-shot classification - from transformers import pipeline - - classifier = pipeline("zero-shot-classification") - categories = ["Electronics", "Fasteners", "Tools", - "Paints", "Hardware"] - result = classifier(item_description, categories) - return result['labels'][0] - ``` - -3. **Usage Analytics** - ```python - class UsageAnalyzer: - def frequently_accessed_items(self, days=30): - # Track item access frequency - # Suggest moving to more accessible location - - def low_stock_prediction(self): - # Analyze usage patterns - # Predict when items will run out - - def suggest_reorganization(self): - # Items often used together - # Should be stored near each other - ``` - -4. **Cross-Reference System** - ```python - def find_substitutes(item): - # Find alternative items - # M6 bolt → M6 screw (if bolt unavailable) - - def find_complementary(item): - # Arduino → jumper wires, breadboard - # M6 bolt → M6 nut, M6 washer - ``` - -5. **Natural Language Queries** - ```python - # Instead of: search "M6 bolt 50mm zinc" - # User asks: "I need a medium-length metric bolt, - # preferably zinc-coated, around 6mm diameter" - - class NLQueryProcessor: - def parse_nl_query(self, query): - # Extract intent and constraints - # Map to structured search - # Return ranked results - ``` - -### Implementation Plan: - -**Week 10, Day 1-2: Model Fine-Tuning** -- Collect training data from actual inventory -- Create positive/negative pairs -- Fine-tune embedding model -- Evaluate improvement - -**Week 10, Day 3-4: Auto-Categorization** -- Implement zero-shot classifier -- Test on existing items -- Add to item creation flow - -**Week 10, Day 5 - Week 11, Day 1: Usage Tracking** -```sql -CREATE TABLE item_access_log ( - item_id INTEGER, - accessed_at TIMESTAMP, - action TEXT -- 'viewed', 'updated', 'moved' -); - -CREATE TABLE usage_analytics ( - item_id INTEGER, - access_count_7d INTEGER, - access_count_30d INTEGER, - last_accessed TIMESTAMP -); -``` - -**Week 11, Day 2-3: Analytics Dashboard** -- Most accessed items -- Low stock warnings -- Reorganization suggestions -- Usage heatmaps - -**Week 11, Day 4-5: NL Query Processing** -- Implement query understanding -- Test with complex queries -- Compare with simple search - -### Test Milestone: -System makes useful organizational suggestions based on usage - ---- - -## 🏆 Phase 8: Production Polish - -**Status:** 📋 Planned -**Timeline:** Week 12+ -**Complexity:** Intermediate - -### Goals: -- Production-ready deployment -- Multi-user support (if needed) -- Mobile optimization -- Professional features - -### Features to Build: - -1. **Authentication & Multi-User** - ```python - from flask_login import LoginManager, login_required - - # Optional - only if needed - # Single-user mode by default - - @app.route('/items') - @login_required # If multi-user enabled - def items(): - pass - ``` - -2. **Mobile-Optimized UI** - - Responsive design (already decent) - - Touch-friendly buttons - - Swipe gestures - - Camera integration for barcode/QR scanning - -3. **QR Code System** - ```python - import qrcode - - def generate_location_qr(location): - # Generate QR code for location - # Scan to quickly add items to location - - def generate_item_qr(item): - # Generate QR code for item - # Scan to view item details - ``` - -4. **Barcode Scanning** - ```python - # Use Zebra Crossing (ZXing) or similar - # Scan UPC/EAN barcodes - # Look up item in database - # Or add new item with pre-filled info - ``` - -5. **Advanced Reports** - - Inventory value report - - Stock level report - - Usage statistics - - Location capacity report - - Export to Excel/PDF - -6. **Backup & Restore UI** - ```python - @app.route('/admin/backup') - def create_backup(): - # Trigger backup - # Download SQL file - - @app.route('/admin/restore', methods=['POST']) - def restore_backup(): - # Upload SQL file - # Restore database - ``` - -7. **Monitoring Dashboard** - ```python - # System health - # Database size - # Item count - # Container status - # Backup status - ``` - -8. **API Documentation** - - Swagger/OpenAPI spec - - Interactive API explorer - - Example requests - -### Production Checklist: - -- [ ] HTTPS with Let's Encrypt -- [ ] Strong passwords for all services -- [ ] Regular automated backups -- [ ] Monitoring and alerting -- [ ] Error logging (Sentry, etc.) -- [ ] Rate limiting -- [ ] Input validation -- [ ] SQL injection prevention (already handled by SQLAlchemy) -- [ ] XSS protection -- [ ] CSRF tokens -- [ ] Security headers -- [ ] Container updates (Watchtower, etc.) - -### Performance Optimization: - -- [ ] Redis caching for search results -- [ ] Database query optimization -- [ ] Image optimization (if adding photos) -- [ ] Lazy loading in UI -- [ ] Pagination for large lists -- [ ] Background job queue (Celery) - -### Mobile App Options: - -**Option 1: Progressive Web App (PWA)** -- Add manifest.json -- Service worker for offline support -- Install prompt - -**Option 2: React Native App** -- Native iOS/Android app -- Better performance -- Native barcode scanner - -**Option 3: Flutter App** -- Cross-platform -- Single codebase -- Native feel - -### Test Milestone: -System runs reliably 24/7 with no issues - ---- - -## Success Metrics - -### Phase 1 (Current): -- ✅ Can add 100+ items easily -- ✅ Search finds items quickly -- ✅ Location hierarchy is clear -- ✅ No crashes or data loss - -### Phase 2: -- Location suggestions make sense -- Reduced time finding storage spots -- Better space utilization - -### Phase 3: -- Duplicate detection catches 90%+ of duplicates -- False positive rate < 10% -- Automatic spec extraction works for common items - -### Phase 4: -- Semantic search returns relevant results -- Natural queries work ("medium bolt") -- Better than keyword search - -### Phase 5: -- Can manage inventory without opening browser -- CLI is faster for bulk operations -- Import/export works reliably - -### Phase 6: -- Voice recognition accuracy > 95% -- Hands-free operation is practical -- Workshop noise doesn't break it - -### Phase 7: -- Recommendations are useful -- Usage analytics provide insights -- Auto-categorization is accurate - -### Phase 8: -- 99.9% uptime -- Fast response times (< 200ms) -- Mobile UI is smooth -- Backup/restore is reliable - ---- - -## Technology Evolution - -### Current (Phase 1): -``` -Python + Flask -PostgreSQL -Docker -Simple templates -``` - -### Mid-term (Phase 4): -``` -+ sentence-transformers -+ pgvector -+ Better UI framework -``` - -### Long-term (Phase 8): -``` -+ React/Vue for frontend? -+ Redis for caching -+ Celery for background jobs -+ Prometheus for monitoring -+ Mobile app? -``` - ---- - -## Resource Requirements - -### Phase 1 (Current): -- 2GB RAM -- 10GB disk -- Any CPU - -### Phase 4 (Semantic Search): -- 4GB RAM (for embeddings) -- 20GB disk -- CPU: Any (GPU optional) - -### Phase 6 (Voice): -- 4GB RAM -- Dedicated microphone -- Raspberry Pi 4+ recommended - -### Phase 8 (Production): -- 8GB RAM -- 50GB disk (for backups) -- SSD recommended -- Reverse proxy (nginx/Caddy) - ---- - -## Timeline Summary - -| Phase | Weeks | Effort | Dependencies | -|-------|-------|--------|--------------| -| 1 | 1-2 | ✅ Done | None | -| 2 | 3 | Medium | Phase 1 | -| 3 | 4 | Medium | Phase 1 | -| 4 | 5-6 | High | Phase 1 | -| 5 | 7 | Medium | Phase 1, 4 | -| 6 | 8-9 | High | Phase 1, 4, 5 | -| 7 | 10-11 | High | Phase 1, 4 | -| 8 | 12+ | Medium | All previous | - -**Total:** ~3 months for full system - ---- - -## What's Next? - -**Immediate:** -1. Deploy Phase 1 -2. Use it for real inventory -3. Provide feedback -4. Identify pain points - -**Soon:** -1. Decide: Which phase is most important to you? - - Need better organization? → Phase 2 - - Have duplicates? → Phase 3 - - Want AI search? → Phase 4 - - Prefer CLI? → Phase 5 - - Need hands-free? → Phase 6 - -**Future:** -- Phases don't have to be done in order -- Can skip phases not needed -- Can combine phases -- Customize to your needs - ---- - -## Questions to Consider - -1. **How many items will you track?** - - < 1000: Current system is fine - - 1000-10000: Need Phase 4 (search) - - 10000+: Need Phase 7 (optimization) - -2. **How will you use it?** - - At desk: Web UI is fine - - In workshop: Voice (Phase 6) helps - - Quickly: CLI (Phase 5) is fastest - -3. **What's your priority?** - - Better organization: Phase 2 - - Better search: Phase 4 - - Automation: Phase 7 - - Mobile: Phase 8 - ---- - -## Contributing Ideas - -As you use the system, you'll discover: -- Missing features -- Better workflows -- UI improvements -- New use cases - -Document these! They'll inform future phases. - ---- - -## Version History - -- **v1.0.0 (Phase 1)** - Foundation ✅ -- **v2.0.0 (Phase 2)** - Smart locations 🔜 -- **v3.0.0 (Phase 3)** - Duplicate detection 📋 -- **v4.0.0 (Phase 4)** - AI search 📋 -- **v5.0.0 (Phase 5)** - CLI 📋 -- **v6.0.0 (Phase 6)** - Voice 📋 -- **v7.0.0 (Phase 7)** - Advanced AI 📋 -- **v8.0.0 (Phase 8)** - Production 📋 - ---- - -**Ready to build?** Start with Phase 1 (already done!), then pick your next adventure. 🚀 diff --git a/inventory-system/docs/START_HERE.md b/inventory-system/docs/START_HERE.md deleted file mode 100644 index c0e409b..0000000 --- a/inventory-system/docs/START_HERE.md +++ /dev/null @@ -1,456 +0,0 @@ -# 🏠 Homelab Inventory System - START HERE - -## 👋 Welcome! - -You've just downloaded a **complete, working inventory management system** for homelabs, makerspaces, and workshops. - -This package includes: -- ✅ **Fully functional web application** (Phase 1 complete) -- ✅ **Docker deployment** (runs anywhere) -- ✅ **Complete documentation** (everything you need) -- ✅ **Sample data** (see it in action) -- ✅ **8-phase roadmap** (future features planned) - ---- - -## 🚀 Get Started in 3 Steps - -### Step 1: Read This (2 minutes) -You're doing it! 👍 - -### Step 2: Quick Deploy (5 minutes) -```bash -# Extract the package and navigate to it -cd inventory-system - -# Start the system -docker-compose up -d - -# Wait 30 seconds for startup -sleep 30 - -# (Optional) Load sample data -python3 create_sample_data.py -``` - -### Step 3: Access (Now!) -Open your browser: -``` -http://localhost:8080 -``` - -**That's it!** You now have a working inventory system. 🎉 - ---- - -## 📚 What to Read Next? - -### New to the System? -Read these in order: -1. **[INDEX.md](INDEX.md)** - Navigation guide (5 minutes) -2. **[PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md)** - What is this? (10 minutes) -3. **[QUICKSTART.md](inventory-system/QUICKSTART.md)** - Deployment guide (5 minutes) - -### Ready to Deploy? -1. **[DEPLOY.md](DEPLOY.md)** - Comprehensive deployment guide -2. **[TESTING_CHECKLIST.md](TESTING_CHECKLIST.md)** - Verify everything works - -### Daily Operations? -1. **[QUICK_REFERENCE.md](QUICK_REFERENCE.md)** - Commands and workflows - -### Curious About Future? -1. **[ROADMAP.md](ROADMAP.md)** - 8-phase development plan - -### Complete Documentation? -1. **[README.md](inventory-system/README.md)** - Full reference manual - ---- - -## 📦 Package Contents - -``` -homelab-inventory-system/ -├── START_HERE.md ⭐ You are here -├── INDEX.md 📚 Documentation guide -├── PROJECT_OVERVIEW.md 📖 System overview -├── DEPLOY.md 🚀 Deployment guide -├── ROADMAP.md 🗺️ Development plan -├── QUICK_REFERENCE.md 📋 Daily cheat sheet -├── TESTING_CHECKLIST.md ✅ Verification guide -└── inventory-system/ 💻 The application - ├── README.md Complete docs - ├── QUICKSTART.md 5-minute start - ├── docker-compose.yml Container config - ├── backend/ Flask app - ├── frontend/ Web UI - └── create_sample_data.py Sample data -``` - ---- - -## 🎯 What Can It Do? - -### Phase 1 (Available Now) ✅ -- Track unlimited inventory items -- Organize in storage hierarchy (Modules → Levels → Locations) -- Search by keyword -- Web-based UI -- REST API -- Multiple items per location -- Quantity tracking -- Categories and tags -- Location grid visualization - -### Coming Soon 🔜 -- **Phase 2**: Smart location suggestions -- **Phase 3**: Duplicate detection -- **Phase 4**: AI semantic search (natural language) -- **Phase 5**: CLI interface -- **Phase 6**: Voice control -- **Phase 7**: Advanced AI features -- **Phase 8**: Production polish & mobile - ---- - -## ⚡ Quick Reference - -### Essential Commands -```bash -# Start system -docker-compose up -d - -# Stop system -docker-compose stop - -# View logs -docker-compose logs -f - -# Backup database -docker-compose exec postgres pg_dump -U inventoryuser inventory > backup.sql - -# Access web UI -http://localhost:8080 -``` - -### First-Time Setup -1. Create a module (storage unit) -2. Add levels (drawers/shelves) -3. Add items with descriptions -4. Search to find them! - ---- - -## 🆘 Need Help? - -### Documentation -- **Lost?** Read [INDEX.md](INDEX.md) -- **Quick start?** Read [QUICKSTART.md](inventory-system/QUICKSTART.md) -- **Troubleshooting?** Check [QUICK_REFERENCE.md](QUICK_REFERENCE.md) -- **Complete info?** Read [README.md](inventory-system/README.md) - -### Common Issues -**Can't access UI:** Check `docker-compose logs nginx` -**Items not saving:** Check `docker-compose logs backend` -**Port in use:** Change port in `docker-compose.yml` -**Database error:** Run `docker-compose restart postgres` - -See [QUICK_REFERENCE.md](QUICK_REFERENCE.md) for complete troubleshooting. - ---- - -## ✅ Verify Installation - -After deploying, verify it works: - -```bash -# Check containers are running -docker-compose ps -# Should show 3 containers: postgres, backend, nginx - -# Check web UI -curl -I http://localhost:8080 -# Should return: HTTP/1.1 200 OK - -# Load sample data (optional) -python3 create_sample_data.py -# Creates test modules and items -``` - -See [TESTING_CHECKLIST.md](TESTING_CHECKLIST.md) for comprehensive verification. - ---- - -## 🎓 Learning Path - -### Day 1: Get Started -- [ ] Read this file (START_HERE.md) -- [ ] Deploy system -- [ ] Load sample data -- [ ] Explore web UI -- [ ] Read [QUICK_REFERENCE.md](QUICK_REFERENCE.md) - -### Week 1: Basic Use -- [ ] Create your storage modules -- [ ] Add 20-50 items -- [ ] Test search -- [ ] Read [PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md) -- [ ] Set up backups - -### Week 2: Advanced -- [ ] Add more inventory -- [ ] Optimize organization -- [ ] Read [README.md](inventory-system/README.md) -- [ ] Run [TESTING_CHECKLIST.md](TESTING_CHECKLIST.md) - -### Week 3+: Mastery -- [ ] Read [ROADMAP.md](ROADMAP.md) -- [ ] Plan Phase 2 needs -- [ ] Customize system -- [ ] Provide feedback - ---- - -## 🔐 Security Note - -**Default settings are for development/home use.** - -For production/internet-facing: -- ⚠️ Change database password -- ⚠️ Set secure SECRET_KEY -- ⚠️ Enable HTTPS -- ⚠️ Configure firewall -- ⚠️ Set up regular backups - -See [DEPLOY.md](DEPLOY.md) security section for details. - ---- - -## 💡 Pro Tips - -1. **Use descriptive item descriptions** - - Good: "Hex head bolt, M6 diameter, 50mm long, zinc plated" - - Bad: "Bolt" - -2. **Tag everything** - - Tags: `bolt, m6, metric, hex, zinc, fastener` - - Makes searching much easier - -3. **Name modules memorably** - - Good: Zeus, Muse, Apollo (or Workshop-Main) - - Bad: Cabinet1, Storage2 - -4. **Backup regularly** - - `docker-compose exec postgres pg_dump -U inventoryuser inventory > backup.sql` - - Set up automated backups (see DEPLOY.md) - -5. **Start small** - - Add 20 items first - - Refine your organization - - Then add more - ---- - -## 📊 System Requirements - -### Minimum -- Docker & Docker Compose -- 2GB RAM -- 10GB disk space -- Any CPU - -### Recommended -- 4GB RAM -- 20GB SSD -- Modern CPU -- Network access for other devices - -### Works On -- Linux (any distro) -- macOS -- Windows (with Docker Desktop) -- Raspberry Pi 4+ -- Proxmox LXC -- VPS/Cloud servers -- Jetson Nano - ---- - -## 🎯 Use Cases - -Perfect for tracking: -- Electronic components (resistors, ICs, modules) -- Fasteners (screws, bolts, nuts, washers) -- Tools (hand tools, power tools, measuring) -- Materials (paints, solvents, adhesives) -- Hardware (standoffs, brackets, connectors) -- Supplies (wire, cable, consumables) - -Ideal environments: -- Homelabs -- Makerspaces -- Home workshops -- Garages -- Electronics labs -- Shared tool libraries - ---- - -## 🏁 Ready to Start? - -### Absolute Minimum to Read: -1. This file (you're reading it!) ✓ -2. [QUICKSTART.md](inventory-system/QUICKSTART.md) (5 min) - -### Recommended: -3. [PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md) (10 min) -4. [QUICK_REFERENCE.md](QUICK_REFERENCE.md) (5 min) - -### Optional but Useful: -5. [README.md](inventory-system/README.md) (complete docs) -6. [ROADMAP.md](ROADMAP.md) (future plans) - ---- - -## 🚀 Deploy Now! - -```bash -cd inventory-system -docker-compose up -d -``` - -Then open: **http://localhost:8080** - ---- - -## 📞 Support - -### Documentation -All docs included: -- [INDEX.md](INDEX.md) - Navigation -- [PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md) - Overview -- [QUICKSTART.md](inventory-system/QUICKSTART.md) - Quick start -- [DEPLOY.md](DEPLOY.md) - Deployment -- [QUICK_REFERENCE.md](QUICK_REFERENCE.md) - Daily ops -- [TESTING_CHECKLIST.md](TESTING_CHECKLIST.md) - Testing -- [ROADMAP.md](ROADMAP.md) - Future features -- [README.md](inventory-system/README.md) - Complete reference - -### Troubleshooting -1. Check logs: `docker-compose logs` -2. Review [QUICK_REFERENCE.md](QUICK_REFERENCE.md) -3. Try sample data: `python3 create_sample_data.py` -4. Reset system: `docker-compose down -v && docker-compose up -d` - ---- - -## 📝 Version Info - -- **Version**: 1.0.0 -- **Phase**: 1 (Foundation) - Complete ✅ -- **Date**: October 2024 -- **Status**: Production Ready - ---- - -## 🎉 What's Next? - -After deploying: -1. ✅ Load sample data to explore -2. ✅ Create your first module -3. ✅ Add 10-20 real items -4. ✅ Read [QUICK_REFERENCE.md](QUICK_REFERENCE.md) -5. ✅ Set up daily backups -6. ✅ Enjoy organized storage! - -Future phases will add: -- AI-powered semantic search (Phase 4) -- Voice control (Phase 6) -- CLI interface (Phase 5) -- Advanced features (Phases 7-8) - -See [ROADMAP.md](ROADMAP.md) for details. - ---- - -## ❓ Quick FAQ - -**Q: How much does it cost?** -A: Free! Self-hosted, no subscriptions. - -**Q: How many items can it handle?** -A: 10,000+ easily. PostgreSQL can handle millions. - -**Q: Do I need internet?** -A: No! Completely offline after installation. - -**Q: Can I customize it?** -A: Yes! It's all open source. - -**Q: Is it secure?** -A: Yes for local use. See DEPLOY.md for production hardening. - -**Q: Can multiple people use it?** -A: Yes, but no user accounts yet (Phase 8). - -**Q: What about mobile?** -A: Web UI works on mobile. Native app planned for Phase 8. - -**Q: Can I backup my data?** -A: Yes! Simple PostgreSQL backup. See QUICK_REFERENCE.md. - ---- - -## 🌟 Why This System? - -### vs Spreadsheets -- ✅ Better search -- ✅ Relationship tracking -- ✅ Location visualization -- ✅ Future AI capabilities - -### vs Commercial Software -- ✅ Self-hosted (your data) -- ✅ No subscription fees -- ✅ Unlimited items -- ✅ Fully customizable - -### vs Basic Database -- ✅ User-friendly UI -- ✅ Built for storage -- ✅ Easy deployment -- ✅ Natural language ready - ---- - -## 🎊 Final Words - -This is **Phase 1** of an 8-phase plan. Even at Phase 1, it's a **fully functional, production-ready** inventory system. - -**Start simple. Grow as needed.** - -### Next Steps: -1. Deploy it (5 minutes) -2. Use it (ongoing) -3. Enjoy organized storage! 🎉 - ---- - -## 🗺️ Where to Go From Here? - -**First time?** → [QUICKSTART.md](inventory-system/QUICKSTART.md) - -**Need overview?** → [PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md) - -**Daily use?** → [QUICK_REFERENCE.md](QUICK_REFERENCE.md) - -**Lost?** → [INDEX.md](INDEX.md) - -**All docs?** → [README.md](inventory-system/README.md) - ---- - -**Ready to organize your homelab? Let's go! 🚀** - ---- - -*Homelab Inventory System v1.0.0 - Phase 1 Complete - October 2024* diff --git a/inventory-system/docs/TESTING_CHECKLIST.md b/inventory-system/docs/TESTING_CHECKLIST.md deleted file mode 100644 index 8456267..0000000 --- a/inventory-system/docs/TESTING_CHECKLIST.md +++ /dev/null @@ -1,763 +0,0 @@ -# ✅ Phase 1 Testing Checklist - -Use this checklist to verify your inventory system is working correctly after deployment. - ---- - -## Pre-Deployment Checks - -### System Requirements -- [ ] Docker installed and running -- [ ] Docker Compose installed -- [ ] Minimum 2GB RAM available -- [ ] Minimum 10GB disk space free -- [ ] Ports 5000, 5432, 8080 available - -**Verification:** -```bash -docker --version -docker-compose --version -df -h -netstat -tuln | grep -E '5000|5432|8080' -``` - ---- - -## Deployment Checks - -### Container Startup -- [ ] All containers start without errors -- [ ] PostgreSQL container is healthy -- [ ] Backend container is running -- [ ] Nginx container is running - -**Verification:** -```bash -cd inventory-system -docker-compose up -d -sleep 30 -docker-compose ps - -# Expected output: 3 containers with "Up" status -# inventory-db Up (healthy) -# inventory-backend Up -# inventory-nginx Up -``` - -### Log Check -- [ ] No errors in PostgreSQL logs -- [ ] Backend initializes database -- [ ] No critical errors in backend logs -- [ ] Nginx starts successfully - -**Verification:** -```bash -docker-compose logs postgres | grep -i error -docker-compose logs backend | grep -i error -docker-compose logs nginx | grep -i error -``` - -### Web Access -- [ ] Can access http://localhost:8080 -- [ ] Homepage loads correctly -- [ ] No 404 or 500 errors -- [ ] Navigation menu appears - -**Verification:** -```bash -curl -I http://localhost:8080 -# Should return: HTTP/1.1 200 OK -``` - ---- - -## Database Checks - -### Connection -- [ ] Backend can connect to PostgreSQL -- [ ] Database 'inventory' exists -- [ ] All tables are created - -**Verification:** -```bash -docker-compose exec postgres psql -U inventoryuser -d inventory -c "\dt" - -# Expected tables: -# - modules -# - levels -# - locations -# - items -# - item_locations -``` - -### Schema -- [ ] `modules` table has correct columns -- [ ] `levels` table has correct columns -- [ ] `locations` table has correct columns -- [ ] `items` table has correct columns -- [ ] `item_locations` table has correct columns -- [ ] All foreign keys are set up -- [ ] All unique constraints exist - -**Verification:** -```bash -docker-compose exec postgres psql -U inventoryuser -d inventory -c "\d modules" -docker-compose exec postgres psql -U inventoryuser -d inventory -c "\d items" -``` - ---- - -## Module Management Tests - -### Create Module -- [ ] Can access "Modules" page -- [ ] "Add Module" button works -- [ ] Can create module with name "TestModule" -- [ ] Description field works -- [ ] Location description field works -- [ ] Module appears in list after creation -- [ ] Can view module details - -**Test Steps:** -1. Navigate to http://localhost:8080/modules -2. Click "Add Module" -3. Fill in: - - Name: TestModule - - Description: Test module for verification - - Location: Test bench -4. Click "Create Module" -5. Verify module appears in list -6. Click module name to view details - -### Edit Module -- [ ] Can click "Edit" on module -- [ ] Can change name -- [ ] Can change description -- [ ] Changes save correctly -- [ ] Updated module displays new info - -### Delete Module -- [ ] Can delete empty module -- [ ] Confirmation prompt appears -- [ ] Module is removed from list - -⚠️ **Note:** Don't delete TestModule yet - needed for level tests - ---- - -## Level Management Tests - -### Create Level -- [ ] Can view module details page -- [ ] "Add Level" button works -- [ ] Can create level with number 1 -- [ ] Row count field works (try 4) -- [ ] Column count field works (try 6) -- [ ] Level appears under module -- [ ] Locations are auto-created (24 for 4×6) - -**Test Steps:** -1. View TestModule details -2. Click "Add Level" -3. Fill in: - - Level Number: 1 - - Name: Test Level - - Rows: 4 - - Columns: 6 - - Description: 4×6 grid test -4. Click "Create Level" -5. Verify level appears -6. Click level to view locations - -### Verify Location Grid -- [ ] Grid displays correctly (4 rows × 6 columns) -- [ ] All locations are present (A1-D6) -- [ ] Can click individual locations -- [ ] Empty locations are indicated -- [ ] Grid is visually clear - -### Edit Level -- [ ] Can edit level details -- [ ] Can change name -- [ ] Can change description -- [ ] Changes save - -⚠️ **Note:** Changing rows/columns after creation doesn't auto-create new locations in Phase 1 - ---- - -## Location Tests - -### View Location -- [ ] Can click on location (e.g., A1) -- [ ] Location details page loads -- [ ] Full address displays (TestModule:1:A1) -- [ ] Empty location shows "No items" - -### Edit Location -- [ ] Can edit location -- [ ] Can set location type (try "medium_bin") -- [ ] Can set dimensions (width, height, depth) -- [ ] Can add notes -- [ ] Changes save correctly - -### Location Types -- [ ] Can select "small_box" -- [ ] Can select "medium_bin" -- [ ] Can select "large_bin" -- [ ] Can select "liquid_container" -- [ ] Can select "smd_container" -- [ ] Can select "general" - ---- - -## Item Management Tests - -### Create Item (Simple) -- [ ] Can access "Items" page -- [ ] "Add Item" button works -- [ ] Can create item with minimal info -- [ ] Name field required -- [ ] Description field required - -**Test Steps:** -1. Navigate to http://localhost:8080/items -2. Click "Add Item" -3. Fill in: - - Name: Test Item - - Description: Simple test item -4. Click "Create Item" -5. Verify item appears in list - -### Create Item (Full) -- [ ] Can fill all fields -- [ ] Category dropdown works -- [ ] Item type dropdown works -- [ ] Quantity field works -- [ ] Unit field works -- [ ] Tags field works (comma-separated) -- [ ] Location can be selected -- [ ] Item saves with all data - -**Test Steps:** -1. Create item with all fields: - - Name: M6x50 Test Bolt - - Description: Hex head bolt, M6 diameter, 50mm long, zinc plated - - Category: Fasteners - - Item Type: solid - - Quantity: 100 - - Unit: pieces - - Tags: bolt, m6, metric, test - - Location: TestModule:1:A1 -2. Verify all data displays correctly - -### View Item -- [ ] Can click item name -- [ ] Item details page loads -- [ ] All fields display correctly -- [ ] Location(s) shown -- [ ] Tags display as list - -### Edit Item -- [ ] Can edit item -- [ ] Can change name -- [ ] Can change description -- [ ] Can change quantity -- [ ] Can add/remove tags -- [ ] Changes save - -### Delete Item -- [ ] Can delete item -- [ ] Confirmation prompt appears -- [ ] Item removed from list - ---- - -## Item-Location Relationship Tests - -### Multiple Locations -- [ ] Can add item to multiple locations -- [ ] Each location shows separately -- [ ] Quantities per location tracked -- [ ] Total quantity calculated correctly - -**Test Steps:** -1. Edit existing item -2. Add to location TestModule:1:B2 -3. Set quantity at B2 to 50 -4. Verify item shows in both A1 and B2 -5. Verify total quantity updates - -### Location Displays Item -- [ ] Navigate to location A1 -- [ ] Item appears in location's item list -- [ ] Item quantity shown -- [ ] Can click item to view details - ---- - -## Search Tests - -### Basic Search -- [ ] Can access search page -- [ ] Search box works -- [ ] Enter "test" finds test items -- [ ] Results display correctly -- [ ] Item details visible in results - -### Search by Name -- [ ] Search for exact item name -- [ ] Item is found -- [ ] Partial name search works - -### Search by Description -- [ ] Search for word in description -- [ ] Items with matching descriptions found -- [ ] Multiple matches displayed - -### Search by Tags -- [ ] Search for tag (e.g., "bolt") -- [ ] Items with that tag found -- [ ] Multiple tags work - -### Search by Category -- [ ] Search for category name -- [ ] Items in that category found - -### No Results -- [ ] Search for non-existent term -- [ ] "No results" message displays -- [ ] No errors occur - ---- - -## API Tests - -### Modules API -```bash -# List modules -curl http://localhost:8080/modules/api/modules - -# Expected: JSON array of modules -``` - -- [ ] Returns valid JSON -- [ ] Contains created modules -- [ ] Status code 200 - -### Items API -```bash -# List items -curl http://localhost:8080/items/api/items - -# Expected: JSON array of items -``` - -- [ ] Returns valid JSON -- [ ] Contains created items -- [ ] Status code 200 - -### Search API -```bash -# Search items -curl "http://localhost:8080/items/api/items?search=test" - -# Expected: JSON array of matching items -``` - -- [ ] Returns filtered results -- [ ] Search query works -- [ ] Status code 200 - -### Locations API -```bash -# List locations -curl http://localhost:8080/locations/api/locations - -# Expected: JSON array of locations -``` - -- [ ] Returns valid JSON -- [ ] Contains created locations -- [ ] Status code 200 - ---- - -## Sample Data Tests - -### Load Sample Data -```bash -python3 create_sample_data.py -``` - -- [ ] Script runs without errors -- [ ] 3 modules created (Zeus, Muse, Apollo) -- [ ] Levels created for each module -- [ ] 10+ items created -- [ ] Items have proper descriptions -- [ ] Can browse sample data in UI - -### Verify Sample Data -- [ ] Zeus module exists -- [ ] Zeus has 3 levels -- [ ] Muse module exists -- [ ] Apollo module exists -- [ ] Electronics items exist -- [ ] Fastener items exist -- [ ] Tool items exist -- [ ] Paint items exist - ---- - -## UI/UX Tests - -### Navigation -- [ ] Home link works -- [ ] Modules link works -- [ ] Items link works -- [ ] Search link works -- [ ] All pages load without errors - -### Responsive Design -- [ ] Desktop view looks good (1920×1080) -- [ ] Laptop view looks good (1366×768) -- [ ] Tablet view works (768×1024) -- [ ] Mobile view works (375×667) - -**Note:** Optimal mobile support comes in Phase 8 - -### Forms -- [ ] All forms have proper labels -- [ ] Required fields marked -- [ ] Validation works -- [ ] Error messages are clear -- [ ] Success messages appear - -### Visual Elements -- [ ] Grid layouts display correctly -- [ ] Tables are readable -- [ ] Buttons are clickable -- [ ] Links are styled -- [ ] Colors are consistent - ---- - -## Performance Tests - -### Load Time -- [ ] Homepage loads in < 2 seconds -- [ ] Module list loads in < 2 seconds -- [ ] Item list loads in < 2 seconds -- [ ] Search results appear quickly - -### With Data -Add 100 items, then: -- [ ] Search still fast (< 1 second) -- [ ] List pages load quickly -- [ ] No noticeable slowdown - -### Concurrent Access -Open 3 browser tabs: -- [ ] All tabs work independently -- [ ] No conflicts -- [ ] Data stays consistent - ---- - -## Data Integrity Tests - -### Relationships -- [ ] Deleting level doesn't orphan locations -- [ ] Deleting module deletes levels -- [ ] Item-location relationships persist -- [ ] No broken foreign keys - -**Test:** -1. Create module with level and items -2. Delete module -3. Verify levels and locations also deleted -4. Items remain but locations removed - -### Unique Constraints -- [ ] Can't create duplicate module names -- [ ] Can't create duplicate level numbers in same module -- [ ] Can't create duplicate locations in same level - -**Test:** -1. Try to create module with existing name -2. Should show error -3. Verify constraint enforced - ---- - -## Backup/Restore Tests - -### Backup -```bash -docker-compose exec postgres pg_dump -U inventoryuser inventory > test_backup.sql -``` - -- [ ] Backup file created -- [ ] File size > 0 bytes -- [ ] Contains SQL statements -- [ ] No errors during backup - -### Restore -```bash -# Reset database -docker-compose down -v -docker-compose up -d -sleep 30 - -# Restore -docker-compose exec -T postgres psql -U inventoryuser inventory < test_backup.sql -``` - -- [ ] Restore completes without errors -- [ ] All modules restored -- [ ] All items restored -- [ ] All relationships intact -- [ ] Can access UI with restored data - ---- - -## Error Handling Tests - -### Invalid Input -- [ ] Empty required fields show error -- [ ] Invalid numbers rejected -- [ ] SQL injection attempts blocked -- [ ] XSS attempts sanitized - -**Test:** -1. Try to create item without name -2. Try to create item without description -3. Try negative quantity -4. Try SQL in name field: `'; DROP TABLE items; --` -5. Verify all rejected gracefully - -### 404 Handling -- [ ] Invalid URLs show 404 page -- [ ] Non-existent item IDs handled -- [ ] Non-existent module IDs handled - -**Test:** -```bash -curl http://localhost:8080/items/999999 -curl http://localhost:8080/invalid-page -``` - -### Network Issues -- [ ] Graceful handling if database unreachable -- [ ] Error message shown to user -- [ ] System recovers after database restart - -**Test:** -```bash -docker-compose stop postgres -# Try to use UI - should show error -docker-compose start postgres -# Wait 10 seconds, try again - should work -``` - ---- - -## Security Tests (Phase 1 Basic) - -### Port Exposure -```bash -netstat -tuln | grep -E '5432|5000|8080' -``` - -- [ ] Port 8080 open (nginx) -- [ ] Port 5432 open locally (if needed for admin) -- [ ] Port 5000 only accessible via nginx -- [ ] No unnecessary ports open - -### SQL Injection -- [ ] Try SQL injection in search -- [ ] Try SQL injection in item name -- [ ] Verify SQLAlchemy prevents injection -- [ ] No direct SQL exposed - -**Test:** -```bash -curl "http://localhost:8080/items/api/items?search=' OR '1'='1" -# Should return empty or safe results, not all items -``` - -⚠️ **Note:** Full security hardening comes in Phase 8 - ---- - -## Clean-up Tests - -### Reset System -```bash -docker-compose down -v -docker-compose up -d -``` - -- [ ] All data deleted -- [ ] Fresh database created -- [ ] System starts clean -- [ ] No orphaned data - -### Storage Space -```bash -du -sh data/ -``` - -- [ ] Database size reasonable (< 100MB for test data) -- [ ] No excessive log files -- [ ] Backups not accumulating - ---- - -## Documentation Tests - -### README -- [ ] All commands in README work -- [ ] Examples are accurate -- [ ] Links are not broken -- [ ] Screenshots match UI (if included) - -### QUICKSTART -- [ ] 5-minute deployment actually works -- [ ] Sample data script works -- [ ] First-time setup instructions accurate - -### API Documentation -- [ ] All listed endpoints work -- [ ] Examples are correct -- [ ] Response formats match documentation - ---- - -## Success Criteria - -Phase 1 is considered fully working if: - -- ✅ All containers run reliably -- ✅ Can create modules, levels, locations -- ✅ Can add, edit, delete items -- ✅ Search finds items correctly -- ✅ Multiple items per location works -- ✅ UI is usable and clear -- ✅ No data loss or corruption -- ✅ API endpoints respond correctly -- ✅ Backup/restore works -- ✅ Sample data loads successfully - ---- - -## Checklist Summary - -### Critical (Must Pass) -- [ ] System starts (docker-compose up -d) -- [ ] Web UI accessible -- [ ] Can create modules -- [ ] Can create items -- [ ] Search works -- [ ] No data loss -- [ ] Backup works - -### Important (Should Pass) -- [ ] All CRUD operations work -- [ ] Grid visualization works -- [ ] API endpoints respond -- [ ] Sample data loads -- [ ] Error handling works - -### Nice to Have (Good if Pass) -- [ ] Performance is good -- [ ] UI is polished -- [ ] Mobile view works -- [ ] Documentation complete - ---- - -## Test Report Template - -``` -# Phase 1 Test Report - -Date: ___________ -Tester: ___________ - -## Environment -- OS: ___________ -- Docker Version: ___________ -- RAM: ___________ -- Disk Space: ___________ - -## Results -- Critical Tests Passed: ___ / ___ -- Important Tests Passed: ___ / ___ -- Nice-to-Have Tests Passed: ___ / ___ - -## Issues Found -1. ___________ -2. ___________ -3. ___________ - -## Overall Status -[ ] PASS - Ready for use -[ ] PARTIAL - Usable with limitations -[ ] FAIL - Not ready - -## Notes -___________________________________________ -___________________________________________ -``` - ---- - -## Next Steps After Testing - -### If All Tests Pass: -1. ✅ Start using the system for real inventory -2. ✅ Add your first 20-50 items -3. ✅ Set up regular backups -4. ✅ Provide feedback for Phase 2 - -### If Some Tests Fail: -1. Document failures -2. Check logs: `docker-compose logs` -3. Try rebuilding: `docker-compose up --build` -4. Reset if needed: `docker-compose down -v && docker-compose up -d` -5. Retry failed tests - -### If Critical Tests Fail: -1. Check prerequisites (Docker, ports, etc.) -2. Review error messages -3. Check system resources -4. Consult troubleshooting in README -5. Open issue with logs - ---- - -## Continuous Testing - -As you use the system: - -### Daily -- [ ] Backup works -- [ ] Items save correctly -- [ ] Search finds items - -### Weekly -- [ ] No performance degradation -- [ ] No data corruption -- [ ] All features still working - -### Monthly -- [ ] Full backup/restore test -- [ ] Review any errors in logs -- [ ] Check disk space usage - ---- - -**Happy Testing! 🧪✅** - -Once you've verified Phase 1 works correctly, you're ready to start using it for real inventory management! diff --git a/inventory-system/docs/VERSION.md b/inventory-system/docs/VERSION.md deleted file mode 100644 index b9c11bd..0000000 --- a/inventory-system/docs/VERSION.md +++ /dev/null @@ -1,249 +0,0 @@ -# Homelab Inventory System - Version Information - -## Current Version -**Version**: 1.0.0 -**Phase**: 1 - Foundation -**Release Date**: 2024 -**Status**: Production Ready - -## Phase 1: Foundation (Current) - -### Features Completed ✅ -- Complete storage hierarchy (Modules → Levels → Locations) -- Full CRUD operations for all entities -- Web UI with responsive design -- Basic keyword search -- Visual location grids -- RESTful API endpoints -- PostgreSQL database backend -- Docker deployment with Docker Compose -- Comprehensive documentation (4 guides) -- Sample data generator - -### Technology Stack -- **Backend**: Python 3.11+, Flask 3.0, SQLAlchemy 2.0 -- **Database**: PostgreSQL 15 -- **Frontend**: HTML5, CSS3, JavaScript ES6+ -- **Infrastructure**: Docker, Docker Compose, nginx -- **Deployment**: VPS, Proxmox, Jetson Nano compatible - -### Known Limitations -- No AI-powered semantic search -- No duplicate detection -- No location suggestions -- No CLI interface -- No voice interface -- Single-user system (no authentication) -- Basic keyword search only - -## Upcoming Phases - -### Phase 2: Smart Location Management (Planned) -**Target**: Week 3 -**Features**: -- Location type constraints -- Smart location suggestions -- Size compatibility checking -- Visual location picker -- Proximity-based suggestions - -### Phase 3: Duplicate Detection (Planned) -**Target**: Week 4 -**Features**: -- Fuzzy string matching -- Pattern recognition for common formats -- Similar item warnings -- Merge suggestions -- Attribute extraction - -### Phase 4: Semantic Search (Planned) -**Target**: Week 5-6 -**Features**: -- Sentence transformer embeddings (BERT/SBERT) -- Natural language queries -- Semantic similarity matching -- Ranked search results -- pgvector integration - -### Phase 5: CLI Interface (Planned) -**Target**: Week 7 -**Features**: -- Command-line tool (invctl) -- Interactive REPL mode -- Batch operations -- CSV import/export -- Tab completion - -### Phase 6: Voice Interface (Planned) -**Target**: Week 8-9 -**Features**: -- Wake word detection (Porcupine) -- Speech-to-text (Whisper/Vosk) -- Text-to-speech response -- Natural language understanding -- Hands-free operation - -### Phase 7: Advanced AI Features (Planned) -**Target**: Week 10-11 -**Features**: -- Fine-tuned semantic models -- Usage analytics -- Smart categorization -- Reorganization suggestions -- Alternative part recommendations - -### Phase 8: Production Polish (Planned) -**Target**: Week 12+ -**Features**: -- User authentication -- Multi-user support -- Mobile optimization -- QR code generation -- Barcode scanning -- Advanced monitoring -- Automated backups - -## Release History - -### v1.0.0 - Phase 1 Foundation (2024) -**Initial Release** -- Complete Phase 1 feature set -- Production-ready deployment -- Comprehensive documentation -- Docker-based deployment -- RESTful API -- Web interface -- Sample data generator - -## Compatibility - -### Minimum Requirements -- **OS**: Linux (Ubuntu 20.04+, Debian 11+, any Docker-capable OS) -- **RAM**: 2GB minimum, 4GB recommended -- **Disk**: 10GB minimum, 20GB recommended -- **Docker**: 20.10+ -- **Docker Compose**: 2.0+ -- **Python**: 3.11+ (for development/sample data) - -### Tested Platforms -- ✅ Ubuntu 22.04 LTS -- ✅ Ubuntu 24.04 LTS -- ✅ Debian 12 -- ✅ Docker Desktop (macOS/Windows) -- ✅ Proxmox LXC containers -- ✅ VPS (DigitalOcean, Linode, AWS EC2) -- ⚠️ Jetson Nano (ARM64 - minor adjustments may be needed) - -### Browser Compatibility -- ✅ Chrome 90+ -- ✅ Firefox 88+ -- ✅ Safari 14+ -- ✅ Edge 90+ -- ✅ Mobile browsers (iOS Safari, Chrome Mobile) - -## Database Schema Version -**Schema Version**: 1.0 -**Tables**: 5 (modules, levels, locations, items, item_locations) -**Migration Support**: Flask-Migrate ready (to be implemented) - -## API Version -**API Version**: 1.0 -**Endpoint Prefix**: `/api/` (for JSON endpoints) -**Authentication**: None (Phase 1) -**Rate Limiting**: None (Phase 1) - -## Dependencies - -### Python Packages -``` -Flask==3.0.0 -Flask-SQLAlchemy==3.1.1 -Flask-Migrate==4.0.5 -psycopg2-binary==2.9.9 -python-dotenv==1.0.0 -sqlalchemy==2.0.23 -``` - -### Docker Images -``` -postgres:15-alpine -python:3.11-slim -nginx:alpine -``` - -## Security - -### Current Security Features -- SQL injection protection (SQLAlchemy ORM) -- CSRF protection (Flask) -- Input validation -- Prepared statements - -### Security Limitations (Phase 1) -- No user authentication -- No authorization/roles -- No TLS/HTTPS (development mode) -- Default database passwords -- No rate limiting -- No audit logging - -### Production Security Checklist -See README.md for complete production deployment guide. - -## Support - -### Documentation -- README.md - Complete user and developer guide -- QUICKSTART.md - 5-minute deployment guide -- ARCHITECTURE.md - Technical deep dive -- DEPLOYMENT_SUMMARY.md - Overview and next steps -- PROJECT_SUMMARY.md - Complete project information - -### Getting Help -1. Check documentation -2. Review troubleshooting sections -3. Check Docker logs -4. Open GitHub issue (when available) - -## License -[Your License Here] - -## Credits -Designed and built for homelab and makerspace inventory management. - -## Roadmap Timeline - -``` -Phase 1 (Current) ████████████ Complete -Phase 2 (Week 3) ░░░░░░░░░░░░ Planned -Phase 3 (Week 4) ░░░░░░░░░░░░ Planned -Phase 4 (Week 5-6) ░░░░░░░░░░░░ Planned -Phase 5 (Week 7) ░░░░░░░░░░░░ Planned -Phase 6 (Week 8-9) ░░░░░░░░░░░░ Planned -Phase 7 (Week 10-11) ░░░░░░░░░░░░ Planned -Phase 8 (Week 12+) ░░░░░░░░░░░░ Planned -``` - -## Migration Notes - -### From Phase 1 to Phase 2 -- No database schema changes -- New service layer for location suggestions -- Backward compatible - -### From Phase 2 to Phase 3 -- No database schema changes -- New duplicate detection service -- Backward compatible - -### From Phase 3 to Phase 4 -- Database extension: pgvector -- New embeddings table -- Migration script provided -- Backward compatible - ---- - -**Last Updated**: 2024 -**Maintained By**: [Your Name/Team] -**Project Status**: Active Development diff --git a/inventory-system/frontend/static/css/style.css b/inventory-system/frontend/static/css/style.css deleted file mode 100644 index 812cab5..0000000 --- a/inventory-system/frontend/static/css/style.css +++ /dev/null @@ -1,829 +0,0 @@ -/* Reset and Base Styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - --primary-color: #2563eb; - --primary-hover: #1d4ed8; - --secondary-color: #64748b; - --success-color: #10b981; - --danger-color: #ef4444; - --warning-color: #f59e0b; - --bg-color: #f8fafc; - --card-bg: #ffffff; - --border-color: #e2e8f0; - --text-primary: #1e293b; - --text-secondary: #64748b; - --text-muted: #94a3b8; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - background-color: var(--bg-color); - color: var(--text-primary); - line-height: 1.6; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 20px; -} - -/* Navigation */ -.navbar { - background-color: var(--card-bg); - border-bottom: 1px solid var(--border-color); - padding: 1rem 0; - margin-bottom: 2rem; -} - -.navbar .container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.nav-brand a { - font-size: 1.5rem; - font-weight: bold; - color: var(--primary-color); - text-decoration: none; -} - -.nav-menu { - display: flex; - list-style: none; - gap: 2rem; -} - -.nav-menu a { - color: var(--text-secondary); - text-decoration: none; - transition: color 0.2s; -} - -.nav-menu a:hover { - color: var(--primary-color); -} - -/* Flash Messages */ -.flash-messages { - margin-bottom: 2rem; -} - -.alert { - padding: 1rem; - border-radius: 0.5rem; - margin-bottom: 1rem; - position: relative; -} - -.alert-success { - background-color: #d1fae5; - color: #065f46; - border: 1px solid #6ee7b7; -} - -.alert-error { - background-color: #fee2e2; - color: #991b1b; - border: 1px solid #fca5a5; -} - -.alert-warning { - background-color: #fef3c7; - color: #92400e; - border: 1px solid #fcd34d; -} - -.alert-close { - position: absolute; - right: 1rem; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: inherit; - opacity: 0.5; -} - -.alert-close:hover { - opacity: 1; -} - -/* Page Header */ -.page-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2rem; -} - -.page-header h1 { - font-size: 2rem; - color: var(--text-primary); -} - -.header-actions { - display: flex; - gap: 1rem; -} - -.breadcrumb { - font-size: 0.875rem; - color: var(--text-secondary); - margin-bottom: 0.5rem; -} - -.breadcrumb a { - color: var(--primary-color); - text-decoration: none; -} - -.breadcrumb a:hover { - text-decoration: underline; -} - -/* Buttons */ -.btn { - display: inline-block; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - text-decoration: none; - border: none; - cursor: pointer; - font-size: 0.875rem; - font-weight: 500; - transition: all 0.2s; -} - -.btn-primary { - background-color: var(--primary-color); - color: white; -} - -.btn-primary:hover { - background-color: var(--primary-hover); -} - -.btn-secondary { - background-color: var(--secondary-color); - color: white; -} - -.btn-secondary:hover { - background-color: #475569; -} - -.btn-danger { - background-color: var(--danger-color); - color: white; -} - -.btn-danger:hover { - background-color: #dc2626; -} - -.btn-sm { - padding: 0.25rem 0.75rem; - font-size: 0.75rem; -} - -.btn-link { - background: none; - color: var(--primary-color); - padding: 0.25rem 0.5rem; - font-size: 0.875rem; -} - -.btn-link:hover { - text-decoration: underline; -} - -.text-danger { - color: var(--danger-color); -} - -/* Dashboard */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1.5rem; - margin-bottom: 3rem; -} - -.stat-card { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 0.5rem; - padding: 1.5rem; -} - -.stat-value { - font-size: 2.5rem; - font-weight: bold; - color: var(--primary-color); - margin-bottom: 0.5rem; -} - -.stat-label { - color: var(--text-secondary); - font-size: 0.875rem; - margin-bottom: 0.5rem; -} - -.stat-link { - color: var(--primary-color); - text-decoration: none; - font-size: 0.875rem; -} - -.stat-link:hover { - text-decoration: underline; -} - -/* Quick Actions */ -.quick-actions { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 0.5rem; - padding: 1.5rem; - margin-bottom: 2rem; -} - -.quick-actions h2 { - margin-bottom: 1rem; - font-size: 1.25rem; -} - -.action-buttons { - display: flex; - gap: 1rem; - flex-wrap: wrap; -} - -/* Modules */ -.modules-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.module-card { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 0.5rem; - padding: 1.5rem; - transition: box-shadow 0.2s; -} - -.module-card:hover { - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -.module-card h3 { - margin-bottom: 0.5rem; -} - -.module-card h3 a { - color: var(--text-primary); - text-decoration: none; -} - -.module-card h3 a:hover { - color: var(--primary-color); -} - -.module-description { - color: var(--text-secondary); - font-size: 0.875rem; - margin-bottom: 1rem; -} - -.module-stats { - display: flex; - gap: 1rem; - font-size: 0.875rem; - color: var(--text-muted); -} - -/* Module List */ -.modules-list { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.module-item { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 0.5rem; - padding: 1.5rem; -} - -.module-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.module-header h2 { - font-size: 1.5rem; - margin: 0; -} - -.module-header h2 a { - color: var(--text-primary); - text-decoration: none; -} - -.module-header h2 a:hover { - color: var(--primary-color); -} - -.module-actions { - display: flex; - gap: 0.5rem; -} - -.module-location { - color: var(--text-secondary); - font-size: 0.875rem; - margin-bottom: 0.5rem; -} - -/* Levels */ -.levels-list { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.level-card { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 0.5rem; - padding: 1.5rem; -} - -.level-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; -} - -.level-header h3 { - font-size: 1.25rem; - margin: 0; -} - -.level-header h3 a { - color: var(--text-primary); - text-decoration: none; -} - -.level-header h3 a:hover { - color: var(--primary-color); -} - -.level-actions { - display: flex; - gap: 0.5rem; -} - -.level-info { - display: flex; - gap: 1rem; - font-size: 0.875rem; - color: var(--text-muted); - margin-top: 0.5rem; -} - -/* Location Grid */ -.location-grid { - overflow-x: auto; - margin-bottom: 1rem; -} - -.grid-table { - width: 100%; - border-collapse: collapse; - background-color: var(--card-bg); -} - -.grid-table th, -.grid-table td { - border: 1px solid var(--border-color); - padding: 0.5rem; - text-align: center; -} - -.grid-table th { - background-color: var(--bg-color); - font-weight: 600; -} - -.grid-cell { - min-width: 80px; - min-height: 60px; - cursor: pointer; - transition: background-color 0.2s; -} - -.grid-cell.occupied { - background-color: #dbeafe; -} - -.grid-cell.empty { - background-color: #f1f5f9; -} - -.grid-cell:hover { - background-color: #bfdbfe; -} - -.location-link { - display: block; - text-decoration: none; - color: var(--text-primary); - padding: 0.5rem; -} - -.location-address { - font-weight: 600; - font-size: 0.875rem; -} - -.location-items { - font-size: 0.75rem; - color: var(--primary-color); - margin-top: 0.25rem; -} - -.location-empty { - font-size: 0.75rem; - color: var(--text-muted); - margin-top: 0.25rem; -} - -.location-missing { - color: var(--text-muted); -} - -.grid-legend { - display: flex; - gap: 2rem; - margin-top: 1rem; - font-size: 0.875rem; -} - -.legend-item { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.legend-color { - width: 20px; - height: 20px; - border: 1px solid var(--border-color); - border-radius: 0.25rem; -} - -.legend-color.occupied { - background-color: #dbeafe; -} - -.legend-color.empty { - background-color: #f1f5f9; -} - -/* Tables */ -.data-table { - width: 100%; - border-collapse: collapse; - background-color: var(--card-bg); - border-radius: 0.5rem; - overflow: hidden; -} - -.data-table th, -.data-table td { - padding: 0.75rem 1rem; - text-align: left; - border-bottom: 1px solid var(--border-color); -} - -.data-table th { - background-color: var(--bg-color); - font-weight: 600; - color: var(--text-secondary); - font-size: 0.875rem; - text-transform: uppercase; -} - -.data-table tbody tr:hover { - background-color: var(--bg-color); -} - -.text-muted { - color: var(--text-muted); -} - -/* Badges */ -.badge { - display: inline-block; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.75rem; - font-weight: 500; - background-color: var(--bg-color); - color: var(--text-secondary); - margin-right: 0.25rem; -} - -.badge-occupied { - background-color: #dbeafe; - color: #1e40af; -} - -.badge-empty { - background-color: #f1f5f9; - color: var(--text-muted); -} - -/* Forms */ -.form { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 0.5rem; - padding: 2rem; - max-width: 800px; -} - -.form-group { - margin-bottom: 1.5rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; - color: var(--text-primary); -} - -.form-group input[type="text"], -.form-group input[type="number"], -.form-group input[type="email"], -.form-group select, -.form-group textarea { - width: 100%; - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color); - border-radius: 0.375rem; - font-size: 1rem; - font-family: inherit; -} - -.form-group input:focus, -.form-group select:focus, -.form-group textarea:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); -} - -.form-group small { - display: block; - margin-top: 0.25rem; - color: var(--text-muted); - font-size: 0.875rem; -} - -.form-row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; -} - -.form-actions { - display: flex; - gap: 1rem; - margin-top: 2rem; -} - -.form-inline { - display: flex; - gap: 1rem; - align-items: center; - flex-wrap: wrap; -} - -.form-inline input, -.form-inline select { - flex: 1; - min-width: 150px; -} - -/* Filters */ -.filters { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 0.5rem; - padding: 1rem; - margin-bottom: 2rem; -} - -.filter-form { - display: flex; - gap: 1rem; - align-items: center; - flex-wrap: wrap; -} - -.filter-form input, -.filter-form select { - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color); - border-radius: 0.375rem; - font-size: 0.875rem; -} - -.filter-form input { - flex: 1; - min-width: 200px; -} - -/* Detail Views */ -.detail-section { - margin-bottom: 1.5rem; -} - -.detail-section strong { - display: block; - margin-bottom: 0.5rem; - color: var(--text-secondary); - font-size: 0.875rem; - text-transform: uppercase; -} - -.detail-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1.5rem; - margin-bottom: 1.5rem; -} - -.detail-item strong { - display: block; - margin-bottom: 0.25rem; - color: var(--text-secondary); - font-size: 0.875rem; -} - -.item-details, -.module-details, -.location-details { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 0.5rem; - padding: 1.5rem; - margin-bottom: 2rem; -} - -.tags { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.tag { - display: inline-block; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - background-color: var(--primary-color); - color: white; - font-size: 0.75rem; -} - -/* Search */ -.search-form { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 0.5rem; - padding: 2rem; - margin-bottom: 2rem; -} - -.search-form form { - display: flex; - gap: 1rem; -} - -.search-form input { - flex: 1; - padding: 0.75rem 1rem; - border: 1px solid var(--border-color); - border-radius: 0.375rem; - font-size: 1rem; -} - -.result-count { - color: var(--text-secondary); - font-size: 0.875rem; - margin-bottom: 1rem; -} - -/* Danger Zone */ -.danger-zone { - background-color: #fef2f2; - border: 1px solid #fecaca; - border-radius: 0.5rem; - padding: 1.5rem; - margin-top: 2rem; -} - -.danger-zone h3 { - color: var(--danger-color); - margin-bottom: 0.5rem; -} - -.danger-zone p { - color: var(--text-secondary); - margin-bottom: 1rem; -} - -/* Empty State */ -.empty-state { - text-align: center; - padding: 3rem; - color: var(--text-muted); -} - -.empty-state p { - margin-bottom: 1rem; -} - -/* Stats Inline */ -.stats-inline { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -/* Footer */ -.footer { - margin-top: 4rem; - padding: 2rem 0; - border-top: 1px solid var(--border-color); - text-align: center; - color: var(--text-muted); - font-size: 0.875rem; -} - -/* Responsive */ -@media (max-width: 768px) { - .navbar .container { - flex-direction: column; - gap: 1rem; - } - - .nav-menu { - flex-direction: column; - gap: 0.5rem; - text-align: center; - } - - .page-header { - flex-direction: column; - align-items: flex-start; - gap: 1rem; - } - - .stats-grid { - grid-template-columns: 1fr; - } - - .form-row { - grid-template-columns: 1fr; - } - - .filter-form { - flex-direction: column; - align-items: stretch; - } - - .filter-form input, - .filter-form select { - width: 100%; - } -} diff --git a/inventory-system/frontend/static/js/main.js b/inventory-system/frontend/static/js/main.js deleted file mode 100644 index d502d48..0000000 --- a/inventory-system/frontend/static/js/main.js +++ /dev/null @@ -1,77 +0,0 @@ -// Main JavaScript for Inventory System - -// Auto-hide flash messages after 5 seconds -document.addEventListener('DOMContentLoaded', function() { - const alerts = document.querySelectorAll('.alert'); - alerts.forEach(alert => { - setTimeout(() => { - alert.style.opacity = '0'; - alert.style.transition = 'opacity 0.5s'; - setTimeout(() => alert.remove(), 500); - }, 5000); - }); -}); - -// Confirm delete actions -function confirmDelete(message) { - return confirm(message || 'Are you sure you want to delete this item?'); -} - -// Dynamic location selector -// This can be expanded in future phases for smarter location suggestions -function initLocationSelector() { - const moduleSelect = document.getElementById('module_select'); - const levelSelect = document.getElementById('level_select'); - const locationSelect = document.getElementById('location_select'); - - if (!moduleSelect || !levelSelect || !locationSelect) return; - - moduleSelect.addEventListener('change', async function() { - const moduleId = this.value; - if (!moduleId) { - levelSelect.innerHTML = ''; - locationSelect.innerHTML = ''; - return; - } - - // Fetch levels for selected module - const response = await fetch(`/modules/api/modules/${moduleId}/levels`); - const levels = await response.json(); - - levelSelect.innerHTML = ''; - levels.forEach(level => { - const option = document.createElement('option'); - option.value = level.id; - option.textContent = `Level ${level.level_number}${level.name ? ' - ' + level.name : ''}`; - levelSelect.appendChild(option); - }); - - locationSelect.innerHTML = ''; - }); - - levelSelect.addEventListener('change', async function() { - const levelId = this.value; - if (!levelId) { - locationSelect.innerHTML = ''; - return; - } - - // Fetch locations for selected level - const response = await fetch(`/locations/api/locations?level_id=${levelId}`); - const locations = await response.json(); - - locationSelect.innerHTML = ''; - locations.forEach(location => { - const option = document.createElement('option'); - option.value = location.id; - const occupied = location.item_count > 0 ? ' (Occupied)' : ''; - option.textContent = `${location.full_address}${occupied}`; - locationSelect.appendChild(option); - }); - }); -} - -// Initialize on page load -document.addEventListener('DOMContentLoaded', function() { - initLocationSelector(); -}); diff --git a/inventory-system/frontend/templates/base.html b/inventory-system/frontend/templates/base.html deleted file mode 100644 index 31d96cf..0000000 --- a/inventory-system/frontend/templates/base.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - {% block title %}Homelab Inventory System{% endblock %} - - {% block extra_css %}{% endblock %} - - - - -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for category, message in messages %} -
- {{ message }} - -
- {% endfor %} -
- {% endif %} - {% endwith %} - - {% block content %}{% endblock %} -
- -
-
-

© 2024 Homelab Inventory System | Phase 1: Foundation

-
-
- - - {% block extra_js %}{% endblock %} - - diff --git a/inventory-system/frontend/templates/index.html b/inventory-system/frontend/templates/index.html deleted file mode 100644 index 0bfeecf..0000000 --- a/inventory-system/frontend/templates/index.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Dashboard - Homelab Inventory{% endblock %} - -{% block content %} -
-

📦 Inventory Dashboard

- -
-
-
{{ stats.modules }}
-
Modules
- View all → -
- -
-
{{ stats.levels }}
-
Levels
-
- -
-
{{ stats.locations }}
-
Locations
- View all → -
- -
-
{{ stats['items'] }}
-
Items
- View all → -
-
- -
-

Quick Actions

- -
- - {% if modules %} -
-

Storage Modules

-
- {% for module in modules %} -
-

- - {{ module.name }} - -

-

{{ module.description or 'No description' }}

-
- {{ module.levels|length }} levels - {% set total_locations = module.levels|map(attribute='locations')|map('length')|sum %} - {{ total_locations }} locations -
-
- {% endfor %} -
-
- {% endif %} - - {% if recent_items %} -
-

Recently Added Items

- - - - - - - - - - - - {% for item in recent_items %} - - - - - - - - {% endfor %} - -
NameDescriptionCategoryLocationsActions
{{ item.name }}{{ item.description[:80] }}{% if item.description|length > 80 %}...{% endif %}{{ item.category or '-' }} - {% if item.item_locations %} - {{ item.item_locations|length }} location(s) - {% else %} - No location - {% endif %} - - View -
-
- {% endif %} -
-{% endblock %} diff --git a/inventory-system/frontend/templates/items/form.html b/inventory-system/frontend/templates/items/form.html deleted file mode 100644 index 2824053..0000000 --- a/inventory-system/frontend/templates/items/form.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{% if item %}Edit{% else %}New{% endif %} Item - Homelab Inventory{% endblock %} - -{% block content %} - - -
-
- - -
- -
- - - Natural language description of the item -
- -
-
- - - - -
- -
- - - - -
-
- -
- - - Comma-separated tags -
- -
- - -
- - {% if not item %} -

Initial Location

-
- - -
- {% endif %} - -
- - Cancel -
-
- -{% if item %} -
-

Danger Zone

-
- -
-
-{% endif %} -{% endblock %} diff --git a/inventory-system/frontend/templates/items/list.html b/inventory-system/frontend/templates/items/list.html deleted file mode 100644 index 1e6ca7d..0000000 --- a/inventory-system/frontend/templates/items/list.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Items - Homelab Inventory{% endblock %} - -{% block content %} - - -
-
- - - - Clear -
-
- -{% if items %} - - - - - - - - - - - - {% for item in items %} - - - - - - - - {% endfor %} - -
NameDescriptionCategoryLocationsActions
{{ item.name }}{{ item.description[:100] }}{% if item.description|length > 100 %}...{% endif %}{{ item.category or '-' }} - {% if item.item_locations %} - {% for il in item.item_locations %} - {{ il.location.full_address() }} - {% endfor %} - {% else %} - No location - {% endif %} - - View - Edit -
-{% else %} -
-

No items found.

- + Add Item -
-{% endif %} -{% endblock %} diff --git a/inventory-system/frontend/templates/items/view.html b/inventory-system/frontend/templates/items/view.html deleted file mode 100644 index cb1bc2f..0000000 --- a/inventory-system/frontend/templates/items/view.html +++ /dev/null @@ -1,107 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ item.name }} - Homelab Inventory{% endblock %} - -{% block content %} - - -
-
- Description: -

{{ item.description }}

-
- -
- {% if item.category %} -
- Category: - {{ item.category }} -
- {% endif %} - - {% if item.item_type %} -
- Type: - {{ item.item_type }} -
- {% endif %} -
- - {% if item.tags %} -
- Tags: -
- {% for tag in item.tags.split(',') %} - {{ tag.strip() }} - {% endfor %} -
-
- {% endif %} - - {% if item.notes %} -
- Notes: -

{{ item.notes }}

-
- {% endif %} -
- -

Storage Locations

- -{% if item.item_locations %} - - - - - - - - - - {% for il in item.item_locations %} - - - - - - {% endfor %} - -
LocationNotesActions
- - {{ il.location.full_address() }} - - {{ il.notes or '-' }} -
- -
-
-{% else %} -
-

This item has no storage locations assigned.

-
-{% endif %} - -
-

Add Storage Location

-
- - - -
-
- -{% endblock %} diff --git a/inventory-system/frontend/templates/levels/form.html b/inventory-system/frontend/templates/levels/form.html deleted file mode 100644 index 7cde937..0000000 --- a/inventory-system/frontend/templates/levels/form.html +++ /dev/null @@ -1,62 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{% if level %}Edit{% else %}New{% endif %} Level - {{ module.name }} - Homelab Inventory{% endblock %} - -{% block content %} - - -
-
- - - Numeric level identifier (1, 2, 3, etc.) -
- -
- - - Optional custom name for this level -
- -
- - - Number of rows (A, B, C... up to Z) -
- -
- - - Number of columns (1, 2, 3...) -
- -
- - - Optional description -
- - {% if level %} -
- Note: Changing the grid size will delete all existing locations and recreate them. -
- {% endif %} - -
- - Cancel -
-
- -{% if level %} -
-

Danger Zone

-

Deleting this level will also delete all its locations. This action cannot be undone.

-
- -
-
-{% endif %} -{% endblock %} diff --git a/inventory-system/frontend/templates/levels/view.html b/inventory-system/frontend/templates/levels/view.html deleted file mode 100644 index 8ecef95..0000000 --- a/inventory-system/frontend/templates/levels/view.html +++ /dev/null @@ -1,81 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ level.module.name }} - Level {{ level.level_number }} - Homelab Inventory{% endblock %} - -{% block content %} - - -{% if level.description %} -
-

{{ level.description }}

-
-{% endif %} - -
- Grid: {{ level.rows }} × {{ level.columns }} - {{ level.locations|length }} locations -
- -

Location Grid

- -
- - - - - {% for col in range(1, level.columns + 1) %} - - {% endfor %} - - - - {% for row_idx in range(level.rows) %} - {% set row = [chr(65 + row_idx)] if level.rows <= 26 else [row_idx + 1|string] %} - - - {% for col in range(1, level.columns + 1) %} - {% set col_str = col|string %} - {% set location = location_grid.get(row[0], {}).get(col_str) %} - - {% endfor %} - - {% endfor %} - -
{{ col }}
{{ row[0] }} - {% if location %} - -
{{ row[0] }}{{ col }}
- {% if location.item_locations %} -
{{ location.item_locations|length }} item(s)
- {% else %} -
Empty
- {% endif %} -
- {% else %} -
-
- {% endif %} -
-
- -
-
- Occupied -
-
- Empty -
-
- -{% endblock %} diff --git a/inventory-system/frontend/templates/locations/form.html b/inventory-system/frontend/templates/locations/form.html deleted file mode 100644 index 342539b..0000000 --- a/inventory-system/frontend/templates/locations/form.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Edit Location - {{ location.full_address() }} - Homelab Inventory{% endblock %} - -{% block content %} - - -
-
- - - - - Type of storage location -
- -

Dimensions (optional)

-
-
- - -
- -
- - -
- -
- - -
-
- -
- - -
- -
- - Cancel -
-
- -{% endblock %} diff --git a/inventory-system/frontend/templates/locations/list.html b/inventory-system/frontend/templates/locations/list.html deleted file mode 100644 index 9e3db61..0000000 --- a/inventory-system/frontend/templates/locations/list.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Locations - Homelab Inventory{% endblock %} - -{% block content %} - - -
-
- - - {% if location_types %} - - {% endif %} - - - Clear -
-
- -{% if locations %} - - - - - - - - - - - - - {% for location in locations %} - - - - - - - - - {% endfor %} - -
LocationModuleLevelTypeItemsActions
{{ location.full_address() }}{{ location.level.module.name if location.level and location.level.module else '-' }}Level {{ location.level.level_number if location.level else '-' }}{{ location.location_type }} - {% if location.item_locations %} - {{ location.item_locations|length }} item(s) - {% else %} - Empty - {% endif %} - - View - Edit -
-{% else %} -
-

No locations found.

-
-{% endif %} -{% endblock %} diff --git a/inventory-system/frontend/templates/locations/view.html b/inventory-system/frontend/templates/locations/view.html deleted file mode 100644 index 4dfcbc5..0000000 --- a/inventory-system/frontend/templates/locations/view.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ location.full_address() }} - Homelab Inventory{% endblock %} - -{% block content %} - - -
-
-
- Type: - {{ location.location_type }} -
- - {% if location.width_mm or location.height_mm or location.depth_mm %} -
- Dimensions: - - {% if location.width_mm %}W: {{ location.width_mm }}mm{% endif %} - {% if location.height_mm %} H: {{ location.height_mm }}mm{% endif %} - {% if location.depth_mm %} D: {{ location.depth_mm }}mm{% endif %} - -
- {% endif %} - -
- Status: - - {% if item_locations %}Occupied ({{ item_locations|length }} items){% else %}Empty{% endif %} - -
-
- - {% if location.notes %} -
- Notes: -

{{ location.notes }}

-
- {% endif %} -
- -

Items Stored Here

- -{% if item_locations %} - - - - - - - - - - - {% for il in item_locations %} - - - - - - - {% endfor %} - -
ItemDescriptionNotesActions
- - {{ il.item.name }} - - {{ il.item.description[:80] }}{% if il.item.description|length > 80 %}...{% endif %}{{ il.notes or '-' }} - View Item -
-{% else %} -
-

This location is empty.

-
-{% endif %} - -{% endblock %} diff --git a/inventory-system/frontend/templates/modules/form.html b/inventory-system/frontend/templates/modules/form.html deleted file mode 100644 index a807621..0000000 --- a/inventory-system/frontend/templates/modules/form.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{% if module %}Edit{% else %}New{% endif %} Module - Homelab Inventory{% endblock %} - -{% block content %} - - -
-
- - - Unique name for this storage module (e.g., "Zeus", "Main Workbench", "Muse") -
- -
- - - Optional description of what's stored here -
- -
- - - Where this module is located in your lab/shop (e.g., "North wall", "Under bench") -
- -
- - Cancel -
-
- -{% if module %} -
-

Danger Zone

-

Deleting this module will also delete all its levels and locations. This action cannot be undone.

-
- -
-
-{% endif %} -{% endblock %} diff --git a/inventory-system/frontend/templates/modules/list.html b/inventory-system/frontend/templates/modules/list.html deleted file mode 100644 index 8613c41..0000000 --- a/inventory-system/frontend/templates/modules/list.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Modules - Homelab Inventory{% endblock %} - -{% block content %} - - -{% if modules %} -
- {% for module in modules %} -
- - - {% if module.description %} -

{{ module.description }}

- {% endif %} - - {% if module.location_description %} -

📍 {{ module.location_description }}

- {% endif %} - -
- {{ module.levels|length }} levels - {% set total_locations = module.levels|map(attribute='locations')|map('length')|sum %} - {{ total_locations }} locations -
-
- {% endfor %} -
-{% else %} -
-

No modules yet. Create your first storage module to get started!

- + Add Module -
-{% endif %} -{% endblock %} diff --git a/inventory-system/frontend/templates/modules/view.html b/inventory-system/frontend/templates/modules/view.html deleted file mode 100644 index 18f1a18..0000000 --- a/inventory-system/frontend/templates/modules/view.html +++ /dev/null @@ -1,74 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ module.name }} - Homelab Inventory{% endblock %} - -{% block content %} - - -
- {% if module.description %} -
- Description: -

{{ module.description }}

-
- {% endif %} - - {% if module.location_description %} -
- Physical Location: -

📍 {{ module.location_description }}

-
- {% endif %} - -
- Statistics: -
- {{ levels|length }} levels - {% set total_locations = levels|map(attribute='locations')|map('length')|sum %} - {{ total_locations }} locations -
-
-
- -

Levels

- -{% if levels %} -
- {% for level in levels %} -
- - - {% if level.description %} -

{{ level.description }}

- {% endif %} - -
- Grid: {{ level.rows }} × {{ level.columns }} - {{ level.locations|length }} locations -
-
- {% endfor %} -
-{% else %} -
-

No levels yet. Add levels to organize storage locations.

- + Add Level -
-{% endif %} -{% endblock %} diff --git a/inventory-system/frontend/templates/search/results.html b/inventory-system/frontend/templates/search/results.html deleted file mode 100644 index 9816212..0000000 --- a/inventory-system/frontend/templates/search/results.html +++ /dev/null @@ -1,64 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Search - Homelab Inventory{% endblock %} - -{% block content %} - - -
-
- - -
-
- -{% if query %} -
-

Results for "{{ query }}"

- - {% if results %} -

Found {{ results|length }} item(s)

- - - - - - - - - - - - - {% for item in results %} - - - - - - - - {% endfor %} - -
NameDescriptionCategoryLocationsActions
{{ item.name }}{{ item.description[:100] }}{% if item.description|length > 100 %}...{% endif %}{{ item.category or '-' }} - {% if item.item_locations %} - {% for il in item.item_locations %} - {{ il.location.full_address() }} - {% endfor %} - {% else %} - No location - {% endif %} - - View -
- {% else %} -
-

No items found matching "{{ query }}"

-
- {% endif %} -
-{% endif %} - -{% endblock %} diff --git a/inventory-system/nginx.conf b/inventory-system/nginx.conf deleted file mode 100644 index bdb2a52..0000000 --- a/inventory-system/nginx.conf +++ /dev/null @@ -1,26 +0,0 @@ -events { - worker_connections 1024; -} - -http { - upstream backend { - server backend:5000; - } - - server { - listen 80; - server_name localhost; - - location / { - proxy_pass http://backend; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /static { - proxy_pass http://backend/static; - } - } -} diff --git a/specification/agents.md b/specification/agents.md new file mode 100644 index 0000000..5132965 --- /dev/null +++ b/specification/agents.md @@ -0,0 +1,729 @@ +# Agents Specification + +## Overview + +Agents are AI personalities with specific instructions and tool access. The system uses a router/specialist pattern where a router agent classifies user intent and delegates to specialist agents. + +## Agent Data Model + +```javascript +// schemas/Agent.js +const agentSchema = new Schema({ + user: { // Owner (agents are per-user) + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + name: { + type: String, + required: true, + lowercase: true + }, + displayName: String, // "Inventory Agent" + description: String, // Short description for router + instructions: { // System prompt + type: String, + required: true + }, + model: { // OpenAI model + type: String, + default: "gpt-4o", + enum: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"] + }, + tools: [String], // Tool names this agent can use + temperature: { + type: Number, + default: 0.7, + min: 0, + max: 1 + }, + isRouter: { // Is this the router agent? + type: Boolean, + default: false + }, + isSystem: { // System agents can't be deleted + type: Boolean, + default: false + }, + active: { // Enable/disable agent + type: Boolean, + default: true + } +}, { timestamps: true }); + +// Compound unique index - name unique per user +agentSchema.index({ user: 1, name: 1 }, { unique: true }); +``` + +## Tool Data Model + +```javascript +// schemas/Tool.js +// Tools are global (not per-user) - defined in code, synced to DB + +const toolParameterSchema = new Schema({ + name: String, + type: String, // "string", "number", "array", "object" + description: String, + required: Boolean, + enum: [String], // Optional allowed values +}, { _id: false }); + +const toolSchema = new Schema({ + name: { + type: String, + required: true, + unique: true + }, + description: String, // For OpenAI function calling + category: { // For grouping in UI + type: String, + enum: ["agents", "items", "modules", "templates", "params", "units", "utility"] + }, + parameters: [toolParameterSchema], + handler: String, // Reference to handler function + isSystem: { // System tools can't be deleted + type: Boolean, + default: true + }, + active: { // Can be toggled off + type: Boolean, + default: true + } +}, { timestamps: true }); +``` + +--- + +## System Agents + +### Router Agent + +The entry point for all user messages. Classifies intent and delegates to specialists. + +```javascript +{ + name: "router", + displayName: "Router", + isRouter: true, + isSystem: true, + model: "gpt-4o", + temperature: 0.7, + tools: ["runModuleAgent", "runInventoryAgent", "runSearchAgent"], + instructions: `You are the front door for a workshop inventory system. + +Analyze the user's message and invoke the appropriate specialist: + +- **runModuleAgent**: User wants to create or modify storage modules, cabinets, + shelves, drawer units, or dimension templates. Keywords: "this is", "new cabinet", + "storage unit", "set up", "module" + +- **runInventoryAgent**: User wants to add, update, or describe items in storage + locations. Keywords: "add", "put", "this bin has", "update", "move", "delete item" + +- **runSearchAgent**: User wants to find items or check what's in a location. + Keywords: "where is", "find", "do I have", "what's in", "search" + +Pass along any images and context to the specialist. + +If the user is just chatting, asking general questions, or you're unsure, respond +directly without invoking a specialist.` +} +``` + +### Module Agent + +Creates and manages storage modules and dimension templates. + +```javascript +{ + name: "module", + displayName: "Module Agent", + description: "Creates and manages storage modules", + isSystem: true, + model: "gpt-4o", + temperature: 0.7, + tools: [ + "searchModules", + "createModule", + "updateModule", + "searchTemplates", + "createTemplate" + ], + instructions: `You help users define storage modules for their workshop. + +## Your Job + +1. Understand the storage unit from user descriptions or images +2. Ask clarifying questions about dimensions (levels, rows, columns, bins) +3. Check for existing templates that match (searchTemplates) +4. Propose module structure before creating +5. **Always confirm with user before saving** + +## Naming Conventions + +- Module names: UPPERCASE, short (FLUX, MUSE, PRUSA) +- Dimension labels: lowercase (level, drawer, bin, row, col) +- Dimension values: lowercase or numbers ("1", "2", "yellow", "blue") + +## Location Path Format + +MODULE:dim-value:dim-value:... + +Examples: +- MUSE:level-3:row-2:col-5 +- PRUSA:drawer-1:box-yellow:row-2:col-3 +- FLUX:level-5:bin-8 + +## Workflow + +1. User describes or shows photo of storage unit +2. You identify the structure (levels, grid layout, etc.) +3. Check if any dimension templates exist that match +4. Propose the module definition +5. Wait for user confirmation +6. Create the module + +## Example Confirmation + +"I'll create this module: + +**MUSE** +- 11 levels +- Levels 1-8: 4×6 Plano grid (plano-4x6 template) +- Levels 9-11: 3×4 Plano grid (plano-3x4 template) + +Valid paths will look like: MUSE:level-3:row-2:col-5 + +Does this look right?"` +} +``` + +### Inventory Agent + +Adds and manages inventory items. + +```javascript +{ + name: "inventory", + displayName: "Inventory Agent", + description: "Adds and manages inventory items", + isSystem: true, + model: "gpt-4o", + temperature: 0.7, + tools: [ + "searchItems", + "createItem", + "updateItem", + "deleteItem", + "searchParams", + "createParam", + "searchUnits", + "createUnit", + "validatePath", + "searchModules" + ], + instructions: `You help users catalog items in their workshop storage. + +## Your Job + +1. Identify items from text, images, or voice descriptions +2. Search existing parameter keys before creating new ones (searchParams) +3. Use industry-standard terminology for parameters +4. Validate location paths against module definitions (validatePath) +5. **Always confirm item details before creating** + +## Parameter Guidelines + +- **Search first**: Always searchParams before creating new parameter keys +- **Naming**: Keys are lowercase, underscore-separated (thread_size, voltage_rating) +- **Units**: Include units where applicable (mm, in, V, ohm, uF) +- **Duplicates OK**: An item can have multiple parameters with the same key + (e.g., a pipe reducer with two thread_size values) + +## Standard Parameter Keys + +Common keys to look for: +- Dimensions: length, width, height, diameter, thread_size +- Electrical: voltage, current, resistance, capacitance, power +- Materials: material, color, finish +- Types: type, category, head_type, drive_type + +## Workflow + +1. User describes item(s) and location +2. Identify items and their characteristics +3. Search for existing parameter keys +4. Validate the location path +5. Propose the item(s) to create +6. Wait for user confirmation +7. Create the item(s) + +## Example Confirmation + +"I'll add this item: + +**10k ohm resistors** +- resistance: 10k ohm +- tolerance: 5% +- power: 0.25W +- type: through-hole + +**Location**: MUSE:level-3:row-2:col-5 + +Does this look right? Any other details to add?"` +} +``` + +### Search Agent + +Finds items in inventory. + +```javascript +{ + name: "search", + displayName: "Search Agent", + description: "Finds items in inventory", + isSystem: true, + model: "gpt-4o", + temperature: 0.7, + tools: [ + "searchItems", + "searchModules" + ], + instructions: `You help users find items in their workshop inventory. + +## Your Job + +1. Parse natural language queries into search parameters +2. Search by name, description, parameters, or location +3. Present results clearly with locations +4. Help narrow down if too many results + +## Search Strategies + +- "where are my 10k resistors" → searchItems by name/parameters +- "what's in MUSE level 3" → searchItems by location prefix +- "do I have any brass fittings" → searchItems by material parameter +- "find something for 5V switching" → searchItems by voltage + description + +## Query Interpretation + +Break down user queries: +- "10mm stainless screws" → parameters: {length: 10mm, material: stainless} +- "in the red cabinet" → location prefix: MUSE (if MUSE is the red cabinet) +- "something like a relay" → text search: relay + +## Result Presentation + +"Found 3 items matching 'resistor': + +1. **10k ohm resistors** + Location: MUSE:level-3:row-2:col-5 + +2. **4.7k ohm resistors** + Location: MUSE:level-3:row-2:col-6 + +3. **100 ohm resistors** + Location: MUSE:level-4:row-1:col-2" + +## No Results + +If nothing found, suggest: +- Alternative search terms +- Checking if the item might be under a different name +- Browsing related categories` +} +``` + +--- + +## Tool Definitions + +### Agent Runner Tools + +Used by the router to invoke specialist agents. + +```javascript +{ + name: "runModuleAgent", + description: "Invoke when user wants to create or modify storage modules, cabinets, shelves, or dimension templates", + category: "agents", + parameters: [ + { + name: "task", + type: "string", + description: "Summary of what the user wants to do", + required: true + } + ], + handler: "agents.runModule" +} + +{ + name: "runInventoryAgent", + description: "Invoke when user wants to add, update, or describe items in storage locations", + category: "agents", + parameters: [ + { + name: "task", + type: "string", + description: "Summary of what the user wants to do", + required: true + } + ], + handler: "agents.runInventory" +} + +{ + name: "runSearchAgent", + description: "Invoke when user wants to find items or check what's in a location", + category: "agents", + parameters: [ + { + name: "task", + type: "string", + description: "Summary of what the user wants to do", + required: true + } + ], + handler: "agents.runSearch" +} +``` + +### Item Tools + +```javascript +{ + name: "searchItems", + description: "Search inventory items by name, parameters, or location", + category: "items", + parameters: [ + { name: "query", type: "string", description: "Text search query", required: false }, + { name: "location", type: "string", description: "Location path or prefix", required: false }, + { name: "parameters", type: "array", description: "Parameter filters [{key, value}]", required: false } + ], + handler: "db.items.search" +} + +{ + name: "createItem", + description: "Create a new inventory item", + category: "items", + parameters: [ + { name: "name", type: "string", description: "Item name", required: true }, + { name: "description", type: "string", description: "Item description", required: false }, + { name: "parameters", type: "array", description: "Array of {key, value, unit}", required: false }, + { name: "location", type: "string", description: "Location path", required: true } + ], + handler: "db.items.create" +} + +{ + name: "updateItem", + description: "Update an existing item", + category: "items", + parameters: [ + { name: "location", type: "string", description: "Location path of item to update", required: true }, + { name: "updates", type: "object", description: "Fields to update", required: true } + ], + handler: "db.items.update" +} + +{ + name: "deleteItem", + description: "Delete an item from inventory", + category: "items", + parameters: [ + { name: "location", type: "string", description: "Location path of item to delete", required: true } + ], + handler: "db.items.delete" +} +``` + +### Module Tools + +```javascript +{ + name: "searchModules", + description: "Search for existing storage modules", + category: "modules", + parameters: [ + { name: "query", type: "string", description: "Search query (name or description)", required: false }, + { name: "name", type: "string", description: "Exact module name", required: false } + ], + handler: "db.modules.search" +} + +{ + name: "createModule", + description: "Create a new storage module", + category: "modules", + parameters: [ + { name: "name", type: "string", description: "Module name (uppercase)", required: true }, + { name: "description", type: "string", description: "Human-readable description", required: false }, + { name: "dimensions", type: "array", description: "Array of dimension definitions", required: true } + ], + handler: "db.modules.create" +} + +{ + name: "updateModule", + description: "Update an existing storage module", + category: "modules", + parameters: [ + { name: "name", type: "string", description: "Module name to update", required: true }, + { name: "updates", type: "object", description: "Fields to update", required: true } + ], + handler: "db.modules.update" +} +``` + +### Template Tools + +```javascript +{ + name: "searchTemplates", + description: "Search for existing dimension templates", + category: "templates", + parameters: [ + { name: "query", type: "string", description: "Search query", required: false } + ], + handler: "db.templates.search" +} + +{ + name: "createTemplate", + description: "Create a new dimension template", + category: "templates", + parameters: [ + { name: "name", type: "string", description: "Template name (lowercase, hyphenated)", required: true }, + { name: "description", type: "string", description: "Human-readable description", required: false }, + { name: "dimensions", type: "array", description: "Array of dimension definitions [{label, values}]", required: true } + ], + handler: "db.templates.create" +} +``` + +### Parameter & Unit Tools + +```javascript +{ + name: "searchParams", + description: "Search existing parameter keys", + category: "params", + parameters: [ + { name: "query", type: "string", description: "Search query", required: false }, + { name: "category", type: "string", description: "Filter by category", required: false } + ], + handler: "db.params.search" +} + +{ + name: "createParam", + description: "Create a new parameter key", + category: "params", + parameters: [ + { name: "key", type: "string", description: "Parameter key (lowercase, underscore-separated)", required: true }, + { name: "description", type: "string", description: "What this parameter represents", required: false }, + { name: "category", type: "string", description: "Category (dimension, material, electrical)", required: false }, + { name: "commonUnits", type: "array", description: "Common units for this parameter", required: false } + ], + handler: "db.params.create" +} + +{ + name: "searchUnits", + description: "Search existing units", + category: "units", + parameters: [ + { name: "query", type: "string", description: "Search query", required: false }, + { name: "type", type: "string", description: "Filter by type (length, voltage, etc.)", required: false } + ], + handler: "db.units.search" +} + +{ + name: "createUnit", + description: "Create a new unit", + category: "units", + parameters: [ + { name: "name", type: "string", description: "Unit abbreviation (lowercase)", required: true }, + { name: "fullName", type: "string", description: "Full name", required: false }, + { name: "type", type: "string", description: "Unit type (length, voltage, etc.)", required: false } + ], + handler: "db.units.create" +} +``` + +### Utility Tools + +```javascript +{ + name: "validatePath", + description: "Validate a location path against module definitions", + category: "utility", + parameters: [ + { name: "path", type: "string", description: "Location path to validate", required: true } + ], + handler: "util.validatePath" +} +``` + +--- + +## Seed Data Strategy + +### On Application Startup + +```javascript +// lib/seeds/index.js +export async function seedDefaults(userId) { + await seedTools(); // Tools are global + await seedAgents(userId); // Agents are per-user + await seedParameterKeys(); // Seed common parameter keys + await seedUnits(); // Seed common units +} +``` + +### Tool Seeding (Global) + +```javascript +// lib/seeds/tools.js +export async function seedTools() { + for (const tool of defaultTools) { + await Tool.findOneAndUpdate( + { name: tool.name }, + { $setOnInsert: tool }, + { upsert: true } + ); + } +} +``` + +### Agent Seeding (Per User) + +```javascript +// lib/seeds/agents.js +export async function seedAgents(userId) { + for (const agent of defaultAgents) { + await Agent.findOneAndUpdate( + { user: userId, name: agent.name }, + { $setOnInsert: { ...agent, user: userId } }, + { upsert: true } + ); + } +} +``` + +--- + +## API Endpoints + +### Agents + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/agents` | List all agents for current user | +| POST | `/api/agents` | Create new agent | +| GET | `/api/agents/:name` | Get single agent | +| PATCH | `/api/agents/:name` | Update agent | +| DELETE | `/api/agents/:name` | Delete agent (custom only) | +| POST | `/api/agents/:name/reset` | Reset to default (system only) | + +### Tools + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/tools` | List all tools | +| GET | `/api/tools/:name` | Get single tool | +| PATCH | `/api/tools/:name` | Toggle tool active status | + +--- + +## Agent Execution Flow + +```javascript +// lib/agentRunner.js + +async function runChat(sessionId, userMessage, images) { + const session = await Session.findById(sessionId); + const userId = session.user; + + // Always start with router + const router = await Agent.findOne({ user: userId, isRouter: true }); + const response = await executeAgent(router, userMessage, images, session.messages, userId); + + // Track in session + await addMessageToSession(sessionId, { role: 'user', content: userMessage, images }); + await addMessageToSession(sessionId, { role: 'assistant', ...response }); + + return response; +} + +async function executeAgent(agent, message, images, history, userId) { + // Get active tools for this agent + const tools = await Tool.find({ + name: { $in: agent.tools }, + active: true + }); + + const functions = tools.map(formatForOpenAI); + + // Call OpenAI + let response = await callOpenAI({ + model: agent.model, + messages: [ + { role: "system", content: agent.instructions }, + ...history, + buildUserMessage(message, images) + ], + tools: functions, + temperature: agent.temperature + }); + + // Process tool calls + while (response.tool_calls) { + const results = []; + + for (const call of response.tool_calls) { + const tool = tools.find(t => t.name === call.function.name); + const args = JSON.parse(call.function.arguments); + + if (tool.handler.startsWith('agents.')) { + // Invoke specialist agent + const specialistName = tool.handler.replace('agents.run', '').toLowerCase(); + const specialist = await Agent.findOne({ user: userId, name: specialistName }); + const result = await executeAgent(specialist, args.task, images, history, userId); + results.push({ call, result }); + } else { + // Execute DB/utility tool + const result = await executeHandler(tool.handler, args, userId); + results.push({ call, result }); + } + } + + // Continue with tool results + response = await callOpenAI({ + model: agent.model, + messages: [ + ...previousMessages, + { role: "assistant", tool_calls: response.tool_calls }, + ...results.map(r => ({ + role: "tool", + tool_call_id: r.call.id, + content: JSON.stringify(r.result) + })) + ], + tools: functions + }); + } + + return { + content: response.content, + agent: agent.name, + toolCalls: response.tool_calls + }; +} +``` diff --git a/specification/ai-engine.md b/specification/ai-engine.md new file mode 100644 index 0000000..f4aa745 --- /dev/null +++ b/specification/ai-engine.md @@ -0,0 +1,274 @@ +# AI Engine + +## Overview + +The AI engine orchestrates conversations between users and the agent system. It handles message routing, tool execution, and conversation flow. + +For agent definitions and tool specifications, see [agents.md](./agents.md). + +## Architecture + +``` +User Input (text/image/voice) + | + v ++---------------------+ +| Router Agent | +| | +| Tools: | +| - runModule |---> Module Agent +| - runInventory |---> Inventory Agent +| - runSearch |---> Search Agent ++---------------------+ + | + v ++---------------------------------------------+ +| Tool Layer | +| Repository Layer handles all DB operations | ++---------------------------------------------+ + | + v + [ MongoDB ] +``` + +## Agent Runner + +The agent runner executes agents and handles tool calls recursively. + +```javascript +// lib/agentRunner.js + +async function runChat(sessionId, userMessage, images) { + const session = await Session.findById(sessionId); + const userId = session.user; + + // Always start with router + const router = await Agent.findOne({ user: userId, isRouter: true }); + const response = await executeAgent(router, userMessage, images, session.messages, userId); + + // Track in session + await addMessageToSession(sessionId, { role: 'user', content: userMessage, images }); + await addMessageToSession(sessionId, { role: 'assistant', ...response }); + + return response; +} + +async function executeAgent(agent, message, images, history, userId) { + // Get active tools for this agent + const tools = await Tool.find({ + name: { $in: agent.tools }, + active: true + }); + + const functions = tools.map(formatForOpenAI); + + // Call OpenAI + let response = await callOpenAI({ + model: agent.model, + messages: [ + { role: "system", content: agent.instructions }, + ...history, + buildUserMessage(message, images) + ], + tools: functions, + temperature: agent.temperature + }); + + // Process tool calls + while (response.tool_calls) { + const results = []; + + for (const call of response.tool_calls) { + const tool = tools.find(t => t.name === call.function.name); + const args = JSON.parse(call.function.arguments); + + if (tool.handler.startsWith('agents.')) { + // Invoke specialist agent + const specialistName = tool.handler.replace('agents.run', '').toLowerCase(); + const specialist = await Agent.findOne({ user: userId, name: specialistName }); + const result = await executeAgent(specialist, args.task, images, history, userId); + results.push({ call, result }); + } else { + // Execute via repository layer + const result = await executeHandler(tool.handler, args, userId); + results.push({ call, result }); + } + } + + // Continue with tool results + response = await callOpenAI({ + model: agent.model, + messages: [ + ...previousMessages, + { role: "assistant", tool_calls: response.tool_calls }, + ...results.map(r => ({ + role: "tool", + tool_call_id: r.call.id, + content: JSON.stringify(r.result) + })) + ], + tools: functions + }); + } + + return { + content: response.content, + agent: agent.name, + toolCalls: response.tool_calls + }; +} +``` + +## Tool Handler Execution + +Tool handlers map to repository methods. The handler string (e.g., `db.items.create`) maps to repository functions. + +```javascript +// lib/toolHandlers.js +import * as itemRepo from '@/repositories/itemRepository'; +import * as moduleRepo from '@/repositories/moduleRepository'; +import * as paramRepo from '@/repositories/parameterRepository'; +// ... + +const handlers = { + 'db.items.search': itemRepo.search, + 'db.items.create': itemRepo.create, + 'db.items.update': itemRepo.update, + 'db.items.delete': itemRepo.remove, + 'db.modules.search': moduleRepo.search, + 'db.modules.create': moduleRepo.create, + 'db.modules.update': moduleRepo.update, + 'db.templates.search': templateRepo.search, + 'db.templates.create': templateRepo.create, + 'db.params.search': paramRepo.search, + 'db.params.create': paramRepo.create, + 'db.units.search': unitRepo.search, + 'db.units.create': unitRepo.create, + 'util.validatePath': validatePath, + 'agents.runModule': null, // Handled specially in executeAgent + 'agents.runInventory': null, + 'agents.runSearch': null, +}; + +async function executeHandler(handlerPath, args, userId) { + const handler = handlers[handlerPath]; + if (!handler) { + throw new Error(`Unknown handler: ${handlerPath}`); + } + + // All repository methods receive userId for scoping + return handler({ ...args, userId }); +} +``` + +## OpenAI Integration + +```javascript +// lib/openai.js +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY +}); + +export async function callOpenAI({ model, messages, tools, temperature }) { + const response = await openai.chat.completions.create({ + model, + messages, + tools: tools?.length ? tools.map(t => ({ + type: 'function', + function: { + name: t.name, + description: t.description, + parameters: t.parameters + } + })) : undefined, + temperature + }); + + return response.choices[0].message; +} + +export function formatForOpenAI(tool) { + return { + name: tool.name, + description: tool.description, + parameters: { + type: 'object', + properties: tool.parameters.reduce((acc, p) => { + acc[p.name] = { + type: p.type, + description: p.description, + enum: p.enum + }; + return acc; + }, {}), + required: tool.parameters.filter(p => p.required).map(p => p.name) + } + }; +} +``` + +## Multimodal Messages + +```javascript +function buildUserMessage(content, images) { + if (!images?.length) { + return { role: "user", content }; + } + + // Multimodal message with images + return { + role: "user", + content: [ + { type: "text", text: content }, + ...images.map(img => ({ + type: "image_url", + image_url: { url: img } // base64 or URL + })) + ] + }; +} +``` + +## Execution Flow Example + +``` +User: "This is MUSE, red cabinet, 11 levels with Plano boxes" + | + v + +---------------+ + | Router Agent | + +---------------+ + | + Tool call: runModuleAgent({ + task: "Create storage module MUSE - red cabinet with 11 levels" + }) + | + v + +---------------+ + | Module Agent | + +---------------+ + | + Tool call: searchTemplates({ query: "plano" }) + | + v + [Repository: templateRepository.search()] + | + v + [Returns existing plano templates] + | + Tool call: createModule({ + name: "MUSE", + dimensions: [...] + }) + | + v + [Repository: moduleRepository.create()] + | + v + "I've created the MUSE module with 11 levels..." + | + v + [Response to user] +``` diff --git a/specification/architecture.md b/specification/architecture.md new file mode 100644 index 0000000..a6999c8 --- /dev/null +++ b/specification/architecture.md @@ -0,0 +1,169 @@ +# WhereTF - Workshop Inventory System + +## Overview + +AI-powered inventory management system for workshop organization. Users describe items through voice, text, or images via a chat interface. The AI agent identifies items, assigns parameters, and stores location information. + +## Tech Stack + +- **Frontend**: Next.js (App Router) +- **Database**: MongoDB with Mongoose ODM +- **AI**: OpenAI (GPT-4o with vision for multimodal input) +- **Authentication**: NextAuth.js v5 with Google OAuth +- **Deployment**: Docker + +## Core Concepts + +### Parameters (not Labels) +Items are described by parameters with key/value/unit structure: +- `{ key: "material", value: "stainless steel" }` +- `{ key: "length", value: "10", unit: "mm" }` + +Parameter keys are not unique per item - an item can have multiple parameters with the same key (e.g., a pipe reducer with two different thread sizes). + +### Storage Modules +Storage units (cabinets, shelves, drawer units) are defined as modules with a hierarchical dimension structure. Modules describe what valid location paths look like but do not generate location records. + +### Location Paths +Items have a location path string like: +- `MUSE:level-3:row-2:col-5` +- `PRUSA:drawer-1:box-yellow:row-2:col-3` +- `FLUX:level-5:bin-8` + +Format: `MODULE:dimension-value:dimension-value:...` + +### Single Source of Truth +- Items are the source of truth for inventory +- Modules describe valid path structures (schema/validation) +- No separate Location collection - paths live on items + +### One Item Per Location +Each location path can only have one item (enforced by unique constraint). Items represent a category of things in a bin, not individual pieces (e.g., "5V relays" not "5V relay x 24"). + +### Multi-Tenant +All data is scoped to authenticated users. Each user has their own modules, items, sessions, etc. + +## Architecture + +``` ++-------------------------------------------------------------+ +| Next.js Frontend | +| +-------------------------------------------------------+ | +| | /auth/signin /chat /sessions /settings | | +| | Sign In Page Chat UI Session Agent | | +| | (Google OAuth) - Text History Config | | +| | - Images | | +| | - Voice | | +| | - Context % | | +| +-------------------------------------------------------+ | ++-------------------------------------------------------------+ + | + v ++-------------------------------------------------------------+ +| Next.js API Routes | +| +-------------------------------------------------------+ | +| | /api/auth/* /api/chat /api/sessions | | +| | NextAuth Agent Runner Session CRUD | | +| | | | +| | /api/agents /api/modules /api/items | | +| | Agent CRUD Module CRUD Item CRUD | | +| +-------------------------------------------------------+ | ++-------------------------------------------------------------+ + | + +---------------+---------------+ + v v v ++---------------+ +---------------+ +-------------------+ +| OpenAI | | Agent | | MongoDB | +| (GPT-4o) | | Runner | | | +| - Vision | | | | - users | +| - Chat | | Router Agent | | - sessions | ++---------------+ | | | | - items | + | v | | - modules | + | +-----------+ | | - templates | + | | Module | | | - agents | + | | Inventory | | | - tools | + | | Search | | | - parameterKeys | + | +-----------+ | | - units | + +---------------+ +-------------------+ +``` + +## Agent Architecture + +``` +User Input (text/image/voice) + | + v ++---------------------+ +| Router Agent | +| | +| Tools: | +| - runModuleAgent |---> Module Agent +| - runInventory |---> Inventory Agent +| - runSearchAgent |---> Search Agent ++---------------------+ + | + v ++---------------------------------------------+ +| Tool Layer | +| DB Tools: Agent Tools: | +| - createModule - runModuleAgent | +| - searchItems - runInventoryAgent | +| - createItem - runSearchAgent | +| - searchParams | +| - createParam | +| - validatePath | ++---------------------------------------------+ + | + v + [ MongoDB ] +``` + +## Pages + +| Route | Description | Auth Required | +|-------|-------------|---------------| +| `/` | Landing page | No | +| `/auth/signin` | Google sign in | No | +| `/chat` | Main chat interface | Yes | +| `/sessions` | Session history | Yes | +| `/settings/agents` | Agent configuration | Yes | +| `/settings/tools` | Tool configuration | Yes | + +## User Workflows + +### 1. Sign In +User signs in with Google OAuth. Redirected to chat. + +### 2. Define Storage Module +User shows a photo or describes a storage unit. AI creates a module definition describing valid paths. + +### 3. Inventory Items +User goes through bins describing contents. AI: +1. Identifies items from input (text/image/voice) +2. Proposes parameters (checking existing parameter keys first) +3. Confirms with user +4. Creates item with location path + +### 4. Find Items +User describes what they need. AI searches by: +- Text search on name/description +- Parameter matching +- Location prefix queries + +### 5. Manage Sessions +User can: +- View session history +- Resume previous sessions +- Compress sessions when context gets full +- Delete sessions + +## Specification Documents + +- [Data Models](./data-models.md) - MongoDB/Mongoose schemas +- [Agents](./agents.md) - AI agent definitions, tools, seed data +- [AI Engine](./ai-engine.md) - Agent runner, tool execution +- [Sessions](./sessions.md) - Session management, context tracking, compression +- [Authentication](./authentication.md) - NextAuth.js, Google OAuth, route protection +- [Frontend](./frontend.md) - UI layout, pages, components + +Also see [AGENTS.md](../AGENTS.md) in the project root for development patterns and conventions. diff --git a/specification/authentication.md b/specification/authentication.md new file mode 100644 index 0000000..13ce58c --- /dev/null +++ b/specification/authentication.md @@ -0,0 +1,351 @@ +# Authentication + +## Overview + +Authentication uses NextAuth.js (Auth.js) with Google as the OAuth provider. Users must sign in to access the application. + +## Tech Stack + +- **NextAuth.js v5** (Auth.js) - Authentication framework for Next.js +- **Google OAuth** - Identity provider +- **MongoDB** - Session and user storage (via NextAuth MongoDB adapter) + +## User Data Model + +```javascript +// schemas/User.js +// NextAuth manages most of this, but we extend with app-specific fields + +const userSchema = new Schema({ + // NextAuth managed fields + name: String, + email: { + type: String, + required: true, + unique: true + }, + emailVerified: Date, + image: String, // Google profile image + + // App-specific fields + role: { + type: String, + enum: ['user', 'admin'], + default: 'user' + }, + settings: { + defaultModel: { + type: String, + default: 'gpt-4o' + }, + theme: { + type: String, + enum: ['light', 'dark', 'system'], + default: 'system' + } + }, + lastActive: Date +}, { timestamps: true }); +``` + +## NextAuth Configuration + +```javascript +// lib/auth.js +import NextAuth from "next-auth"; +import Google from "next-auth/providers/google"; +import { MongoDBAdapter } from "@auth/mongodb-adapter"; +import clientPromise from "./mongodb"; + +export const { handlers, auth, signIn, signOut } = NextAuth({ + adapter: MongoDBAdapter(clientPromise), + providers: [ + Google({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET + }) + ], + callbacks: { + async session({ session, user }) { + // Add user ID to session + session.user.id = user.id; + session.user.role = user.role || 'user'; + return session; + }, + async signIn({ user, account, profile }) { + // Optional: restrict to specific domains + // if (!profile.email.endsWith('@yourdomain.com')) { + // return false; + // } + return true; + } + }, + pages: { + signIn: '/auth/signin', + error: '/auth/error' + } +}); +``` + +## Environment Variables + +```bash +# .env.local + +# NextAuth +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=your-secret-key-here # Generate with: openssl rand -base64 32 + +# Google OAuth +GOOGLE_CLIENT_ID=your-google-client-id +GOOGLE_CLIENT_SECRET=your-google-client-secret + +# MongoDB +MONGODB_URI=mongodb://localhost:27017/wheretf +``` + +## Google OAuth Setup + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select existing +3. Enable Google+ API +4. Go to Credentials → Create Credentials → OAuth Client ID +5. Application type: Web application +6. Authorized redirect URIs: + - Development: `http://localhost:3000/api/auth/callback/google` + - Production: `https://yourdomain.com/api/auth/callback/google` + +## API Routes + +### Auth Routes (handled by NextAuth) + +``` +GET /api/auth/signin - Sign in page +GET /api/auth/signout - Sign out +GET /api/auth/session - Get current session +GET /api/auth/providers - List providers +POST /api/auth/callback/* - OAuth callbacks +``` + +### Route Handler Setup + +```javascript +// app/api/auth/[...nextauth]/route.js +import { handlers } from "@/lib/auth"; +export const { GET, POST } = handlers; +``` + +## Middleware (Route Protection) + +```javascript +// middleware.js +import { auth } from "@/lib/auth"; + +export default auth((req) => { + const isLoggedIn = !!req.auth; + const isAuthPage = req.nextUrl.pathname.startsWith('/auth'); + const isApiAuthRoute = req.nextUrl.pathname.startsWith('/api/auth'); + const isPublicRoute = req.nextUrl.pathname === '/'; + + // Allow auth routes + if (isApiAuthRoute) { + return; + } + + // Redirect logged-in users away from auth pages + if (isAuthPage && isLoggedIn) { + return Response.redirect(new URL('/chat', req.nextUrl)); + } + + // Protect all other routes + if (!isLoggedIn && !isAuthPage && !isPublicRoute) { + return Response.redirect(new URL('/auth/signin', req.nextUrl)); + } +}); + +export const config = { + matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] +}; +``` + +## Protected API Routes + +```javascript +// Example: app/api/chat/route.js +import { auth } from "@/lib/auth"; + +export async function POST(req) { + const session = await auth(); + + if (!session) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userId = session.user.id; + // ... handle request with userId +} +``` + +## Client Components + +### Session Provider + +```javascript +// app/providers.js +"use client"; + +import { SessionProvider } from "next-auth/react"; + +export function Providers({ children }) { + return ( + + {children} + + ); +} + +// app/layout.js +import { Providers } from "./providers"; + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ); +} +``` + +### Using Session in Components + +```javascript +// components/UserMenu.jsx +"use client"; + +import { useSession, signIn, signOut } from "next-auth/react"; + +export function UserMenu() { + const { data: session, status } = useSession(); + + if (status === "loading") { + return
Loading...
; + } + + if (!session) { + return ; + } + + return ( +
+ {session.user.name} + {session.user.name} + +
+ ); +} +``` + +## Sign In Page + +```javascript +// app/auth/signin/page.jsx +import { signIn } from "@/lib/auth"; + +export default function SignInPage() { + return ( +
+

WhereTF

+

Workshop Inventory System

+ +
{ + "use server"; + await signIn("google", { redirectTo: "/chat" }); + }} + > + +
+
+ ); +} +``` + +## Data Relationships + +All user-owned data includes a `user` reference: + +```javascript +// Sessions belong to users +const sessionSchema = new Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + // ... +}); + +// Items belong to users (multi-tenant) +const itemSchema = new Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + // ... +}); + +// Modules belong to users +const storageModuleSchema = new Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + // ... +}); +``` + +## Query Patterns with User Scope + +```javascript +// Always filter by user +const userItems = await Item.find({ user: session.user.id }); + +const userModules = await StorageModule.find({ user: session.user.id }); + +const userSessions = await Session.find({ user: session.user.id }) + .sort({ updatedAt: -1 }); +``` + +## UI Flow + +``` ++------------------+ +------------------+ +------------------+ +| Landing Page | --> | Sign In Page | --> | Chat Page | +| (public) | | Google OAuth | | (protected) | ++------------------+ +------------------+ +------------------+ + | + v + +------------------+ + | User Menu | + | - Profile | + | - Settings | + | - Sign Out | + +------------------+ +``` + +## Security Considerations + +1. **CSRF Protection** - NextAuth handles this automatically +2. **Session Security** - Sessions stored in MongoDB, not JWT by default +3. **HTTPS** - Required in production for OAuth +4. **Environment Variables** - Never commit secrets to git +5. **Domain Restriction** - Optionally restrict sign-in to specific email domains diff --git a/specification/data-models.md b/specification/data-models.md new file mode 100644 index 0000000..6b4b2c1 --- /dev/null +++ b/specification/data-models.md @@ -0,0 +1,269 @@ +# Data Models + +## ParameterKey + +Registry of known parameter keys. AI checks this before creating new keys to avoid duplication. + +```javascript +const parameterKeySchema = new Schema({ + key: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true + }, + description: String, + category: String, // "dimension", "material", "electrical", etc. + commonUnits: [String], // ["mm", "in", "cm"] - hints for the AI +}, { timestamps: true }); +``` + +**Examples:** +- `{ key: "length", category: "dimension", commonUnits: ["mm", "in", "cm"] }` +- `{ key: "voltage", category: "electrical", commonUnits: ["V", "mV"] }` +- `{ key: "material", category: "material" }` + + +## Unit + +Registry of known units. + +```javascript +const unitSchema = new Schema({ + name: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true + }, + fullName: String, // "millimeters" + type: String, // "length", "weight", "voltage", etc. + siConversion: Number, // optional, for future conversion features +}, { timestamps: true }); +``` + +**Examples:** +- `{ name: "mm", fullName: "millimeters", type: "length", siConversion: 0.001 }` +- `{ name: "V", fullName: "volts", type: "voltage" }` + + +## DimensionTemplate + +Reusable templates for common storage subdivisions (grids within drawers, Plano boxes, etc.). + +```javascript +const dimensionTemplateSchema = new Schema({ + name: { type: String, required: true, unique: true }, + description: String, // "Small SMD component box (yellow)" + dimensions: [{ + label: String, // "row", "col" + values: [String], // ["1","2","3","4"] + _id: false + }] +}, { timestamps: true }); +``` + +**Examples:** +```javascript +{ + name: "plano-4x6", + description: "Plano box with 4 rows, 6 columns", + dimensions: [ + { label: "row", values: ["1","2","3","4"] }, + { label: "col", values: ["1","2","3","4","5","6"] } + ] +} + +{ + name: "smd-box-small", + description: "Small SMD component box (4x6 grid)", + dimensions: [ + { label: "row", values: ["1","2","3","4"] }, + { label: "col", values: ["1","2","3","4","5","6"] } + ] +} + +{ + name: "smd-box-large", + description: "Large SMD component box (8x10 grid)", + dimensions: [ + { label: "row", values: ["1","2","3","4","5","6","7","8"] }, + { label: "col", values: ["1","2","3","4","5","6","7","8","9","10"] } + ] +} +``` + + +## StorageModule + +Describes valid path structure for a storage unit. Does not generate locations - just defines the schema for validation. + +```javascript +const dimensionSchema = new Schema({ + label: String, // "drawer", "box", "row", "col", "level", "bin" + values: [String], // ["1", "2"] or ["yellow", "blue", "green"] + template: { type: Schema.Types.ObjectId, ref: 'DimensionTemplate' }, + templateMapping: { // which value uses which template + type: Map, + of: String // value -> template name + }, + _id: false +}); + +const storageModuleSchema = new Schema({ + name: { type: String, required: true, unique: true }, + description: String, // "Red cabinet with Plano boxes" + dimensions: [dimensionSchema] +}, { timestamps: true }); +``` + +**Examples:** + +### FLUX (blue bin shelves) +```javascript +{ + name: "FLUX", + description: "Blue bin shelf unit", + dimensions: [ + { label: "level", values: ["1","2","3","4","5","6","7","8"] }, + { label: "bin", values: ["1","2","3","4","5","6"] } + ] +} +// Paths: FLUX:level-1:bin-1 through FLUX:level-8:bin-6 +``` + +### MUSE (red cabinet with Plano boxes) +```javascript +{ + name: "MUSE", + description: "Red cabinet with Plano boxes", + dimensions: [ + { + label: "level", + values: ["1","2","3","4","5","6","7","8","9","10","11"], + templateMapping: { + "1": "plano-4x6", + "2": "plano-4x6", + // ... 3-8 same + "9": "plano-3x4", + "10": "plano-3x4", + "11": "plano-3x4" + } + } + ] +} +// Paths: MUSE:level-1:row-1:col-1 through MUSE:level-11:row-3:col-4 +``` + +### PRUSA (drawer unit with SMD boxes) +```javascript +{ + name: "PRUSA", + description: "White drawer unit with SMD component boxes", + dimensions: [ + { label: "drawer", values: ["1", "2"] }, + { + label: "box", + values: ["yellow", "blue", "green", "pink", "white"], + templateMapping: { + "yellow": "smd-box-small", + "blue": "smd-box-large", + "green": "smd-box-large", + "pink": "smd-box-large", + "white": "smd-box-large" + } + } + ] +} +// Paths: PRUSA:drawer-1:box-yellow:row-1:col-1 through PRUSA:drawer-2:box-white:row-8:col-10 +``` + + +## Item + +The actual inventory. One item per location. + +```javascript +const parameterValueSchema = new Schema({ + key: { type: String, required: true, lowercase: true }, + value: { type: String, required: true }, + unit: { type: String, lowercase: true }, +}, { _id: false }); + +const itemSchema = new Schema({ + name: { type: String, required: true }, + description: String, + parameters: [parameterValueSchema], + location: { + type: String, // "PRUSA:drawer-1:box-yellow:row-2:col-3" + required: true, + unique: true, + index: true + } +}, { timestamps: true }); + +// Text index for searching by name/description +itemSchema.index({ name: 'text', description: 'text' }); + +// Index for parameter-based queries +itemSchema.index({ 'parameters.key': 1, 'parameters.value': 1 }); +``` + +**Examples:** +```javascript +{ + name: "10k ohm resistors", + description: "1/4 watt, 5% tolerance, through-hole", + parameters: [ + { key: "resistance", value: "10000", unit: "ohm" }, + { key: "power", value: "0.25", unit: "W" }, + { key: "tolerance", value: "5", unit: "%" }, + { key: "type", value: "through-hole" } + ], + location: "MUSE:level-3:row-2:col-5" +} + +{ + name: "Pipe fitting union reducer", + description: "Brass reducer, 1/2 inch to 1/4 inch", + parameters: [ + { key: "thread_size", value: "0.5", unit: "in" }, + { key: "thread_size", value: "0.25", unit: "in" }, + { key: "material", value: "brass" }, + { key: "type", value: "union reducer" } + ], + location: "FLUX:level-3:bin-7" +} +``` + +Note: Parameter keys can repeat on an item (see thread_size example above). + + +## Query Patterns + +### Find all items in a module +```javascript +Item.find({ location: /^FLUX:/ }) +``` + +### Find items in a specific drawer +```javascript +Item.find({ location: /^PRUSA:drawer-1:/ }) +``` + +### Find by parameter +```javascript +Item.find({ 'parameters.key': 'resistance', 'parameters.value': '10000' }) +``` + +### Text search +```javascript +Item.find({ $text: { $search: "resistor 10k" } }) +``` + +### Exact location +```javascript +Item.findOne({ location: "MUSE:level-3:row-2:col-5" }) +``` diff --git a/specification/frontend.md b/specification/frontend.md new file mode 100644 index 0000000..5d44138 --- /dev/null +++ b/specification/frontend.md @@ -0,0 +1,431 @@ +# Frontend Specification + +## Overview + +Next.js App Router application with a simple layout: sidebar navigation + main content area. The primary interface is an AI chat window. + +## Pages + +| Route | Description | Auth | +|-------|-------------|------| +| `/` | Landing page with sign-in CTA | No | +| `/auth/signin` | Google OAuth sign-in | No | +| `/auth/error` | Auth error page | No | +| `/chat` | Main chat interface (new session) | Yes | +| `/chat/:sessionId` | Chat with specific session | Yes | +| `/sessions` | Session history list | Yes | +| `/settings` | Settings overview | Yes | +| `/settings/agents` | Agent list | Yes | +| `/settings/agents/new` | Create new agent | Yes | +| `/settings/agents/:name` | Edit agent | Yes | +| `/settings/tools` | Tool list (toggle active) | Yes | + +## Directory Structure + +``` +app/ +├── layout.js # Root layout (providers) +├── page.js # Landing page (public) +├── globals.css +├── auth/ +│ ├── signin/page.js +│ └── error/page.js +└── (protected)/ # Route group - requires auth + ├── layout.js # Sidebar + header layout + ├── chat/ + │ ├── page.js # New chat + │ └── [sessionId]/page.js + ├── sessions/ + │ └── page.js + └── settings/ + ├── page.js + ├── agents/ + │ ├── page.js + │ ├── new/page.js + │ └── [name]/page.js + └── tools/ + └── page.js + +components/ +├── layout/ +│ ├── Sidebar.jsx +│ ├── Header.jsx +│ └── UserMenu.jsx +├── chat/ +│ ├── ChatContainer.jsx +│ ├── MessageList.jsx +│ ├── Message.jsx +│ ├── MessageInput.jsx +│ ├── ImageUpload.jsx +│ ├── VoiceInput.jsx +│ └── ContextIndicator.jsx +├── agents/ +│ ├── AgentList.jsx +│ ├── AgentCard.jsx +│ ├── AgentEditor.jsx +│ └── ToolSelector.jsx +└── sessions/ + ├── SessionList.jsx + └── SessionCard.jsx + +lib/ +├── auth.js # NextAuth config +├── mongodb.js # MongoDB connection +├── agentRunner.js # Agent execution +├── contextManager.js # Token tracking +└── seeds/ + ├── index.js + ├── agents.js + └── tools.js +``` + +## Layout + +### Protected Layout (sidebar + content) + +``` ++------------------------------------------------------------------+ +| LOGO WhereTF 👤 | ++----------+-------------------------------------------------------+ +| | | +| Chat | | +| | | +| Sessions | MAIN CONTENT | +| | | +| -------- | | +| | | +| Settings | | +| Agents | | +| Tools | | +| | | ++----------+-------------------------------------------------------+ +``` + +### Sidebar Navigation + +``` ++------------------+ +| WhereTF | ++------------------+ +| | +| 💬 Chat | <- /chat +| | +| 📋 Sessions | <- /sessions +| | +| ──────────── | +| | +| ⚙️ Settings | <- /settings +| Agents | <- /settings/agents +| Tools | <- /settings/tools +| | ++------------------+ +| 👤 Nick | +| Sign Out | ++------------------+ +``` + +--- + +## Chat Interface (`/chat`) + +The primary interface. Full-height chat window with input at bottom. + +``` ++------------------------------------------------------------------+ +| Chat: Workshop Inventory Context: 62% ⚠️ | ++------------------------------------------------------------------+ +| | +| USER | +| This is MUSE, red cabinet, 11 levels with Plano boxes | +| 📷 [image thumbnail] | +| | +| ───────────────────────────────────────────────────────────── | +| | +| ASSISTANT (Module Agent) | +| I'll set that up. Here's the module I'll create: | +| | +| **MUSE** | +| - 11 levels | +| - Levels 1-8: 4×6 Plano grid | +| - Levels 9-11: 3×4 Plano grid | +| | +| Does this look right? | +| | +| ▶ Tool calls (2) | +| | +| ───────────────────────────────────────────────────────────── | +| | +| USER | +| Yes, create it | +| | +| ───────────────────────────────────────────────────────────── | +| | +| ASSISTANT (Module Agent) | +| ✓ Created MUSE module with 11 levels. | +| | ++------------------------------------------------------------------+ +| 📷 🎤 | Type a message... | Send | ++------------------------------------------------------------------+ +``` + +### Chat Features + +- **Message input**: Text area, Enter to send (Shift+Enter for newline) +- **Image upload**: Click button, drag & drop, or paste from clipboard +- **Voice input**: Web Speech API for voice-to-text +- **Context indicator**: Progress bar showing token usage (yellow at 75%, red at 90%) +- **Agent badge**: Shows which agent handled each response +- **Tool calls**: Collapsible section showing tool invocations +- **Markdown**: Render markdown in assistant messages + +--- + +## Sessions Page (`/sessions`) + +List of chat sessions with resume/delete actions. + +``` ++------------------------------------------------------------------+ +| Sessions [+ New Session] | ++------------------------------------------------------------------+ +| | +| ACTIVE | +| | +| +--------------------------------------------------------------+ | +| | Workshop Inventory | | +| | Last active: 5 minutes ago | 47 messages | Context: 62% | | +| | [Resume] [Delete] | | +| +--------------------------------------------------------------+ | +| | +| +--------------------------------------------------------------+ | +| | Electronics Sorting | | +| | Last active: 2 hours ago | 12 messages | Context: 15% | | +| | [Resume] [Delete] | | +| +--------------------------------------------------------------+ | +| | +| ARCHIVED | +| | +| +--------------------------------------------------------------+ | +| | Initial Setup (compressed) | | +| | Archived: Dec 14, 2024 | | +| | Summary: Created modules FLUX, MUSE, PRUSA... | | +| | [View] [Delete] | | +| +--------------------------------------------------------------+ | +| | ++------------------------------------------------------------------+ +``` + +--- + +## Agent List (`/settings/agents`) + +List of agents with edit/delete actions. + +``` ++------------------------------------------------------------------+ +| Agents [+ New Agent] | ++------------------------------------------------------------------+ +| | +| SYSTEM AGENTS | +| | +| +--------------------------------------------------------------+ | +| | Router [Edit] | | +| | Routes requests to specialist agents | | +| | Model: gpt-4o | Tools: 3 | | +| +--------------------------------------------------------------+ | +| | +| +--------------------------------------------------------------+ | +| | Module Agent [Edit] | | +| | Creates and manages storage modules | | +| | Model: gpt-4o | Tools: 5 | | +| +--------------------------------------------------------------+ | +| | +| +--------------------------------------------------------------+ | +| | Inventory Agent [Edit] | | +| | Adds and manages inventory items | | +| | Model: gpt-4o | Tools: 10 | | +| +--------------------------------------------------------------+ | +| | +| +--------------------------------------------------------------+ | +| | Search Agent [Edit] | | +| | Finds items in inventory | | +| | Model: gpt-4o | Tools: 2 | | +| +--------------------------------------------------------------+ | +| | +| CUSTOM AGENTS | +| | +| +--------------------------------------------------------------+ | +| | Electronics Sorter [Edit] [Delete] | | +| | Specialized for electronic components | | +| | Model: gpt-4o | Tools: 8 | | +| +--------------------------------------------------------------+ | +| | ++------------------------------------------------------------------+ +``` + +--- + +## Agent Editor (`/settings/agents/:name`) + +Form to edit agent configuration. + +``` ++------------------------------------------------------------------+ +| Edit Agent: Inventory Agent [← Back] | ++------------------------------------------------------------------+ +| | +| BASIC INFO | +| | +| Name [inventory ] (readonly) | +| Display Name [Inventory Agent ] | +| Description [Adds and manages inventory ] | +| | +| MODEL | +| | +| Model [gpt-4o ▼] | +| Temperature [0.7] --------●-------- (0.0 - 1.0) | +| | +| INSTRUCTIONS | +| | +| +--------------------------------------------------------------+ | +| | You help users catalog items in their storage. | | +| | | | +| | Your job: | | +| | 1. Identify items from text, images, or voice descriptions | | +| | 2. Search existing parameter keys before creating new ones | | +| | ... | | +| +--------------------------------------------------------------+ | +| | +| TOOLS | +| | +| Items Parameters | +| [x] searchItems [x] searchParams | +| [x] createItem [x] createParam | +| [x] updateItem [x] searchUnits | +| [x] deleteItem [x] createUnit | +| | +| Modules Utility | +| [x] searchModules [x] validatePath | +| [ ] createModule | +| [ ] updateModule | +| | +| [Reset to Default] [Save Changes] | ++------------------------------------------------------------------+ +``` + +--- + +## Tool List (`/settings/tools`) + +Read-only list of tools with active toggle. + +``` ++------------------------------------------------------------------+ +| Tools | ++------------------------------------------------------------------+ +| | +| Tools are defined in code. Toggle to enable/disable. | +| | +| AGENT RUNNERS | +| | +| [x] runModuleAgent | +| Invoke the Module Agent for storage module tasks | +| | +| [x] runInventoryAgent | +| Invoke the Inventory Agent for adding/updating items | +| | +| [x] runSearchAgent | +| Invoke the Search Agent for finding items | +| | +| ITEMS | +| | +| [x] searchItems | +| Search inventory items by name, parameters, location | +| | +| [x] createItem | +| Create a new inventory item | +| | +| [x] updateItem | +| Update an existing item | +| | +| [x] deleteItem | +| Delete an item | +| | +| MODULES | +| | +| [x] searchModules | +| [x] createModule | +| [x] updateModule | +| | +| ... | ++------------------------------------------------------------------+ +``` + +--- + +## API Endpoints + +### Chat + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/chat` | Send message, get AI response | + +### Sessions + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/sessions` | List user's sessions | +| POST | `/api/sessions` | Create new session | +| GET | `/api/sessions/:id` | Get session with messages | +| DELETE | `/api/sessions/:id` | Delete session | +| POST | `/api/sessions/:id/compress` | Compress session | + +### Agents + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/agents` | List agents | +| POST | `/api/agents` | Create agent | +| GET | `/api/agents/:name` | Get agent | +| PATCH | `/api/agents/:name` | Update agent | +| DELETE | `/api/agents/:name` | Delete agent | +| POST | `/api/agents/:name/reset` | Reset to default | + +### Tools + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/tools` | List tools | +| PATCH | `/api/tools/:name` | Toggle active | + +--- + +## State Management + +Minimal state management using: + +- **NextAuth SessionProvider** - Auth state +- **React Query / SWR** - Data fetching and caching +- **React Context** - Chat state (current session, messages) + +```javascript +// providers.js + {/* NextAuth */} + {/* React Query */} + {/* Chat context */} + {children} + + + +``` + +--- + +## Styling + +- **Tailwind CSS** - Utility-first CSS +- **shadcn/ui** - Component library (optional) +- **Dark mode** - System preference or toggle + +Simple, clean design. Focus on usability over aesthetics. diff --git a/specification/sessions.md b/specification/sessions.md new file mode 100644 index 0000000..c7128f1 --- /dev/null +++ b/specification/sessions.md @@ -0,0 +1,420 @@ +# Session Management + +## Overview + +Sessions track conversation history with the AI. Users can have multiple sessions, resume previous sessions, and compress sessions when context gets full. + +## Session Data Model + +```javascript +// schemas/Session.js +const messageSchema = new Schema({ + role: { + type: String, + enum: ['user', 'assistant', 'tool', 'system'], + required: true + }, + content: String, + images: [String], // URLs or base64 for uploaded images + toolCalls: [{ + id: String, + name: String, + arguments: Schema.Types.Mixed, + result: Schema.Types.Mixed + }], + agent: String, // Which agent handled this message + tokenCount: Number, // Track tokens for this message + timestamp: { type: Date, default: Date.now } +}, { _id: false }); + +const sessionSchema = new Schema({ + user: { // Owner of this session + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + name: String, // Optional user-provided name + messages: [messageSchema], + totalTokens: { // Running total + type: Number, + default: 0 + }, + maxTokens: { // Context window limit (model-dependent) + type: Number, + default: 128000 // GPT-4o default + }, + status: { + type: String, + enum: ['active', 'archived', 'compressed'], + default: 'active' + }, + compressedSummary: String, // If compressed, store summary here + parentSession: { // If this was split from another session + type: Schema.Types.ObjectId, + ref: 'Session' + } +}, { timestamps: true }); + +// Virtual for context usage percentage +sessionSchema.virtual('contextUsage').get(function() { + return Math.round((this.totalTokens / this.maxTokens) * 100); +}); + +// Index for listing user's sessions +sessionSchema.index({ user: 1, updatedAt: -1 }); +``` + +## Context Tracking + +### Thresholds + +```javascript +const WARNING_THRESHOLD = 0.75; // 75% - show warning +const CRITICAL_THRESHOLD = 0.90; // 90% - strongly suggest compression +``` + +### Token Estimation + +```javascript +// lib/contextManager.js + +function estimateTokens(message) { + let count = 0; + + if (message.content) { + // Rough estimate: ~4 characters per token + count += Math.ceil(message.content.length / 4); + } + + if (message.images?.length) { + // GPT-4o: ~765 tokens for low-res, ~1105 for high-res per image + count += message.images.length * 1000; + } + + if (message.toolCalls?.length) { + count += Math.ceil(JSON.stringify(message.toolCalls).length / 4); + } + + return count; +} +``` + +### Adding Messages + +```javascript +async function addMessageToSession(sessionId, message) { + const session = await Session.findById(sessionId); + + // Estimate tokens + const tokenEstimate = estimateTokens(message); + + session.messages.push({ + ...message, + tokenCount: tokenEstimate, + timestamp: new Date() + }); + session.totalTokens += tokenEstimate; + + await session.save(); + + // Return context status + const usage = session.totalTokens / session.maxTokens; + + return { + session, + contextStatus: { + used: session.totalTokens, + max: session.maxTokens, + percentage: Math.round(usage * 100), + warning: usage >= WARNING_THRESHOLD, + critical: usage >= CRITICAL_THRESHOLD + } + }; +} +``` + +## Context Compression + +When context gets full, compress the session into a summary and start fresh. + +```javascript +// lib/contextCompression.js + +async function compressSession(sessionId) { + const session = await Session.findById(sessionId); + + // Use AI to summarize the conversation + const summary = await callOpenAI({ + model: "gpt-4o-mini", // Cheaper model for summarization + messages: [ + { + role: "system", + content: `Summarize this inventory management conversation. Include: + - Storage modules created/modified (with their structure) + - Items added (with locations and key parameters) + - Key decisions made + - Any pending tasks or questions + + Keep it concise but preserve important details for continuing the conversation.` + }, + { + role: "user", + content: JSON.stringify(session.messages.map(m => ({ + role: m.role, + content: m.content, + agent: m.agent + }))) + } + ] + }); + + // Archive current session + session.status = 'compressed'; + session.compressedSummary = summary; + await session.save(); + + // Create new session with summary as context + const newSession = await Session.create({ + user: session.user, + name: session.name, + parentSession: session._id, + messages: [{ + role: 'system', + content: `Previous conversation summary:\n\n${summary}\n\nContinue helping the user with their workshop inventory.`, + tokenCount: estimateTokens({ content: summary }) + }], + totalTokens: estimateTokens({ content: summary }) + }); + + return newSession; +} +``` + +### Session Chain + +Get full history for a session (including parent sessions). + +```javascript +async function getSessionChain(sessionId) { + const sessions = []; + let current = await Session.findById(sessionId); + + while (current) { + sessions.unshift(current); + if (current.parentSession) { + current = await Session.findById(current.parentSession); + } else { + current = null; + } + } + + return sessions; +} +``` + +## API Endpoints + +### POST /api/chat + +Send a message to the AI. + +**Request:** +```json +{ + "sessionId": "abc123", + "message": "Add 10k resistors to level 3 row 2", + "images": ["data:image/jpeg;base64,..."] +} +``` + +**Response:** +```json +{ + "message": { + "role": "assistant", + "content": "I've added the 10k resistors to MUSE:level-3:row-2:col-5", + "agent": "inventory" + }, + "context": { + "used": 45000, + "max": 128000, + "percentage": 35, + "warning": false, + "critical": false + }, + "sessionId": "abc123" +} +``` + +**Response (warning threshold):** +```json +{ + "message": { ... }, + "context": { + "used": 98000, + "max": 128000, + "percentage": 77, + "warning": true, + "critical": false, + "suggestion": "Context is getting full. Consider compressing this session." + } +} +``` + +### GET /api/sessions + +List user's sessions. + +**Response:** +```json +{ + "sessions": [ + { + "id": "abc123", + "name": "Workshop Inventory", + "status": "active", + "contextUsage": 35, + "messageCount": 23, + "updatedAt": "2024-12-15T10:30:00Z" + }, + { + "id": "def456", + "name": "Initial Setup", + "status": "compressed", + "compressedSummary": "Created modules FLUX, MUSE, PRUSA...", + "updatedAt": "2024-12-14T15:00:00Z" + } + ] +} +``` + +### POST /api/sessions + +Create a new session. + +**Request:** +```json +{ + "name": "Electronics Inventory" +} +``` + +**Response:** +```json +{ + "session": { + "id": "ghi789", + "name": "Electronics Inventory", + "status": "active", + "contextUsage": 0, + "messageCount": 0 + } +} +``` + +### POST /api/sessions/:id/compress + +Compress a session. + +**Response:** +```json +{ + "oldSession": { + "id": "abc123", + "status": "compressed" + }, + "newSession": { + "id": "jkl012", + "name": "Workshop Inventory", + "status": "active", + "contextUsage": 5, + "parentSession": "abc123" + } +} +``` + +### GET /api/sessions/:id + +Get session details including messages. + +**Response:** +```json +{ + "session": { + "id": "abc123", + "name": "Workshop Inventory", + "status": "active", + "contextUsage": 35, + "messages": [ + { + "role": "user", + "content": "This is MUSE, a red cabinet...", + "timestamp": "2024-12-15T10:00:00Z" + }, + { + "role": "assistant", + "content": "I've created the MUSE module...", + "agent": "module", + "timestamp": "2024-12-15T10:00:05Z" + } + ] + } +} +``` + +### DELETE /api/sessions/:id + +Delete a session (and its children if compressed). + +## UI Components + +### Context Indicator + +Shows in chat header: + +``` ++------------------------------------------+ +| Chat: "Workshop Inventory" | +| Context: [=========> ] 77% ⚠️ | +| [Compress Session] | ++------------------------------------------+ +``` + +### Session List (/sessions) + +``` ++------------------------------------------+ +| Sessions [New Session] | ++------------------------------------------+ +| > Workshop Inventory (active) 35% | +| Dec 15, 2024 - 23 messages | +| | +| > Initial Setup (compressed) -- | +| Dec 14, 2024 - archived | +| "Created modules FLUX, MUSE..." | +| | +| > Testing (active) 12% | +| Dec 15, 2024 - 5 messages | ++------------------------------------------+ +``` + +### Compression Dialog + +When user clicks "Compress Session": + +``` ++------------------------------------------+ +| Compress Session? | +| | +| This will: | +| - Summarize the current conversation | +| - Archive this session | +| - Start a new session with the summary | +| | +| You can still view the archived session | +| in your session history. | +| | +| [Cancel] [Compress] | ++------------------------------------------+ +``` diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/web/.nvmrc b/web/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/web/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/web/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/web/app/(protected)/chat/page.tsx b/web/app/(protected)/chat/page.tsx new file mode 100644 index 0000000..7f9405d --- /dev/null +++ b/web/app/(protected)/chat/page.tsx @@ -0,0 +1,13 @@ +import { Header } from '@/components/layout'; +import { ChatContainer } from '@/components/chat'; + +export default function ChatPage() { + return ( + <> +
+
+ +
+ + ); +} diff --git a/web/app/(protected)/layout.tsx b/web/app/(protected)/layout.tsx new file mode 100644 index 0000000..067b26f --- /dev/null +++ b/web/app/(protected)/layout.tsx @@ -0,0 +1,24 @@ +import { redirect } from 'next/navigation'; +import { auth } from '@/lib/auth'; +import { Sidebar } from '@/components/layout'; + +export default async function ProtectedLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await auth(); + + if (!session) { + redirect('/auth/signin'); + } + + return ( +
+ +
+ {children} +
+
+ ); +} diff --git a/web/app/(protected)/sessions/page.tsx b/web/app/(protected)/sessions/page.tsx new file mode 100644 index 0000000..45f54ca --- /dev/null +++ b/web/app/(protected)/sessions/page.tsx @@ -0,0 +1,26 @@ +import { Header } from '@/components/layout'; + +export default function SessionsPage() { + return ( + <> +
+
+
+
+

Your Sessions

+
+ +
+ + + +

No sessions yet

+

+ Start a chat to create your first session +

+
+
+
+ + ); +} diff --git a/web/app/(protected)/settings/agents/page.tsx b/web/app/(protected)/settings/agents/page.tsx new file mode 100644 index 0000000..a8440d8 --- /dev/null +++ b/web/app/(protected)/settings/agents/page.tsx @@ -0,0 +1,31 @@ +import { Header } from '@/components/layout'; + +export default function AgentsPage() { + return ( + <> +
+
+
+
+
+

AI Agents

+

+ Customize how AI agents behave and respond +

+
+
+ +
+ + + +

Agent management coming soon

+

+ Agents will be seeded on first use +

+
+
+
+ + ); +} diff --git a/web/app/(protected)/settings/page.tsx b/web/app/(protected)/settings/page.tsx new file mode 100644 index 0000000..b8cff76 --- /dev/null +++ b/web/app/(protected)/settings/page.tsx @@ -0,0 +1,55 @@ +import { Header } from '@/components/layout'; +import Link from 'next/link'; + +const settingsLinks = [ + { + href: '/settings/agents', + title: 'Agents', + description: 'Customize AI agent instructions and behavior', + icon: ( + + + + ), + }, + { + href: '/settings/tools', + title: 'Tools', + description: 'Enable or disable AI tools', + icon: ( + + + + + ), + }, +]; + +export default function SettingsPage() { + return ( + <> +
+
+
+
+ {settingsLinks.map((link) => ( + +
+ {link.icon} +
+
+

{link.title}

+

{link.description}

+
+ + ))} +
+
+
+ + ); +} diff --git a/web/app/(protected)/settings/tools/page.tsx b/web/app/(protected)/settings/tools/page.tsx new file mode 100644 index 0000000..f86ec53 --- /dev/null +++ b/web/app/(protected)/settings/tools/page.tsx @@ -0,0 +1,32 @@ +import { Header } from '@/components/layout'; + +export default function ToolsPage() { + return ( + <> +
+
+
+
+
+

AI Tools

+

+ Enable or disable tools available to AI agents +

+
+
+ +
+ + + + +

Tool management coming soon

+

+ Tools will be seeded on first use +

+
+
+
+ + ); +} diff --git a/web/app/api/agents/[name]/route.ts b/web/app/api/agents/[name]/route.ts new file mode 100644 index 0000000..2797c64 --- /dev/null +++ b/web/app/api/agents/[name]/route.ts @@ -0,0 +1,87 @@ +import { NextRequest } from 'next/server'; +import { auth } from '@/lib/auth'; +import { agentRepository } from '@/repositories'; + +type RouteContext = { params: Promise<{ name: string }> }; + +// GET /api/agents/[name] - Get a single agent +export async function GET(request: NextRequest, context: RouteContext) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { name } = await context.params; + + try { + const agent = await agentRepository.findByName(session.user.id, name); + if (!agent) { + return Response.json({ error: 'Agent not found' }, { status: 404 }); + } + return Response.json({ agent }); + } catch (error) { + console.error('Error getting agent:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to get agent' }, + { status: 500 } + ); + } +} + +// PATCH /api/agents/[name] - Update an agent +export async function PATCH(request: NextRequest, context: RouteContext) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { name } = await context.params; + + try { + const body = await request.json(); + const { displayName, description, instructions, aiModel, tools, temperature, active } = body; + + const agent = await agentRepository.update({ + userId: session.user.id, + name, + updates: { + displayName, + description, + instructions, + aiModel, + tools, + temperature, + ...(typeof active === 'boolean' && { active }), + }, + }); + + return Response.json({ agent }); + } catch (error) { + console.error('Error updating agent:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to update agent' }, + { status: 400 } + ); + } +} + +// DELETE /api/agents/[name] - Delete an agent +export async function DELETE(request: NextRequest, context: RouteContext) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { name } = await context.params; + + try { + await agentRepository.remove(session.user.id, name); + return Response.json({ success: true }); + } catch (error) { + console.error('Error deleting agent:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to delete agent' }, + { status: 400 } + ); + } +} diff --git a/web/app/api/agents/route.ts b/web/app/api/agents/route.ts new file mode 100644 index 0000000..10abaca --- /dev/null +++ b/web/app/api/agents/route.ts @@ -0,0 +1,66 @@ +import { NextRequest } from 'next/server'; +import { auth } from '@/lib/auth'; +import { agentRepository } from '@/repositories'; +import { ensureUserSeeded } from '@/lib/seeds'; + +// GET /api/agents - List all agents for current user +export async function GET() { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + // Ensure user has default agents seeded + await ensureUserSeeded(session.user.id); + + const agents = await agentRepository.list(session.user.id); + return Response.json({ agents }); + } catch (error) { + console.error('Error listing agents:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to list agents' }, + { status: 500 } + ); + } +} + +// POST /api/agents - Create a new agent +export async function POST(request: NextRequest) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + const body = await request.json(); + const { name, displayName, description, instructions, aiModel, tools, temperature } = body; + + if (!name || !instructions) { + return Response.json( + { error: 'Name and instructions are required' }, + { status: 400 } + ); + } + + const agent = await agentRepository.create({ + userId: session.user.id, + name, + displayName, + description, + instructions, + aiModel, + tools, + temperature, + isSystem: false, + }); + + return Response.json({ agent }, { status: 201 }); + } catch (error) { + console.error('Error creating agent:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to create agent' }, + { status: 400 } + ); + } +} diff --git a/web/app/api/auth/[...nextauth]/route.ts b/web/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..c55a45e --- /dev/null +++ b/web/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/lib/auth"; + +export const { GET, POST } = handlers; diff --git a/web/app/api/chat/route.ts b/web/app/api/chat/route.ts new file mode 100644 index 0000000..b19df5d --- /dev/null +++ b/web/app/api/chat/route.ts @@ -0,0 +1,101 @@ +import { NextRequest } from 'next/server'; +import { auth } from '@/lib/auth'; +import { sessionRepository } from '@/repositories'; +import { runChat } from '@/lib/agentRunner'; +import { estimateTokens, calculateContextStatus } from '@/lib/contextManager'; +import { ensureUserSeeded, seedGlobalDefaults } from '@/lib/seeds'; + +// POST /api/chat - Send a message to the AI +export async function POST(request: NextRequest) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + const body = await request.json(); + const { sessionId, message, images } = body as { + sessionId?: string; + message: string; + images?: string[]; + }; + + if (!message) { + return Response.json({ error: 'Message is required' }, { status: 400 }); + } + + // Ensure user has agents seeded + await ensureUserSeeded(session.user.id); + await seedGlobalDefaults(); + + // Get or create session + let chatSession; + if (sessionId) { + chatSession = await sessionRepository.findById(session.user.id, sessionId); + if (!chatSession) { + return Response.json({ error: 'Session not found' }, { status: 404 }); + } + } else { + // Create a new session + chatSession = await sessionRepository.create({ + userId: session.user.id, + name: message.slice(0, 50) + (message.length > 50 ? '...' : ''), + }); + } + + // Add user message to session + const userMessage = { + role: 'user' as const, + content: message, + images, + tokenCount: estimateTokens({ content: message, images }), + timestamp: new Date(), + }; + + await sessionRepository.addMessage(session.user.id, chatSession._id.toString(), userMessage); + + // Run the AI + const aiResponse = await runChat( + session.user.id, + message, + images, + chatSession.messages + ); + + // Add assistant message to session + const assistantMessage = { + role: 'assistant' as const, + content: aiResponse.content, + agent: aiResponse.agent, + toolCalls: aiResponse.toolCalls, + tokenCount: estimateTokens({ + content: aiResponse.content, + toolCalls: aiResponse.toolCalls, + }), + timestamp: new Date(), + }; + + const { contextStatus } = await sessionRepository.addMessage( + session.user.id, + chatSession._id.toString(), + assistantMessage + ); + + return Response.json({ + message: { + role: 'assistant', + content: aiResponse.content, + agent: aiResponse.agent, + toolCalls: aiResponse.toolCalls, + }, + context: contextStatus, + sessionId: chatSession._id.toString(), + }); + } catch (error) { + console.error('Error in chat:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Chat failed' }, + { status: 500 } + ); + } +} diff --git a/web/app/api/sessions/[id]/compress/route.ts b/web/app/api/sessions/[id]/compress/route.ts new file mode 100644 index 0000000..cb48702 --- /dev/null +++ b/web/app/api/sessions/[id]/compress/route.ts @@ -0,0 +1,90 @@ +import { NextRequest } from 'next/server'; +import { auth } from '@/lib/auth'; +import { sessionRepository } from '@/repositories'; +import OpenAI from 'openai'; + +type RouteContext = { params: Promise<{ id: string }> }; + +// POST /api/sessions/[id]/compress - Compress a session +export async function POST(request: NextRequest, context: RouteContext) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await context.params; + + try { + // Get the session to compress + const chatSession = await sessionRepository.findById(session.user.id, id); + if (!chatSession) { + return Response.json({ error: 'Session not found' }, { status: 404 }); + } + + if (chatSession.status === 'compressed') { + return Response.json({ error: 'Session is already compressed' }, { status: 400 }); + } + + // Generate summary using OpenAI + const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + }); + + const messagesForSummary = chatSession.messages.map((m) => ({ + role: m.role, + content: m.content, + agent: m.agent, + })); + + const summaryResponse = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'system', + content: `Summarize this inventory management conversation. Include: +- Storage modules created/modified (with their structure) +- Items added (with locations and key parameters) +- Key decisions made +- Any pending tasks or questions + +Keep it concise but preserve important details for continuing the conversation.`, + }, + { + role: 'user', + content: JSON.stringify(messagesForSummary), + }, + ], + max_tokens: 1000, + }); + + const summary = summaryResponse.choices[0]?.message?.content || 'Unable to generate summary'; + + // Compress the session + const { oldSession, newSession } = await sessionRepository.compress( + session.user.id, + id, + summary + ); + + return Response.json({ + oldSession: { + id: oldSession._id, + status: oldSession.status, + compressedSummary: oldSession.compressedSummary, + }, + newSession: { + id: newSession._id, + name: newSession.name, + status: newSession.status, + contextUsage: newSession.contextUsage, + parentSession: newSession.parentSession, + }, + }); + } catch (error) { + console.error('Error compressing session:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to compress session' }, + { status: 500 } + ); + } +} diff --git a/web/app/api/sessions/[id]/route.ts b/web/app/api/sessions/[id]/route.ts new file mode 100644 index 0000000..6126e4e --- /dev/null +++ b/web/app/api/sessions/[id]/route.ts @@ -0,0 +1,100 @@ +import { NextRequest } from 'next/server'; +import { auth } from '@/lib/auth'; +import { sessionRepository } from '@/repositories'; + +type RouteContext = { params: Promise<{ id: string }> }; + +// GET /api/sessions/[id] - Get a session with messages +export async function GET(request: NextRequest, context: RouteContext) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await context.params; + + try { + const chatSession = await sessionRepository.findById(session.user.id, id); + if (!chatSession) { + return Response.json({ error: 'Session not found' }, { status: 404 }); + } + + const contextStatus = await sessionRepository.getContextStatus(session.user.id, id); + + return Response.json({ + session: { + id: chatSession._id, + name: chatSession.name, + status: chatSession.status, + messages: chatSession.messages, + context: contextStatus, + compressedSummary: chatSession.compressedSummary, + parentSession: chatSession.parentSession, + createdAt: chatSession.createdAt, + updatedAt: chatSession.updatedAt, + }, + }); + } catch (error) { + console.error('Error getting session:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to get session' }, + { status: 500 } + ); + } +} + +// PATCH /api/sessions/[id] - Update session (rename) +export async function PATCH(request: NextRequest, context: RouteContext) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await context.params; + + try { + const body = await request.json(); + const { name } = body; + + if (!name) { + return Response.json({ error: 'Name is required' }, { status: 400 }); + } + + const chatSession = await sessionRepository.updateName(session.user.id, id, name); + + return Response.json({ + session: { + id: chatSession._id, + name: chatSession.name, + status: chatSession.status, + }, + }); + } catch (error) { + console.error('Error updating session:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to update session' }, + { status: 400 } + ); + } +} + +// DELETE /api/sessions/[id] - Delete a session +export async function DELETE(request: NextRequest, context: RouteContext) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { id } = await context.params; + + try { + await sessionRepository.remove(session.user.id, id); + return Response.json({ success: true }); + } catch (error) { + console.error('Error deleting session:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to delete session' }, + { status: 400 } + ); + } +} diff --git a/web/app/api/sessions/route.ts b/web/app/api/sessions/route.ts new file mode 100644 index 0000000..21b2420 --- /dev/null +++ b/web/app/api/sessions/route.ts @@ -0,0 +1,71 @@ +import { NextRequest } from 'next/server'; +import { auth } from '@/lib/auth'; +import { sessionRepository } from '@/repositories'; + +// GET /api/sessions - List all sessions for current user +export async function GET() { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + const sessions = await sessionRepository.list(session.user.id); + return Response.json({ + sessions: sessions.map((s) => ({ + id: s._id, + name: s.name, + status: s.status, + contextUsage: s.contextUsage, + messageCount: s.messages?.length || 0, + compressedSummary: s.compressedSummary, + parentSession: s.parentSession, + createdAt: s.createdAt, + updatedAt: s.updatedAt, + })), + }); + } catch (error) { + console.error('Error listing sessions:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to list sessions' }, + { status: 500 } + ); + } +} + +// POST /api/sessions - Create a new session +export async function POST(request: NextRequest) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + const body = await request.json().catch(() => ({})); + const { name } = body; + + const newSession = await sessionRepository.create({ + userId: session.user.id, + name, + }); + + return Response.json( + { + session: { + id: newSession._id, + name: newSession.name, + status: newSession.status, + contextUsage: 0, + messageCount: 0, + }, + }, + { status: 201 } + ); + } catch (error) { + console.error('Error creating session:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to create session' }, + { status: 400 } + ); + } +} diff --git a/web/app/api/tools/[name]/route.ts b/web/app/api/tools/[name]/route.ts new file mode 100644 index 0000000..f2ab02f --- /dev/null +++ b/web/app/api/tools/[name]/route.ts @@ -0,0 +1,50 @@ +import { NextRequest } from 'next/server'; +import { auth } from '@/lib/auth'; +import { toolRepository } from '@/repositories'; + +type RouteContext = { params: Promise<{ name: string }> }; + +// GET /api/tools/[name] - Get a single tool +export async function GET(request: NextRequest, context: RouteContext) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { name } = await context.params; + + try { + const tool = await toolRepository.findByName(name); + if (!tool) { + return Response.json({ error: 'Tool not found' }, { status: 404 }); + } + return Response.json({ tool }); + } catch (error) { + console.error('Error getting tool:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to get tool' }, + { status: 500 } + ); + } +} + +// PATCH /api/tools/[name] - Toggle tool active status +export async function PATCH(request: NextRequest, context: RouteContext) { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { name } = await context.params; + + try { + const tool = await toolRepository.toggleActive(name); + return Response.json({ tool }); + } catch (error) { + console.error('Error toggling tool:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to toggle tool' }, + { status: 400 } + ); + } +} diff --git a/web/app/api/tools/route.ts b/web/app/api/tools/route.ts new file mode 100644 index 0000000..580916c --- /dev/null +++ b/web/app/api/tools/route.ts @@ -0,0 +1,25 @@ +import { auth } from '@/lib/auth'; +import { toolRepository } from '@/repositories'; +import { seedGlobalDefaults } from '@/lib/seeds'; + +// GET /api/tools - List all tools +export async function GET() { + const session = await auth(); + if (!session?.user?.id) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + // Ensure tools are seeded + await seedGlobalDefaults(); + + const tools = await toolRepository.list(); + return Response.json({ tools }); + } catch (error) { + console.error('Error listing tools:', error); + return Response.json( + { error: error instanceof Error ? error.message : 'Failed to list tools' }, + { status: 500 } + ); + } +} diff --git a/web/app/auth/error/page.tsx b/web/app/auth/error/page.tsx new file mode 100644 index 0000000..0384793 --- /dev/null +++ b/web/app/auth/error/page.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { Suspense } from "react"; + +const errorMessages: Record = { + Configuration: "There is a problem with the server configuration.", + AccessDenied: "You do not have access to this application.", + Verification: "The verification link has expired or has already been used.", + Default: "An error occurred during authentication.", +}; + +function ErrorContent() { + const searchParams = useSearchParams(); + const error = searchParams.get("error") || "Default"; + const message = errorMessages[error] || errorMessages.Default; + + return ( + <> +
+

+ Authentication Error +

+

{message}

+
+ +
+ + Try Again + +
+ +

+ If this problem persists, please contact support. +

+ + ); +} + +export default function AuthErrorPage() { + return ( +
+
+ Loading...
}> + + +
+ + ); +} diff --git a/web/app/auth/signin/page.tsx b/web/app/auth/signin/page.tsx new file mode 100644 index 0000000..9dac661 --- /dev/null +++ b/web/app/auth/signin/page.tsx @@ -0,0 +1,56 @@ +import { signIn } from "@/lib/auth"; + +export default function SignInPage() { + return ( +
+
+
+

+ WhereTF +

+

+ AI-powered workshop inventory management +

+
+ +
+
{ + "use server"; + await signIn("google", { redirectTo: "/chat" }); + }} + > + +
+
+ +

+ Sign in to start organizing your workshop inventory +

+
+
+ ); +} diff --git a/web/app/favicon.ico b/web/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/web/app/globals.css b/web/app/globals.css new file mode 100644 index 0000000..21e6160 --- /dev/null +++ b/web/app/globals.css @@ -0,0 +1,18 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); +} diff --git a/web/app/layout.tsx b/web/app/layout.tsx new file mode 100644 index 0000000..69e84b9 --- /dev/null +++ b/web/app/layout.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import { Providers } from "./providers"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "WhereTF", + description: "AI-powered workshop inventory management", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/web/app/page.tsx b/web/app/page.tsx new file mode 100644 index 0000000..b77d88f --- /dev/null +++ b/web/app/page.tsx @@ -0,0 +1,117 @@ +import Link from 'next/link'; +import { auth } from '@/lib/auth'; +import { redirect } from 'next/navigation'; + +export default async function Home() { + const session = await auth(); + + // If logged in, redirect to chat + if (session) { + redirect('/chat'); + } + + return ( +
+ {/* Header */} +
+
WhereTF
+ + Sign In + +
+ + {/* Hero */} +
+
+

+ Never lose track of your stuff again +

+

+ WhereTF is an AI-powered inventory system for your workshop. + Just describe items with text, voice, or photos — the AI handles the rest. +

+ + Get Started + + + + +
+ + {/* Features */} +
+
+
+ + + +
+

Natural Language

+

+ Just describe your items naturally. "10k resistors in the red cabinet, level 3" +

+
+ +
+
+ + + + +
+

Photo Recognition

+

+ Snap a photo of components. AI identifies and catalogs them automatically. +

+
+ +
+
+ + + +
+

Smart Search

+

+ Find anything instantly. "Where are my M3 stainless screws?" +

+
+
+ + {/* How it works */} +
+

How it works

+
+ {[ + { step: '1', title: 'Define Storage', desc: 'Tell the AI about your cabinets, shelves, and bins' }, + { step: '2', title: 'Add Items', desc: 'Describe or photograph items as you store them' }, + { step: '3', title: 'Ask Questions', desc: 'Natural language queries to find anything' }, + { step: '4', title: 'Stay Organized', desc: 'AI helps maintain and update your inventory' }, + ].map((item) => ( +
+
+ {item.step} +
+

{item.title}

+

{item.desc}

+
+ ))} +
+
+
+ + {/* Footer */} +
+

+ Built for makers who have too much stuff and not enough memory. +

+
+
+ ); +} diff --git a/web/app/providers.tsx b/web/app/providers.tsx new file mode 100644 index 0000000..0635cd0 --- /dev/null +++ b/web/app/providers.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { SessionProvider } from 'next-auth/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useState } from 'react'; + +export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, // 1 minute + }, + }, + }) + ); + + return ( + + + {children} + + + ); +} diff --git a/web/components/chat/ChatContainer.tsx b/web/components/chat/ChatContainer.tsx new file mode 100644 index 0000000..c7a15f9 --- /dev/null +++ b/web/components/chat/ChatContainer.tsx @@ -0,0 +1,182 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { ChatMessage } from './ChatMessage'; +import { ChatInput } from './ChatInput'; +import { ContextIndicator } from './ContextIndicator'; + +interface Message { + role: 'user' | 'assistant'; + content: string; + agent?: string; + toolCalls?: { + id: string; + name: string; + arguments: Record; + result: unknown; + }[]; +} + +interface ContextStatus { + used: number; + max: number; + percentage: number; + warning: boolean; + critical: boolean; + suggestion?: string; +} + +interface ChatContainerProps { + sessionId?: string; + initialMessages?: Message[]; +} + +export function ChatContainer({ sessionId: initialSessionId, initialMessages = [] }: ChatContainerProps) { + const [messages, setMessages] = useState(initialMessages); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [sessionId, setSessionId] = useState(initialSessionId); + const [context, setContext] = useState(); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const sendMessage = async (content: string, images?: string[]) => { + setError(null); + setIsLoading(true); + + // Add user message immediately + const userMessage: Message = { role: 'user', content }; + setMessages((prev) => [...prev, userMessage]); + + try { + const response = await fetch('/api/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId, + message: content, + images, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Chat request failed'); + } + + // Update session ID if this was a new session + if (data.sessionId && !sessionId) { + setSessionId(data.sessionId); + // Update URL without page reload + window.history.replaceState({}, '', `/chat?session=${data.sessionId}`); + } + + // Add assistant message + const assistantMessage: Message = { + role: 'assistant', + content: data.message.content, + agent: data.message.agent, + toolCalls: data.message.toolCalls, + }; + setMessages((prev) => [...prev, assistantMessage]); + + // Update context status + if (data.context) { + setContext(data.context); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + // Remove the user message on error + setMessages((prev) => prev.slice(0, -1)); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* Messages area */} +
+ {messages.length === 0 ? ( +
+
+ + + +
+

Welcome to WhereTF

+

+ Your AI-powered workshop inventory assistant. Describe items, take photos, or ask questions about your inventory. +

+
+

Try saying:

+
    +
  • + + "Create a storage module called MUSE with 11 levels" +
  • +
  • + + "Add 10k resistors to MUSE level 3, row 2" +
  • +
  • + + "Where are my M3 screws?" +
  • +
+
+
+ ) : ( + <> + {messages.map((msg, idx) => ( + + ))} + {isLoading && ( +
+
+
+
+
+
+
+
+
+ )} + + )} +
+
+ + {/* Error display */} + {error && ( +
+ {error} +
+ )} + + {/* Context indicator */} + {context && ( +
+ +
+ )} + + {/* Input area */} + +
+ ); +} diff --git a/web/components/chat/ChatInput.tsx b/web/components/chat/ChatInput.tsx new file mode 100644 index 0000000..a851ed0 --- /dev/null +++ b/web/components/chat/ChatInput.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { useState, useRef, KeyboardEvent } from 'react'; + +interface ChatInputProps { + onSend: (message: string, images?: string[]) => void; + disabled?: boolean; +} + +export function ChatInput({ onSend, disabled }: ChatInputProps) { + const [message, setMessage] = useState(''); + const [images, setImages] = useState([]); + const fileInputRef = useRef(null); + + const handleSend = () => { + if (!message.trim() && images.length === 0) return; + onSend(message.trim(), images.length > 0 ? images : undefined); + setMessage(''); + setImages([]); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const handleImageUpload = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files) return; + + Array.from(files).forEach((file) => { + const reader = new FileReader(); + reader.onload = (e) => { + const base64 = e.target?.result as string; + setImages((prev) => [...prev, base64]); + }; + reader.readAsDataURL(file); + }); + + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const removeImage = (index: number) => { + setImages((prev) => prev.filter((_, i) => i !== index)); + }; + + return ( +
+ {images.length > 0 && ( +
+ {images.map((img, idx) => ( +
+ {`Upload + +
+ ))} +
+ )} +
+ + + +
+ +
+
+ + +
+
+

New Module

+

Define the primary dimension

+
+
+
+
+
+
+
+
+ + + + +
What are the subdivisions called?
+
+
+ + +
+
+
+
1Level 1
+
2Level 2
+
3Level 3
+
4Level 4
+
5Level 5
+
+ +
+
+ + +
+
+

New Module

+

Configure levels

+
+
+
+
+
+
+ + + + + + + + + + + +
LabelTypeNotes
+
+ + +
+ +
+
+ + +
+
+

New Module

+

Review and create

+
+
+
+
+
+
+
+
+
+
BENCH
+
Workbench pegboard and shelf unit, 5 levels
+
+
5 levels
+
+ + + + + + +
Level 1receptacle
Level 2receptacle
Level 3receptacle
Level 4receptacle
Level 5receptacle
+
+ +
+
+ + +
+
+
+
+

MUSE

+

Red metal cabinet, 11 shelf levels, under workbench

+
+ 11 levels + 7 inserts + 62% occupied +
+
+
+ + + + + + + + + + + + + + + + + +
LevelTypeInsertStatusOcc.
1rec.Plano 3600 #1active18/24
2rec.Plano 3600 #2active12/24
3rec.Plano 3600 #3active20/22
4rec.Plano 3600 #4active6/24
5rec.Plano 3600 #5active22/24
6rec.Plano 3600 #6active9/24
7rec.Plano 3600 #7active0/24
8rec.active
9rec.active
10rec.disabled
11rec.active
+
+
+ + +
+
+
+
+ +
+
+
+
+ + +
+
+

Templates

+ +
+ + + + + + + + + + +
NameTypeVersionDimensionsInstances
Plano 3600 Stowawayfixedv24 × 67
Gridfinity Baseplateparametricv11×1 – 12×124
Akro-Mils 30220fixedv11 × 112
Akro-Mils 30230fixedv11 × 16
+
+ + +
+
+
+

New Template

+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+

Plano 3600 Stowaway

+ fixed +
+

4-row × 6-column organizer with removable dividers. Standard Plano Stowaway form factor.

+ +

Version History

+ + + + + + +
VersionDimsPublishedInstances
v24 × 6Mar 15, 20265
v14 × 6Mar 1, 20262
+ + +

Instances

+
+
Plano 3600 #1MUSE → Level 1 (v2)
+
Plano 3600 #2MUSE → Level 2 (v2)
+
Plano 3600 #3MUSE → Level 3 (v2)
+
Plano 3600 #4MUSE → Level 4 (v1)
+
Plano 3600 #5MUSE → Level 5 (v2)
+
+
+
+
+
+
+
+ + +
+
+
+
+

LOUVER

+

Wall-mounted louvered panel, 8 rows, hanging bins

+
+ 8 rows + 12 inserts + Continuous-dimension +
+
+
+ + + + + + + + + + + + +
RowWidthUsedInserts
136 in14.5 in (40%)3
236 in22 in (61%)4
336 in8.75 in (24%)2
436 in10.875 in (30%)1
536 in0 in0
636 in0 in0
736 in0 in0
836 in0 in0
+
+
+
+
+

Row 1 — 36 in capacity

+
+
+
+
30220
+
30220
+
30230
+
+
+
+ 14.5 in used + 21.5 in remaining +
+
+
+ +
+
+
+
+
+
+ +
+
+ + + + + +
+ + + + + diff --git a/specification/storage-definition-design.md b/specification/storage-definition-design.md new file mode 100644 index 0000000..0759098 --- /dev/null +++ b/specification/storage-definition-design.md @@ -0,0 +1,408 @@ +# Storage Definition — UI/UX Specification + +Defines the workflows for creating and configuring physical storage structures: modules, levels, templates, and inserts. This is the setup phase — building the scaffold before items get assigned. + +Complements [storage-navigator-design.md](storage-navigator-design.md) (browsing and interacting with storage) and [storage-model.md](storage-model.md) (data model reference). + +--- + +## Scope + +In scope: +- Module CRUD (create, view, edit, delete) +- Level/drawer generation and per-level configuration +- Template CRUD and version publishing +- Insert creation and placement into receptacles +- SVG grid visualization as structural confirmation +- Associating templates with receptacle locations + +- Overrides: merge, divide, disable (grid-interactive operations) +- Continuous-dimension locations (louver panels, open shelves) + +Out of scope (covered elsewhere or deferred): +- Item assignment to locations (storage-navigator-design) +- Drag-and-drop insert relocation (future) +- Template community catalog (future) + +--- + +## Navigation + +Module management lives at `/modules`. Accessed from sidebar icon rail. + +``` +/modules — module list +/modules/new — module creation wizard +/modules/:id — module detail (level table + grid preview) +/modules/:id/levels/:id — level detail (insert config, grid view) +/templates — template list +/templates/new — template creation +/templates/:id — template detail + version history +``` + +--- + +## Module List (`/modules`) + +Card grid. Each card shows: +- Module name (prominent) +- Description (one line, truncated) +- Primary dimension summary: "11 levels" or "9 drawers" +- Occupancy bar — simple fill indicator (assigned locations / total leaf locations) + +**Actions:** +- Card click → navigate to module detail +- "New Module" button (top-right, prominent) → navigate to creation wizard + +**Empty state:** "No modules yet. Create your first storage module to start organizing." + +**Sort:** by name (default), by recently modified, by occupancy + +--- + +## Module Creation Wizard (`/modules/new`) + +Multi-step form. Not a modal — a full page. Modules are created infrequently, so a deliberate process is appropriate. + +### Step 1: Identity + +- **Name** — short identifier (e.g., "MUSE", "ALEX"). Required. +- **Description** — what this module physically is (e.g., "Red metal cabinet, 11 shelf levels, under workbench"). Optional. + +### Step 2: Primary Dimension + +- **Dimension label** — what are the top-level subdivisions called? Freeform text with suggestions: "level", "drawer", "shelf", "row", "bay". Required. +- **Count** — how many? Numeric input, minimum 1. Required. +- **Preview** — as the user types, show a vertical stack diagram of the generated levels with auto-labels. Labels follow the convention: sequential numbers (1, 2, 3...) by default. + +### Step 3: Level Configuration + +Table of the generated levels. Columns: +- **Label** — editable (default: "1", "2", "3"...) +- **Type** — dropdown: "receptacle" (default) or "fixed" +- **Notes** — freeform text, optional + +All levels start as receptacles. The user can change individual levels or multi-select + batch apply. + +**Batch operations** (via multi-select checkboxes): +- Set type (receptacle/fixed) for selected levels +- Set notes for selected levels + +### Step 4: Review & Create + +Summary card showing: +- Module name and description +- Dimension label and count +- Level configuration table (read-only) + +"Create Module" button. On success → navigate to module detail page. + +--- + +## Module Detail (`/modules/:id`) + +Two-panel layout within the main content area (sidebar remains). + +### Left: Module Info + Level Table + +**Module header** — name (editable inline), description (editable inline), dimension summary. + +**Level table** — all levels for this module. Columns: +- Label +- Type (receptacle / fixed) +- Insert (name of placed insert, or "—" if empty) +- Status (active / disabled + reason) +- Occupancy (filled / total leaf locations, or "—" if no sub-structure) + +Row click → selects level, updates right panel. + +**Actions on module:** +- Edit name/description (inline) +- Add levels (append to end) +- Delete module (with confirmation — destructive) + +**Actions on level (via row selection or level detail):** +- Place insert (if receptacle and empty) +- Remove insert (if receptacle and occupied) +- Configure fixed structure (if fixed type) +- Disable / enable +- Delete level + +### Right: Level Preview + +When a level is selected: +- If the level has sub-structure (insert placed or fixed template applied) → **SVG grid preview** showing the position layout. Labels on axes (rows alpha, columns numeric). Cells show occupancy state (empty, occupied, disabled). Read-only in this context — clicking a cell does nothing here (that's the navigator's job). +- If the level is an empty receptacle → "No insert placed. Place an insert to define this level's internal structure." with a "Place Insert" button. +- If the level is an empty fixed location → "No structure defined. Apply a template to define this level's layout." with an "Apply Template" button. + +When no level is selected → "Select a level to view its layout." + +--- + +## Place Insert Flow + +Triggered from level detail when the user clicks "Place Insert" on an empty receptacle. + +### Step 1: Choose Template + +Searchable list of templates. Each row shows: +- Template name +- Type (fixed / parametric) +- Dimensions (e.g., "4 rows × 6 columns") +- Version number + +Click a template → shows a grid preview of the template's positions below the list. + +### Step 2: Configure (parametric templates only) + +If the template is parametric, the user specifies dimensions: +- Grid size (e.g., 6 × 4 for a Gridfinity baseplate) +- Constrained by template's min/max + +Live grid preview updates as dimensions change. + +Fixed templates skip this step. + +### Step 3: Name & Confirm + +- **Insert name** — defaults to template name + auto-incrementing number (e.g., "Plano 3600 #4"). Editable. +- Grid preview showing the final layout within the level +- "Place Insert" button + +On confirm: +1. Insert record created (references template + version) +2. Child locations generated from template positions +3. Receptacle's compatibility is set by cloning the template's interface data +4. Level preview updates to show the new grid +5. Notification: "Plano 3600 #4 placed in MUSE Level 3" + +--- + +## Apply Template to Fixed Location + +Similar to Place Insert, but for fixed locations. The template is applied permanently — no insert record, locations are created directly as children of the fixed location. + +Same step sequence (choose template → configure if parametric → confirm), but: +- No insert name (there's no insert object) +- Messaging reflects permanence: "Apply Template" instead of "Place Insert" +- Notification: "Gridfinity 6×4 applied to ALEX Drawer 3" + +--- + +## Template List (`/templates`) + +Table view. Columns: +- Name +- Type (fixed / parametric) +- Current version +- Dimensions (rows × columns for latest version) +- Instance count (how many inserts + fixed applications use this template) + +**Actions:** +- Row click → navigate to template detail +- "New Template" button → navigate to template creation + +**Empty state:** "No templates defined. Create a template to define reusable storage layouts." + +--- + +## Template Creation (`/templates/new`) + +Single-page form (not a wizard — templates are simpler than modules). + +**Fields:** +- **Name** — e.g., "Plano 3600 Stowaway". Required. +- **Description** — optional. +- **Type** — fixed or parametric. Required. +- **Rows** — number. For parametric: this is the default, with min/max constraints. +- **Columns** — number. Same as rows. +- **Min/Max rows** (parametric only) +- **Min/Max columns** (parametric only) +- **Labeling scheme** — rows: alpha (default) or numeric. Columns: numeric (default) or alpha. +- **Origin** — which corner is position A1. Default: top-left. +- **Unit system** — metric or imperial. Default: metric. + +**Live preview** — SVG grid updates as the user changes rows/columns. Shows labels on axes. Gives immediate confirmation that the layout matches the physical product. + +"Create Template" button. Creates the template with version 1 containing the specified configuration. + +--- + +## Template Detail (`/templates/:id`) + +### Header +Template name (editable inline), description (editable inline), type badge, current version number. + +### Grid Preview +SVG rendering of the current version's layout. Same visual style as module level preview. + +### Version History +Table of versions, most recent first: +- Version number +- Dimensions (rows × columns) +- Date published +- Instance count (inserts/fixed locations using this version) + +"Publish New Version" button → opens a form pre-filled with the current version's values. Edit and publish. + +### Instances +List of inserts and fixed locations that reference this template, grouped by module. Each row shows: +- Insert name (or "fixed" for direct applications) +- Module name → level label +- Version applied + +--- + +## Overrides (Grid-Interactive Operations) + +Overrides modify the structure of a placed insert or a fixed location. All three types are accessed from the grid view — select cells, then apply the operation. + +### Merge + +Combine adjacent cells into a single larger cell. + +1. User selects two or more adjacent cells in the grid (click first, shift-click additional) +2. Selected cells highlight with accent border +3. "Merge" action appears in a toolbar above the grid +4. Click merge → cells combine into a single region, labeled by the origin cell +5. Merged region renders as one `` spanning the combined area (or `` for non-rectangular shapes) +6. Toast: "Merged A3–A4" with undo + +**Constraints:** +- Cells must be adjacent and contiguous +- Template may restrict merge axes (e.g., Plano rows are molded walls — merge within rows only, not across) +- Existing assignments at affected cells must be migrated or removed first (enforced, not warned) + +### Divide + +Split a cell into named child positions. + +1. User selects a single cell in the grid +2. "Divide" action appears in the toolbar +3. Click divide → panel shows options: + - **Template-defined subdivision** — if the template defines subdivision options (e.g., "front/rear divider"), show those as named presets + - **Ad-hoc** — user specifies count and labels (e.g., 2 children: "Left", "Right") +4. Preview shows the cell with internal subdivision lines +5. Confirm → child locations created, parent cell becomes non-assignable +6. Toast: "Divided B2 into Left, Right" with undo + +**Constraints:** +- Existing assignment at the cell must be reassigned to a child or elsewhere before dividing (enforced) + +### Disable + +Mark a cell as unavailable. + +1. User selects a cell in the grid +2. "Disable" action appears in the toolbar +3. Click disable → optional reason prompt (e.g., "cracked divider") +4. Cell renders with diagonal stripe fill, reduced opacity +5. Toast: "Disabled C5: cracked divider" with undo + +Re-enable: select a disabled cell → "Enable" action appears → restores cell to active state. + +**Constraints:** +- Existing assignment must be reassigned or removed first (enforced) + +--- + +## Continuous-Dimension Locations + +Some storage defines capacity by physical dimensions rather than discrete grid positions. Louver panels and open shelves are the primary examples. + +### Template Configuration + +When creating a template for continuous-dimension storage, additional fields: +- **Dimension type** — "discrete" (default, grid-based) or "continuous" +- **Row width** — total available width per row (e.g., 36 inches) +- **Row pitch** — vertical spacing between rows (e.g., 3.5 inches) +- **Overflow direction** — "down" (hanging, like louver panels) or "up" (sitting, like shelves) + +### Visualization + +Continuous-dimension levels render differently from grids: +- Each row is a horizontal bar showing total width +- Placed inserts appear as blocks within the bar, sized proportionally to their consumed width (insert width + buffer) +- Remaining capacity shown as empty space +- Utilization percentage displayed per row +- Overflow indicators: if an insert's height exceeds row pitch, a visual indicator extends into the adjacent row + +### Placement + +Placing an insert into a continuous-dimension location: +1. System checks dimensional fit (insert width + buffer ≤ remaining width) +2. Insert appears in the row visualization +3. Ordering within a row is optional — inserts can be reordered or treated as unordered + +--- + +## SVG Grid Visualization + +Used in three contexts during storage definition: +1. **Module detail** — level preview (read-only) +2. **Place insert / apply template** — preview of what will be created +3. **Template detail** — canonical layout preview + +### Rendering Rules + +- SVG within a React component. Scales to fit available width, maintains aspect ratio. +- Rows labeled on left axis (A, B, C... or per labeling scheme) +- Columns labeled on top axis (1, 2, 3... or per labeling scheme) +- Each cell is a `` with label text centered +- Cell border: `#475569` (slate-600) +- Cell fill: transparent (empty), `#dbeafe` light blue (occupied — has an assignment) +- Disabled cells: diagonal stripe pattern, reduced opacity +- Origin cell (A1): subtle accent dot in corner for orientation +- Hover: border thickens, shows cell label tooltip (DOM overlay) + +### Sizing + +- Cells are square by default, minimum 40px +- Grid scales down for large templates (many columns), minimum cell size 24px +- Horizontal scroll if the grid exceeds available width at minimum cell size + +--- + +## Empty States + +| Context | Message | Action | +|---|---|---| +| Module list, no modules | "No modules yet. Create your first storage module to start organizing." | "New Module" button | +| Module detail, no levels | Should not happen — levels are auto-generated | — | +| Level selected, empty receptacle | "No insert placed." | "Place Insert" button | +| Level selected, empty fixed | "No structure defined." | "Apply Template" button | +| Template list, no templates | "No templates defined. Create a template to define reusable storage layouts." | "New Template" button | +| Place insert, no templates exist | "No templates available. Create a template first." | Link to /templates/new | + +--- + +## Data Flow + +### Module creation +1. POST `/api/modules` → creates module +2. POST `/api/locations` × N → creates one location per level (children of module) +3. Each location: `moduleId`, `label`, `path` (e.g., "MUSE:3"), `locationType: "receptacle"` + +### Place insert +1. POST `/api/inserts` → creates insert record (references template + version) +2. POST `/api/inserts/:id/place` → places insert at receptacle location (validates compatibility) +3. System generates child locations from template version's position definitions +4. GET `/api/locations?moduleId=X` → refresh level table and grid preview + +### Apply template to fixed location +1. Locations created directly as children of the fixed location, referencing the template version +2. No insert record — the structure is permanent + +### Template creation +1. POST `/api/templates` → creates template with version 1 +2. Subsequent versions: POST `/api/templates/:id/versions` + +--- + +## Resolved Questions + +1. **Level reordering** — deferred. No clear use case yet. +2. **Module photos** — deferred. Metadata field supports it; upload UI comes later. +3. **Template sharing** — no import/export. Multi-tenant: templates promoted to system level via referential links, not per-tenant copies. +4. **Undo** — always implemented. Toast notification with undo button on every mutation. Undo via transaction log per ui-paradigms.md. Only omit undo when explicitly agreed. + From 66e0b550b7f2547d19b92c99875f08d6abbe5ed0 Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:34:10 -0400 Subject: [PATCH 054/179] Add merge axis constraint to templates --- prototypes/storage-def-v1.html | 12 +++++++++++- specification/storage-definition-design.md | 5 +++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/prototypes/storage-def-v1.html b/prototypes/storage-def-v1.html index c65c7d5..996709c 100644 --- a/prototypes/storage-def-v1.html +++ b/prototypes/storage-def-v1.html @@ -415,7 +415,7 @@

MUSE

- +
@@ -485,6 +485,16 @@

New Template

+
+ + +
Which directions can cells be merged? Depends on physical divider construction.
+
diff --git a/specification/storage-definition-design.md b/specification/storage-definition-design.md index 0759098..070bff0 100644 --- a/specification/storage-definition-design.md +++ b/specification/storage-definition-design.md @@ -119,7 +119,7 @@ Row click → selects level, updates right panel. **Actions on module:** - Edit name/description (inline) - Add levels (append to end) -- Delete module (with confirmation — destructive) +- Delete module (immediate with undo toast — per ui-paradigms.md, no confirmation dialog) **Actions on level (via row selection or level detail):** - Place insert (if receptacle and empty) @@ -219,6 +219,7 @@ Single-page form (not a wizard — templates are simpler than modules). - **Min/Max rows** (parametric only) - **Min/Max columns** (parametric only) - **Labeling scheme** — rows: alpha (default) or numeric. Columns: numeric (default) or alpha. +- **Merge axis** — which directions allow cell merging. Options: "columns only" (within rows — e.g., Plano 3600 where row walls are molded but column dividers are removable), "rows only" (within columns), "both" (default), "none". This constraint is enforced when the user attempts a merge operation on an insert using this template. - **Origin** — which corner is position A1. Default: top-left. - **Unit system** — metric or imperial. Default: metric. @@ -270,7 +271,7 @@ Combine adjacent cells into a single larger cell. **Constraints:** - Cells must be adjacent and contiguous -- Template may restrict merge axes (e.g., Plano rows are molded walls — merge within rows only, not across) +- Template's **merge axis** setting is enforced. If set to "columns only", the system blocks any merge that spans rows and explains why ("Row dividers on this template are permanent"). The merge action button is disabled for invalid selections. - Existing assignments at affected cells must be migrated or removed first (enforced, not warned) ### Divide From 6bb2e749bdc13413ea7877bc8d04807f30686c7c Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:35:33 -0400 Subject: [PATCH 055/179] Add selective version upgrade flow for template instances --- prototypes/storage-def-v1.html | 46 +++++++++++++++++++--- specification/storage-definition-design.md | 17 ++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/prototypes/storage-def-v1.html b/prototypes/storage-def-v1.html index 996709c..866fe17 100644 --- a/prototypes/storage-def-v1.html +++ b/prototypes/storage-def-v1.html @@ -534,13 +534,47 @@

Publish New Version -

Instances

+
+

Instances

+ +
-
Plano 3600 #1MUSE → Level 1 (v2)
-
Plano 3600 #2MUSE → Level 2 (v2)
-
Plano 3600 #3MUSE → Level 3 (v2)
-
Plano 3600 #4MUSE → Level 4 (v1)
-
Plano 3600 #5MUSE → Level 5 (v2)
+
+ + Plano 3600 #1 + MUSE → Level 1 + v2 +
+
+ + Plano 3600 #2 + MUSE → Level 2 + v2 +
+
+ + Plano 3600 #3 + MUSE → Level 3 + v2 +
+
+ + Plano 3600 #4 + MUSE → Level 4 + v1 — upgrade available +
+
+ + Plano 3600 #6 + MUSE → Level 6 + v1 — upgrade available +
+
+ + Plano 3600 #5 + MUSE → Level 5 + v2 +
diff --git a/specification/storage-definition-design.md b/specification/storage-definition-design.md index 070bff0..c26cf55 100644 --- a/specification/storage-definition-design.md +++ b/specification/storage-definition-design.md @@ -251,6 +251,23 @@ List of inserts and fixed locations that reference this template, grouped by mod - Insert name (or "fixed" for direct applications) - Module name → level label - Version applied +- **Upgrade indicator** — if the instance is on an older version, show a version badge with an "Upgrade" action + +### Version Upgrade Flow + +When a new version is published, existing instances stay on their current version. The user upgrades selectively: + +1. From the **Instances** list on the template detail page, instances on older versions show an "Upgrade available" indicator +2. User can select individual instances (checkboxes) or "Select all outdated" +3. Click "Upgrade Selected" → **preview panel** shows per-instance impact: + - **No conflicts** — structure is compatible, upgrade is clean. Shows before/after grid side by side. + - **Override conflicts** — the instance has overrides (merges, divides) that conflict with the new version's structure (e.g., new version removed a column that was part of a merge). Lists each conflict with resolution options: keep override, drop override, or skip this instance. + - **Assignment conflicts** — locations that exist in the old version but not the new have active assignments. Lists affected assignments with options: reassign to a new location, unassign, or skip this instance. +4. User resolves any conflicts per instance, or skips instances that need manual attention +5. Click "Apply Upgrades" → instances updated, child locations restructured, toast with undo +6. Skipped instances remain on the old version — no partial upgrades per instance + +The upgrade is a compound transaction — all changes for one instance are grouped and undone atomically. --- From 986a91091fc7f285af592f24abc895bac618a8a9 Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:36:54 -0400 Subject: [PATCH 056/179] Show version properties on template detail --- prototypes/storage-def-v1.html | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/prototypes/storage-def-v1.html b/prototypes/storage-def-v1.html index 866fe17..ef4caf8 100644 --- a/prototypes/storage-def-v1.html +++ b/prototypes/storage-def-v1.html @@ -524,6 +524,27 @@

Plano 3600 Stowaway

4-row × 6-column organizer with removable dividers. Standard Plano Stowaway form factor.

+

Current Version (v2) Properties

+
+
+ Dimensions + 4 rows × 6 columns + Row labels + Alpha (A, B, C, D) + Column labels + Numeric (1–6) + Origin + Top-left (A1) + Merge axis + Columns only — row dividers are permanent + Unit system + Imperial +
+
+ +
+
+

Version History

From 192da0cd82b1f1193d87536dcd22a1a2366553ac Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:43:45 -0400 Subject: [PATCH 057/179] Move properties to right panel, show permanent dividers and origin in grid --- prototypes/storage-def-v1.html | 72 +++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/prototypes/storage-def-v1.html b/prototypes/storage-def-v1.html index ef4caf8..3d2efcf 100644 --- a/prototypes/storage-def-v1.html +++ b/prototypes/storage-def-v1.html @@ -517,34 +517,13 @@

New Template

-
+

Plano 3600 Stowaway

fixed

4-row × 6-column organizer with removable dividers. Standard Plano Stowaway form factor.

-

Current Version (v2) Properties

-
-
- Dimensions - 4 rows × 6 columns - Row labels - Alpha (A, B, C, D) - Column labels - Numeric (1–6) - Origin - Top-left (A1) - Merge axis - Columns only — row dividers are permanent - Unit system - Imperial -
-
- -
-
-

Version History

VersionDimsPublishedInstances
@@ -555,10 +534,7 @@

Publish New Version -
-

Instances

- -
+

Instances

@@ -597,9 +573,32 @@

v2

+
+ +
-
-
+
+
+

v2 Layout

+
+ +
+

Properties

+
+
+ Dimensions + 4 rows × 6 columns + Merge axis + Columns only — row dividers permanent + Unit system + Imperial +
+
+ +
+
+
+
@@ -826,6 +825,23 @@

Place Insert — MUSE Level 8

} } } + // Permanent row dividers (merge axis = columns only → rows are permanent) + // Draw thick lines between rows to indicate non-mergeable boundaries + const showPermanentRows = (mode === 'plano' || mode === 'plano-merged' || mode === 'empty-tpl'); + if (showPermanentRows && rows > 1) { + for (let r = 1; r < rows; r++) { + const y = labelH + r * (cellSize + gap) - gap / 2; + const x1 = labelW; + const x2 = labelW + cols * (cellSize + gap) - gap; + html += ``; + } + } + + // Origin marker — small accent triangle at top-left of A1 + const originX = labelW + 2; + const originY = labelH + 2; + html += ``; + html += ``; if (showActions && mode !== 'empty-tpl') { From 21b43bb3e18295b6b1607570868a172d0ada254a Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:49:03 -0400 Subject: [PATCH 058/179] Template detail: selectable versions, active badge, apply-to-selected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Version rows clickable — updates grid preview and properties - v1 shows 4×4 layout, v2 shows 4×6 layout with permanent row dividers - Active version marked with green badge, "Set as Active" on non-active - "Apply vX to Selected" replaces "Upgrade Selected to v2" - Properties panel moved to right sidebar beside grid --- prototypes/storage-def-v1.html | 119 +++++++++++++++++++++++++++------ 1 file changed, 98 insertions(+), 21 deletions(-) diff --git a/prototypes/storage-def-v1.html b/prototypes/storage-def-v1.html index 3d2efcf..8831970 100644 --- a/prototypes/storage-def-v1.html +++ b/prototypes/storage-def-v1.html @@ -154,6 +154,10 @@ .toast .undo:hover { text-decoration: underline; } @keyframes slideIn { from { transform: translateY(10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } + /* Version rows */ + .version-row:hover { background: #1e293b; } + .version-row.selected-version { background: rgba(255, 102, 0, 0.08); } + /* Place insert modal */ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 50; display: flex; align-items: center; justify-content: center; } .modal { background: #1e293b; border: 1px solid #334155; border-radius: 10px; width: 680px; max-height: 80vh; display: flex; flex-direction: column; } @@ -526,10 +530,10 @@

Plano 3600 Stowaway

Version History

VersionDimsPublishedInstances
- + - - + +
VersionDimsPublishedInstances
VersionDimsPublishedInstances
v24 × 6Mar 15, 20265
v14 × 6Mar 1, 20262
v24 × 6Mar 15, 20265active
v14 × 6Mar 1, 20262
@@ -573,28 +577,38 @@

v2 -
- +
+
-
-

v2 Layout

-
- -
-

Properties

-
-
- Dimensions - 4 rows × 6 columns - Merge axis - Columns only — row dividers permanent - Unit system - Imperial +
+
+

v2 Layout

+
+
+
+

Properties

+
+
+
+
Dimensions
+
4 rows × 6 columns
+
+
+
Merge axis
+
Columns only — row dividers permanent
+
+
+
Unit system
+
Imperial
+
+
+
+
-
- +
@@ -904,6 +918,69 @@

Place Insert — MUSE Level 8

setTimeout(() => toast.remove(), 6000); } + // Template version selection + let selectedVersion = 2; + let activeVersion = 2; + + const versionData = { + 1: { dims: '4 rows × 4 columns', rows: 4, cols: 4, merge: 'Both — all dividers removable', mergeColor: '#86efac', unit: 'Imperial', date: 'Mar 1, 2026', instances: 2 }, + 2: { dims: '4 rows × 6 columns', rows: 4, cols: 6, merge: 'Columns only — row dividers permanent', mergeColor: '#fbbf24', unit: 'Imperial', date: 'Mar 15, 2026', instances: 5 } + }; + + function selectVersion(v) { + selectedVersion = v; + document.querySelectorAll('.version-row').forEach(r => r.classList.remove('selected-version')); + document.querySelector(`.version-row[data-version="${v}"]`).classList.add('selected-version'); + + const data = versionData[v]; + document.getElementById('tpl-version-title').textContent = `v${v} Layout`; + document.getElementById('tpl-prop-dims').textContent = data.dims; + document.getElementById('tpl-prop-merge').innerHTML = data.merge; + document.getElementById('tpl-prop-merge').style.color = data.mergeColor; + document.getElementById('tpl-prop-unit').textContent = data.unit; + + // Update edit button + const nextV = Math.max(...Object.keys(versionData).map(Number)) + 1; + document.getElementById('tpl-edit-btn').textContent = `Edit → Publish as v${nextV}`; + + // Show/hide "Set as Active" button + const setActiveContainer = document.getElementById('tpl-set-active-container'); + setActiveContainer.style.display = (v === activeVersion) ? 'none' : 'block'; + + // Update apply button text + const applyBtn = document.querySelector('#apply-version-btn button'); + applyBtn.textContent = `Apply v${v} to Selected`; + applyBtn.onclick = () => applyVersionToSelected(); + + // Re-render grid for this version + renderGrid('tpl-detail-preview', data.rows, data.cols, 'empty-tpl'); + } + + function setActiveVersion() { + const oldActive = activeVersion; + activeVersion = selectedVersion; + + // Update badges in version table + document.querySelectorAll('.version-row').forEach(r => { + const v = parseInt(r.dataset.version); + const badgeCell = r.querySelector('td:last-child'); + badgeCell.innerHTML = (v === activeVersion) ? 'active' : ''; + }); + + document.getElementById('tpl-set-active-container').style.display = 'none'; + showToast(`v${activeVersion} set as active version`); + } + + function applyVersionToSelected() { + const checked = document.querySelectorAll('#view-template-detail .tpl-form-left input[type="checkbox"]:checked'); + const count = checked.length; + if (count === 0) { + showToast('No instances selected'); + return; + } + showToast(`${count} instance${count > 1 ? 's' : ''} updated to v${selectedVersion}`); + } + // Init showView('modules'); From efcc4f0f1c9fcd0e7cf7e2b9875fc4892f06373f Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:53:49 -0400 Subject: [PATCH 059/179] Template detail: larger grid, editable properties, version removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Grid uses bigger cells (72px) and larger fonts on template detail - Properties panel widened to 260px with normal-sized fonts - Edit Properties toggles inline editing (dims, merge axis, unit) - Live grid preview updates during dimension edits - Publish as vN button appears during edit, Cancel to revert - Version rows show × to hide versions with 0 instances - Fixed v1 dims display (4×4 not 4×6) --- prototypes/storage-def-v1.html | 189 ++++++++++++++++++++++++++------- 1 file changed, 152 insertions(+), 37 deletions(-) diff --git a/prototypes/storage-def-v1.html b/prototypes/storage-def-v1.html index 8831970..d093926 100644 --- a/prototypes/storage-def-v1.html +++ b/prototypes/storage-def-v1.html @@ -530,10 +530,10 @@

Plano 3600 Stowaway

Version History

- - - - + + + +
VersionDimsPublishedInstances
v24 × 6Mar 15, 20265active
v14 × 6Mar 1, 20262
VersionDimsPublishedInstances
v24 × 6Mar 15, 20265active
v14 × 4Mar 1, 20262
@@ -583,32 +583,55 @@

-
-

v2 Layout

-
+
+

v2 Layout

+
-
-

Properties

-
-
+
+

Properties

+
+
-
Dimensions
-
4 rows × 6 columns
+
Dimensions
+
4 rows × 6 columns
+
-
Merge axis
-
Columns only — row dividers permanent
+
Merge axis
+
Columns only — row dividers permanent
+
-
Unit system
-
Imperial
+
Unit system
+
Imperial
+
-
- +
+ + +
@@ -759,10 +782,13 @@

Place Insert — MUSE Level 8

// SVG Grid renderer function renderGrid(containerId, rows, cols, mode) { const container = document.getElementById(containerId); - const cellSize = 52; - const gap = 2; - const labelW = 28; - const labelH = 24; + const isTemplateDetail = (containerId === 'tpl-detail-preview'); + const cellSize = isTemplateDetail ? 72 : 52; + const gap = isTemplateDetail ? 3 : 2; + const labelW = isTemplateDetail ? 36 : 28; + const labelH = isTemplateDetail ? 32 : 24; + const axisFont = isTemplateDetail ? 14 : 11; + const cellFont = isTemplateDetail ? 12 : 9; const w = labelW + cols * (cellSize + gap); const h = labelH + rows * (cellSize + gap); @@ -799,13 +825,13 @@

Place Insert — MUSE Level 8

// Column labels for (let c = 0; c < cols; c++) { const x = labelW + c * (cellSize + gap) + cellSize / 2; - html += `${c + 1}`; + html += `${c + 1}`; } // Row labels const rowLabels = 'ABCDEFGHIJKLMNOP'; for (let r = 0; r < rows; r++) { const y = labelH + r * (cellSize + gap) + cellSize / 2; - html += `${rowLabels[r]}`; + html += `${rowLabels[r]}`; } // Cells for (let r = 0; r < rows; r++) { @@ -821,21 +847,21 @@

Place Insert — MUSE Level 8

const mw = m.spanCols * (cellSize + gap) - gap; const cls = occupied.has(cellId) ? 'cell merged occupied' : 'cell merged'; html += ``; - html += `${m.label}`; + html += `${m.label}`; } else if (divided[cellId]) { const parts = divided[cellId]; const partW = (cellSize - 1) / parts.length; for (let p = 0; p < parts.length; p++) { const px = x + p * partW + (p > 0 ? 1 : 0); html += ``; - html += `${parts[p]}`; + html += `${parts[p]}`; } } else { let cls = 'cell'; if (disabled.has(cellId)) cls += ' disabled'; else if (occupied.has(cellId)) cls += ' occupied'; html += ``; - html += `${cellId}`; + html += `${cellId}`; } } } @@ -929,19 +955,18 @@

Place Insert — MUSE Level 8

function selectVersion(v) { selectedVersion = v; + // Cancel any in-progress edit + cancelPropEdit(); + document.querySelectorAll('.version-row').forEach(r => r.classList.remove('selected-version')); document.querySelector(`.version-row[data-version="${v}"]`).classList.add('selected-version'); const data = versionData[v]; document.getElementById('tpl-version-title').textContent = `v${v} Layout`; - document.getElementById('tpl-prop-dims').textContent = data.dims; - document.getElementById('tpl-prop-merge').innerHTML = data.merge; - document.getElementById('tpl-prop-merge').style.color = data.mergeColor; - document.getElementById('tpl-prop-unit').textContent = data.unit; - - // Update edit button - const nextV = Math.max(...Object.keys(versionData).map(Number)) + 1; - document.getElementById('tpl-edit-btn').textContent = `Edit → Publish as v${nextV}`; + document.getElementById('tpl-prop-dims-display').innerHTML = data.dims; + document.getElementById('tpl-prop-merge-display').innerHTML = data.merge; + document.getElementById('tpl-prop-merge-display').style.color = data.mergeColor; + document.getElementById('tpl-prop-unit-display').textContent = data.unit; // Show/hide "Set as Active" button const setActiveContainer = document.getElementById('tpl-set-active-container'); @@ -971,6 +996,96 @@

Place Insert — MUSE Level 8

showToast(`v${activeVersion} set as active version`); } + // Property editing + let editingProps = false; + + function togglePropEdit() { + editingProps = true; + const data = versionData[selectedVersion]; + // Populate edit fields from current version + document.getElementById('tpl-edit-rows').value = data.rows; + document.getElementById('tpl-edit-cols').value = data.cols; + const mergeMap = { + 'Columns only': 'columns', 'Rows only': 'rows', 'Both': 'both', 'None': 'none' + }; + const mergeKey = data.merge.split('<')[0].trim(); + document.getElementById('tpl-edit-merge').value = mergeMap[mergeKey] || 'both'; + document.getElementById('tpl-edit-unit').value = data.unit; + + // Show edit fields, hide display values + document.getElementById('tpl-prop-dims-display').style.display = 'none'; + document.getElementById('tpl-prop-dims-edit').style.display = 'block'; + document.getElementById('tpl-prop-merge-display').style.display = 'none'; + document.getElementById('tpl-prop-merge-edit').style.display = 'block'; + document.getElementById('tpl-prop-unit-display').style.display = 'none'; + document.getElementById('tpl-prop-unit-edit').style.display = 'block'; + + // Show publish/cancel, hide edit button + document.getElementById('tpl-edit-toggle-btn').style.display = 'none'; + document.getElementById('tpl-publish-btn').style.display = 'inline-flex'; + document.getElementById('tpl-cancel-edit-btn').style.display = 'inline-flex'; + + const nextV = Math.max(...Object.keys(versionData).map(Number)) + 1; + document.getElementById('tpl-publish-btn').textContent = `Publish as v${nextV}`; + + onPropEdit(); + } + + function cancelPropEdit() { + editingProps = false; + document.getElementById('tpl-prop-dims-display').style.display = ''; + document.getElementById('tpl-prop-dims-edit').style.display = 'none'; + document.getElementById('tpl-prop-merge-display').style.display = ''; + document.getElementById('tpl-prop-merge-edit').style.display = 'none'; + document.getElementById('tpl-prop-unit-display').style.display = ''; + document.getElementById('tpl-prop-unit-edit').style.display = 'none'; + + document.getElementById('tpl-edit-toggle-btn').style.display = ''; + document.getElementById('tpl-publish-btn').style.display = 'none'; + document.getElementById('tpl-cancel-edit-btn').style.display = 'none'; + + // Restore grid to selected version + const data = versionData[selectedVersion]; + if (data) renderGrid('tpl-detail-preview', data.rows, data.cols, 'empty-tpl'); + } + + function onPropEdit() { + // Live-update the grid preview as the user edits dimensions + const rows = parseInt(document.getElementById('tpl-edit-rows').value) || 1; + const cols = parseInt(document.getElementById('tpl-edit-cols').value) || 1; + renderGrid('tpl-detail-preview', Math.min(rows, 16), Math.min(cols, 16), 'empty-tpl'); + } + + function publishNewVersion() { + const nextV = Math.max(...Object.keys(versionData).map(Number)) + 1; + const rows = parseInt(document.getElementById('tpl-edit-rows').value) || 1; + const cols = parseInt(document.getElementById('tpl-edit-cols').value) || 1; + const mergeVal = document.getElementById('tpl-edit-merge').value; + const unitVal = document.getElementById('tpl-edit-unit').value; + + const mergeLabels = { + 'both': 'Both — all dividers removable', + 'columns': 'Columns only — row dividers permanent', + 'rows': 'Rows only — column dividers permanent', + 'none': 'None — all dividers permanent' + }; + const mergeColors = { 'both': '#86efac', 'columns': '#fbbf24', 'rows': '#fbbf24', 'none': '#f87171' }; + + cancelPropEdit(); + showToast(`v${nextV} published`); + } + + function removeVersion(v) { + // Only allow removing versions with 0 instances and not the active version + if (v === activeVersion) return; + const row = document.querySelector(`.version-row[data-version="${v}"]`); + if (row) row.remove(); + if (selectedVersion === v) { + selectVersion(activeVersion); + } + showToast(`v${v} hidden from list`); + } + function applyVersionToSelected() { const checked = document.querySelectorAll('#view-template-detail .tpl-form-left input[type="checkbox"]:checked'); const count = checked.length; From 5d0da22f472e7dc6e350f7c5ed519b6062561ef8 Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:01:34 -0400 Subject: [PATCH 060/179] Template properties: labeling, origin, per-axis dividers, larger grid gaps - Row/column labels: alpha or numeric, shown in display and editable - Origin: 4-corner visual picker (TL/TR/BL/BR), triangle moves in grid - Row dividers / Column dividers: independent fixed/removable toggles replacing the combined merge axis dropdown - Cell gap increased to 8px on template detail for divider clarity - Fixed dividers render as thick lines on both axes independently - Properties panel widened to 280px with full edit form --- prototypes/storage-def-v1.html | 295 +++++++++++++++++++++++---------- 1 file changed, 204 insertions(+), 91 deletions(-) diff --git a/prototypes/storage-def-v1.html b/prototypes/storage-def-v1.html index d093926..d8f367e 100644 --- a/prototypes/storage-def-v1.html +++ b/prototypes/storage-def-v1.html @@ -158,6 +158,10 @@ .version-row:hover { background: #1e293b; } .version-row.selected-version { background: rgba(255, 102, 0, 0.08); } + /* Origin picker */ + .origin-btn.active { background: #ff6600 !important; color: white !important; border-color: #ff6600 !important; font-weight: 600; } + .origin-btn:hover:not(.active) { border-color: #64748b !important; color: #e2e8f0 !important; } + /* Place insert modal */ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 50; display: flex; align-items: center; justify-content: center; } .modal { background: #1e293b; border: 1px solid #334155; border-radius: 10px; width: 680px; max-height: 80vh; display: flex; flex-direction: column; } @@ -587,53 +591,109 @@

v2 Layout

-
+

Properties

-
+
-
Dimensions
+
Dimensions
4 rows × 6 columns
-
-
Merge axis
-
Columns only — row dividers permanent
- +
Row labels
+
Alpha (A, B, C…)
+
+
+
Column labels
+
Numeric (1, 2, 3…)
+
+
+
Origin
+
Top-left
+
+
+
Row dividers
+
Fixed — cannot merge across rows
-
Unit system
+
Column dividers
+
Removable — can merge within rows
+
+
+
Unit system
Imperial
-
-
+
- -
- + + + +
@@ -784,7 +844,7 @@

Place Insert — MUSE Level 8

const container = document.getElementById(containerId); const isTemplateDetail = (containerId === 'tpl-detail-preview'); const cellSize = isTemplateDetail ? 72 : 52; - const gap = isTemplateDetail ? 3 : 2; + const gap = isTemplateDetail ? 8 : 2; const labelW = isTemplateDetail ? 36 : 28; const labelH = isTemplateDetail ? 32 : 24; const axisFont = isTemplateDetail ? 14 : 11; @@ -865,22 +925,43 @@

Place Insert — MUSE Level 8

} } } - // Permanent row dividers (merge axis = columns only → rows are permanent) - // Draw thick lines between rows to indicate non-mergeable boundaries - const showPermanentRows = (mode === 'plano' || mode === 'plano-merged' || mode === 'empty-tpl'); - if (showPermanentRows && rows > 1) { + // Permanent dividers — read from current divider settings + const dividerSettings = getDividerSettings(mode); + if (dividerSettings.rowFixed && rows > 1) { for (let r = 1; r < rows; r++) { const y = labelH + r * (cellSize + gap) - gap / 2; const x1 = labelW; const x2 = labelW + cols * (cellSize + gap) - gap; - html += ``; + html += ``; + } + } + if (dividerSettings.colFixed && cols > 1) { + for (let c = 1; c < cols; c++) { + const x = labelW + c * (cellSize + gap) - gap / 2; + const y1 = labelH; + const y2 = labelH + rows * (cellSize + gap) - gap; + html += ``; } } - // Origin marker — small accent triangle at top-left of A1 - const originX = labelW + 2; - const originY = labelH + 2; - html += ``; + // Origin marker — accent triangle at the origin corner + const origin = getOriginSetting(mode); + let originX, originY, originPts; + const triSize = isTemplateDetail ? 10 : 8; + if (origin === 'tl') { + originX = labelW + 2; originY = labelH + 2; + originPts = `${originX},${originY} ${originX+triSize},${originY} ${originX},${originY+triSize}`; + } else if (origin === 'tr') { + originX = labelW + cols * (cellSize + gap) - gap - 2; originY = labelH + 2; + originPts = `${originX},${originY} ${originX-triSize},${originY} ${originX},${originY+triSize}`; + } else if (origin === 'bl') { + originX = labelW + 2; originY = labelH + rows * (cellSize + gap) - gap - 2; + originPts = `${originX},${originY} ${originX+triSize},${originY} ${originX},${originY-triSize}`; + } else { + originX = labelW + cols * (cellSize + gap) - gap - 2; originY = labelH + rows * (cellSize + gap) - gap - 2; + originPts = `${originX},${originY} ${originX-triSize},${originY} ${originX},${originY-triSize}`; + } + html += ``; html += `
`; @@ -949,13 +1030,48 @@

Place Insert — MUSE Level 8

let activeVersion = 2; const versionData = { - 1: { dims: '4 rows × 4 columns', rows: 4, cols: 4, merge: 'Both — all dividers removable', mergeColor: '#86efac', unit: 'Imperial', date: 'Mar 1, 2026', instances: 2 }, - 2: { dims: '4 rows × 6 columns', rows: 4, cols: 6, merge: 'Columns only — row dividers permanent', mergeColor: '#fbbf24', unit: 'Imperial', date: 'Mar 15, 2026', instances: 5 } + 1: { rows: 4, cols: 4, rowLabel: 'alpha', colLabel: 'numeric', origin: 'tl', rowDiv: 'removable', colDiv: 'removable', unit: 'Imperial', date: 'Mar 1, 2026', instances: 2 }, + 2: { rows: 4, cols: 6, rowLabel: 'alpha', colLabel: 'numeric', origin: 'tl', rowDiv: 'fixed', colDiv: 'removable', unit: 'Imperial', date: 'Mar 15, 2026', instances: 5 } }; + // Helper to get divider settings for grid rendering + function getDividerSettings(mode) { + if (mode === 'empty-tpl' && editingProps) { + return { + rowFixed: document.getElementById('tpl-edit-rowdiv')?.value === 'fixed', + colFixed: document.getElementById('tpl-edit-coldiv')?.value === 'fixed' + }; + } + if (mode === 'empty-tpl' && selectedVersion && versionData[selectedVersion]) { + return { + rowFixed: versionData[selectedVersion].rowDiv === 'fixed', + colFixed: versionData[selectedVersion].colDiv === 'fixed' + }; + } + // Module detail views — Plano 3600 always has fixed rows, removable cols + if (mode === 'plano' || mode === 'plano-merged') { + return { rowFixed: true, colFixed: false }; + } + return { rowFixed: false, colFixed: false }; + } + + function getOriginSetting(mode) { + if (mode === 'empty-tpl' && editingProps) { + return currentEditOrigin || 'tl'; + } + if (mode === 'empty-tpl' && selectedVersion && versionData[selectedVersion]) { + return versionData[selectedVersion].origin || 'tl'; + } + return 'tl'; + } + + let currentEditOrigin = 'tl'; + + const labelNames = { alpha: 'Alpha (A, B, C…)', numeric: 'Numeric (1, 2, 3…)' }; + const originNames = { tl: 'Top-left', tr: 'Top-right', bl: 'Bottom-left', br: 'Bottom-right' }; + function selectVersion(v) { selectedVersion = v; - // Cancel any in-progress edit cancelPropEdit(); document.querySelectorAll('.version-row').forEach(r => r.classList.remove('selected-version')); @@ -963,9 +1079,25 @@

Place Insert — MUSE Level 8

const data = versionData[v]; document.getElementById('tpl-version-title').textContent = `v${v} Layout`; - document.getElementById('tpl-prop-dims-display').innerHTML = data.dims; - document.getElementById('tpl-prop-merge-display').innerHTML = data.merge; - document.getElementById('tpl-prop-merge-display').style.color = data.mergeColor; + document.getElementById('tpl-prop-dims-display').textContent = `${data.rows} rows × ${data.cols} columns`; + document.getElementById('tpl-prop-rowlabel-display').textContent = labelNames[data.rowLabel]; + document.getElementById('tpl-prop-collabel-display').textContent = labelNames[data.colLabel]; + document.getElementById('tpl-prop-origin-display').textContent = originNames[data.origin]; + + const rowDivEl = document.getElementById('tpl-prop-rowdiv-display'); + if (data.rowDiv === 'fixed') { + rowDivEl.innerHTML = 'Fixed — cannot merge across rows'; + } else { + rowDivEl.innerHTML = 'Removable — can merge across rows'; + } + + const colDivEl = document.getElementById('tpl-prop-coldiv-display'); + if (data.colDiv === 'fixed') { + colDivEl.innerHTML = 'Fixed — cannot merge within rows'; + } else { + colDivEl.innerHTML = 'Removable — can merge within rows'; + } + document.getElementById('tpl-prop-unit-display').textContent = data.unit; // Show/hide "Set as Active" button @@ -985,11 +1117,11 @@

Place Insert — MUSE Level 8

const oldActive = activeVersion; activeVersion = selectedVersion; - // Update badges in version table + // Update badges in version table (5th column = badge column) document.querySelectorAll('.version-row').forEach(r => { const v = parseInt(r.dataset.version); - const badgeCell = r.querySelector('td:last-child'); - badgeCell.innerHTML = (v === activeVersion) ? 'active' : ''; + const badgeCell = r.querySelectorAll('td')[4]; + if (badgeCell) badgeCell.innerHTML = (v === activeVersion) ? 'active' : ''; }); document.getElementById('tpl-set-active-container').style.display = 'none'; @@ -1005,25 +1137,20 @@

Place Insert — MUSE Level 8

// Populate edit fields from current version document.getElementById('tpl-edit-rows').value = data.rows; document.getElementById('tpl-edit-cols').value = data.cols; - const mergeMap = { - 'Columns only': 'columns', 'Rows only': 'rows', 'Both': 'both', 'None': 'none' - }; - const mergeKey = data.merge.split('<')[0].trim(); - document.getElementById('tpl-edit-merge').value = mergeMap[mergeKey] || 'both'; + document.getElementById('tpl-edit-rowlabel').value = data.rowLabel; + document.getElementById('tpl-edit-collabel').value = data.colLabel; + document.getElementById('tpl-edit-rowdiv').value = data.rowDiv; + document.getElementById('tpl-edit-coldiv').value = data.colDiv; document.getElementById('tpl-edit-unit').value = data.unit; + currentEditOrigin = data.origin; - // Show edit fields, hide display values - document.getElementById('tpl-prop-dims-display').style.display = 'none'; - document.getElementById('tpl-prop-dims-edit').style.display = 'block'; - document.getElementById('tpl-prop-merge-display').style.display = 'none'; - document.getElementById('tpl-prop-merge-edit').style.display = 'block'; - document.getElementById('tpl-prop-unit-display').style.display = 'none'; - document.getElementById('tpl-prop-unit-edit').style.display = 'block'; + // Set origin picker + document.querySelectorAll('.origin-btn').forEach(b => b.classList.remove('active')); + document.querySelector(`.origin-btn[data-origin="${data.origin}"]`).classList.add('active'); - // Show publish/cancel, hide edit button - document.getElementById('tpl-edit-toggle-btn').style.display = 'none'; - document.getElementById('tpl-publish-btn').style.display = 'inline-flex'; - document.getElementById('tpl-cancel-edit-btn').style.display = 'inline-flex'; + // Toggle panels + document.getElementById('tpl-props-display').style.display = 'none'; + document.getElementById('tpl-props-edit').style.display = 'block'; const nextV = Math.max(...Object.keys(versionData).map(Number)) + 1; document.getElementById('tpl-publish-btn').textContent = `Publish as v${nextV}`; @@ -1033,24 +1160,23 @@

Place Insert — MUSE Level 8

function cancelPropEdit() { editingProps = false; - document.getElementById('tpl-prop-dims-display').style.display = ''; - document.getElementById('tpl-prop-dims-edit').style.display = 'none'; - document.getElementById('tpl-prop-merge-display').style.display = ''; - document.getElementById('tpl-prop-merge-edit').style.display = 'none'; - document.getElementById('tpl-prop-unit-display').style.display = ''; - document.getElementById('tpl-prop-unit-edit').style.display = 'none'; - - document.getElementById('tpl-edit-toggle-btn').style.display = ''; - document.getElementById('tpl-publish-btn').style.display = 'none'; - document.getElementById('tpl-cancel-edit-btn').style.display = 'none'; + document.getElementById('tpl-props-display').style.display = ''; + document.getElementById('tpl-props-edit').style.display = 'none'; // Restore grid to selected version const data = versionData[selectedVersion]; if (data) renderGrid('tpl-detail-preview', data.rows, data.cols, 'empty-tpl'); } + function setOrigin(corner) { + currentEditOrigin = corner; + document.querySelectorAll('.origin-btn').forEach(b => b.classList.remove('active')); + document.querySelector(`.origin-btn[data-origin="${corner}"]`).classList.add('active'); + onPropEdit(); + } + function onPropEdit() { - // Live-update the grid preview as the user edits dimensions + // Live-update the grid preview as the user edits const rows = parseInt(document.getElementById('tpl-edit-rows').value) || 1; const cols = parseInt(document.getElementById('tpl-edit-cols').value) || 1; renderGrid('tpl-detail-preview', Math.min(rows, 16), Math.min(cols, 16), 'empty-tpl'); @@ -1058,19 +1184,6 @@

Place Insert — MUSE Level 8

function publishNewVersion() { const nextV = Math.max(...Object.keys(versionData).map(Number)) + 1; - const rows = parseInt(document.getElementById('tpl-edit-rows').value) || 1; - const cols = parseInt(document.getElementById('tpl-edit-cols').value) || 1; - const mergeVal = document.getElementById('tpl-edit-merge').value; - const unitVal = document.getElementById('tpl-edit-unit').value; - - const mergeLabels = { - 'both': 'Both — all dividers removable', - 'columns': 'Columns only — row dividers permanent', - 'rows': 'Rows only — column dividers permanent', - 'none': 'None — all dividers permanent' - }; - const mergeColors = { 'both': '#86efac', 'columns': '#fbbf24', 'rows': '#fbbf24', 'none': '#f87171' }; - cancelPropEdit(); showToast(`v${nextV} published`); } From 6edcc2b8ddc6e631f131ef830ea1a4f200082c01 Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:07:36 -0400 Subject: [PATCH 061/179] Template properties: correct control types, label validation, live grid - Divider fixed/removable: checkboxes (checked = fixed) - Unit system: radio buttons (Imperial / Metric) - Row/col labels: radio buttons with validation preventing same type - Origin: dropdown (Top-left, Top-right, Bottom-left, Bottom-right) - Grid axis labels and cell labels update live when label type changes - Publish button disabled when label conflict exists --- prototypes/storage-def-v1.html | 133 +++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 48 deletions(-) diff --git a/prototypes/storage-def-v1.html b/prototypes/storage-def-v1.html index d8f367e..cfe9a62 100644 --- a/prototypes/storage-def-v1.html +++ b/prototypes/storage-def-v1.html @@ -158,9 +158,8 @@ .version-row:hover { background: #1e293b; } .version-row.selected-version { background: rgba(255, 102, 0, 0.08); } - /* Origin picker */ - .origin-btn.active { background: #ff6600 !important; color: white !important; border-color: #ff6600 !important; font-weight: 600; } - .origin-btn:hover:not(.active) { border-color: #64748b !important; color: #e2e8f0 !important; } + /* Radio/checkbox styling */ + input[type="radio"], input[type="checkbox"] { accent-color: #ff6600; } /* Place insert modal */ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 50; display: flex; align-items: center; justify-content: center; } @@ -642,47 +641,48 @@

Row labels

- +
+ + +
Column labels
- +
+ + +
+
Origin
-
- - - - -
+
Row dividers
- +
Column dividers
- +
Unit system
- +
+ + +
@@ -882,21 +882,25 @@

Place Insert — MUSE Level 8

html += `
`; html += ``; + // Label settings + const labelSettings = getLabelSettings(mode); + // Column labels for (let c = 0; c < cols; c++) { const x = labelW + c * (cellSize + gap) + cellSize / 2; - html += `${c + 1}`; + html += `${getLabel(labelSettings.colLabel, c)}`; } // Row labels - const rowLabels = 'ABCDEFGHIJKLMNOP'; for (let r = 0; r < rows; r++) { const y = labelH + r * (cellSize + gap) + cellSize / 2; - html += `${rowLabels[r]}`; + html += `${getLabel(labelSettings.rowLabel, r)}`; } // Cells + const rowLabelsStr = 'ABCDEFGHIJKLMNOP'; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { - const cellId = rowLabels[r] + (c + 1); + const cellId = rowLabelsStr[r] + (c + 1); // internal ID for mock data lookups + const cellLabel = getLabel(labelSettings.rowLabel, r) + getLabel(labelSettings.colLabel, c); if (skipCells.has(cellId)) continue; const x = labelW + c * (cellSize + gap); @@ -921,7 +925,7 @@

Place Insert — MUSE Level 8

if (disabled.has(cellId)) cls += ' disabled'; else if (occupied.has(cellId)) cls += ' occupied'; html += ``; - html += `${cellId}`; + html += `${cellLabel}`; } } } @@ -1038,8 +1042,8 @@

Place Insert — MUSE Level 8

function getDividerSettings(mode) { if (mode === 'empty-tpl' && editingProps) { return { - rowFixed: document.getElementById('tpl-edit-rowdiv')?.value === 'fixed', - colFixed: document.getElementById('tpl-edit-coldiv')?.value === 'fixed' + rowFixed: document.getElementById('tpl-edit-rowdiv')?.checked || false, + colFixed: document.getElementById('tpl-edit-coldiv')?.checked || false }; } if (mode === 'empty-tpl' && selectedVersion && versionData[selectedVersion]) { @@ -1067,6 +1071,27 @@

Place Insert — MUSE Level 8

let currentEditOrigin = 'tl'; + function getLabelSettings(mode) { + if (mode === 'empty-tpl' && editingProps) { + return { + rowLabel: document.querySelector('input[name="tpl-edit-rowlabel"]:checked')?.value || 'alpha', + colLabel: document.querySelector('input[name="tpl-edit-collabel"]:checked')?.value || 'numeric' + }; + } + if (mode === 'empty-tpl' && selectedVersion && versionData[selectedVersion]) { + return { + rowLabel: versionData[selectedVersion].rowLabel || 'alpha', + colLabel: versionData[selectedVersion].colLabel || 'numeric' + }; + } + return { rowLabel: 'alpha', colLabel: 'numeric' }; + } + + function getLabel(type, index) { + if (type === 'alpha') return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[index] || ''; + return String(index + 1); + } + const labelNames = { alpha: 'Alpha (A, B, C…)', numeric: 'Numeric (1, 2, 3…)' }; const originNames = { tl: 'Top-left', tr: 'Top-right', bl: 'Bottom-left', br: 'Bottom-right' }; @@ -1137,16 +1162,24 @@

Place Insert — MUSE Level 8

// Populate edit fields from current version document.getElementById('tpl-edit-rows').value = data.rows; document.getElementById('tpl-edit-cols').value = data.cols; - document.getElementById('tpl-edit-rowlabel').value = data.rowLabel; - document.getElementById('tpl-edit-collabel').value = data.colLabel; - document.getElementById('tpl-edit-rowdiv').value = data.rowDiv; - document.getElementById('tpl-edit-coldiv').value = data.colDiv; - document.getElementById('tpl-edit-unit').value = data.unit; + + // Radio buttons for labels + document.querySelector(`input[name="tpl-edit-rowlabel"][value="${data.rowLabel}"]`).checked = true; + document.querySelector(`input[name="tpl-edit-collabel"][value="${data.colLabel}"]`).checked = true; + + // Origin dropdown + document.getElementById('tpl-edit-origin').value = data.origin; currentEditOrigin = data.origin; - // Set origin picker - document.querySelectorAll('.origin-btn').forEach(b => b.classList.remove('active')); - document.querySelector(`.origin-btn[data-origin="${data.origin}"]`).classList.add('active'); + // Divider checkboxes (checked = fixed) + document.getElementById('tpl-edit-rowdiv').checked = (data.rowDiv === 'fixed'); + document.getElementById('tpl-edit-coldiv').checked = (data.colDiv === 'fixed'); + + // Unit radio + document.querySelector(`input[name="tpl-edit-unit"][value="${data.unit}"]`).checked = true; + + // Hide conflict message + document.getElementById('label-conflict-msg').style.display = 'none'; // Toggle panels document.getElementById('tpl-props-display').style.display = 'none'; @@ -1168,14 +1201,18 @@

Place Insert — MUSE Level 8

if (data) renderGrid('tpl-detail-preview', data.rows, data.cols, 'empty-tpl'); } - function setOrigin(corner) { - currentEditOrigin = corner; - document.querySelectorAll('.origin-btn').forEach(b => b.classList.remove('active')); - document.querySelector(`.origin-btn[data-origin="${corner}"]`).classList.add('active'); + function onLabelChange(axis) { + const rowVal = document.querySelector('input[name="tpl-edit-rowlabel"]:checked').value; + const colVal = document.querySelector('input[name="tpl-edit-collabel"]:checked').value; + const conflict = (rowVal === colVal); + document.getElementById('label-conflict-msg').style.display = conflict ? 'block' : 'none'; + document.getElementById('tpl-publish-btn').disabled = conflict; onPropEdit(); } function onPropEdit() { + // Update origin from dropdown + currentEditOrigin = document.getElementById('tpl-edit-origin')?.value || 'tl'; // Live-update the grid preview as the user edits const rows = parseInt(document.getElementById('tpl-edit-rows').value) || 1; const cols = parseInt(document.getElementById('tpl-edit-cols').value) || 1; From 82309a476d675abf77858d3a2f8b87ae8a55d760 Mon Sep 17 00:00:00 2001 From: Nicky <6326614+ndemarco@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:13:28 -0400 Subject: [PATCH 062/179] Always-editable properties, origin-aware labels, external origin marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Properties always editable — no edit mode button - Publish as vN and Revert appear only when form differs from version - Origin changes label ordering (BL = A at bottom, TR = 1 at right) - Origin marker moved outside cells into label gutter corner - Label conflict validation prevents publish when row/col same type --- prototypes/storage-def-v1.html | 276 +++++++++++++-------------------- 1 file changed, 112 insertions(+), 164 deletions(-) diff --git a/prototypes/storage-def-v1.html b/prototypes/storage-def-v1.html index cfe9a62..afb4e6d 100644 --- a/prototypes/storage-def-v1.html +++ b/prototypes/storage-def-v1.html @@ -592,43 +592,7 @@

v2 Layout

Properties

-
-
-
-
Dimensions
-
4 rows × 6 columns
-
-
-
Row labels
-
Alpha (A, B, C…)
-
-
-
Column labels
-
Numeric (1, 2, 3…)
-
-
-
Origin
-
Top-left
-
-
-
Row dividers
-
Fixed — cannot merge across rows
-
-
-
Column dividers
-
Removable — can merge within rows
-
-
-
Unit system
-
Imperial
-
-
-
- -
-
- -