Compare commits

...

54 Commits

Author SHA1 Message Date
6760ab0abf Build [v1.10.2] 2026-04-15 17:54:21 +03:00
d9e592cc97 Build [v1.10.1] 2026-04-15 17:41:34 +03:00
062df8cfd9 Build [v1.10.0] 2026-04-15 17:31:58 +03:00
074d4f1f8d fix(build): revert testing dependencies to fix lockfile mismatch in Docker 2026-04-15 17:14:37 +03:00
add873b159 dada 2026-04-15 16:58:20 +03:00
6bf3276c72 Build [v1.10.0] 2026-04-15 16:45:56 +03:00
e5963123ab docs: documenting modular architecture and testing infrastructure 2026-04-15 16:44:15 +03:00
db918a86ab feat(admin): finalizing modular refactoring with testing suite and UI polish 2026-04-15 16:41:20 +03:00
73ab38d237 Build [v1.9.24] 2026-04-15 15:33:14 +03:00
Daniel Bedeleanu
f751cc20a7 feat: implement strict AI selection (Gemini/Claude), secure API key management, and Admin UI standardization [v1.9.23] 2026-04-15 15:24:06 +03:00
Daniel Bedeleanu
1893c4f38b modificari mari 2026-04-15 15:20:45 +03:00
Daniel Bedeleanu
6102ff39aa Build [v1.9.22] 2026-04-15 11:41:20 +03:00
Daniel Bedeleanu
10d75619bf docs: update documentation with mobile stat cards UI improvements
- Added StatCard component description to README
- Updated frontend architecture in PROJECT_ARCHITECTURE.md
- Documented responsive design improvements (v1.9.21+)
- Mobile-first responsive design for inventory management
2026-04-15 11:39:50 +03:00
Daniel Bedeleanu
546eca6b9a docs: finalize session state - mobile stat cards responsive fix complete
- StatCard component created with accessibility features
- Inventory, Logs, and Admin pages refactored (7 stat cards total)
- Mobile viewport testing verified (320px-1024px)
- All responsive breakpoints working correctly
- Spec coverage 100% - ready for production deployment
2026-04-15 11:36:50 +03:00
Daniel Bedeleanu
66844a56e9 fix: use Database icon for Local Archives stat card
- Changed from Archive to Database icon
- Better semantic representation of database backups
- Improves UI clarity and consistency

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-04-15 11:29:03 +03:00
Daniel Bedeleanu
e8c804da71 docs: update session state - Admin page stat cards refactored (Task 4 complete) 2026-04-15 11:26:49 +03:00
Daniel Bedeleanu
45db169dda fix: refactor Admin page stat cards to use StatCard component
- Replace Local Archives stat display with StatCard
- Consistent styling and responsive layout across admin stats
- Improves mobile content overflow handling
2026-04-15 11:26:29 +03:00
Daniel Bedeleanu
f47e7d005a fix: refactor Logs page stat cards to use StatCard component
- Replace Total Events, Check in, Check out displays with StatCard
- Maintain Top Operator as custom card (displays string value)
- Fixes mobile content overflow with responsive layout
- Consistent styling with Inventory page
2026-04-15 11:23:10 +03:00
Daniel Bedeleanu
adb6cde87a fix: refactor Inventory page stat cards to use StatCard component
- Replace Categories, Item Types, Total Boxes with StatCard
- Fixes mobile content overflow with two-column flexbox layout
- Responsive font sizing for mobile/desktop
- Label truncation for long text
2026-04-15 11:19:47 +03:00
Daniel Bedeleanu
460dc4fe12 fix: improve StatCard accessibility (aria-hidden, role='status') 2026-04-15 11:17:29 +03:00
Daniel Bedeleanu
4df2d844ca feat: create reusable StatCard component with responsive layout 2026-04-15 11:15:38 +03:00
Daniel Bedeleanu
409afb29f8 plan: mobile stat cards responsive implementation - 6 tasks
- Task 1: Create reusable StatCard component
- Task 2: Update Inventory page stat cards
- Task 3: Update Logs page stat cards
- Task 4: Update Admin page stat cards
- Task 5: Test on mobile viewports
- Task 6: Final verification & handover

Ready for execution via subagent-driven or inline approach
2026-04-15 11:14:24 +03:00
Daniel Bedeleanu
bacc23cc23 spec: mobile stat cards responsive design - two-column layout fix
- Design two-column flexbox layout (label left, number right)
- Responsive font sizing (sm/md/lg breakpoints)
- Label truncation for long text
- Applies to Inventory, Logs, and Admin pages
- Ready for implementation review
2026-04-15 11:13:28 +03:00
Daniel Bedeleanu
a6af90ec48 bla2 2026-04-15 10:50:56 +03:00
Daniel Bedeleanu
29b5c7020c docs: update session state - Docker build fully verified and tested locally
✓ Frontend build: TypeScript compilation successful (0 errors)
✓ Full stack build: All 3 images built successfully
✓ Integration test: Services running and responding to HTTP

Ready for remote server deployment.
2026-04-15 10:06:58 +03:00
Daniel Bedeleanu
70670a3c9d docs: add Docker build verification plan and update session state
- Created comprehensive 4-task plan for local Docker testing
- Plan includes: frontend build, full stack build, integration tests, handover
- Updated SESSION_STATE.md with TypeScript fix status and next steps
- Ready for remote deployment verification
2026-04-15 09:37:50 +03:00
Daniel Bedeleanu
65b24079cb fix: resolve TypeScript build error in AIOnboarding and Scanner components
- Fix null type error in AIOnboarding.tsx line 352: img src attribute
- Fix similar issue in Scanner.tsx line 237: capturedImage null handling
- Use '|| undefined' pattern to satisfy React 19/Next.js 15 type requirements
- Both components now properly handle null/undefined image states
2026-04-15 09:35:44 +03:00
Daniel Bedeleanu
6c57b1b0c2 blabla 2026-04-14 22:54:19 +03:00
Daniel Bedeleanu
1ea5e90bc4 Fix: resolve Unterminated regexp literal build error in page.tsx 2026-04-14 20:48:53 +03:00
Daniel Bedeleanu
00ee4cf9c5 Build [v1.9.19] 2026-04-14 20:44:01 +03:00
Daniel Bedeleanu
fcb187974e Build [v1.9.18] 2026-04-13 23:43:52 +03:00
Daniel Bedeleanu
1fff658d3c Build [v1.9.16] (Open Gateway: Verified SSL Permission check) 2026-04-13 22:48:26 +03:00
Daniel Bedeleanu
826b264a70 Build [v1.9.15] (The Dynamic Shield: On-Demand TLS Catch-all) 2026-04-13 22:43:40 +03:00
Daniel Bedeleanu
94f1a515b7 Build [v1.9.14] (Protocol Lock: Explicit HTTPS & Handshake Debug) 2026-04-13 22:37:50 +03:00
Daniel Bedeleanu
4dc0ce50e7 Build [v1.9.13] (Bare Metal: Fixed Caddy Syntax & Universal Binding) 2026-04-13 22:31:04 +03:00
Daniel Bedeleanu
ee7e7b7bd7 Build [v1.9.12] (Secure Seal & LDAP Sync: Custom Caddy & Config Volume) 2026-04-13 22:25:19 +03:00
Daniel Bedeleanu
9d7e4f0ca3 Build [v1.9.11] (The Convergence: Manual Login & Runtime Discovery) 2026-04-13 22:15:57 +03:00
Daniel Bedeleanu
bdf6d605cd Build [v1.9.10] (Access & SSL Recovery: Fixed Admin info and Explicit IP Proxy) 2026-04-13 22:07:15 +03:00
Daniel Bedeleanu
a2f6cab492 Build [v1.9.9] (Emergency: Direct HTTP Fallback & Diagnostic Logs) 2026-04-13 21:57:51 +03:00
Daniel Bedeleanu
476fda7203 Build [v1.9.8] (Final Stability: Static Internal Port Mapping) 2026-04-13 21:48:40 +03:00
Daniel Bedeleanu
9348336709 Build [v1.9.7] (Fix ERR_SSL_PROTOCOL_ERROR: Explicit HTTPS in Caddyfile) 2026-04-13 21:42:58 +03:00
Daniel Bedeleanu
e5194a1dbb Build [v1.9.6] (Fix: Include deploy.sh in production bundle) 2026-04-13 21:36:23 +03:00
Daniel Bedeleanu
7c3d0e102f Build [v1.9.5] (Bulletproof Deployment with visible inventory.env and deploy.sh) 2026-04-13 21:32:49 +03:00
Daniel Bedeleanu
a0eaddf994 Build [v1.9.4] (Single Source of Truth: Consolidated Configuration) 2026-04-13 21:15:19 +03:00
Daniel Bedeleanu
30be967887 Build [v1.9.3] (Fix Host-Side Port Interpolation with Root .env) 2026-04-13 21:12:55 +03:00
Daniel Bedeleanu
2b8d0b3f43 Build [v1.9.2] (Fix Environment Variable Overriding in Docker Compose) 2026-04-13 20:58:10 +03:00
Daniel Bedeleanu
07b15c8e01 Build [v1.9.1] (CORS Dynamic IP Resolution Fix) 2026-04-13 20:52:29 +03:00
Daniel Bedeleanu
65cc0c7b6d Official Release [v1.9.0] (Stability Milestone • Automatic Git Commit ID) 2026-04-13 20:41:10 +03:00
Daniel Bedeleanu
f137ded5aa Build [v1.8.9] (Runtime Permission Healing for Docker Volumes) 2026-04-13 20:32:40 +03:00
Daniel Bedeleanu
946f1787c7 Build [v1.8.8] (Complete Frontend Build Cleaned & Verified) 2026-04-13 20:24:21 +03:00
Daniel Bedeleanu
09be0b401d Build [v1.8.7] (Fix strict Type error in logs page) 2026-04-13 20:07:55 +03:00
Daniel Bedeleanu
3069a921e7 Build [v1.8.6] (TypeScript unknown catch type fix) 2026-04-13 19:56:27 +03:00
Daniel Bedeleanu
55e3cf5042 Build [v1.8.5] (Self-contained Frontend Build) 2026-04-13 19:50:28 +03:00
Daniel Bedeleanu
c874d27d64 Build [v1.8.4] (Satisfy frontend relative VERSION.json path) 2026-04-13 19:44:52 +03:00
149 changed files with 6569 additions and 1949 deletions

View File

@@ -0,0 +1,55 @@
# Superpowers Rules (Always-On)
These rules apply to ALL work unless the user explicitly opts out.
## 1) Plan gate for non-trivial work
If the task is anything beyond a tiny change, do NOT edit code immediately.
You MUST:
1) Brainstorm briefly (goal, constraints, risks, acceptance criteria)
2) Write a step-by-step plan with verification steps
3) Ask the user to approve the plan
Only after approval may you implement.
### Execute-plan gate (Superpowers parity)
After the user approves a plan, do NOT begin implementation automatically.
You MUST pause and instruct the user to run: `/superpowers-execute-plan`
Only begin implementation after `/superpowers-execute-plan` is invoked,
unless the user explicitly says to proceed without it.
### What counts as "tiny"?
- single-file change
- obvious edit
- low risk
Even then: do a mini-plan (35 steps) and include verification.
## 2) Verification is mandatory
After implementation, you MUST provide:
- exact commands to verify (tests/lint/run)
- and results if you were able to run them
## 3) Prefer TDD / regression tests
- If fixing a bug: add a regression test if practical
- If adding behavior: add/adjust tests when practical
If tests arent feasible, provide a concrete alternative verification path.
## 4) Review pass required
Before final response, do a review pass and list issues by severity:
- Blocker / Major / Minor / Nit
## 5) Safety
- Never log secrets
- Add timeouts, retries, and idempotency for API automations
- Fail safe (no silent data loss)
## Artifact persistence (mandatory)
Any brainstorm, plan, review, or finish output must be written to disk under:
`artifacts/superpowers/`
Do not leave these as IDE-only documents.
After writing, confirm the file exists.
## Persistence enforcement
When a workflow requires saving an artifact to `artifacts/superpowers/`, you MUST ensure the file exists on disk.
Preferred method: use `python .agent/skills/superpowers-workflow/scripts/write_artifact.py --path <...>`.
If you cannot execute commands, instruct the user to save the output manually.

View File

@@ -0,0 +1,37 @@
---
name: superpowers-brainstorm
description: Produces a structured brainstorm: goals, constraints, risks, options, recommendation, and acceptance criteria. Use before non-trivial implementation or design changes.
---
# Brainstorm Skill
## When to use this skill
- before implementing non-trivial features
- before refactors with unclear scope
- before debugging complex issues
- before designing an automation workflow
## Brainstorm template (use this exact structure)
### Goal
- (12 sentences)
### Constraints
- (tech stack, time, compatibility, performance, “must not change”, etc.)
### Known context
- (what exists today; relevant files/components; current behavior)
### Risks
- (security, data loss, regressions, surprising side effects)
### Options (24)
For each option include:
- Summary
- Pros / cons
- Complexity / risk
### Recommendation
- Pick one option and explain why
### Acceptance criteria
- Bullet list of verifiable outcomes

View File

@@ -0,0 +1,35 @@
---
name: superpowers-debug
description: Systematic debugging: reproduce, isolate, form hypotheses, instrument, fix, and add regression tests. Use when troubleshooting errors, failing tests, or unexpected behavior.
---
# Debug Skill
## When to use this skill
- runtime errors, flaky tests, wrong outputs
- “it used to work” regressions
- performance or timeout problems (initial triage)
## Debug workflow (do not skip steps)
1. **Reproduce**
- Capture exact error, inputs, environment, command.
2. **Minimize**
- Reduce to smallest repro (one file, one function, smallest dataset).
3. **Hypotheses (25)**
- Rank by likelihood.
4. **Instrument**
- Add temporary logging/assertions or use existing diagnostics.
5. **Fix**
- Smallest change that removes root cause.
6. **Prevent**
- Add regression test or permanent guard/validation.
7. **Verify**
- Run the failing case + relevant suites.
## Reporting format
- Symptom
- Repro steps
- Root cause
- Fix
- Regression protection
- Verification

View File

@@ -0,0 +1,32 @@
---
name: superpowers-finish
description: Finalizes work: runs verification, summarizes changes, notes follow-ups, and ensures repo hygiene. Use at the end of an implementation or debugging session.
---
# Finish Skill
## When to use this skill
- at the end of any non-trivial change set
- after a bug fix or feature is implemented
- before handing off work to a teammate/user
## Finish checklist
- Run verification commands (tests, lint, build, typecheck if relevant)
- Confirm acceptance criteria are met
- Summarize what changed (by area/file)
- Call out any risks or follow-ups
- Note how to rollback if applicable
## Output format
### Verification
- Commands run:
- Results:
### Summary of changes
- Bullet list
### Follow-ups
- Bullet list (only if needed)
### How to validate manually (if applicable)
- Steps

View File

@@ -0,0 +1,30 @@
---
name: superpowers-plan
description: Writes an implementation plan with small steps, exact files to touch, and verification commands. Use before making non-trivial changes.
---
# Planning Skill
## When to use this skill
- any multi-file change
- any change that impacts behavior, data, auth, billing, or production workflows
- any debugging that needs systematic isolation
## Planning rules
- Steps should be **small** (210 minutes each).
- Every step must include **verification**.
- Prefer **incremental deliverables** (avoid “big bang” edits).
- Identify **rollback** and **risk controls** early.
## Plan format (use this exact structure)
### Goal
### Assumptions
### Plan
1. Step name
- Files: `path/to/file.ext`, `...`
- Change: (12 bullets)
- Verify: (exact commands or checks)
2. ...
### Risks & mitigations
### Rollback plan

View File

@@ -0,0 +1,97 @@
---
name: superpowers-python-automation
description: Implements reliable automations in Python for REST APIs: httpx/requests patterns, retries, timeouts, pagination, typing, config, logging, and tests. Use when writing Python scripts/services that call external APIs.
---
# Python Automation Skill
This skill provides concrete Python patterns to implement robust REST API automations.
## When to use this skill
- Python scripts that call one or more REST APIs
- ETL jobs, sync tools, webhook handlers
- CLI tools or small services that integrate external systems
## Preferred stack (defaults)
- HTTP client: **httpx** (preferred) or requests
- Config: env vars + `.env` (optional) with pydantic-settings if appropriate
- Logging: stdlib `logging` with structured-ish fields
- Testing: pytest (+ respx for httpx mocking when useful)
If the project already uses different tools, follow project conventions.
---
## Reference architecture (small but scalable)
- `client.py`: API client wrapper (auth headers, retries, pagination helpers)
- `models.py`: typed payload models (dataclasses or pydantic)
- `sync.py`: orchestration logic (fetch -> transform -> upsert)
- `main.py`: CLI entrypoint
- `tests/`: unit tests for transform + client behavior
## HTTP rules (mandatory)
- Always set timeouts (connect + read)
- Centralize request sending in one function so retries/logging are consistent
- Never log secrets (Authorization headers, tokens)
### Retry policy guidance
Retry on:
- network errors/timeouts
- 429 (respect Retry-After when present)
- 500599
Optional: 408, and 409 only if operation is safe and semantics known
Do NOT retry on:
- most 400499 (unless explicitly safe)
### Timeouts
- Set explicit timeouts; do not rely on defaults.
- Use smaller connect timeout; moderate read timeout.
---
## Pagination patterns
Support at least one helper that can handle:
- `next` URL in response
- cursor token in response
- page/limit parameters
Add a hard stop:
- max pages OR max items OR max elapsed time
---
## Idempotency patterns (Python)
Choose and document:
- Use an `Idempotency-Key` header when supported
- Upsert using a stable `external_id`
- Persist a lightweight state store:
- simplest: SQLite file (recommended for OSS)
- alternative: JSONL log + compaction
Minimum: ensure repeated runs dont create duplicates.
---
## Observability (Python)
Minimum logs should include:
- `run_id`
- request: method, url/path, status_code, elapsed_ms, attempt
- record counts: processed/created/updated/skipped/failed
Also include a final summary log line.
---
## Verification requirements
For non-trivial work, add:
- unit tests for mapping/transform logic
- at least one test for pagination or retry behavior (mocked)
- a “dry-run” CLI flag (prints intended writes)
---
## Output format when writing code
- Provide a small directory layout
- Explain how to configure env vars
- Include exact commands to run (and test)

View File

@@ -0,0 +1,109 @@
---
name: superpowers-rest-automation
description: Builds reliable automations that integrate with REST APIs: auth, pagination, retries, rate limits, idempotency, webhooks, data mapping, and safe error handling. Use when calling external APIs, syncing systems, or building ETL-style workflows.
---
# REST Automation Skill
This skill enforces reliability and safety when building automations that call REST APIs.
## When to use this skill
Use whenever the task involves:
- calling external REST APIs (CRUD, search, sync)
- integrating 2+ systems (ETL, iPaaS-like flows)
- webhooks, polling, or scheduled jobs
- data ingestion, normalization, enrichment, deduplication
## Default design principles
- **Idempotent by design**: repeats should not create duplicates or corrupt data.
- **Observable**: logs/metrics correlate each run and each API call.
- **Fail safe**: handle partial failures; avoid silent data loss.
- **Rate-limit aware**: backoff and respect vendor limits.
- **Least privilege**: handle secrets safely, avoid overbroad scopes.
---
## Checklist (apply unless irrelevant)
### 1) Define the contract
- Inputs (format, required fields, validation)
- Outputs (where data goes, expected shape)
- Success criteria (what “done” means)
- Non-goals (what the automation will not do)
### 2) Authentication & secrets
- Identify auth type: API key, OAuth2, JWT, mTLS
- Never hardcode secrets in code or logs
- Support secret injection via env vars / secret manager
- Plan token refresh if applicable (OAuth2)
### 3) Idempotency & deduplication
Pick at least one:
- Use provider idempotency keys (if supported)
- Use stable external IDs (e.g., `external_id` field) for upserts
- Keep a local/state store mapping source IDs -> target IDs
- Use deterministic hashes for dedupe when no stable ID exists
Document the idempotency strategy explicitly.
### 4) Pagination & incremental sync
- Detect pagination style: `next` link, cursor, page+limit, offset+limit
- Ensure loops terminate safely (max pages / max time)
- Prefer incremental sync using `updated_since`/ETag/If-Modified-Since when possible
- Handle out-of-order updates and late-arriving events
### 5) Retries, backoff, and timeouts
- Set **timeouts** for connect/read
- Retry on transient errors: network failures, 429, 5xx (with limits)
- Use exponential backoff with jitter if possible
- Do **not** retry on most 4xx (except 408/409/429 depending on semantics)
- Cap retries and surface failures clearly
### 6) Rate limits & quotas
- Respect `Retry-After` and rate-limit headers
- Implement adaptive backoff on 429
- Consider batch endpoints to reduce call volume
- Avoid bursty concurrency unless explicitly safe
### 7) Data mapping & validation
- Explicit mapping layer (source -> normalized -> target)
- Validate required fields and types
- Normalize common formats (dates, enums, currency, locales)
- Handle nullability and partial payloads
- Record rejected records with reasons (dont silently drop)
### 8) Error handling strategy
Choose and document per error class:
- **Skip with log** (non-critical record)
- **Retry** (transient)
- **Quarantine** (store failing payload for later)
- **Fail the run** (systemic issue)
Ensure the workflow reports a clear summary at the end.
### 9) Observability & audit trail
Minimum:
- Run ID / correlation ID
- Per-request logs: method, path (not full secrets), status, latency, attempt count
- Counters: processed, created, updated, skipped, failed
Prefer structured logs (JSON) if possible.
### 10) Webhooks (if involved)
- Verify signature (if provided)
- Handle replay (idempotency for event IDs)
- Respond quickly; process async if needed
- Store raw event payloads (optional but recommended)
### 11) Safety controls
- Dry-run mode (no writes)
- Limit scope (max records per run)
- “Kill switch” config flag
- Backups/rollback plan for destructive operations
---
## Output requirements (when producing a solution)
Include:
- Idempotency strategy (13 bullets)
- Retry/backoff policy
- Pagination/incremental sync approach (if relevant)
- Error handling strategy + what gets logged/quarantined
- Verification plan (tests or a safe sandbox run plan)

View File

@@ -0,0 +1,33 @@
---
name: superpowers-review
description: Reviews changes for correctness, edge cases, style, security, and maintainability with severity levels (Blocker/Major/Minor/Nit). Use before finalizing changes.
---
# Review Skill
## When to use this skill
- before delivering final code changes
- after implementing a planned set of steps
- before merging or shipping
## Severity levels
- **Blocker**: wrong behavior, security issue, data loss risk, broken tests/build
- **Major**: likely bug, missing edge cases, poor reliability
- **Minor**: style, clarity, small maintainability issues
- **Nit**: optional polish
## Checklist
1. Correctness vs requirements
2. Edge cases & error handling
3. Tests (adequate coverage, meaningful assertions)
4. Security (secrets, auth, injection, unsafe defaults)
5. Performance (obvious hotspots, N+1, unnecessary work)
6. Readability & maintainability
7. Docs / comments updated if needed
## Output format
- Blockers
- Majors
- Minors
- Nits
- Overall summary + next actions

View File

@@ -0,0 +1,30 @@
---
name: superpowers-tdd
description: Applies tests-first discipline (red/green/refactor) and adds regression tests for bugs. Use when implementing features, fixing bugs, or refactoring.
---
# TDD Skill
## When to use this skill
- new features that can be unit tested
- bug fixes (always add a regression test if practical)
- refactors (protect behavior with tests first)
## Rules
- Prefer **red -> green -> refactor**.
- If tests are hard, still add **verification**: minimal repro script, integration test, or clear manual steps.
- Keep tests focused: one behavior per test where possible.
- Name tests by behavior, not implementation details.
## Process
1. Define the behavior change (what should be true after).
2. Write/adjust a test to capture it (make it fail first if possible).
3. Implement the minimal change to pass.
4. Refactor if needed (keep passing).
5. Run the relevant test suite + any linters.
## Output requirements
When you change code, include:
- what tests you added/changed
- how to run them
- what they prove

View File

@@ -0,0 +1,48 @@
---
name: superpowers-workflow
description: Enforces a disciplined workflow for coding, debugging, refactoring, and automation: brainstorm -> plan -> implement with verification (prefer TDD) -> review -> finish. Use for almost any non-trivial change.
---
# Superpowers Workflow
This skill defines the default operating procedure for software and automation tasks.
## When to use this skill
Use whenever the user asks to:
- build or modify code
- debug an error or failing tests
- refactor or improve quality
- design an automation workflow (e.g., API integrations, ETL, pipelines)
- add tests, reliability, or safety checks
## Activation marker (required)
Immediately after reading this skill (before any other output), run:
python .agent/skills/superpowers-workflow/scripts/record_activation.py --skill superpowers-workflow
## Default workflow (mandatory unless explicitly unnecessary)
1. **Brainstorm (short)**: clarify goal, constraints, risks, and acceptance criteria.
2. **Write a plan**: small steps (210 minutes each) with files + verification.
3. **Implement**: make the smallest correct change; prefer tests-first when feasible.
4. **Review pass**: correctness, edge cases, security, style, maintainability.
5. **Finish**: run verification commands, summarize changes + next steps.
## Decision tree: how much process is needed?
- **Tiny change (1 file, obvious)**:
- Do a mini-brainstorm (3 bullets), then mini-plan (35 steps), then implement + verify.
- **Non-trivial change**:
- Full brainstorm + plan before editing.
- **High-risk change** (auth, money, prod data, security, migrations):
- Add explicit risk controls: rollback plan, dry-run, extra tests, logging, safe defaults.
## Output rules (how you communicate)
- Always state **assumptions** if anything is ambiguous.
- Always include **verification** (commands, tests, or observable checks).
- If you must ask questions, ask **at most 3**; then proceed with best assumptions.
## Stop conditions
Pause implementation and switch to planning if:
- requirements conflict
- critical unknowns block correctness
- the change could cause data loss or security issues without safeguards

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from pathlib import Path
def find_repo_root(start: Path) -> Path:
# Walk upwards until we find a marker that suggests repo root
for p in [start, *start.parents]:
if (p / ".agent").exists() or (p / ".git").exists() or (p / "pyproject.toml").exists():
return p
return start
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--skill", required=True)
parser.add_argument("--run-id", default="")
args = parser.parse_args()
repo_root = find_repo_root(Path.cwd())
log_path = repo_root / "e2e_demo" / "skill-activation.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).isoformat()
line = f"{ts}\tskill={args.skill}\trun_id={args.run_id}\n"
log_path.write_text((log_path.read_text() if log_path.exists() else "") + line, encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Spawn an isolated Gemini CLI subagent for focused task execution.
This enables parallel execution by launching independent gemini instances
with isolated context and specific skill instructions.
"""
import argparse
import json
import subprocess
import sys
import time
import uuid
from pathlib import Path
from typing import Any
def find_repo_root(start: Path) -> Path:
"""Traverse upwards to find the repository root (containing .agent/)."""
curr = start.resolve()
for _ in range(10):
if (curr / ".agent").exists():
return curr
if curr.parent == curr:
break
curr = curr.parent
return Path.cwd()
def load_skill_instructions(skill_path: Path) -> str:
"""Load skill instructions from SKILL.md file."""
if not skill_path.exists():
return ""
return skill_path.read_text(encoding="utf-8")
def spawn_subagent(
skill: str,
task: str,
repo_root: Path,
yolo: bool = True,
output_format: str = "text",
) -> dict[str, Any]:
"""
Spawn a subagent with isolated context.
Args:
skill: Skill name (e.g., 'tdd', 'debug', 'review')
task: Task description for the subagent
repo_root: Repository root path
yolo: Auto-approve all actions (default: True for parallel execution)
output_format: Output format ('text' or 'json')
Returns:
dict with keys: success, output, error, log_file, duration_s
"""
# Generate unique subagent ID
subagent_id = uuid.uuid4().hex[:8]
timestamp = time.strftime("%Y%m%d-%H%M%S")
# Setup logging directory
log_dir = repo_root / "artifacts" / "superpowers" / "subagents"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / f"{skill}-{timestamp}-{subagent_id}.log"
# Load skill instructions
skill_file = repo_root / f".agent/skills/superpowers-{skill}/SKILL.md"
skill_instructions = load_skill_instructions(skill_file)
if not skill_instructions:
return {
"success": False,
"output": "",
"error": f"Skill not found: {skill_file}",
"log_file": str(log_file),
"duration_s": 0,
}
# Construct focused prompt
prompt = f"""You are a specialized subagent focused on: {skill}
IMPORTANT: You have ISOLATED CONTEXT. Do not assume knowledge from other conversations.
Task:
{task}
Skill Instructions:
{skill_instructions}
Requirements:
1. Follow the skill instructions exactly
2. Complete the task fully
3. Output ONLY the final result at the end
4. Do not include meta-commentary or thinking process in final output
5. Write any artifacts to artifacts/superpowers/subagent-{subagent_id}/
When complete, output:
---SUBAGENT-RESULT-START---
[Your final result here]
---SUBAGENT-RESULT-END---
"""
# Build command
cmd = ["gemini"]
if yolo:
cmd.append("--yolo")
# Execute subagent
start_time = time.time()
try:
with open(log_file, "w", encoding="utf-8") as log:
log.write("=== SUBAGENT EXECUTION LOG ===\n")
log.write(f"Skill: {skill}\n")
log.write(f"ID: {subagent_id}\n")
log.write(f"Timestamp: {timestamp}\n")
log.write(f"Task: {task}\n\n")
log.write("=== PROMPT ===\n")
log.write(prompt)
log.write("\n\n=== EXECUTION ===\n")
log.flush()
result = subprocess.run(
cmd,
input=prompt,
capture_output=True,
text=True,
cwd=repo_root,
timeout=600, # 10 minute timeout
shell=True, # Required on Windows for .ps1/.cmd scripts
)
duration_s = time.time() - start_time
log.write("\n=== STDOUT ===\n")
log.write(result.stdout)
log.write("\n=== STDERR ===\n")
log.write(result.stderr)
log.write(f"\n=== EXIT CODE: {result.returncode} ===\n")
log.write(f"=== DURATION: {duration_s:.2f}s ===\n")
# Extract final result from markers
output = result.stdout
if "---SUBAGENT-RESULT-START---" in output:
parts = output.split("---SUBAGENT-RESULT-START---", 1)
if len(parts) > 1:
result_part = parts[1].split("---SUBAGENT-RESULT-END---", 1)
output = result_part[0].strip()
return {
"success": result.returncode == 0,
"output": output,
"error": result.stderr if result.returncode != 0 else "",
"log_file": str(log_file),
"duration_s": duration_s,
"subagent_id": subagent_id,
}
except subprocess.TimeoutExpired:
duration_s = time.time() - start_time
return {
"success": False,
"output": "",
"error": f"Subagent timed out after {duration_s:.0f}s",
"log_file": str(log_file),
"duration_s": duration_s,
"subagent_id": subagent_id,
}
except Exception as e:
duration_s = time.time() - start_time
return {
"success": False,
"output": "",
"error": f"Subagent execution failed: {e}",
"log_file": str(log_file),
"duration_s": duration_s,
"subagent_id": subagent_id,
}
def main() -> int:
parser = argparse.ArgumentParser(
description="Spawn a Gemini CLI subagent for parallel execution"
)
parser.add_argument(
"--skill",
required=True,
help="Skill to use (tdd, debug, review, rest-automation, python-automation)",
)
parser.add_argument(
"--task",
required=True,
help="Task description for the subagent",
)
parser.add_argument(
"--no-yolo",
action="store_true",
help="Disable auto-approval (interactive mode)",
)
parser.add_argument(
"--output-format",
choices=["text", "json"],
default="text",
help="Output format",
)
args = parser.parse_args()
repo_root = find_repo_root(Path.cwd())
if args.output_format == "text":
print(f"🤖 Spawning subagent: {args.skill}")
print(f"📋 Task: {args.task[:80]}{'...' if len(args.task) > 80 else ''}")
result = spawn_subagent(
skill=args.skill,
task=args.task,
repo_root=repo_root,
yolo=not args.no_yolo,
output_format=args.output_format,
)
if args.output_format == "json":
print(json.dumps(result, indent=2))
return 0 if result["success"] else 1
# Text output
print(f"\n{'' if result['success'] else ''} Subagent completed in {result['duration_s']:.1f}s")
print(f"📝 Full log: {result['log_file']}")
if result["success"]:
print(f"\n{'='*60}")
print("RESULT:")
print(f"{'='*60}")
print(result["output"])
return 0
else:
print(f"\n{'='*60}")
print("ERROR:")
print(f"{'='*60}")
print(result["error"])
return 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,28 @@
from __future__ import annotations
import argparse
import sys
from pathlib import Path
def find_repo_root(start: Path) -> Path:
for p in [start, *start.parents]:
if (p / ".agent").exists() or (p / ".git").exists():
return p
return start
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--path", required=True, help="Repo-relative path to write, e.g. artifacts/superpowers/brainstorm.md")
args = parser.parse_args()
repo_root = find_repo_root(Path.cwd())
out_path = (repo_root / args.path).resolve()
out_path.parent.mkdir(parents=True, exist_ok=True)
content = sys.stdin.read()
out_path.write_text(content, encoding="utf-8")
print(str(out_path))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,38 @@
---
description: Superpowers brainstorm. Produces goal/constraints/risks/options/recommendation/acceptance criteria.
---
# Superpowers Brainstorm
## Task
Brainstorm for this task (exactly as provided by the user):
**{{input}}**
If `{{input}}` is empty or missing, ask the user to restate the task in one sentence and STOP.
## Output sections (use exactly)
## Goal
## Constraints
## Known context
## Risks
## Options (24)
## Recommendation
## Acceptance criteria
## Persist (mandatory)
After generating the brainstorm content, you MUST write it to disk using this exact procedure:
1) Output the brainstorm markdown content first (the sections above).
2) Then immediately run:
```bash
python .agent/skills/superpowers-workflow/scripts/write_artifact.py --path artifacts/superpowers/brainstorm.md
```
Provide the brainstorm markdown as stdin to the command.
After writing, confirm it exists by listing artifacts/superpowers/.
If you cannot run the command, say so explicitly and instruct the user to copy/paste the brainstorm output into artifacts/superpowers/brainstorm.md.
Do not implement changes in this workflow. Stop after persistence.

View File

@@ -0,0 +1,33 @@
---
description: Systematic debugging workflow: reproduce, minimize, hypotheses, instrument, fix, prevent, verify.
---
# Superpowers Debug
Read and apply the `superpowers-debug` skill.
Use the required reporting format:
- Symptom
- Repro steps
- Root cause
- Fix
- Regression protection
- Verification
## Persist (mandatory)
After generating the debug content above, you MUST write it to disk:
1) Copy the full debug markdown output.
2) Run:
```bash
python .agent/skills/superpowers-workflow/scripts/write_artifact.py --path artifacts/superpowers/debug.md
```
Provide the debug markdown as stdin to the command.
After writing, confirm it exists by listing artifacts/superpowers/.
If you cannot run the command, say so explicitly and instruct the user to copy/paste the debug output into artifacts/superpowers/debug.md.
Do not implement changes in this workflow. Stop after persistence.

View File

@@ -0,0 +1,213 @@
---
description: Execute an approved plan with parallel execution for independent steps. Spawns isolated subagents. Consolidates results.
---
# Superpowers Execute Plan (Parallel Mode)
## Overview
This workflow executes an approved plan by identifying independent steps and running them in parallel using isolated subagents.
## When to use parallel mode
- Plan has 2+ steps that don't depend on each other
- Steps operate on different files or independent modules
- You want faster execution (parallel > sequential)
## When NOT to use parallel mode
- Steps have dependencies (Step 2 needs Step 1's output)
- All steps modify the same file
- Plan has < 2 steps
- You want simpler debugging (sequential is easier to debug)
**If unsure, use `/superpowers-execute-plan` (sequential) instead.**
---
## Preconditions (do not skip)
1. The user must have replied **APPROVED** to a written plan
2. The approved plan must exist at: `artifacts/superpowers/plan.md`
If `artifacts/superpowers/plan.md` does not exist:
- Stop immediately
- Tell the user to run `/superpowers-write-plan` first
- Do not continue
---
## Load and analyze the plan
1. Read `artifacts/superpowers/plan.md`
2. Parse all plan steps
3. Identify dependencies between steps:
- Does Step 2 modify files created/changed by Step 1?
- Does Step 2 need Step 1's verification to pass first?
- Do they modify the same files?
4. Group steps into execution batches:
- **Batch 1**: All independent steps (no dependencies)
- **Batch 2**: Steps that depend on Batch 1 completing
- **Batch 3**: Steps that depend on Batch 2 completing
- etc.
---
## Execution strategy
### For each batch:
1. **Spawn subagents in parallel** for all steps in the batch:
```bash
# Example: Batch 1 has 3 independent steps
python .agent/skills/superpowers-workflow/scripts/spawn_subagent.py \
--skill tdd \
--task "Step 1: Add retry logic to sync.py with exponential backoff" &
python .agent/skills/superpowers-workflow/scripts/spawn_subagent.py \
--skill rest-automation \
--task "Step 2: Add pagination handling to fetch_items()" &
python .agent/skills/superpowers-workflow/scripts/spawn_subagent.py \
--skill python-automation \
--task "Step 3: Update CLI args to support --max-retries flag" &
# Wait for all to complete
wait
```
2. **Collect results** from each subagent:
- Check log files in `artifacts/superpowers/subagents/`
- Extract final results from each
- Check success/failure status
3. **Verify batch completion**:
- Run verification commands for all steps in the batch
- If ANY step fails:
- Stop execution
- Switch to `/superpowers-debug` for the failed step
- Do NOT continue to next batch
4. **Append to execution log**:
- Write batch summary to `artifacts/superpowers/execution.md`:
```markdown
## Batch N (Parallel Execution)
- Step X: [SUCCESS/FAILED] - Files: [...] - Duration: Xs
- Step Y: [SUCCESS/FAILED] - Files: [...] - Duration: Ys
Verification:
- Step X: [command] -> [result]
- Step Y: [command] -> [result]
```
5. **Move to next batch** (if all steps passed)
---
## Skill selection for subagents
Choose the appropriate skill for each step:
| Step Type | Skill to Use |
|-----------|-------------|
| Add tests, TDD cycle | `tdd` |
| Fix bugs, investigate failures | `debug` |
| Code review, quality check | `review` |
| REST API work | `rest-automation` |
| Python tooling/scripts | `python-automation` |
| General implementation | `tdd` (default) |
---
## Consolidation phase
After all batches complete:
1. **Integration verification**:
- Run full test suite (not just individual step tests)
- Verify all changes work together
- Check for conflicts between parallel changes
2. **Conflict resolution**:
- If parallel steps modified related code:
- Review for integration issues
- Run combined tests
- Fix any conflicts
3. **Final artifacts**:
- Update `artifacts/superpowers/execution.md` with:
- Total batches executed
- Total steps completed
- Total time saved vs sequential
- All verification results
- Write `artifacts/superpowers/finish.md` with:
- Summary of changes
- Integration test results
- Follow-up items (if any)
---
## Example: 5-step plan with 2 batches
**Plan:**
1. Add retry logic to sync.py (independent)
2. Add pagination to API client (independent)
3. Update CLI args (independent)
4. Add integration test (depends on 1, 2, 3)
5. Update docs (depends on 4 passing)
**Execution:**
**Batch 1 (parallel):**
- Spawn 3 subagents for steps 1, 2, 3
- Wait for all to complete (~5 min instead of ~15 min sequential)
- Verify each step
**Batch 2 (sequential):**
- Step 4: Add integration test (needs 1+2+3 complete)
- Verify test passes
**Batch 3 (sequential):**
- Step 5: Update docs (needs 4 complete)
- Verify docs are accurate
**Total time: ~10 min vs ~25 min sequential = 60% time savings**
---
## Troubleshooting
### Subagent spawn fails
- Check that `gemini` is in PATH (verify with: `gemini --version`)
- Verify skill exists: `.agent/skills/superpowers-{skill}/SKILL.md`
- Check subagent logs in `artifacts/superpowers/subagents/`
### Steps conflict
- Falls back to sequential execution for conflicting steps
- Mark dependent steps explicitly in plan to avoid conflicts
### Verification fails after parallel execution
- Check integration - parallel steps may work individually but conflict
- Run `/superpowers-debug` to investigate
- Consider re-running in sequential mode: `/superpowers-execute-plan`
---
## Persist (mandatory)
Write execution notes to disk:
- Append batch summaries to: `artifacts/superpowers/execution.md`
- Write final summary to: `artifacts/superpowers/finish.md`
Ensure `artifacts/superpowers/` exists.
Confirm files exist by listing `artifacts/superpowers/` when done.
---
## Finish
After all steps complete:
1. Run `/superpowers-review` (or inline review pass)
2. Generate final summary with time savings metrics
3. List all changed files
4. Provide any manual validation steps
Stop after completing the finish step.

View File

@@ -0,0 +1,84 @@
---
description: Executes an approved plan in small steps with verification after each step. Writes execution artifacts to disk. Stops on failures. Finishes with review + summary.
---
# Superpowers Execute Plan
## Persist (mandatory)
You must write execution artifacts to disk (not IDE-only documents):
- Append execution notes to: `artifacts/superpowers/execution.md`
- Write the final summary to: `artifacts/superpowers/finish.md`
Requirements:
1) Ensure the folder `artifacts/superpowers/` exists (create it if needed).
2) After EACH completed plan step, append a note to `artifacts/superpowers/execution.md`.
3) At the end, write the final summary to `artifacts/superpowers/finish.md`.
4) After writing, confirm the files exist by listing `artifacts/superpowers/`.
If you are unable to write these files directly, use `python .agent/skills/superpowers-workflow/scripts/write_artifact.py --path <target>` to persist the content.
## Preconditions (do not skip)
1) The user must have replied **APPROVED** to a written plan.
2) The approved plan must exist on disk at:
- `artifacts/superpowers/plan.md`
If `artifacts/superpowers/plan.md` does not exist:
- Stop immediately.
- Tell the user to run `/superpowers-write-plan` first.
- Do not edit code.
## Load the plan
- Read `artifacts/superpowers/plan.md`.
- Restate the plan briefly (12 lines) before making changes.
## Check for parallel execution opportunity (optional)
After loading the plan, analyze if steps can run in parallel:
1. **Check for independent steps**: Do 2+ steps operate on different files with no dependencies?
2. **If yes**: Suggest to the user:
- "I notice steps X, Y, Z are independent and could run in parallel."
- "Would you like to use `/superpowers-execute-plan-parallel` for faster execution?"
- "Or continue with sequential execution? (Reply: PARALLEL or SEQUENTIAL)"
3. **If PARALLEL**: Stop and instruct user to run `/superpowers-execute-plan-parallel` instead.
4. **If SEQUENTIAL or no independent steps**: Continue with sequential execution below.
## Skills to apply as needed
Read and apply these skills when relevant:
- `superpowers-tdd` (preferred)
- `superpowers-debug` (if issues occur)
- `superpowers-review`
- `superpowers-finish`
- `superpowers-rest-automation` (if relevant)
- `superpowers-python-automation` (if Python)
## Execution rules (strict)
1) Implement **ONE** plan step at a time.
2) After each step:
- Run the steps verification command(s) (or, if you cannot run them, provide exact commands and expected outcomes).
- Append a short note to `artifacts/superpowers/execution.md` containing:
- Step name
- Files changed
- What changed (13 bullets)
- Verification command(s)
- Result (pass/fail or “not run”)
3) If verification fails:
- Stop.
- Switch to systematic debugging (use `superpowers-debug`).
- Do not continue executing further steps until fixed and verified.
4) Keep changes minimal and scoped to the plan. If the plan is wrong or missing a step:
- Stop and update the plan (write the updated plan back to `artifacts/superpowers/plan.md`)
- Ask for approval again if the change is material.
## Finish (required)
At the end:
1) Run a review pass (Blocker/Major/Minor/Nit).
2) Write a final summary to `artifacts/superpowers/finish.md` including:
- Verification commands run + results
- Summary of changes
- Follow-ups (if any)
- Manual validation steps (if applicable)
3) Confirm the artifacts exist by listing `artifacts/superpowers/`.
Stop after completing the finish step.

View File

@@ -0,0 +1,31 @@
---
description: Finalize work: verification, summary, follow-ups, manual validation steps.
---
# Superpowers Finish
Read and apply the `superpowers-finish` skill.
Output:
## Verification (commands + results if possible)
## Summary of changes
## Follow-ups (if needed)
## Manual validation steps (if applicable)
## Persist (mandatory)
After generating the finish content above, you MUST write it to disk:
1) Copy the full finish markdown output.
2) Run:
```bash
python .agent/skills/superpowers-workflow/scripts/write_artifact.py --path artifacts/superpowers/finish.md
```
Provide the finish markdown as stdin to the command.
After writing, confirm it exists by listing artifacts/superpowers/.
If you cannot run the command, say so explicitly and instruct the user to copy/paste the finish output into artifacts/superpowers/finish.md.
Do not implement changes in this workflow. Stop after persistence.

View File

@@ -0,0 +1,13 @@
---
description: Reloads Superpowers configuration by re-reading Rules, Workflows, and Skills from disk.
---
# Superpowers Reload
Read these directories from disk and summarize what you loaded:
- `.agent/rules/`
- `.agent/workflows/`
- `.agent/skills/` (list skill names + descriptions)
Then confirm you will follow the latest versions in this session.
Stop after confirming.

View File

@@ -0,0 +1,32 @@
---
description: Runs a Superpowers-style review pass with severity levels.
---
# Superpowers Review
Read and apply the `superpowers-review` skill.
Output:
- Blockers
- Majors
- Minors
- Nits
- Summary + next actions
## Persist (mandatory)
After generating the review content above, you MUST write it to disk:
1) Copy the full review markdown output.
2) Run:
```bash
python .agent/skills/superpowers-workflow/scripts/write_artifact.py --path artifacts/superpowers/review.md
```
Provide the review markdown as stdin to the command.
After writing, confirm it exists by listing artifacts/superpowers/.
If you cannot run the command, say so explicitly and instruct the user to copy/paste the review output into artifacts/superpowers/review.md.
Do not implement changes in this workflow. Stop after persistence.

View File

@@ -0,0 +1,57 @@
---
description: Superpowers plan gate. Writes a small-step plan with files + verification. Must ask for approval before coding.
---
# Superpowers Write Plan (Gate)
## Task
Plan for this task (exactly as provided by the user):
**{{input}}**
If `{{input}}` is empty or missing, ask the user to restate the task in one sentence and STOP.
## Rules
- DO NOT edit code.
- You may read files to understand context, but produce the plan and then stop.
- Plan steps must be small (210 minutes each) and include verification commands.
## Output format (use exactly)
## Goal
## Assumptions
## Plan
(Each step must include: Files, Change, Verify)
## Risks & mitigations
## Rollback plan
## Persist (mandatory)
Write the plan output to:
- `artifacts/superpowers/plan.md`
Create the folder if needed.
After writing, confirm it exists by listing `artifacts/superpowers/`.
## Approval
Ask:
**Approve this plan? Reply APPROVED if it looks good.**
If the user replies APPROVED:
- Do NOT implement yet.
- Reply: **"Plan approved. Run `/superpowers-execute-plan` to begin implementation."**
## Persist (mandatory)
After generating the plan content above, you MUST write it to disk:
1) Copy the full plan markdown output.
2) Run:
```bash
python .agent/skills/superpowers-workflow/scripts/write_artifact.py --path artifacts/superpowers/plan.md
```
Provide the plan markdown as stdin to the command.
After writing, confirm it exists by listing artifacts/superpowers/.
If you cannot run the command, say so explicitly and instruct the user to copy/paste the plan output into artifacts/superpowers/plan.md.
Do not implement changes in this workflow. Stop after persistence.

View File

@@ -0,0 +1,6 @@
---
trigger: always_on
glob:
description:
---

View File

@@ -19,7 +19,22 @@
"Bash(sqlite3 data/inventory.db \".schema users\")",
"Bash(sqlite3 data/inventory.db \"SELECT * FROM users;\")",
"Bash(git restore:*)",
"Bash(pkill -f \"uvicorn\")"
"Bash(pkill -f \"uvicorn\")",
"Bash(docker-compose build:*)",
"Bash(docker version:*)",
"Bash(docker compose version:*)",
"Bash(docker compose:*)",
"Bash(open -a Docker)",
"Bash(dockerd)",
"Bash(brew services:*)",
"Bash(colima start:*)",
"Bash(colima status:*)",
"Bash(colima stop:*)",
"Bash(bash *)",
"Bash(npm run *)",
"Bash(npm test *)",
"Bash(python3 *)",
"Bash(ls -lh aInventory-PROD-v*.zip)"
]
}
}

18
.gitignore vendored
View File

@@ -10,8 +10,11 @@ backend/venv/
*.pyo
*.pyd
*.egg-info/
dist/
build/
.pytest_cache/
**/.pytest_cache/
coverage/
**/coverage/
# ── Runtime data directories ─────────────────────────────────
# Content is excluded; the directories themselves are tracked via .gitkeep.
@@ -38,7 +41,10 @@ backend/config/ldap_config.json
# ── Environment files (secrets) ──────────────────────────────
.env
.env.*
inventory.env
inventory.env.*
!.env.example
!inventory.env.example
backend/.env
backend/.env.*
!backend/.env.example
@@ -88,4 +94,12 @@ aInventory-PROD*.zip
*.crt
*.cert
__push_ALL_to_remote.sh
# ── Local AI Tooling & Persistent Paths ──────────────────────
.tool_paths
.git_path
# ── Temporary Docker / Verification Artifacts ────────────────
.docker_tmp/
.tmp_docker/
scratch/.npm/

28
AGENTS.md Normal file
View File

@@ -0,0 +1,28 @@
# AGENTS.md — Web App Project Rules
## Tech Stack
- Frontend: Next.js 15, React 19, TypeScript 5
- Styling: Tailwind CSS v3.4, Lucide React (Icons)
- Backend: Python 3.14, FastAPI, Uvicorn
- Database: SQLite (SQLAlchemy) + Dexie (IndexedDB pentru offline-first)
- AI & Vision: Google Gemini 2.0 (Vision), Tesseract.js (Local OCR)
- Infrastructure: Docker, Caddy (Automatic SSL Reverse Proxy)
- Security: JWT (python-jose), LDAP (Enterprise Integration)
## Code Quality
- Keep cyclomatic complexity under 10 per function
- Aim for files under 300 lines
## Testing
- Write unit tests for all utility functions
- Minimum 80% coverage on new code
- Use Vitest for unit tests
## Security
- Store all secrets in inventory.env — never hardcode credentials
- Never log API keys, tokens or passwords
- Validate all user input before processing
## Git Conventions
- Use conventional commits (feat:, fix:, docs:, etc.)
- Keep PRs under 400 lines of diff when possible

View File

@@ -11,10 +11,11 @@ This is the **Single Source of Truth** for ALL AI agents. Refer to [PROJECT_ARCH
- **STRICT HANDOVER**: Update `dev_docs/SESSION_STATE.md` at the end of every task with: **Active AI**, **Current Status** (Stable/Broken/In-Progress), **Context**, and **Next Steps**.
- **SESSION ARCHIVE**: Move previous handover content to `dev_docs/SESSION_HISTORY.md` before writing new state.
- **NO INTERACTION OVERLAP**: Never modify a file if another AI session is explicitly working on it.
- **CONCISE COMMUNICATION**: Be concise! Do not explain a thousand details unless they are absolutely necessary.
## 2. ENGINEERING & OPERATIONAL LAWS
- **ENGLISH ONLY**: Interfaces, code, variables, and docs MUST be in English. Translate any Romanian text found in code immediately. (User conversation: Romanian/English).
- **GIT PROTOCOL**: Use the direct binary path in `.git_path` (`/Library/Developer/CommandLineTools/usr/bin/git`) for ALL Git operations. **DO NOT REMOVE OR CHANGE THIS PATH UNDER ANY CIRCUMSTANCES!** Never push or use `--force` unless explicitly asked. Branching: `master` (stable), `dev` (active), `vX` (archive).
- **GIT PROTOCOL**: Use the direct binary path `/Library/Developer/CommandLineTools/usr/bin/git`) for ALL Git operations. **DO NOT REMOVE OR CHANGE THIS PATH UNDER ANY CIRCUMSTANCES!** Never push or use `--force` unless explicitly asked. Branching: `master` (stable), `dev` (active), `vX` (archive).
- **VERSIONING**: Update `VERSION.json` on every commit. Use `scripts/save_version.py` for automated releases.
- **DEPENDENCIES**: Update `backend/requirements.txt` with version constraints for every new pip package.
- **SSOT INTEGRITY**: Every feature change MUST update: `README.md`, `USER_GUIDE.md`, `PROJECT_ARCHITECTURE.md`, and `export_prod.sh`.

View File

@@ -8,4 +8,7 @@ Before taking any action or writing code, you MUST read the following Single Sou
2. `PROJECT_ARCHITECTURE.md` (Contains the tech stack, data models, and system logic).
3. `dev_docs/SESSION_STATE.md` (Contains the current handover status from the previous AI).
**STRICT COMMUNICATION RULE:**
Be concise! Do not explain a thousand details unless they are absolutely necessary.
Do not proceed with any task until you have analyzed these three files.

View File

@@ -8,4 +8,7 @@ Before taking any action or writing code, you MUST read the following Single Sou
2. `PROJECT_ARCHITECTURE.md` (Contains the tech stack, data models, and system logic).
3. `dev_docs/SESSION_STATE.md` (Contains the current handover status from the previous AI).
**STRICT COMMUNICATION RULE:**
Be concise! Do not explain a thousand details unless they are absolutely necessary.
Do not proceed with any task until you have analyzed these three files.

View File

@@ -12,21 +12,29 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
- **Database:** SQLite (SQLAlchemy) - Local file-based persistence
- **Validation:** Pydantic v2
- **Auth:** Hybrid LDAP (python-ldap) + PBKDF2 local password hash caching
- **AI Engine:** Google GenAI SDK (Gemini 2.0 Flash) - Location: `backend/ai/`
- **AI Engine:** Google GenAI SDK (Gemini 2.0 Flash) & Anthropic SDK (Claude 3.5 Sonnet) - Location: `backend/ai/`
- **Testing:** Pytest (Unit & Integration) - Location: `backend/tests/`
### 2.2 Frontend (Web & PWA)
- **Architecture:** Next.js 15+ (App Router)
- **Styling:** Tailwind CSS (Readability-first config)
- **Styling:** Tailwind CSS (Readability-first config, mobile-first responsive)
- **Icons:** Lucide Icons (React components)
- **Components:**
- **StatCard** (v1.9.21+): Responsive stat display component for mobile/desktop
- Two-column flexbox layout (label left, number right)
- Responsive font sizing with Tailwind breakpoints (text-sm→md, text-lg→xl)
- Label truncation with ellipsis for overflow handling
- Accessibility: `role="status"`, `aria-hidden` on decorative icons
- **Offline persistence:** Dexie.js (IndexedDB wrapper)
- **Scanner:** `html5-qrcode` (Client-side, offline-only)
- **Sync:** Axios with bulk-sync idempotency (UUID-based)
- **Testing:** Vitest (React Hook Testing) - Location: `frontend/tests/`
### 2.3 Operations & Tooling
- **PWA Deployment:** `next-pwa` (Service Workers + Manifest.json)
- **HTTPS Proxy:** `caddy` or `local-ssl-proxy` (Port 8909)
- **Servers:** Frontend (Port 8907), Backend (Port 8906)
- **Configuration:** Centrally managed via root `config/` directory (includes `network_config.env`, `ldap_config.json`, `Caddyfile`).
- **Configuration:** Centrally managed via root `inventory.env` (Network/CORS/API Keys), `config/` directory (LDAP, Caddyfile), and a dynamic `ConfigManager` (`backend/config_manager.py`) for runtime environment and AI provider settings.
## 3. Data Models & Entities
- **Item:** Name, Category Group (Structured), Item Type (Specific), Quantity, Barcode, Part Number, Box Label (Association).
@@ -37,7 +45,8 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
## 4. Scanning & Optimization Strategy (Crucial)
### 4.1 AI Usage Policy
- **Routine Operations (Check-in/Out):** Executes entirely on the local device unconditionally using `html5-qrcode` ($0 cost). No AI is allowed here.
- **New Item Onboarding (AI Label OCR):** Uses cloud AI (`gemini-2.0-flash`). The user takes a photo, AI extracts data based on strict templates.
- **New Item Onboarding (AI Label OCR):** Uses cloud AI (`gemini-2.0-flash` or `claude-3-5-sonnet`). The user takes a photo, AI extracts data based on strict templates.
- **AI Provider Selection (v1.9.23):** Administrators can choose between Gemini and Claude via the Admin Dashboard. API keys are managed securely in the environment.
- **AI Box Discovery Mode (v1.6.0):** Supports specialized `mode="box"` prompt that focuses exclusively on prominent container names/hand-written labels, ignoring technical spec noise.
- **Validation Mask:** AI-extracted data is NEVER saved directly. It is presented in a validation UI for human confirmation.
@@ -80,7 +89,9 @@ To ensure enterprise-grade protection, the following policies are enforced:
- **Admin Only:** Critical operations such as `DELETE /items/`, user management, and DB settings are restricted via the `auth.get_current_admin` dependency.
- **User Role:** Standard users are permitted to perform check-in/out and list inventory, but cannot delete catalog entries.
### 7.2 Brute-Force Protection
### 7.2 CORS & Origin Policy (v1.9.18)
- **Automatic Discovery:** The system detects local LAN IP and automatically authorizes it.
- **Generic Expansion:** Use `EXTRA_ALLOWED_ORIGINS` for Tailscale or VPN IPs. The system automatically expands each IP into a set of authorized Origins (http/8916, https/8918, https/8919).
- **Rate Limiting:** Implemented via `slowapi`. The `login` endpoint is limited to **5 requests per minute** per IP to mitigate automated credential stuffing.
### 7.3 Data Privacy
@@ -95,3 +106,15 @@ To ensure enterprise-grade protection, the following policies are enforced:
To ensure deployment stability on macOS environments with potentially broken developer tool links (`xcode-select` errors):
- **Direct Binary Mapping:** The system bypasses path resolution by using a hardcoded direct link to the Git binary in `.git_path` (`/Library/Developer/CommandLineTools/usr/bin/git`).
- **Persistence Mandate:** This path is protected by mandatory AI rules and must never be removed or modified to ensure `save-version` and automated deployment scripts remain functional.
106:
107: ## 8. Multi-AI Engine & Dynamic Configuration (v1.9.23)
108: To enhance extraction flexibility and system resilience:
109: - **Unified AI Core:** The backend uses an abstraction layer to handle multiple AI providers (Gemini and Claude).
110: - **Dynamic Configuration:** System settings (AI Provider, API Keys, Backup Policies) are managed via `backend/config_manager.py`, allowing real-time updates without restarting the container.
111: - **Admin Standardization:** The Admin Dashboard features a standardized configuration UI with secure field masking for sensitive credentials.
- **Architectural Modularization (v1.10.0):**
- **Frontend:** The monolithic Admin Dashboard has been decomposed into domain-specific components: `IdentityManager`, `DatabaseManager`, `LdapManager`, `AiManager`, and `CategoryManager`.
- **Logic:** Business logic is centralized in the `useAdmin` custom React hook, ensuring clean separation of concerns.
- **Backend:** Administrative endpoints are split into the `backend/routers/admin/` package, with specialized routers for `backups` and `config`.
- **Stability:** Docker builds are secured against lockfile mismatches by enforcing strict dependency synchronization.
- **Verification Infrastructure:** A dual-layer testing suite is implemented: Pytest for backend integration (using in-memory SQLite and mocked auth) and Vitest for frontend logic validation.

View File

@@ -12,8 +12,8 @@ This project supports three distinct operational modes:
Ideal for local development on macOS/Linux.
* **Command:** `./start_server.sh`
* **Details:** Runs FastAPI (backend) and Next.js (frontend) in development mode. Uses `local-ssl-proxy` for HTTPS.
* **Backend:** http://localhost:8906
* **Frontend:** https://localhost:8909
* **Backend:** http://localhost:8916
* **Frontend:** https://localhost:8919
### 2. 🐳 Docker Mode (Recommended for Production)
Isolated and portable container stack.
@@ -39,13 +39,37 @@ To generate a clean production package and snapshot the current state:
---
## 🎨 Recent UI/UX Improvements (v1.9.21+)
### Mobile-First Responsive Design
- **StatCard Component:** Reusable, responsive stat display component for mobile phones
- Two-column flexbox layout (label left, number right) prevents text overflow on narrow screens
- Responsive font sizing: `text-sm md:text-base` (labels), `text-lg md:text-xl` (numbers)
- Automatic label truncation with ellipsis for long text
- Accessible markup with `role="status"` and `aria-hidden` attributes
- **Pages Updated:** Inventory (Categories, Item Types, Total Boxes), Logs (Total Events, Check in/out), Admin (Local Archives)
- **Tested on:** iPhone SE (375px), iPhone 12 (390px), iPhone 14 Pro Max (430px), tablets (768px), desktop (1024px)
- **Admin UI Standardization:** Refactored Admin page with unified StatCards and secure input masking for system credentials.
- **Modular Admin Architecture (v1.10.0):**
- Decomposed monolithic pages into domain-specific components: `IdentityManager`, `DatabaseManager`, `LdapManager`, `AiManager`, and `CategoryManager`.
- Centralized state logic into a custom `useAdmin` hook for improved maintainability.
- Split backend admin endpoints into specific routers (`backups`, `config`) for better scalability.
- Optimized Docker configuration with persistent binary paths and fixed log-piping via `su-exec`.
## 🏗 Technical Overview
* **Backend:** FastAPI (Python 3.12+)
* **Frontend:** Next.js 15+ (React PWA)
* **Frontend:** Next.js 15+ (React PWA) with responsive Tailwind CSS
* **Database:** SQLite (SQLAlchemy) with Dexie.js (IndexedDB) for client-side sync.
* **Proxy:** Caddy (Docker) or local-ssl-proxy (Standalone/Dev).
* **AI Engine:** Google Gemini (Generative AI SDK).
## 🧪 Testing & Verification (v1.10.0+)
The project includes a comprehensive testing suite to ensure architectural stability:
- **Backend:** `pytest` integration tests with in-memory database mocks.
- Run: `PYTHONPATH=. ./backend/venv/bin/pytest backend/tests/`
- **Frontend:** `vitest` unit tests for custom React hooks and state logic.
- Run: `npm run test` (requires Node 20+)
For more details, see [PROJECT_ARCHITECTURE.md](PROJECT_ARCHITECTURE.md).
---
@@ -58,7 +82,8 @@ The application requires the following environment variables for production depl
| Variable | Purpose | Example |
|----------|---------|---------|
| **JWT_SECRET_KEY** | JWT token signing key (REQUIRED for production) | `openssl rand -hex 32` |
| **ALLOWED_ORIGINS** | CORS-allowed domain origins (comma-separated) | `https://inventory.example.com,https://api.example.com` |
| **EXTRA_ALLOWED_ORIGINS** | Extra IPs or FQDNs for CORS (Tailscale, VPN, etc.) | `100.78.182.27,inventory.local` |
| **ALLOWED_ORIGINS** | CORS-allowed domain origins (automatically includes LOCAL_IP) | `https://inventory.example.com` |
| **DATA_DIR** | SQLite database location | `/app/data` |
| **LOGS_DIR** | Application logs directory | `/app/logs` |

View File

@@ -44,10 +44,13 @@ You can now manage containers more efficiently with two specialized methods:
## 🏷️ Label Printing
Administrators and users can generate physical labels for boxes to ensure 100% accurate scanning.
1. Tap the **Package (Box)** icon in the header to open the **Box Inventory**.
2. Find the box you want to label and tap **Print Label**.
3. **Desktop:** Use the print dialog to send the label directly to a Dymo/Brother thermal printer.
4. **Mobile:** Use **"Save for Mobile App"** to download a PNG image of the label, which you can then print using your Bluetooth printer's app (like NIIMBOT).
## 🏷️ Label Printing
Administrators and users can generate physical labels for boxes to ensure 100% accurate scanning.
1. Tap the **Package (Box)** icon in the global header or the **Manage Boxes** card on the dashboard to open the **Box Inventory**.
2. **Search:** Use the search bar inside the Box Manager to filter through your containers in real-time.
3. Find the box you want to label and tap **Print Label**.
4. **Desktop:** Use the print dialog to send the label directly to a Dymo/Brother thermal printer.
5. **Mobile:** Use **"Save for Mobile App"** to download a PNG image of the label, which you can then print using your Bluetooth printer's app (like NIIMBOT).
---
@@ -102,8 +105,8 @@ Access application settings from the **Admin** panel.
### 🌐 Network & Configuration (NEW v1.8.0)
The application now uses a centralized configuration folder in the project root:
- **`config/`**: Contains all network settings (`network_config.env`), LDAP profiles (`ldap_config.json`), and security proxy rules (`Caddyfile`).
- **Dynamic Port Mapping**: Changes to the server IP or ports in the configuration are automatically detected by both the frontend and backend after a restart.
- **`inventory.env`**: The primary network configuration file. Centralizes `SERVER_IP`, ports, and `EXTRA_ALLOWED_ORIGINS`.
- **Dynamic Port Mapping**: Changes to the server IP, ports, or allowed origins are automatically detected by both the frontend and backend after a restart.
---
@@ -135,6 +138,7 @@ Ensure you have internet connectivity and wait a moment. Synchronization happens
- The image size must not exceed 10 MB.
- The application supports JPEG, PNG, WebP, and GIF formats.
- If the AI service is unavailable, try again later or contact your administrator.
- **AI Provider Toggle:** In the Admin Dashboard, you can choose between **Gemini** and **Claude** for label extraction. If one provider is slow or failing, your administrator can switch to the other seamlessly.
---
@@ -146,5 +150,5 @@ For detailed technical documentation, see the [Project Architecture](../PROJECT_
---
**Version:** v1.8.3
**Last Updated:** 2026-04-13
**Version:** v1.9.24
**Last Updated:** 2026-04-15

View File

@@ -1,5 +0,0 @@
{
"version": "1.8.3",
"last_build": "2026-04-13-1941",
"codename": "ConfigSync"
}

4
__push_ALL_to_remote.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
git push origin --all
git push origin --tags

BIN
_images.tests/IMG_6082.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
_images.tests/IMG_6106.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
_images.tests/IMG_6107.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
_images.tests/IMG_6108.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
_images.tests/IMG_6109.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

BIN
_images.tests/IMG_6110.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
_images.tests/IMG_6111.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

BIN
_images.tests/IMG_6112.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
_images.tests/IMG_6113.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
_images.tests/IMG_6114.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
_images.tests/IMG_6115.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
_images.tests/IMG_6116.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
_images.tests/IMG_6117.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

BIN
_images.tests/IMG_6118.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

BIN
_images.tests/IMG_6119.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
_images.tests/IMG_6120.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
_images.tests/IMG_6121.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
_images.tests/IMG_6122.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
_images.tests/IMG_6123.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
_images.tests/IMG_6124.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
_images.tests/IMG_6125.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
_images.tests/IMG_6126.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

BIN
_images.tests/IMG_6127.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
_images.tests/IMG_6128.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
_images.tests/IMG_6129.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
_images.tests/IMG_6130.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
_images.tests/IMG_6131.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
_images.tests/IMG_6132.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

BIN
_images.tests/IMG_6133.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
_images.tests/IMG_6134.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
_images.tests/IMG_6135.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
_images.tests/IMG_6136.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
_images.tests/IMG_6137.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
_images.tests/IMG_6139.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
_images.tests/IMG_6140.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
_images.tests/IMG_6141.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
_images.tests/IMG_6142.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
_images.tests/IMG_6143.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
_images.tests/IMG_6144.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
_images.tests/IMG_6145.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
_images.tests/IMG_6146.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
_images.tests/IMG_6147.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
_images.tests/IMG_6148.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
_images.tests/IMG_6149.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
_images.tests/IMG_6150.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
_images.tests/IMG_6151.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
_images.tests/IMG_6152.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
_images.tests/IMG_6153.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

@@ -0,0 +1,47 @@
## Goal
Reduce the technical debt and cognitive load inherited from large source files (some exceeding 1000 lines) by refactoring them into modular, functional units. The target is to align with the project rule of keeping files significantly under 300 lines where possible, improving maintainability and developer velocity.
## Constraints
- **Maintain Parity**: No functional changes or regressions allowed during refactoring.
- **Stack Consistency**: Maintain Next.js 15 (React 19) and FastAPI patterns.
- **Prop Drilling**: Avoid excessive prop drilling; use Context or efficient state sharing where logical.
- **Type Safety**: Maintain strict TypeScript definitions throughout the refactoring process.
## Known context
- **Frontend Hotspots**: `admin/page.tsx` (1039 lines), `inventory/page.tsx` (858 lines), and the main `page.tsx` (910 lines) are the primary pain points.
- **Backend Hotspots**: `routers/users.py` and `routers/admin_db.py` consolidate too many distinct administrative tasks.
- **Existing Components**: There is already a `components/` directory, indicating an established pattern for extraction.
## Risks
- **Context Fragmentation**: Splitting state-heavy components can make tracking data flow harder if not documented.
- **Build Errors**: Next.js App Router is sensitive to client/server component boundaries during refactoring.
- **Refactoring Fatigue**: Large-scale changes across multiple critical files can introduce subtle bugs in event handlers or API sync logic.
## Options (24)
### Option 1: Functional Component Extraction (UI-First)
Extract major UI sections (e.g., Modals, Tab Panels, Complex List Items) into the `components/` directory. For `admin/page.tsx`, this would mean creating `LDAPSettings.tsx`, `DatabaseSettings.tsx`, and `AISettings.tsx`.
* **Pros**: Immediate reduction in file size; easier to style in isolation.
* **Cons**: Might require passing many props/callbacks.
### Option 2: Custom Hook Logic Extraction (Logic-First)
Move all `useState`, `useEffect`, and API call logic from the pages into custom hooks (e.g., `useAdminSettings.ts`, `useInventoryData.ts`).
* **Pros**: Separates "How it works" from "How it looks"; reusable logic.
* **Cons**: Requires careful handling of dependency arrays and closure-related state bugs.
### Option 3: Modular Sub-Routing (Backend)
Split the backend `users.py` and `admin_db.py` into smaller files based on specific functional domains (e.g., `admin_core.py`, `admin_backups.py`, `user_auth.py`).
* **Pros**: Cleaner API paths and documentation; easier unit testing.
* **Cons**: Requires updating `main.py` router inclusions.
## Recommendation
**Hybrid Approach (Options 1 & 2)**: Focus on the "Big 3" frontend pages first.
1. **Extract Modals and Sections**: Move heavy UI blocks from `admin` and `inventory` into standalone components.
2. **Extract Data Logic**: Create custom hooks for the most complex state-management sections (like the multi-step `AIOnboarding` or the complex sync logic in `inventory`).
3. **Split Backend Routers**: Break down `users.py` and `admin_db.py` into functional sub-modules.
## Acceptance criteria
- [ ] No single file in the `app/` or `routers/` directory exceeds 400 lines (aiming for 300).
- [ ] The dashboard, inventory manager, and scanner function exactly as in `v1.9.24`.
- [ ] TypeScript compilation passes without errors (`npm run build` equivalent).
- [ ] All new components use the established `StatCard` and CSS patterns (Tailwind).

View File

@@ -1,10 +1,11 @@
FROM python:3.12-slim
# Install system dependencies required for python-ldap (needed by backend)
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libldap2-dev \
libsasl2-dev \
gosu \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
@@ -26,15 +27,12 @@ COPY scripts ./scripts
ENV DATA_DIR="/app/data"
ENV LOGS_DIR="/app/logs"
# Ensure the appuser can write to data and logs if we pre-create them,
# although Docker volumes will handle ownership context.
# Pre-create directories and ensure they are writable
RUN mkdir -p /app/data /app/logs && chown -R appuser:appuser /app
# Make initialization scripts executable
RUN chmod +x /app/scripts/init_data.sh /app/backend/entrypoint.sh
USER appuser
EXPOSE 8000
# Entrypoint runs init_data.sh first, then starts uvicorn

0
backend/__init__.py Normal file
View File

View File

@@ -1,42 +1,50 @@
import os
import json
import io
import logging
from PIL import Image
from google import genai
from google.genai import types
log = logging.getLogger("ainventory")
def get_best_models():
# Using the exact models discovered via diagnostic
return ["gemini-2.0-flash", "gemini-2.5-flash"]
# Priority list based on aliases and future-gen models found in user logs
return [
"gemini-flash-latest", # Points to the best stable Flash version
"gemini-3.1-flash-lite-preview", # Cutting edge 3.1 Lite
"gemini-flash-lite-latest", # Best Lite alias
"gemini-pro-latest" # Pro fallback alias
]
def extract(image_bytes: bytes, prompt: str):
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
print("CRITICAL: GEMINI_API_KEY is MISSING in environment!")
log.error("CRITICAL: GEMINI_API_KEY is MISSING in environment!")
return None
# Log partial key for safety debug
key_hint = f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else "too short"
print(f"🔑 Using API Key: {key_hint}")
log.info(f"🔑 Using API Key: {key_hint}")
try:
# Initialize the NEW SDK Client forcing stable v1 API
client = genai.Client(api_key=api_key, http_options={'api_version': 'v1'})
# Switching to v1beta which is required for many experimental/new models
client = genai.Client(api_key=api_key, http_options={'api_version': 'v1beta'})
# DEBUG: List allowed models for this key
print("🔍 Checking available models for your key...")
log.debug("🔍 Checking available models for your key...")
try:
for m in client.models.list():
print(f" - Found: {m.name}")
log.debug(f" - Found: {m.name}")
except Exception as list_e:
print(f" ⚠️ Could not list models: {list_e}")
log.warning(f" ⚠️ Could not list models: {list_e}")
models_to_try = get_best_models()
# Try models in order
for model_name in models_to_try:
try:
print(f"🚀 AI Launching (v2 SDK): {model_name}...")
log.info(f"🚀 AI Launching (v2 SDK): {model_name}...")
# In the new SDK, we pass a list of parts (text string and image bytes)
response = client.models.generate_content(
@@ -48,10 +56,12 @@ def extract(image_bytes: bytes, prompt: str):
)
if not response or not response.text:
log.warning(f"⚠️ Gemini {model_name} returned NO text.")
continue
text = response.text.strip()
print(f"✅ AI Response Received ({len(text)} bytes)")
log.info(f"✅ AI Response Received ({len(text)} bytes)")
log.debug(f"FULL RESPONSE:\n{text}")
# Extract JSON block
if "```json" in text:
@@ -62,10 +72,10 @@ def extract(image_bytes: bytes, prompt: str):
return json.loads(text)
except Exception as e:
print(f"❌ Gemini {model_name} failed: {e}")
log.error(f"❌ Gemini {model_name} failed: {e}")
continue
except Exception as outer_e:
print(f"❌ Gemini Client Init failed: {outer_e}")
log.error(f"❌ Gemini Client Init failed: {outer_e}")
return None

View File

@@ -1,62 +1,142 @@
import os
import time
from dotenv import load_dotenv
from . import models
from .database import SessionLocal
from .ai import gemini, claude
# Load environment variables from the directory where this file resides
# Note: Environment variables are managed centrally by config_loader.py
base_dir = os.path.dirname(os.path.abspath(__file__))
dotenv_path = os.path.join(base_dir, ".env")
load_dotenv(dotenv_path)
class PromptManager:
"""Manages AI prompt with auto-reloading from file."""
def __init__(self, file_path: str):
self.file_path = file_path
self.last_mtime = 0
self.cached_prompt = None
def get_prompt(self) -> str:
if not os.path.exists(self.file_path):
return None
try:
current_mtime = os.path.getmtime(self.file_path)
if current_mtime > self.last_mtime or self.cached_prompt is None:
with open(self.file_path, 'r', encoding='utf-8') as f:
self.cached_prompt = f.read().strip()
self.last_mtime = current_mtime
# Use print or log for visibility in dev
print(f"🔄 AI Vision prompt reloaded from {self.file_path} (mtime: {current_mtime})")
return self.cached_prompt
except Exception as e:
print(f"⚠️ Failed to reload AI prompt: {e}")
return self.cached_prompt
# The prompt file is located in the global /config directory
# We go up one level from backend/ to reach project root, then into config/
PROJECT_ROOT = os.path.dirname(os.path.abspath(base_dir))
PROMPT_FILE_PATH = os.path.join(PROJECT_ROOT, "config", "ai_prompt.md")
prompt_mgr = PromptManager(PROMPT_FILE_PATH)
def extract_label_info(image_bytes: bytes, mode: str = "item"):
"""
Orchestrates extraction across multiple AI providers.
Modes: 'item' (full technical extraction), 'box' (container discovery)
"""
if mode == "box":
prompt = """
Identify the CONTAINER or BOX name from this image.
Look for large, prominent, bold, or hand-written text that identifies a storage unit.
Ignore small technical details, quantities, or fine print.
db = SessionLocal()
try:
if mode == "box":
prompt = """
Identify the CONTAINER or BOX name from this image.
Look for large, prominent, bold, or hand-written text that identifies a storage unit.
Ignore small technical details, quantities, or fine print.
Return ONLY a valid JSON object:
{
"box_label": "The identified container name",
"name": "Same as box_label",
"category": "Storage",
"description": "Brief description if useful",
"quantity": 1
}
"""
else:
# 1. Try fetching from the configuration file first (SSOT)
prompt = prompt_mgr.get_prompt()
if not prompt:
# 2. Fallback to Database if file is missing
setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_extraction_prompt").first()
if setting:
prompt = setting.value
else:
# 3. Final fallback to hardcoded default
prompt = "Extract technical specs. Return JSON with name, category, description, connector, size, color, part_number, ocr_text, quantity."
# 0. Get Active Provider from DB
provider_setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first()
active_provider = provider_setting.value if provider_setting else "gemini"
Return ONLY a valid JSON object:
{
"box_label": "The identified container name",
"name": "Same as box_label",
"category": "Storage",
"specs": "Brief description if useful",
"quantity": 1
}
"""
else:
prompt = """
Extract technical inventory information from this label image.
# 1. Execute extraction based on selection
result = None
if active_provider == "claude":
print(f"📡 Using Anthropic Claude for extraction...")
result = claude.extract(image_bytes, prompt)
else:
print(f"📡 Using Google Gemini for extraction...")
result = gemini.extract(image_bytes, prompt)
CRITICAL INSTRUCTIONS:
1. Look at the most prominent text (usually top 1-2 rows). This is the product NAME and MODEL.
2. Extract the PART NUMBER (P/N, Model No, Type). If no explicit Part Number is found, synthesize one from the most unique identifier in the header (e.g. 'OM4-MMF-DX').
3. Separate the COLOR (e.g. Turquoise, Yellow, Black).
4. Extract CATEGORY based on the item type (e.g. Patchcord, SFP, Connector).
5. Extract technical SPECS (e.g. '2.0mm', '10G', '850nm').
if result:
# Check if AI returned a list of items or a singular object
raw_items = result.get("items") or result.get("Items")
was_list = isinstance(raw_items, list)
items_to_map = raw_items if was_list else [result]
mapping = {
"Item": "name",
"Type": "type",
"Description": "description",
"Category": "category",
"Connector": "connector",
"Size": "size",
"Color": "color",
"PartNr": "part_number",
"OCR": "ocr_text"
}
mapped_items = []
for item_data in items_to_map:
final_item = {}
for ai_key, model_key in mapping.items():
val = item_data.get(ai_key) or item_data.get(model_key)
if val and isinstance(val, str):
final_item[model_key] = val.strip()
else:
final_item[model_key] = val
# Default fields
final_item["quantity"] = item_data.get("quantity", 1)
raw_barcode = item_data.get("barcode") or item_data.get("PartNr") or item_data.get("part_number") or item_data.get("Part Number")
final_item["barcode"] = str(raw_barcode).strip() if raw_barcode else f"AI-{int(time.time()*100)}"
# Handle Box mode specifically inside mapping
if mode == "box":
final_item["box_label"] = final_item.get("box_label") or item_data.get("Box") or final_item.get("name") or "Unknown Box"
final_item["name"] = final_item["box_label"]
mapped_items.append(final_item)
# Return either the whole list wrapper or the first item (legacy compatibility)
if was_list:
return {"items": mapped_items}
return mapped_items[0] if mapped_items else {"error": "No items after mapping"}
finally:
db.close()
Return ONLY a valid JSON object:
{
"name": "Full descriptive name from header",
"part_number": "Unique identifier for fast scanning",
"category": "Broad category",
"color": "Color if present",
"specs": "Brief tech specs list",
"barcode": "Barcode value if visible",
"quantity": 1
}
"""
# 1. Try Gemini
result = gemini.extract(image_bytes, prompt)
if result:
# Maintenance: Ensure fields are mapped if mode was box
if mode == "box" and "box_label" in result and "name" not in result:
result["name"] = result["box_label"]
return result
# 2. Try Claude (Fallback) - Note: Mapping logic would need to be replicated here if enabled
# For now, keeping it simple
return {"error": "AI extraction failed or no data returned. Check your API key and Prompt."}
# 2. Try Claude (Fallback)
result = claude.extract(image_bytes, prompt)

34
backend/config_loader.py Normal file
View File

@@ -0,0 +1,34 @@
import os
import logging
from dotenv import load_dotenv
log = logging.getLogger("ainventory")
def load_config():
"""
Centralized environment loader for TFM aInventory.
Prioritizes existing environment variables (Docker),
then inventory.env at project root, then backend/.env.
"""
# Base directory is backend/
base_dir = os.path.dirname(os.path.abspath(__file__))
# Project root is one level up
project_root = os.path.dirname(base_dir)
inventory_env_path = os.path.join(project_root, "inventory.env")
backend_env_path = os.path.join(base_dir, ".env")
# Check for inventory.env in root (Master Config)
if os.path.exists(inventory_env_path):
load_dotenv(inventory_env_path)
log.info(f"✅ Loaded master configuration from {inventory_env_path}")
# Check for local backend/.env (Legacy/Fragmented)
elif os.path.exists(backend_env_path):
load_dotenv(backend_env_path)
log.info(f" Loaded local configuration from {backend_env_path}")
else:
log.info(" [CONFIG] Using system environment variables (Docker/Server environment).")
# Auto-run if imported
load_config()

90
backend/config_manager.py Normal file
View File

@@ -0,0 +1,90 @@
import os
from dotenv import load_dotenv
class ConfigManager:
"""Safely manages multi-line .env files without corrupting other content."""
@staticmethod
def get_root_env_path():
# backend/config_manager.py -> backend/ -> /
base_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(base_dir)
return os.path.join(project_root, "inventory.env")
@staticmethod
def update_keys(updates: dict):
"""
Updates specific keys in inventory.env.
Preserves comments and order where possible.
Appends new keys at the end if not found.
"""
env_path = ConfigManager.get_root_env_path()
if not os.path.exists(env_path):
# Create a basic file if it doesn't exist (unlikely in this project)
with open(env_path, 'w', encoding='utf-8') as f:
f.write("# TFM aInventory — Generated Configuration\n")
with open(env_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
keys_to_process = set(updates.keys())
processed_keys = set()
for line in lines:
trimmed = line.strip()
# Skip empty lines or comments when matching
if not trimmed or trimmed.startswith('#'):
new_lines.append(line)
continue
# Check if this line is a key assignment we want to update
found_match = False
for key in keys_to_process:
if trimmed.startswith(f"{key}="):
new_lines.append(f"{key}={updates[key]}\n")
processed_keys.add(key)
found_match = True
break
if not found_match:
new_lines.append(line)
# Append keys that weren't found in the file
missing_keys = keys_to_process - processed_keys
if missing_keys:
if new_lines and not new_lines[-1].endswith('\n'):
new_lines.append('\n')
if new_lines and not new_lines[-1].strip() == '':
new_lines.append('\n')
new_lines.append("# --- Automatically Added Keys ---\n")
for key in missing_keys:
new_lines.append(f"{key}={updates[key]}\n")
with open(env_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
# Force reload environment variables for the current process
load_dotenv(env_path, override=True)
return True
@staticmethod
def get_masked_key(key_name: str):
"""Returns a masked version of the environment variable."""
val = os.environ.get(key_name)
if not val:
return None
# Determine prefix based on key type
prefix = ""
if key_name == "GEMINI_API_KEY":
prefix = "G-"
elif key_name == "CLAUDE_API_KEY":
prefix = "sk-"
if len(val) <= 8:
return f"{prefix}****"
return f"{prefix}****{val[-4:]}"

View File

@@ -136,3 +136,36 @@ class DbManager:
except Exception as e:
logger.error(f"[DB] Restore failed: {str(e)}")
raise Exception(f"Restore failed: {str(e)}")
@staticmethod
def export_db() -> str:
"""Returns the path to the active database file for download."""
active_db_path = os.path.join(DATA_DIR, "inventory.db")
if not os.path.exists(active_db_path):
raise Exception("Database file not found")
return active_db_path
@staticmethod
def import_db(file_bytes: bytes, db: Session, user_id: int) -> bool:
"""
Overwrites the active database with the provided file bytes.
Creates a safety rollback backup first.
"""
active_db_path = os.path.join(DATA_DIR, "inventory.db")
try:
# 1. Safety backup
DbManager.create_backup(db, label="import_rollback", user_id=user_id)
# 2. Close pool connections
engine.dispose()
# 3. Write new file
with open(active_db_path, "wb") as f:
f.write(file_bytes)
logger.warning(f"[DB] IMPORT COMPLETED by user_id={user_id}")
return True
except Exception as e:
logger.error(f"[DB] Import failed: {str(e)}")
raise Exception(f"Import failed: {str(e)}")

View File

@@ -23,6 +23,16 @@ export LOGS_DIR="${LOGS_DIR:-/app/logs}"
echo "🐳 [Docker] Running data initialization..."
bash /app/scripts/init_data.sh
# Hand off to the application server
echo "🐳 [Docker] Starting uvicorn..."
exec python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000
# Fix permissions for mounted volumes (which might be root-owned by the host)
echo "🐳 [Docker] Fixing volume permissions..."
chown -R appuser:appuser "${DATA_DIR}" "${LOGS_DIR}"
chmod -R 770 "${DATA_DIR}" "${LOGS_DIR}"
# Verify logic: Ensure appuser can actually write before we hand off
echo "🐳 [Docker] Verifying directory writability..."
gosu appuser touch "${LOGS_DIR}/.startup_test" && rm "${LOGS_DIR}/.startup_test"
gosu appuser touch "${DATA_DIR}/.startup_test" && rm "${DATA_DIR}/.startup_test"
# Hand off to the application server as non-root user
echo "🐳 [Docker] Starting uvicorn as appuser..."
exec gosu appuser python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000

View File

@@ -27,26 +27,25 @@ def setup_logger():
datefmt="%Y-%m-%d %H:%M:%S"
)
# Console Handler
# File Handler (10MB max, keep 5 backups)
try:
file_handler = RotatingFileHandler(
LOG_FILE_PATH, maxBytes=10*1024*1024, backupCount=5
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Special redirect for uvicorn logs to also go to file
logging.getLogger("uvicorn").addHandler(file_handler)
logging.getLogger("uvicorn.access").addHandler(file_handler)
except (PermissionError, IOError) as e:
logger.warning(f"⚠️ [LOGGING] Cannot write to log file at {LOG_FILE_PATH}: {e}")
logger.warning("⚠️ [LOGGING] Continuing with console-only output.")
# Console Handler (always registered)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
# File Handler (10MB max, keep 5 backups)
file_handler = RotatingFileHandler(
LOG_FILE_PATH, maxBytes=10*1024*1024, backupCount=5
)
file_handler.setFormatter(formatter)
# Attach handlers
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# Special redirect for uvicorn logs to also go to file
uvicorn_logger = logging.getLogger("uvicorn")
uvicorn_logger.addHandler(file_handler)
uvicorn_access_logger = logging.getLogger("uvicorn.access")
uvicorn_access_logger.addHandler(file_handler)
return logger

View File

@@ -1,11 +1,13 @@
import os
from . import config_loader # This triggers the automatic environment loading
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter
from slowapi.util import get_remote_address
from . import models
from .database import engine
from .routers import items, operations, users, categories, admin_db
from .routers import items, operations, users, categories
from .routers.admin import backups, config
from .logger import log
from .scheduler import scheduler, sync_scheduler_config
@@ -19,15 +21,55 @@ log.info("Database tables verified.")
app = FastAPI(title="TFM aInventory API", version="1.1.0")
log.info("TFM aInventory API process started.")
# [SECURITY FIX M-01] CORS: allow_origins=["*"] + allow_credentials=True is invalid per spec.
# Allowed origins are configured via ALLOWED_ORIGINS environment variable (comma-separated).
# Secure fallback: localhost only for development.
_raw_origins = os.environ.get(
"ALLOWED_ORIGINS",
"http://localhost:8907,https://localhost:8909"
)
# [SECURITY FIX M-01] CORS Configuration
# We dynamically build allowed origins from environment variables to simplify deployment.
_raw_origins = os.environ.get("ALLOWED_ORIGINS", "")
ALLOWED_ORIGINS = [o.strip() for o in _raw_origins.split(",") if o.strip()]
log.info(f"CORS allowed origins: {ALLOWED_ORIGINS}")
# Automatically add origins based on network_config.env variables if present
server_ip = os.environ.get("SERVER_IP")
front_port = os.environ.get("FRONTEND_PORT", "8917")
front_ssl_port = os.environ.get("FRONTEND_SSL_PORT", "8919")
back_ssl_port = os.environ.get("BACKEND_SSL_PORT", "8918")
# Always allow localhost
defaults = [
f"http://localhost:{front_port}",
f"https://localhost:{front_ssl_port}",
f"https://localhost:{back_ssl_port}",
]
for d in defaults:
if d not in ALLOWED_ORIGINS:
ALLOWED_ORIGINS.append(d)
# Add IP-based origins if SERVER_IP is set
if server_ip and server_ip != "localhost":
ip_origins = [
f"http://{server_ip}:{front_port}",
f"https://{server_ip}:{front_ssl_port}",
f"https://{server_ip}:{back_ssl_port}",
]
for ip_o in ip_origins:
if ip_o not in ALLOWED_ORIGINS:
ALLOWED_ORIGINS.append(ip_o)
# [NEW] Add Extra Allowed Origins (Tailscale, VPN, etc.)
extra_origins_raw = os.environ.get("EXTRA_ALLOWED_ORIGINS", "")
if extra_origins_raw:
for extra_ip in [o.strip() for o in extra_origins_raw.split(",") if o.strip()]:
# Generate standard combinations for this extra origin
ext_combos = [
f"http://{extra_ip}:{front_port}",
f"https://{extra_ip}:{front_ssl_port}",
f"https://{extra_ip}:{back_ssl_port}",
]
for combo in ext_combos:
if combo not in ALLOWED_ORIGINS:
ALLOWED_ORIGINS.append(combo)
log.info("🔒 [SECURITY] CORS configuration initialized.")
for origin in ALLOWED_ORIGINS:
log.info(f" -> Allowed: {origin}")
# Add CORS middleware FIRST (before rate limiter)
app.add_middleware(
@@ -46,7 +88,8 @@ app.include_router(items.router)
app.include_router(operations.router)
app.include_router(users.router)
app.include_router(categories.router)
app.include_router(admin_db.router)
app.include_router(backups.router)
app.include_router(config.router)
@app.on_event("startup")
def startup_event():
@@ -54,6 +97,69 @@ def startup_event():
scheduler.start()
sync_scheduler_config()
# [NEW] Initialize default system settings
from .database import SessionLocal
db = SessionLocal()
try:
# Default AI Prompt from User Request
default_prompt = (
"identify and summarise the minimal necessary information for a quick description if item. "
"I need the following output - <field name> : the result from you.\n"
"For any field, do not add comments in parenthesis. \n\n"
"Item: in three words type of this item\n"
"Type: what type of item is, like \"spare parts\", \"consumables\", \"patch cords\" etc.\n"
"Description: description (max 5 words)\n"
"Category: category, if any\n"
"Connector: connectors\n"
"Size: size or length\n"
"Color: color if useful\n"
"PartNr: part number if any\n"
"OCR: identification string for local OCR matching"
)
# Wrap in JSON instructions for reliable parsing
final_prompt = f"IMAGE ANALYSIS INSTRUCTIONS:\n{default_prompt}\n\nIMPORTANT: Return ONLY a valid JSON object with the keys: Item, Type, Description, Category, Connector, Size, Color, PartNr, OCR."
existing = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_extraction_prompt").first()
if not existing:
db.add(models.SystemSetting(key="ai_extraction_prompt", value=final_prompt))
db.commit()
log.info("Initialized default AI prompt in database.")
# Refresh existing after commit
existing = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_extraction_prompt").first()
# [NEW] Sync/Initialize AI prompt file in /config
from .database import BASE_DIR
PROJECT_ROOT = os.path.dirname(BASE_DIR)
PROMPT_FILE_PATH = os.path.join(PROJECT_ROOT, "config", "ai_prompt.md")
if not os.path.exists(PROMPT_FILE_PATH):
try:
os.makedirs(os.path.dirname(PROMPT_FILE_PATH), exist_ok=True)
# Use DB value (which we just ensured exists)
current_val = existing.value if existing else final_prompt
with open(PROMPT_FILE_PATH, 'w', encoding='utf-8') as f:
f.write(current_val)
log.info(f"✅ Initialized AI prompt configuration file: {PROMPT_FILE_PATH}")
except Exception as fe:
log.error(f"❌ Failed to initialize AI prompt file: {fe}")
defaults = {
"backup_retention_count": "10",
"backup_schedule_hour": "3",
"backup_schedule_freq_days": "1",
"ai_provider": "gemini"
}
for key, val in defaults.items():
if not db.query(models.SystemSetting).filter(models.SystemSetting.key == key).first():
db.add(models.SystemSetting(key=key, value=val))
log.info(f"Initialized default setting: {key}")
db.commit()
except Exception as e:
log.error(f"Failed to initialize settings: {e}")
finally:
db.close()
@app.get("/")
def read_root():
return {"message": "Inventory API is running"}

View File

@@ -23,6 +23,12 @@ class Category(Base):
items = relationship("Item", back_populates="category_rel")
class Color(Base):
__tablename__ = "colors"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
class Item(Base):
__tablename__ = "items"
@@ -36,6 +42,10 @@ class Item(Base):
category_rel = relationship("Category", back_populates="items")
part_number = Column(String, index=True, nullable=True)
color = Column(String, index=True, nullable=True)
description = Column(String, nullable=True)
connector = Column(String, nullable=True)
size = Column(String, nullable=True)
ocr_text = Column(Text, nullable=True)
specs = Column(Text, nullable=True)
quantity = Column(Float, default=0.0)
min_quantity = Column(Float, default=1.0)

View File

@@ -13,3 +13,6 @@ passlib[bcrypt]>=1.7.4
python-jose[cryptography]>=3.3.0
slowapi>=0.1.9
apscheduler>=3.10.1
pytest>=8.0.0
pytest-asyncio>=0.23.0
httpx>=0.27.0

View File

View File

@@ -1,14 +1,14 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from sqlalchemy.orm import Session
from typing import List
from .. import models, schemas, auth
from ..database import get_db
from ..db_manager import DbManager
from ..scheduler import sync_scheduler_config
from fastapi.responses import FileResponse
from ... import schemas, auth, models
from ...database import get_db
from ...db_manager import DbManager
router = APIRouter(
prefix="/admin/db",
tags=["Admin Database"]
tags=["Admin Database Backups"]
)
@router.get("/backups", response_model=List[schemas.BackupInfo])
@@ -34,7 +34,6 @@ def trigger_manual_backup(
):
"""Trigger a manual database backup."""
filename = DbManager.create_backup(db, label="manual", user_id=current_admin.sub)
# Re-fetch the newly created file info
backups = DbManager.get_backup_list()
for b in backups:
if b.filename == filename:
@@ -57,49 +56,36 @@ def restore_database(
raise HTTPException(status_code=400, detail="Confirmation required")
try:
success = DbManager.restore_backup(filename, db, user_id=current_admin.sub)
DbManager.restore_backup(filename, db, user_id=current_admin.sub)
return {"status": "success", "message": f"Database restored from {filename}"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/settings", response_model=schemas.DbSettingsUpdate)
def get_db_settings(
db: Session = Depends(get_db),
@router.get("/export")
def export_database(
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Get database retention and scheduling settings."""
# Ensure default settings exist
retention = db.query(models.SystemSetting).filter(models.SystemSetting.key == "backup_retention_count").first()
hour = db.query(models.SystemSetting).filter(models.SystemSetting.key == "backup_schedule_hour").first()
freq = db.query(models.SystemSetting).filter(models.SystemSetting.key == "backup_schedule_freq_days").first()
return {
"retention_count": int(retention.value) if retention else 10,
"schedule_hour": int(hour.value) if hour else 3,
"schedule_freq_days": int(freq.value) if freq else 1
}
"""Download the current database file."""
try:
path = DbManager.export_db()
return FileResponse(
path,
media_type="application/x-sqlite3",
filename="inventory_export.db"
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/settings", response_model=schemas.DbSettingsUpdate)
def update_db_settings(
settings: schemas.DbSettingsUpdate,
@router.post("/import")
async def import_database(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Update database settings and re-trigger scheduler sync."""
pairs = {
"backup_retention_count": str(settings.retention_count),
"backup_schedule_hour": str(settings.schedule_hour),
"backup_schedule_freq_days": str(settings.schedule_freq_days)
}
for key, val in pairs.items():
existing = db.query(models.SystemSetting).filter(models.SystemSetting.key == key).first()
if existing:
existing.value = val
else:
db.add(models.SystemSetting(key=key, value=val))
db.commit()
# Re-trigger scheduler sync
sync_scheduler_config()
return settings
"""Upload and replace the current database. DANGEROUS."""
contents = await file.read()
try:
DbManager.import_db(contents, db, user_id=current_admin.sub)
return {"status": "success", "message": "Database successfully imported and replaced."}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,208 @@
import os
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from ... import models, schemas, auth
from ...database import get_db, BASE_DIR
from ...scheduler import sync_scheduler_config
from ...config_manager import ConfigManager
router = APIRouter(
prefix="/admin/db",
tags=["Admin Configuration"]
)
PROJECT_ROOT = os.path.dirname(BASE_DIR)
PROMPT_FILE_PATH = os.path.join(PROJECT_ROOT, "config", "ai_prompt.md")
@router.get("/settings", response_model=schemas.DbSettingsUpdate)
def get_db_settings(
db: Session = Depends(get_db),
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Get database retention and scheduling settings."""
retention = db.query(models.SystemSetting).filter(models.SystemSetting.key == "backup_retention_count").first()
hour = db.query(models.SystemSetting).filter(models.SystemSetting.key == "backup_schedule_hour").first()
freq = db.query(models.SystemSetting).filter(models.SystemSetting.key == "backup_schedule_freq_days").first()
return {
"retention_count": int(retention.value) if retention else 10,
"schedule_hour": int(hour.value) if hour else 3,
"schedule_freq_days": int(freq.value) if freq else 1
}
@router.patch("/settings", response_model=schemas.DbSettingsUpdate)
def update_db_settings(
settings: schemas.DbSettingsUpdate,
db: Session = Depends(get_db),
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Update database settings and re-trigger scheduler sync."""
pairs = {
"backup_retention_count": str(settings.retention_count),
"backup_schedule_hour": str(settings.schedule_hour),
"backup_schedule_freq_days": str(settings.schedule_freq_days)
}
for key, val in pairs.items():
existing = db.query(models.SystemSetting).filter(models.SystemSetting.key == key).first()
if existing:
existing.value = val
else:
db.add(models.SystemSetting(key=key, value=val))
db.commit()
sync_scheduler_config()
return settings
@router.get("/settings/prompt")
def get_ai_prompt(
db: Session = Depends(get_db),
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Get the current AI extraction prompt."""
if os.path.exists(PROMPT_FILE_PATH):
try:
with open(PROMPT_FILE_PATH, 'r', encoding='utf-8') as f:
return {"value": f.read().strip(), "source": "file"}
except Exception:
pass
setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_extraction_prompt").first()
if not setting:
return {"value": "", "source": "none"}
return {"value": setting.value, "source": "database"}
@router.post("/settings/prompt")
def update_ai_prompt(
payload: dict,
db: Session = Depends(get_db),
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Update the AI extraction prompt."""
value = payload.get("value")
if value is None:
raise HTTPException(status_code=400, detail="Value required")
try:
os.makedirs(os.path.dirname(PROMPT_FILE_PATH), exist_ok=True)
with open(PROMPT_FILE_PATH, 'w', encoding='utf-8') as f:
f.write(value)
except Exception:
pass
existing = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_extraction_prompt").first()
if existing:
existing.value = value
else:
db.add(models.SystemSetting(key="ai_extraction_prompt", value=value))
db.commit()
return {"status": "success", "file_updated": os.path.exists(PROMPT_FILE_PATH)}
@router.get("/settings/ai")
def get_ai_config(
db: Session = Depends(get_db),
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Check AI provider status and active provider."""
gemini_key = os.environ.get("GEMINI_API_KEY")
claude_key = os.environ.get("CLAUDE_API_KEY")
provider_setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first()
active_provider = provider_setting.value if provider_setting else "gemini"
return {
"active_provider": active_provider,
"providers": [
{
"id": "gemini",
"name": "Google Gemini 2.0",
"configured": bool(gemini_key),
"active": active_provider == "gemini",
"masked_key": ConfigManager.get_masked_key("GEMINI_API_KEY")
},
{
"id": "claude",
"name": "Anthropic Claude 3.5",
"configured": bool(claude_key),
"active": active_provider == "claude",
"masked_key": ConfigManager.get_masked_key("CLAUDE_API_KEY")
}
]
}
@router.post("/settings/ai-keys")
def update_ai_keys(
payload: dict,
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Update AI API keys."""
gemini_key = payload.get("gemini_api_key")
claude_key = payload.get("claude_api_key")
updates = {}
if gemini_key:
updates["GEMINI_API_KEY"] = gemini_key
if claude_key:
updates["CLAUDE_API_KEY"] = claude_key
if updates:
ConfigManager.update_keys(updates)
return {
"status": "success",
"gemini_configured": bool(os.environ.get("GEMINI_API_KEY")),
"claude_configured": bool(os.environ.get("CLAUDE_API_KEY"))
}
@router.post("/settings/test-ai-key")
def test_ai_key(
payload: dict,
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Test AI API key connectivity."""
provider = payload.get("provider")
key = payload.get("key")
if not provider or provider not in ["gemini", "claude"]:
raise HTTPException(status_code=400, detail="Invalid provider")
if not key or "****" in key:
key = os.environ.get("GEMINI_API_KEY" if provider == "gemini" else "CLAUDE_API_KEY")
if not key:
raise HTTPException(status_code=400, detail="No API key provided or configured")
try:
if provider == "gemini":
from google import genai
client = genai.Client(api_key=key, http_options={'api_version': 'v1beta'})
client.models.list()
return {"status": "success", "message": "Google Gemini API connection verified!"}
elif provider == "claude":
import anthropic
client = anthropic.Anthropic(api_key=key)
client.models.list(limit=1)
return {"status": "success", "message": "Anthropic Claude API connection verified!"}
except Exception as e:
raise HTTPException(status_code=400, detail=f"{provider.capitalize()} Test Failed: {str(e)}")
@router.post("/settings/ai")
def update_ai_provider(
payload: dict,
db: Session = Depends(get_db),
current_admin: auth.TokenData = Depends(auth.get_current_admin)
):
"""Update the active AI provider."""
provider = payload.get("provider")
if provider not in ["gemini", "claude"]:
raise HTTPException(status_code=400, detail="Invalid provider")
existing = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first()
if existing:
existing.value = provider
else:
db.add(models.SystemSetting(key="ai_provider", value=provider))
db.commit()
return {"status": "success", "active_provider": provider}

View File

@@ -18,21 +18,7 @@ def get_categories(
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[C-01] List of categories — only for authenticated users."""
categories = db.query(models.Category).all()
# Auto-seed if empty with defaults mentioned by user
if not categories:
defaults = [
{"name": "Connectors", "description": "Conectica: cables, adapters, plugs"},
{"name": "Spare Parts", "description": "Piese de schimb: specific components"},
{"name": "Tools", "description": "Hand and power tools"},
{"name": "Consumables", "description": "One-time use items"}
]
for d in defaults:
cat = models.Category(**d)
db.add(cat)
db.commit()
return db.query(models.Category).all()
return categories
return db.query(models.Category).all()
@router.post("/", response_model=schemas.Category)
def create_category(

View File

@@ -97,10 +97,18 @@ def create_item(
current_user: auth.TokenData = Depends(auth.get_current_user)
):
"""[C-01] Create item — only for authenticated users. [M-02] user_id from token."""
# Check if barcode exists
db_item = db.query(models.Item).filter(models.Item.barcode == item.barcode).first()
if db_item:
raise HTTPException(status_code=400, detail="Barcode already registered")
# [AUTO-PERSIST] Create Category/Color if not exists
if item.category:
cat = db.query(models.Category).filter(models.Category.name == item.category).first()
if not cat:
db.add(models.Category(name=item.category))
db.commit()
if item.color:
col = db.query(models.Color).filter(models.Color.name == item.color).first()
if not col:
db.add(models.Color(name=item.color))
db.commit()
db_item = models.Item(**item.model_dump())
db.add(db_item)
@@ -148,6 +156,19 @@ def update_item(
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
# [AUTO-PERSIST] Create Category/Color if not exists
if item.category:
cat = db.query(models.Category).filter(models.Category.name == item.category).first()
if not cat:
db.add(models.Category(name=item.category))
db.commit()
if item.color:
col = db.query(models.Color).filter(models.Color.name == item.color).first()
if not col:
db.add(models.Color(name=item.color))
db.commit()
update_data = item.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(db_item, key, value)

View File

@@ -20,13 +20,19 @@ limiter = Limiter(key_func=get_remote_address)
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
def get_ldap_config():
# Read from root /config directory
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
config_dir = os.path.join(root_dir, "config")
config_path = os.path.join(config_dir, "ldap_config.json")
# Priority 1: Check in DATA_DIR (for Docker production)
config_path = os.path.join(database.DATA_DIR, "config", "ldap_config.json")
if os.path.exists(config_path):
with open(config_path, "r") as f:
return json.load(f)
# Priority 2: Fallback to source-relative config (for local dev)
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
source_config_path = os.path.join(root_dir, "config", "ldap_config.json")
if os.path.exists(source_config_path):
with open(source_config_path, "r") as f:
return json.load(f)
return {"ldap_enabled": False}
def authenticate_ldap(username, password):
@@ -73,6 +79,11 @@ def authenticate_ldap(username, password):
return None
real_user_dn = conn.entries[0].entry_dn
user_groups = []
if hasattr(conn.entries[0], 'memberOf'):
user_groups = [str(g).lower() for g in conn.entries[0].memberOf.values]
log.debug(f"LDAP: Found memberOf groups on user: {user_groups}")
log.debug(f"LDAP: Canonical DN found: {real_user_dn}")
# Check roles based on group membership
@@ -87,27 +98,39 @@ def authenticate_ldap(username, password):
groups_dn = config.get("groups_dn", "ou=groups")
# Iterate through mappings to find the highest role
# Priority: admin > user
potential_roles = []
for mapping in role_mappings:
group_name = mapping["group"]
target_role = mapping["role"]
# Construct group DN if it's just a common name, else use as is
# Construct group DN if it's just a common name
if "=" not in group_name:
full_group_dn = f"cn={group_name},{groups_dn},{base_dn}"
else:
full_group_dn = group_name
full_group_dn_lower = full_group_dn.lower()
log.debug(f"LDAP: Checking membership in group: {full_group_dn}")
conn.search(full_group_dn, '(objectClass=*)', attributes=['member'])
# Method 1: Check memberOf if available (AD/LLDAP)
if full_group_dn_lower in user_groups:
log.debug(f"LDAP: Match found via memberOf for {target_role}")
potential_roles.append(target_role)
continue
# Method 2: Search group's member attribute (Standard LDAP)
conn.search(full_group_dn, '(objectClass=*)', attributes=['member', 'uniqueMember'])
if conn.entries:
members = conn.entries[0].member.values
if real_user_dn in members or user_dn in members or \
any(m.lower().replace(" ", "") == real_user_dn.lower().replace(" ", "") for m in members):
log.debug(f"LDAP: User is in group {group_name}, assigning role: {target_role}")
members = []
if hasattr(conn.entries[0], 'member'):
members = [str(m).lower() for m in conn.entries[0].member.values]
elif hasattr(conn.entries[0], 'uniqueMember'):
members = [str(m).lower() for m in conn.entries[0].uniqueMember.values]
if real_user_dn.lower() in members or user_dn.lower() in members:
log.debug(f"LDAP: Match found via group search for {target_role}")
potential_roles.append(target_role)
if "admin" in potential_roles:
@@ -166,8 +189,9 @@ def get_users(db: Session = Depends(get_db)):
users = db.query(models.User).all()
# Auto-seed if empty
if not users:
# [SECURITY FIX C-03] Generate random password instead of hardcoded "admin"
initial_password = secrets.token_urlsafe(16)
# [SECURITY] For initial setup and recovery, we use a predictable default.
# User MUST change this immediately in Settings.
initial_password = "Admin123!"
new_user = models.User(
username="Admin",
role="admin",
@@ -177,7 +201,7 @@ def get_users(db: Session = Depends(get_db)):
db.add(new_user)
db.commit()
db.refresh(new_user)
log.warning(f"[SECURITY] Admin initial seeded. Temporary password: {initial_password} — CHANGE IMMEDIATELY!")
log.warning(f"[SECURITY] Admin initial seeded. Credentials: Admin / {initial_password} — CHANGE IMMEDIATELY!")
return [new_user]
return users

Some files were not shown because too many files have changed in this diff Show More