Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6102ff39aa | ||
|
|
10d75619bf | ||
|
|
546eca6b9a | ||
|
|
66844a56e9 | ||
|
|
e8c804da71 | ||
|
|
45db169dda | ||
|
|
f47e7d005a | ||
|
|
adb6cde87a | ||
|
|
460dc4fe12 | ||
|
|
4df2d844ca | ||
|
|
409afb29f8 | ||
|
|
bacc23cc23 | ||
|
|
a6af90ec48 | ||
|
|
29b5c7020c | ||
|
|
70670a3c9d | ||
|
|
65b24079cb | ||
|
|
6c57b1b0c2 | ||
|
|
1ea5e90bc4 | ||
|
|
00ee4cf9c5 | ||
|
|
fcb187974e | ||
|
|
1fff658d3c | ||
|
|
826b264a70 | ||
|
|
94f1a515b7 | ||
|
|
4dc0ce50e7 | ||
|
|
ee7e7b7bd7 | ||
|
|
9d7e4f0ca3 | ||
|
|
bdf6d605cd | ||
|
|
a2f6cab492 | ||
|
|
476fda7203 | ||
|
|
9348336709 | ||
|
|
e5194a1dbb | ||
|
|
7c3d0e102f | ||
|
|
a0eaddf994 | ||
|
|
30be967887 | ||
|
|
2b8d0b3f43 | ||
|
|
07b15c8e01 | ||
|
|
65cc0c7b6d |
55
.agent/rules/superpowers.md
Normal 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 (3–5 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 aren’t 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.
|
||||
37
.agent/skills/superpowers-brainstorm/SKILL.md
Normal 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
|
||||
- (1–2 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 (2–4)
|
||||
For each option include:
|
||||
- Summary
|
||||
- Pros / cons
|
||||
- Complexity / risk
|
||||
|
||||
### Recommendation
|
||||
- Pick one option and explain why
|
||||
|
||||
### Acceptance criteria
|
||||
- Bullet list of verifiable outcomes
|
||||
35
.agent/skills/superpowers-debug/SKILL.md
Normal 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 (2–5)**
|
||||
- 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
|
||||
32
.agent/skills/superpowers-finish/SKILL.md
Normal 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
|
||||
30
.agent/skills/superpowers-plan/SKILL.md
Normal 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** (2–10 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: (1–2 bullets)
|
||||
- Verify: (exact commands or checks)
|
||||
2. ...
|
||||
|
||||
### Risks & mitigations
|
||||
### Rollback plan
|
||||
97
.agent/skills/superpowers-python-automation/SKILL.md
Normal 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)
|
||||
- 500–599
|
||||
Optional: 408, and 409 only if operation is safe and semantics known
|
||||
|
||||
Do NOT retry on:
|
||||
- most 400–499 (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 don’t 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)
|
||||
109
.agent/skills/superpowers-rest-automation/SKILL.md
Normal 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 (don’t 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 (1–3 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)
|
||||
33
.agent/skills/superpowers-review/SKILL.md
Normal 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
|
||||
30
.agent/skills/superpowers-tdd/SKILL.md
Normal 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
|
||||
48
.agent/skills/superpowers-workflow/SKILL.md
Normal 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 (2–10 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 (3–5 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
|
||||
@@ -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())
|
||||
245
.agent/skills/superpowers-workflow/scripts/spawn_subagent.py
Executable 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())
|
||||
28
.agent/skills/superpowers-workflow/scripts/write_artifact.py
Normal 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())
|
||||
38
.agent/workflows/superpowers-brainstorm.md
Normal 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 (2–4)
|
||||
## 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.
|
||||
33
.agent/workflows/superpowers-debug.md
Normal 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.
|
||||
213
.agent/workflows/superpowers-execute-plan-parallel.md
Normal 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.
|
||||
84
.agent/workflows/superpowers-execute-plan.md
Normal 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 (1–2 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 step’s 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 (1–3 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.
|
||||
31
.agent/workflows/superpowers-finish.md
Normal 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.
|
||||
13
.agent/workflows/superpowers-reload.md
Normal 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.
|
||||
32
.agent/workflows/superpowers-review.md
Normal 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.
|
||||
57
.agent/workflows/superpowers-write-plan.md
Normal 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 (2–10 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.
|
||||
@@ -19,7 +19,21 @@
|
||||
"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 *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
3
.gitignore
vendored
@@ -38,7 +38,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
|
||||
|
||||
27
AGENTS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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
|
||||
|
||||
## 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
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -16,8 +16,14 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
|
||||
|
||||
### 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)
|
||||
@@ -26,7 +32,7 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
|
||||
- **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) and `config/` directory (LDAP, Caddyfile).
|
||||
|
||||
## 3. Data Models & Entities
|
||||
- **Item:** Name, Category Group (Structured), Item Type (Specific), Quantity, Barcode, Part Number, Box Label (Association).
|
||||
@@ -80,7 +86,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
|
||||
|
||||
20
README.md
@@ -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,9 +39,20 @@ 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)
|
||||
|
||||
## 🏗 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).
|
||||
@@ -58,7 +69,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` |
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -146,5 +149,5 @@ For detailed technical documentation, see the [Project Architecture](../PROJECT_
|
||||
|
||||
---
|
||||
|
||||
**Version:** v1.8.4
|
||||
**Last Updated:** 2026-04-13
|
||||
**Version:** v1.9.19
|
||||
**Last Updated:** 2026-04-14
|
||||
|
||||
BIN
_images.tests/IMG_6082.jpeg
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
_images.tests/IMG_6106.jpeg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
_images.tests/IMG_6107.jpeg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
_images.tests/IMG_6108.jpeg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
_images.tests/IMG_6109.jpeg
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
_images.tests/IMG_6110.jpeg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
_images.tests/IMG_6111.jpeg
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
_images.tests/IMG_6112.jpeg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
_images.tests/IMG_6113.jpeg
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
_images.tests/IMG_6114.jpeg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
_images.tests/IMG_6115.jpeg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
_images.tests/IMG_6116.jpeg
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
_images.tests/IMG_6117.jpeg
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
_images.tests/IMG_6118.jpeg
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
_images.tests/IMG_6119.jpeg
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
_images.tests/IMG_6120.jpeg
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
_images.tests/IMG_6121.jpeg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
_images.tests/IMG_6122.jpeg
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
_images.tests/IMG_6123.jpeg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
_images.tests/IMG_6124.jpeg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
_images.tests/IMG_6125.jpeg
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
_images.tests/IMG_6126.jpeg
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
_images.tests/IMG_6127.jpeg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
_images.tests/IMG_6128.jpeg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
_images.tests/IMG_6129.jpeg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
_images.tests/IMG_6130.jpeg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
_images.tests/IMG_6131.jpeg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
_images.tests/IMG_6132.jpeg
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
_images.tests/IMG_6133.jpeg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
_images.tests/IMG_6134.jpeg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
_images.tests/IMG_6135.jpeg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
_images.tests/IMG_6136.jpeg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
_images.tests/IMG_6137.jpeg
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
_images.tests/IMG_6139.jpeg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
_images.tests/IMG_6140.jpeg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
_images.tests/IMG_6141.jpeg
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
_images.tests/IMG_6142.jpeg
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
_images.tests/IMG_6143.jpeg
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
_images.tests/IMG_6144.jpeg
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
_images.tests/IMG_6145.jpeg
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
_images.tests/IMG_6146.jpeg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
_images.tests/IMG_6147.jpeg
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
_images.tests/IMG_6148.jpeg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
_images.tests/IMG_6149.jpeg
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
_images.tests/IMG_6150.jpeg
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
_images.tests/IMG_6151.jpeg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
_images.tests/IMG_6152.jpeg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
_images.tests/IMG_6153.jpeg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
@@ -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
|
||||
|
||||
@@ -1,62 +1,132 @@
|
||||
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."
|
||||
|
||||
# 1. Try Gemini
|
||||
result = gemini.extract(image_bytes, prompt)
|
||||
|
||||
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.
|
||||
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()
|
||||
|
||||
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').
|
||||
|
||||
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
@@ -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.warning("⚠️ No .env config files found. Relying on system environment variables.")
|
||||
|
||||
# Auto-run if imported
|
||||
load_config()
|
||||
@@ -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)}")
|
||||
|
||||
119
backend/main.py
@@ -1,4 +1,5 @@
|
||||
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
|
||||
@@ -19,15 +20,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(
|
||||
@@ -54,6 +95,68 @@ 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"
|
||||
}
|
||||
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"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,6 +5,8 @@ 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 fastapi import UploadFile, File
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/admin/db",
|
||||
@@ -103,3 +105,87 @@ def update_db_settings(
|
||||
# Re-trigger scheduler sync
|
||||
sync_scheduler_config()
|
||||
return settings
|
||||
|
||||
@router.get("/export")
|
||||
def export_database(
|
||||
current_admin: auth.TokenData = Depends(auth.get_current_admin)
|
||||
):
|
||||
"""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.post("/import")
|
||||
async def import_database(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: auth.TokenData = Depends(auth.get_current_admin)
|
||||
):
|
||||
"""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))
|
||||
|
||||
import os
|
||||
from ..database import BASE_DIR
|
||||
PROJECT_ROOT = os.path.dirname(BASE_DIR)
|
||||
PROMPT_FILE_PATH = os.path.join(PROJECT_ROOT, "config", "ai_prompt.md")
|
||||
|
||||
@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 (prioritizes config file)."""
|
||||
# 1. Try file first
|
||||
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 as e:
|
||||
print(f"Error reading prompt file: {e}")
|
||||
|
||||
# 2. Fallback to DB
|
||||
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 (writes to both file and DB)."""
|
||||
value = payload.get("value")
|
||||
if value is None:
|
||||
raise HTTPException(status_code=400, detail="Value required")
|
||||
|
||||
# 1. Update File (Primary)
|
||||
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 as e:
|
||||
print(f"Failed to write prompt file: {e}")
|
||||
# We continue to DB update anyway
|
||||
|
||||
# 2. Update DB (Backup/Sync)
|
||||
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)}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -51,6 +51,19 @@ class Category(CategoryBase):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# --- Colors ---
|
||||
class ColorBase(BaseModel):
|
||||
name: str
|
||||
|
||||
class ColorCreate(ColorBase):
|
||||
pass
|
||||
|
||||
class Color(ColorBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# --- Items ---
|
||||
class ItemBase(BaseModel):
|
||||
name: str
|
||||
@@ -60,6 +73,10 @@ class ItemBase(BaseModel):
|
||||
barcode: str
|
||||
part_number: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
connector: Optional[str] = None
|
||||
size: Optional[str] = None
|
||||
ocr_text: Optional[str] = None
|
||||
specs: Optional[str] = None
|
||||
quantity: float = 0.0
|
||||
min_quantity: float = 1.0
|
||||
|
||||
51
backend/scripts/init_settings.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from database import SessionLocal, engine
|
||||
import models
|
||||
|
||||
def init_settings():
|
||||
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.
|
||||
For any field, do not add comments in parenthesis.
|
||||
|
||||
Item: in three words type of this item
|
||||
Type: what type of item is, like "spare parts", "consumables", "patch cords" etc.
|
||||
Description: description (max 5 words)
|
||||
Category: category, if any
|
||||
Connector: connectors
|
||||
Size: size or length
|
||||
Color: color if useful
|
||||
PartNr: part number if any
|
||||
OCR: identification string for local OCR matching"""
|
||||
|
||||
# We will wrap this in a instruction to return JSON for easier parsing while keeping the user's content
|
||||
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))
|
||||
print("Added default AI prompt setting.")
|
||||
|
||||
# Add default backup settings if missing
|
||||
defaults = {
|
||||
"backup_retention_count": "10",
|
||||
"backup_schedule_hour": "3",
|
||||
"backup_schedule_freq_days": "1"
|
||||
}
|
||||
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))
|
||||
print(f"Added default setting: {key}")
|
||||
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_settings()
|
||||
49
backend/scripts/migrate_v4_v5.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import sqlite3
|
||||
import os
|
||||
from ..database import db_path
|
||||
from ..logger import log
|
||||
|
||||
def migrate():
|
||||
"""
|
||||
Migration script to upgrade the items table from schema v4 to v5.
|
||||
Adds columns: description, connector, size, ocr_text.
|
||||
"""
|
||||
log.info(f"🚀 Starting database migration on {db_path}...")
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
log.error(f"❌ Database file not found at {db_path}")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check existing columns
|
||||
cursor.execute("PRAGMA table_info(items)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
|
||||
new_columns = [
|
||||
("description", "VARCHAR"),
|
||||
("connector", "VARCHAR"),
|
||||
("size", "VARCHAR"),
|
||||
("ocr_text", "TEXT")
|
||||
]
|
||||
|
||||
for col_name, col_type in new_columns:
|
||||
if col_name not in columns:
|
||||
log.info(f"➕ Adding column '{col_name}' to 'items' table...")
|
||||
cursor.execute(f"ALTER TABLE items ADD COLUMN {col_name} {col_type}")
|
||||
else:
|
||||
log.info(f"✔️ Column '{col_name}' already exists.")
|
||||
|
||||
conn.commit()
|
||||
log.info("✅ Migration completed successfully.")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"❌ Migration failed: {e}")
|
||||
conn.rollback()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
41
backend/scripts/reset_admin.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
import sys
|
||||
from sqlalchemy.orm import Session
|
||||
from passlib.context import CryptContext
|
||||
from ..database import SessionLocal
|
||||
from .. import models
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
def reset_admin():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
username = "Admin"
|
||||
password = "Admin123!"
|
||||
hashed_password = pwd_context.hash(password)
|
||||
|
||||
user = db.query(models.User).filter(models.User.username == username).first()
|
||||
if user:
|
||||
user.hashed_password = hashed_password
|
||||
user.role = "admin"
|
||||
print(f"✅ User '{username}' found. Password has been reset to: {password}")
|
||||
else:
|
||||
new_user = models.User(
|
||||
username=username,
|
||||
role="admin",
|
||||
origin="local",
|
||||
hashed_password=hashed_password
|
||||
)
|
||||
db.add(new_user)
|
||||
print(f"✅ User '{username}' not found. Created new admin with password: {password}")
|
||||
|
||||
db.commit()
|
||||
print("💾 Changes saved to database.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error resetting admin: {e}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
reset_admin()
|
||||
@@ -1,12 +1,41 @@
|
||||
# TFM aInventory - Caddy Self-Signed Internal Proxy
|
||||
# This replaces the need for `local-ssl-proxy` in Node.
|
||||
# TFM aInventory - Caddy Patched IP Configuration
|
||||
# Version 1.9.17 - The Dynamic Shield (Production Polish)
|
||||
{
|
||||
:{$FRONTEND_SSL_PORT} {
|
||||
tls internal
|
||||
reverse_proxy frontend:3000
|
||||
admin off
|
||||
# Global TLS options for self-signed certificates
|
||||
local_certs
|
||||
skip_install_trust
|
||||
|
||||
# Configure on-demand TLS for private network IPs
|
||||
on_demand_tls {
|
||||
# Pointing to the backend root which returns 200 OK
|
||||
# This allows Caddy to generate internal certs for any IP/domain.
|
||||
ask http://backend:8000/
|
||||
}
|
||||
}
|
||||
|
||||
# Dynamic SSL Proxy (Matches ANY IP or hostname)
|
||||
:{$BACKEND_SSL_PORT} {
|
||||
tls internal
|
||||
https:// {
|
||||
tls internal {
|
||||
on_demand
|
||||
}
|
||||
|
||||
reverse_proxy frontend:3000
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
}
|
||||
|
||||
# Specific port listener for backend (8918 -> 444)
|
||||
https://:444 {
|
||||
tls internal {
|
||||
on_demand
|
||||
}
|
||||
reverse_proxy backend:8000
|
||||
}
|
||||
}
|
||||
|
||||
37
config/ai_prompt.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Technical Inventory Multi-Label Extraction Protocol
|
||||
|
||||
Your goal is to extract precise hardware specifications for ALL relevant inventory items found in the image.
|
||||
|
||||
## Intelligence Rules:
|
||||
1. **Filtering Strategy**:
|
||||
- **INCLUDE**: Standalone physical hardware, modules, cables, servers, and storage units.
|
||||
- **EXCLUDE**: Generic mounting hardware (screws, nails, rails, brackets), paper licenses/documentation, or empty packaging, UNLESS they are the primary and only subject of the image.
|
||||
- In cases like image labels listing multiple SKUs (e.g., 5m cable and 7m cable), TREAT EACH LINE AS A SEPARATE ITEM.
|
||||
|
||||
2. **Field Definitions**:
|
||||
- **Item**: Primary identity (Manufacturer + Core Class). Max 3 words (e.g., "Cisco SFP Module").
|
||||
- **Type**: Asset classification (e.g., "SFP", "patch cords", "NVMe").
|
||||
- **Description**: Technical summary emphasizing speed/capacity. Max 5 words.
|
||||
- **Category**: Broad ecosystem (e.g., "Network", "Storage").
|
||||
- **Connector**: Physical interface (e.g., "LC/UPC", "RJ45").
|
||||
- **Size**: Capacity or Length (e.g., "1.6TB", "5m").
|
||||
- **Color**: Physical distinguish color.
|
||||
- **PartNr**: Absolute technical identifier (P/N, SKU). Omit Serial Numbers.
|
||||
- **OCR**: Concise technical string for local heuristic matching. **EXCLUDE ALL SERIAL NUMBERS.**
|
||||
|
||||
## Output Format:
|
||||
Return ONLY a valid JSON object with the key "items" containing an array of objects.
|
||||
Example:
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"Item": "SFP Module",
|
||||
"Type": "SFP",
|
||||
"Description": "64G Gen7 Fibre Channel",
|
||||
"Category": "Network",
|
||||
"Connector": "LC",
|
||||
"PartNr": "SFP-64G-G7",
|
||||
"OCR": "SFP 64G GEN7 FC"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# ============================================================
|
||||
# TFM aInventory — Backend Environment Variables
|
||||
# Copy this file to .env and fill in real values.
|
||||
# NEVER commit the real .env file to Git!
|
||||
# ============================================================
|
||||
|
||||
# --- AI API Keys ---
|
||||
# Google Gemini API Key (required for AI label OCR onboarding)
|
||||
GEMINI_API_KEY=your_gemini_api_key_here
|
||||
|
||||
# --- Security ---
|
||||
# JWT secret key — generate a strong random value for production:
|
||||
# python3 -c "import secrets; print(secrets.token_urlsafe(64))"
|
||||
# If not set, an ephemeral key is generated per-run (tokens invalidated on restart).
|
||||
JWT_SECRET_KEY=change-me-generate-a-secure-random-value
|
||||
|
||||
# --- CORS ---
|
||||
# Comma-separated list of allowed frontend origins
|
||||
# Example for LAN deployment:
|
||||
# ALLOWED_ORIGINS=http://192.168.84.113:8907,https://192.168.84.113:8909
|
||||
ALLOWED_ORIGINS=http://localhost:8907,https://localhost:8909
|
||||
|
||||
# --- Data Paths (overridden by start_server.sh / docker-compose) ---
|
||||
# DATA_DIR=/absolute/path/to/data
|
||||
# LOGS_DIR=/absolute/path/to/logs
|
||||
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"_comment": "Copy this file to ldap_config.json and fill in real values. NEVER commit ldap_config.json to Git.",
|
||||
"ldap_enabled": false,
|
||||
"server_uri": "ldap://YOUR_LDAP_SERVER_IP:389",
|
||||
"base_dn": "dc=yourdomain,dc=com",
|
||||
"user_template": "cn={username},ou=people,dc=yourdomain,dc=com",
|
||||
"groups_dn": "ou=groups",
|
||||
"server_uri": "ldap://192.168.1.100:389",
|
||||
"use_tls": false,
|
||||
"ignore_cert": false,
|
||||
"base_dn": "dc=example,dc=com",
|
||||
"user_template": "uid={username},ou=users,dc=example,dc=com",
|
||||
"role_mappings": [
|
||||
{
|
||||
"group": "inventory_admins",
|
||||
"group": "cn=inventory_admins,ou=groups,dc=example,dc=com",
|
||||
"role": "admin"
|
||||
},
|
||||
{
|
||||
"group": "inventory_users",
|
||||
"group": "cn=inventory_users,ou=groups,dc=example,dc=com",
|
||||
"role": "user"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# =============================================================================
|
||||
# TFM aInventory - Central Network Configuration
|
||||
# =============================================================================
|
||||
# Use this file to customize the ports and IP address used by the application
|
||||
# without needing to modify the source code.
|
||||
#
|
||||
# IMPORTANT: After modifying this file, restart the application for changes
|
||||
# to take effect.
|
||||
# =============================================================================
|
||||
|
||||
# The primary IP address where the application will be accessed.
|
||||
# Used for CORS settings and documentation links.
|
||||
SERVER_IP=192.168.84.113
|
||||
|
||||
# --- BACKEND PORTS ---
|
||||
# Internal port for the FastAPI server (Backend)
|
||||
BACKEND_PORT=8906
|
||||
|
||||
# External port for the Backend HTTPS Proxy (Caddy/local-ssl-proxy)
|
||||
BACKEND_SSL_PORT=8908
|
||||
|
||||
# --- FRONTEND PORTS ---
|
||||
# Internal port for the Next.js dev/prod server
|
||||
FRONTEND_PORT=8907
|
||||
|
||||
# External port for the Frontend HTTPS Proxy (Caddy/local-ssl-proxy)
|
||||
# This is the port you will use in your browser (e.g. https://192.168.84.113:8909)
|
||||
FRONTEND_SSL_PORT=8909
|
||||
8
config/proxy/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM caddy:2-alpine
|
||||
|
||||
# Install nss-tools to allow Caddy to manage its internal trust store (fixes certutil warning)
|
||||
# Install ca-certificates to ensure Caddy can trust external sites if needed
|
||||
RUN apk add --no-cache nss-tools ca-certificates
|
||||
|
||||
# Expose the internal proxy ports
|
||||
EXPOSE 80 443 444
|
||||
83
deploy.sh
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# TFM aInventory - Bulletproof Deployment Script (v1.9.12)
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Load environment variables (from root inventory.env)
|
||||
if [ -f inventory.env ]; then
|
||||
export $(grep -v '^#' inventory.env | xargs)
|
||||
echo "✅ Loaded configuration from inventory.env"
|
||||
else
|
||||
echo "⚠️ inventory.env not found. Using default values."
|
||||
fi
|
||||
|
||||
# Parse arguments
|
||||
RESET_SSL=false
|
||||
RESET_ADMIN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--reset-ssl)
|
||||
RESET_SSL=true
|
||||
shift
|
||||
;;
|
||||
--reset-admin)
|
||||
RESET_ADMIN=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: ./deploy.sh [options]"
|
||||
echo "Options:"
|
||||
echo " --reset-ssl Clear Caddy storage and reset certificates (Aggressive)"
|
||||
echo " --reset-admin Force reset Admin password to 'Admin123!'"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$RESET_SSL" = true ]; then
|
||||
echo "🧹 Aggressive SSL Reset in progress..."
|
||||
docker compose down
|
||||
# Clear internal docker volumes
|
||||
docker volume rm -f inventory_caddy_data 2>/dev/null || true
|
||||
docker volume rm -f inventory_caddy_config 2>/dev/null || true
|
||||
# Clear persistent host volumes if they exist
|
||||
rm -rf ./data/caddy_data/* 2>/dev/null || true
|
||||
rm -rf ./data/caddy_config/* 2>/dev/null || true
|
||||
echo "✅ SSL storage completely cleared."
|
||||
fi
|
||||
|
||||
echo "🚀 Starting TFM aInventory Services..."
|
||||
# Use --build to ensure the custom Caddy image is built
|
||||
docker compose --env-file inventory.env up -d --build --remove-orphans
|
||||
|
||||
if [ "$RESET_ADMIN" = true ]; then
|
||||
echo "🔐 Resetting Admin credentials..."
|
||||
# Wait for container to be ready
|
||||
sleep 3
|
||||
docker compose exec backend python3 -m backend.scripts.reset_admin
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔍 Verifying port mapping..."
|
||||
docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
|
||||
|
||||
echo ""
|
||||
echo "🚀 DIAGNOSTIC LOGS (Proxy Status):"
|
||||
docker compose logs proxy --tail 20
|
||||
|
||||
echo ""
|
||||
echo "✅ Deployment complete (v1.9.12)."
|
||||
echo " ------------------------------------------------------------"
|
||||
echo " ACCESS COORDINATES:"
|
||||
echo " 1. SECURE: https://${SERVER_IP:-localhost}:${FRONTEND_SSL_PORT:-8919}"
|
||||
echo " 2. DIRECT: http://${SERVER_IP:-localhost}:${FRONTEND_PORT:-8917}"
|
||||
echo " ------------------------------------------------------------"
|
||||
echo " CREDENTIALS (if first run or reset):"
|
||||
echo " User: Admin"
|
||||
echo " Pass: Admin123!"
|
||||
echo " ------------------------------------------------------------"
|
||||
echo ""
|
||||
@@ -123,3 +123,97 @@ Obiectiv: audit de securitate complet înainte de producție.
|
||||
1. Implement Phase 5: Gemini AI Vision Integration for New Item Onboarding.
|
||||
2. Build the "History / Audit" view in the frontend.
|
||||
3. Add "Trash/Discard" logic to the frontend UI.
|
||||
# CURRENT AI WORKING SESSION — HANDOVER
|
||||
|
||||
**Active AI:** Gemini (Antigravity)
|
||||
**Last Updated:** 2026-04-12
|
||||
**Current Version:** v1.6.0 (BoxMaster)
|
||||
**Branch:** dev
|
||||
|
||||
---
|
||||
|
||||
## STATUS: 🟢 STABLE — ADVANCED BOX MANAGEMENT & AI MODES COMPLETE (v1.6.0)
|
||||
|
||||
**CRITICAL FOR NEXT AI:** The "Box/Container Management" feature is **FINISHED**. Do NOT attempt to re-implement or look for a plan. The core logic is already in `frontend/app/page.tsx` (`onOCRMatch` and `BoxManager`), `backend/models.py`, and `frontend/lib/labels.ts`.
|
||||
|
||||
---
|
||||
|
||||
## WHAT WAS DONE THIS SESSION
|
||||
|
||||
### 1. Box Management Architecture (Backend)
|
||||
- **Database Schema** — Added `box_label` column to the `items` table.
|
||||
- **Audit Integrity** — Updated `AuditLog` snapshots to capture `box_label` at the time of each transaction, ensuring immutable historical traceability even if items are moved.
|
||||
- **API Support** — Exposed `box_label` in Pydantic schemas and item routers.
|
||||
|
||||
### 2. Intelligent Scanner Routing (Frontend)
|
||||
- **Box Match Priority** — Rewrote the scanner's `onOCRMatch` logic to prioritize box labels.
|
||||
- **Multi-Item Support** — Developed a "Box Contents" interstitial modal that handles containers with multiple distinct item types.
|
||||
- **Token Matching** — Implemented a local fuzzy token-matching engine for generic box text recognition without AI costs.
|
||||
|
||||
### 3. Dependency-Free Label System
|
||||
- **Native Generation** — Built a zero-dependency SVG engine for Code 128 Barcodes and QR Codes (`lib/labels.ts`).
|
||||
- **Box Manager Dashboard** — Added a dedicated UI to view all existing boxes and trigger label generation.
|
||||
- **Hybrid Printing** — Implemented CSS `@media print` for professional desktop printers and "Save as PNG" rasterization for portable Bluetooth printers on mobile.
|
||||
|
||||
### 4. UI/UX: Targeted Field Scanning
|
||||
- **Camera Capture** — Added a dedicated scan button in Edit modals that redirects OCR results directly to the "Box Label" field without performing general item matches.
|
||||
|
||||
### 5. Multi-Mode AI Discovery
|
||||
- **Contextual Prompts** — Implemented a dual-mode toggle (Item/Box) in the AI Onboarding screen.
|
||||
- **Box Extraction** — Created a specialized prompt for Gemini 2.0 Flash to extract container names while filtering out technical noise from product labels.
|
||||
|
||||
### 6. Operational Rigor: Step 0 Rule
|
||||
- **Mandatory Documentation** — Updated `AI_RULES.md` to force documentation verification before any `save-version` (git commit) operation.
|
||||
- **Master Branch Sync** — Confirmed `scripts/save_version.py` logic to keep `master` branch in sync with the latest releases automatically.
|
||||
|
||||
---
|
||||
|
||||
## WHAT THE NEXT AI MUST DO
|
||||
|
||||
1. **Database Encryption** — Consider implementing SQLite encryption at rest (SQLCipher) if requested.
|
||||
2. **Persistent JWT** — If requested, move the `JWT_SECRET_KEY` to a `.env` file for session persistence across server restarts.
|
||||
3. **Advanced Filtering** — Extend the Box Manager to allow bulk movements between boxes.
|
||||
3. **LDAP Probe** — The "Test Connection" button may show "Partial Success" (handshake rejected) due to anonymous bind restrictions; login itself works fine.
|
||||
4. **Monitoring** — If the rate limiter triggers too frequently for legitimate users, adjust the `slowapi` limit in `backend/routers/users.py`.
|
||||
|
||||
---
|
||||
|
||||
## SYSTEM STATE
|
||||
|
||||
**Active database:** `<project_root>/data/inventory.db`
|
||||
**LDAP config:** `config/ldap_config.json`
|
||||
**Network config:** `config/network_config.env`
|
||||
**Proxy config:** `config/Caddyfile`
|
||||
**Production Bundle:** `aInventory-PROD-v1.8.0.zip` (ConfigSync Final)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Git Access Fix**: The `xcode-select` breakage is bypassed by using the direct binary path: `/Library/Developer/CommandLineTools/usr/bin/git` (stored in `.git_path`). **DO NOT change this path.** Operations now work correctly via this direct link.
|
||||
|
||||
**How to start:**
|
||||
```bash
|
||||
./start_server.sh
|
||||
```
|
||||
- Frontend: `https://192.168.84.113:8909`
|
||||
- Backend: `https://192.168.84.113:8908`
|
||||
|
||||
**Environment variables set by start_server.sh:**
|
||||
- `ALLOWED_ORIGINS` — auto-detected
|
||||
- `DATA_DIR` — absolute path
|
||||
- `JWT_SECRET_KEY` — ephemeral (regenerates on restart)
|
||||
## [Archived] Gemini (Antigravity) — 2026-04-13 — CORS & Config Centralization
|
||||
|
||||
**Active AI:** Gemini (Antigravity)
|
||||
**Archived:** 2026-04-14
|
||||
**Version:** v1.9.18 | **Branch:** dev
|
||||
|
||||
### Status
|
||||
STABLE — GENERIC CORS & CONFIG CENTRALIZATION COMPLETE.
|
||||
The CORS system has been upgraded to support `EXTRA_ALLOWED_ORIGINS` in `inventory.env`.
|
||||
|
||||
### What was done
|
||||
1. **Generic CORS**: Introduced `EXTRA_ALLOWED_ORIGINS` in `inventory.env`. Backend expansion in `main.py` handles all required ports.
|
||||
2. **Config Centralization**: `inventory.env` is now the Primary SSOT for networking and AI keys.
|
||||
3. **Startup**: Enhanced `start_server.sh` with better LAN/VPN URL discovery and display.
|
||||
|
||||
---
|
||||
|
||||
|
||||