Compare commits
53 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 | ||
|
|
f137ded5aa | ||
|
|
946f1787c7 | ||
|
|
09be0b401d | ||
|
|
3069a921e7 | ||
|
|
55e3cf5042 | ||
|
|
c874d27d64 | ||
|
|
939ad8648d | ||
|
|
6f6caf3c5a | ||
|
|
5e648002f0 | ||
|
|
81b775c9ae | ||
|
|
994920bda2 | ||
|
|
0a6368b9f6 | ||
|
|
26e8f034a2 | ||
|
|
2e5c666cc8 | ||
|
|
e383b97d44 | ||
|
|
50ae3671c9 |
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.
|
||||||
43
.agents/rules/api-testing-postman-rest-asured.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
trigger: manual
|
||||||
|
description: You are an expert in API Testing using tools like Postman and REST Assured.
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Testing (Postman, REST Assured)
|
||||||
|
|
||||||
|
You are an expert in API Testing using tools like Postman and REST Assured.
|
||||||
|
|
||||||
|
Key Principles:
|
||||||
|
- Test the business logic layer directly
|
||||||
|
- Faster and more stable than UI tests
|
||||||
|
- Validate request/response contracts
|
||||||
|
- Check status codes, headers, and body
|
||||||
|
- Ensure security and performance
|
||||||
|
|
||||||
|
Postman:
|
||||||
|
- Collections and Folders
|
||||||
|
- Environment and Global variables
|
||||||
|
- Pre-request scripts and Tests (JavaScript)
|
||||||
|
- Newman CLI for CI/CD integration
|
||||||
|
- Mock Servers
|
||||||
|
|
||||||
|
REST Assured (Java):
|
||||||
|
- Fluent BDD-like syntax (Given-When-Then)
|
||||||
|
- Easy integration with JUnit/TestNG
|
||||||
|
- JSON/XML Schema validation
|
||||||
|
- Request/Response logging
|
||||||
|
- Authentication support (OAuth, Basic)
|
||||||
|
|
||||||
|
What to Test:
|
||||||
|
- Status Codes (200, 201, 400, 401, 403, 404, 500)
|
||||||
|
- Response Payload (JSON structure and data)
|
||||||
|
- Headers (Content-Type, Cache-Control)
|
||||||
|
- Performance (Response time)
|
||||||
|
- Security (Auth, Rate limiting)
|
||||||
|
|
||||||
|
Best Practices:
|
||||||
|
- Chain requests (Extract token -> Use token)
|
||||||
|
- Use JSON Schema validation
|
||||||
|
- Data-driven testing (CSV/JSON files)
|
||||||
|
- Clean up created resources
|
||||||
|
- Run API tests in CI pipeline
|
||||||
74
.agents/rules/modern-css-and-esponsive-design-expert.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
trigger: manual
|
||||||
|
description: You are an expert in modern CSS and responsive web design.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Modern CSS & Responsive Design Expert
|
||||||
|
|
||||||
|
You are an expert in modern CSS and responsive web design.
|
||||||
|
|
||||||
|
Key Principles:
|
||||||
|
- Use mobile-first approach
|
||||||
|
- Implement responsive design with CSS Grid and Flexbox
|
||||||
|
- Use CSS custom properties (variables)
|
||||||
|
- Follow BEM or similar naming convention
|
||||||
|
- Write maintainable and scalable CSS
|
||||||
|
|
||||||
|
Layout:
|
||||||
|
- Use CSS Grid for two-dimensional layouts
|
||||||
|
- Use Flexbox for one-dimensional layouts
|
||||||
|
- Use CSS Grid auto-fit and auto-fill
|
||||||
|
- Implement proper spacing with gap property
|
||||||
|
- Use logical properties (inline, block)
|
||||||
|
|
||||||
|
Responsive Design:
|
||||||
|
- Use mobile-first media queries
|
||||||
|
- Use relative units (rem, em, %)
|
||||||
|
- Implement fluid typography with clamp()
|
||||||
|
- Use container queries when appropriate
|
||||||
|
- Test on multiple devices and screen sizes
|
||||||
|
|
||||||
|
Modern CSS Features:
|
||||||
|
- Use CSS custom properties for theming
|
||||||
|
- Use CSS Grid and Flexbox
|
||||||
|
- Use aspect-ratio for maintaining proportions
|
||||||
|
- Use clamp() for fluid sizing
|
||||||
|
- Use min(), max() for responsive values
|
||||||
|
- Use :is(), :where() for cleaner selectors
|
||||||
|
|
||||||
|
Animations:
|
||||||
|
- Use CSS transitions for simple animations
|
||||||
|
- Use CSS animations for complex sequences
|
||||||
|
- Use transform for better performance
|
||||||
|
- Respect prefers-reduced-motion
|
||||||
|
- Use will-change sparingly
|
||||||
|
|
||||||
|
Performance:
|
||||||
|
- Minimize CSS file size
|
||||||
|
- Remove unused CSS
|
||||||
|
- Use CSS containment
|
||||||
|
- Avoid expensive selectors
|
||||||
|
- Use CSS Grid/Flexbox over floats
|
||||||
|
- Minimize repaints and reflows
|
||||||
|
|
||||||
|
Architecture:
|
||||||
|
- Use BEM or similar methodology
|
||||||
|
- Organize CSS logically
|
||||||
|
- Use CSS custom properties for consistency
|
||||||
|
- Implement design tokens
|
||||||
|
- Use utility classes sparingly
|
||||||
|
|
||||||
|
Accessibility:
|
||||||
|
- Ensure sufficient color contrast
|
||||||
|
- Use focus-visible for focus styles
|
||||||
|
- Don't rely on color alone
|
||||||
|
- Test with high contrast mode
|
||||||
|
- Ensure text is readable
|
||||||
|
|
||||||
|
Best Practices:
|
||||||
|
- Use CSS reset or normalize
|
||||||
|
- Implement consistent spacing scale
|
||||||
|
- Use semantic class names
|
||||||
|
- Avoid !important
|
||||||
|
- Comment complex CSS
|
||||||
|
- Use CSS linting tools
|
||||||
88
.agents/rules/progressive-web-app-pwa-expert.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
trigger: manual
|
||||||
|
description: Progressive Web App (PWA) Expert
|
||||||
|
---
|
||||||
|
|
||||||
|
# Progressive Web App (PWA) Expert
|
||||||
|
|
||||||
|
You are an expert in Progressive Web App development.
|
||||||
|
|
||||||
|
Key Principles:
|
||||||
|
- Implement offline-first strategy
|
||||||
|
- Use service workers for caching
|
||||||
|
- Make app installable
|
||||||
|
- Ensure fast loading
|
||||||
|
- Provide app-like experience
|
||||||
|
|
||||||
|
Service Workers:
|
||||||
|
- Implement proper caching strategies
|
||||||
|
- Use Cache API effectively
|
||||||
|
- Handle offline scenarios
|
||||||
|
- Implement background sync
|
||||||
|
- Use workbox for easier implementation
|
||||||
|
- Handle service worker updates
|
||||||
|
|
||||||
|
Manifest:
|
||||||
|
- Create comprehensive web app manifest
|
||||||
|
- Define app icons for all sizes
|
||||||
|
- Set appropriate display mode
|
||||||
|
- Define theme and background colors
|
||||||
|
- Set start URL and scope
|
||||||
|
- Add screenshots for app stores
|
||||||
|
|
||||||
|
Caching Strategies:
|
||||||
|
- Use cache-first for static assets
|
||||||
|
- Use network-first for dynamic content
|
||||||
|
- Implement stale-while-revalidate
|
||||||
|
- Use cache-only for offline pages
|
||||||
|
- Implement proper cache versioning
|
||||||
|
|
||||||
|
Offline Experience:
|
||||||
|
- Provide offline fallback page
|
||||||
|
- Cache critical resources
|
||||||
|
- Implement background sync
|
||||||
|
- Show offline indicator
|
||||||
|
- Queue failed requests
|
||||||
|
|
||||||
|
Performance:
|
||||||
|
- Implement lazy loading
|
||||||
|
- Use code splitting
|
||||||
|
- Optimize images
|
||||||
|
- Minimize JavaScript
|
||||||
|
- Use HTTP/2 push
|
||||||
|
- Implement resource hints
|
||||||
|
|
||||||
|
Installability:
|
||||||
|
- Meet PWA criteria
|
||||||
|
- Implement beforeinstallprompt
|
||||||
|
- Provide install UI
|
||||||
|
- Test installation flow
|
||||||
|
- Handle app updates
|
||||||
|
|
||||||
|
Push Notifications:
|
||||||
|
- Implement push notification API
|
||||||
|
- Request permission appropriately
|
||||||
|
- Handle notification clicks
|
||||||
|
- Implement notification best practices
|
||||||
|
- Test on multiple platforms
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- Serve over HTTPS
|
||||||
|
- Implement CSP headers
|
||||||
|
- Validate all inputs
|
||||||
|
- Use secure authentication
|
||||||
|
- Implement proper CORS
|
||||||
|
|
||||||
|
Testing:
|
||||||
|
- Use Lighthouse for audits
|
||||||
|
- Test offline functionality
|
||||||
|
- Test on multiple devices
|
||||||
|
- Test installation flow
|
||||||
|
- Test push notifications
|
||||||
|
|
||||||
|
Best Practices:
|
||||||
|
- Follow PWA checklist
|
||||||
|
- Implement progressive enhancement
|
||||||
|
- Provide app shell architecture
|
||||||
|
- Use PRPL pattern
|
||||||
|
- Monitor performance metrics
|
||||||
44
.agents/rules/security-and-penetration-testing.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
trigger: manual
|
||||||
|
description: You are an expert in Security Testing and Penetration Testing.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security & Penetration Testing
|
||||||
|
|
||||||
|
You are an expert in Security Testing and Penetration Testing.
|
||||||
|
|
||||||
|
Key Principles:
|
||||||
|
- Think like an attacker
|
||||||
|
- Defense in Depth
|
||||||
|
- Shift Left (Security early in SDLC)
|
||||||
|
- Validate controls and mitigations
|
||||||
|
- Compliance and Risk Management
|
||||||
|
|
||||||
|
OWASP Top 10 (Focus Areas):
|
||||||
|
- Broken Access Control
|
||||||
|
- Cryptographic Failures
|
||||||
|
- Injection (SQLi, XSS)
|
||||||
|
- Insecure Design
|
||||||
|
- Security Misconfiguration
|
||||||
|
|
||||||
|
Testing Types:
|
||||||
|
- SAST (Static Application Security Testing): Code analysis (SonarQube)
|
||||||
|
- DAST (Dynamic Application Security Testing): Runtime analysis (OWASP ZAP, Burp Suite)
|
||||||
|
- SCA (Software Composition Analysis): Dependency checks (Snyk, Dependabot)
|
||||||
|
- Penetration Testing: Manual exploitation
|
||||||
|
|
||||||
|
Tools:
|
||||||
|
- Burp Suite: Proxy and scanner
|
||||||
|
- OWASP ZAP: Open source scanner
|
||||||
|
- Metasploit: Exploitation framework
|
||||||
|
- Nmap: Network scanning
|
||||||
|
- Wireshark: Packet analysis
|
||||||
|
|
||||||
|
Best Practices:
|
||||||
|
- Sanitize all inputs
|
||||||
|
- Encode all outputs
|
||||||
|
- Use parameterized queries
|
||||||
|
- Implement proper authentication/authorization
|
||||||
|
- Keep dependencies updated
|
||||||
|
- Conduct regular vulnerability scans
|
||||||
|
- Perform manual code reviews for security logic
|
||||||
@@ -19,7 +19,21 @@
|
|||||||
"Bash(sqlite3 data/inventory.db \".schema users\")",
|
"Bash(sqlite3 data/inventory.db \".schema users\")",
|
||||||
"Bash(sqlite3 data/inventory.db \"SELECT * FROM users;\")",
|
"Bash(sqlite3 data/inventory.db \"SELECT * FROM users;\")",
|
||||||
"Bash(git restore:*)",
|
"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) ──────────────────────────────
|
# ── Environment files (secrets) ──────────────────────────────
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
inventory.env
|
||||||
|
inventory.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!inventory.env.example
|
||||||
backend/.env
|
backend/.env
|
||||||
backend/.env.*
|
backend/.env.*
|
||||||
!backend/.env.example
|
!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
|
||||||
122
AI_RULES.md
@@ -1,81 +1,61 @@
|
|||||||
# AI AGENT RULES - MANDATORY ENTRY POINT
|
# AI AGENT RULES - MANDATORY SSOT ENTRY POINT
|
||||||
|
|
||||||
**READ THIS ENTIRE FILE BEFORE EXECUTING ANY TASK.**
|
**READ THIS ENTIRE FILE BEFORE EXECUTING ANY TASK.**
|
||||||
This is the **Single Source of Truth** for ALL Artificial Intelligence agents (Claude, Gemini, etc.) working on this project.
|
This is the **Single Source of Truth** for ALL AI agents. Refer to [PROJECT_ARCHITECTURE.md](PROJECT_ARCHITECTURE.md) for technical logic.
|
||||||
(Automatic IDE entry points like `GEMINI.md` and `CLAUDE.md` exist solely to redirect agents to this main file).
|
|
||||||
For technical architecture, data models, and stack details, refer to [PROJECT_ARCHITECTURE.md](PROJECT_ARCHITECTURE.md).
|
|
||||||
|
|
||||||
## 1. Multi-AI Coordination & Memory
|
---
|
||||||
- **MANDATORY STARTUP**: Every AI session MUST first read `dev_docs/SESSION_STATE.md` to understand current context.
|
|
||||||
- **MANDATORY HANDOVER**: At the end of every task/session, update the handover note in `dev_docs/SESSION_STATE.md`. Specify: **Active AI**, **Current Status**, **Technical Context**, and **Next Steps**.
|
## 1. AI MEMORY, TRACEABILITY & HANDOVER
|
||||||
- **ARCHIVAL RULE**: Before writing new state, move all content of the *previous* session handover into the top of `dev_docs/SESSION_HISTORY.md` to prevent bloat.
|
- **MANDATORY STARTUP**: Read `dev_docs/SESSION_STATE.md` immediately at session start.
|
||||||
|
- **PLAN RETIREMENT**: Mark a completed "Master Plan" as `[COMPLETED]` in the file itself. Move technical details to `dev_docs/ARCHIVE_LOGS.md` and `PLAN.md` entries to `dev_docs/PLAN_HISTORY.md`.
|
||||||
|
- **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.
|
- **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. Global Operational Laws
|
## 2. ENGINEERING & OPERATIONAL LAWS
|
||||||
- **STRICT ENGLISH POLICY**: All web interfaces, code, variables, scripts, and documentation MUST BE DIRECTLY AND ONLY IN ENGLISH. If you find Romanian text in code, translate it immediately.
|
- **ENGLISH ONLY**: Interfaces, code, variables, and docs MUST be in English. Translate any Romanian text found in code immediately. (User conversation: Romanian/English).
|
||||||
- **COMMUNICATION LANGUAGE**: Conversation with the user will be in Romanian or English (preferably Romanian).
|
- **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).
|
||||||
- **GIT BINARY PATH**: On this macOS environment, the system `git` is often broken. ALWAYS use the binary path stored in `.git_path` at the project root for all Git operations.
|
- **VERSIONING**: Update `VERSION.json` on every commit. Use `scripts/save_version.py` for automated releases.
|
||||||
- **COMMIT & PUSH STRICT RULE**:
|
- **DEPENDENCIES**: Update `backend/requirements.txt` with version constraints for every new pip package.
|
||||||
- Never push to remote (`git push`) or use force flags (`--hard`, `--force`) unless explicitly requested.
|
- **SSOT INTEGRITY**: Every feature change MUST update: `README.md`, `USER_GUIDE.md`, `PROJECT_ARCHITECTURE.md`, and `export_prod.sh`.
|
||||||
- Always update `VERSION.json` on every commit.
|
|
||||||
- Do NOT add AI co-author signatures (e.g., `Co-Authored-By: AI...`) to commit messages.
|
|
||||||
- Branching: `master` (stable), `dev` (active development), `vX` (archival releases).
|
|
||||||
- **PACKAGE MANAGEMENT**: Any new package installed via `pip install` MUST be added to `backend/requirements.txt` with version constraints (e.g., `slowapi>=0.1.9`). Keep requirements.txt synchronized with installed packages.
|
|
||||||
- **MANDATORY LOGGING**: Document coding/architecture changes in `dev_docs/ARCHIVE_LOGS.md` at the end of the task.
|
|
||||||
- **TRIPLE CONFIRMATION (Safe Forget)**: You cannot delete a physical location, item, or critical entity without explicit user permission three times.
|
|
||||||
|
|
||||||
## 3. UI/UX Fidelity Specifications
|
## 3. UI/UX "PREMIUM" FIDELITY STANDARDS
|
||||||
- **Fidelity First**: Never simplify the UI unless asked. Density and aesthetics must remain "Premium".
|
- **Aesthetics**: Density/aesthetics must remain "Premium". Use Tailwind CSS. NO simplification.
|
||||||
- **Styling**: Tailwind CSS. Font weights/spacing must be consistent (Inter/Roboto).
|
- **Typography Rules**:
|
||||||
- **Readability**: NO `uppercase` or `tracking-widest` styles. Use standard camel/Title case.
|
- **NO UPPERCASE** or **NO ITALICS** in headers, labels, buttons, or metadata.
|
||||||
- **Icons**: Use **Lucide Icons** exclusively. NO emojis.
|
- **NO `tracking-widest`**. Use standard camel/Title case.
|
||||||
- **Interactive Affordance**: All select/dropdown boxes MUST have a `ChevronDown` icon. Masked passwords should have reduced opacity (`text-white/50`).
|
- Use `font-black` for main headings.
|
||||||
- **Iconography Standardization**:
|
- **Layout**: Main pages MUST use `max-w-7xl`.
|
||||||
- **Categories**: ALWAYS use `Layers` (Color: `text-primary`).
|
- **Unified Headers**: Icon box (`p-4 bg-primary/10 border-primary/20`) + Title (`text-3xl font-black`) + Subtitle (`text-xs text-slate-500`).
|
||||||
- **Item Types**: ALWAYS use `Package` (Color: `text-green-500`).
|
- **Iconography**: Use **Lucide Icons** exclusively (NO emojis).
|
||||||
|
- **Categories**: `Layers` (text-primary).
|
||||||
|
- **Item Types**: `Package` (text-green-500).
|
||||||
|
- **Affordance**: Dropdowns MUST have a `ChevronDown`. Passwords: `text-white/50`. Logout MUST be `text-rose-500`.
|
||||||
|
|
||||||
## 4. Documentation Maintenance
|
## 4. DATA INTEGRITY & AUDIT POLICY
|
||||||
- **SSOT INTEGRITY**: Whenever a feature is added, modified, or removed, you MUST update all corresponding documentation files:
|
- **RESTRICTED ACTIONS**: `DELETE /items/` and Admin settings require `auth.get_current_admin`.
|
||||||
- `README.md` (General usage & technical modes).
|
- **AUDIT IMMUTABILITY**: Deleting an `Item` MUST NOT delete its `AuditLog` entries.
|
||||||
- `USER_GUIDE.md` (End-user instructions).
|
- **TRACEABILITY**: Log deletions to `logs/backend.log` with `USER[id]`, `ITEM[id]`, `Name`, `PN`.
|
||||||
- `PROJECT_ARCHITECTURE.md` (Technical logic & data models).
|
- **CONFIRMATION**:
|
||||||
- `export_prod.sh` (If new scripts or files must be included in the production bundle).
|
- **Triple Confirmation**: Deleting critical entities (Locations/Items) requires user confirmation 3 times.
|
||||||
- **REAL-TIME UPDATES**: Documentation updates are NOT optional and must be performed within the same session as the code changes.
|
- **Native Alerts**: Use `window.confirm` for all destructive UI actions and Logout.
|
||||||
|
|
||||||
## 6. AI Command Shortcuts
|
## 5. AI COMMAND SHORTCUTS
|
||||||
- **`save-version`**: When the user triggers this command, the AI MUST:
|
- **`save-version`**:
|
||||||
1. Increment the patch version in `VERSION.json`.
|
0. **MANDATORY**: Verify and update ALL documentation (`.md` files: README, USER_GUIDE, ARCHITECTURE, etc.) with explanations of all current changes.
|
||||||
2. Stage all current changes (`git add .`).
|
1. Increment `VERSION.json`.
|
||||||
3. Commit changes with message `Build [vX.Y.Z]`.
|
2. Git add/commit (`Build [vX.Y.Z]`).
|
||||||
4. Create a new branch named `vX.Y.Z` from the current state.
|
3. Create branch `vX.Y.Z` (Snapshot).
|
||||||
5. Generate a production bundle ZIP (calls `./export_prod.sh`).
|
4. Automatic Sync: Merge changes into `master` branch to keep it up-to-date.
|
||||||
6. Stay on the current branch (`dev`).
|
5. Run `./export_prod.sh`.
|
||||||
- *Implementation*: Use `python3 scripts/save_version.py` to ensure consistency.
|
(Always use `python3 scripts/save_version.py`).
|
||||||
|
|
||||||
## 7. UI Fidelity Laws (Strict)
|
---
|
||||||
- **Consistent Layout**: All main pages (Inventory, Audit, Admin) MUST use `max-w-7xl` for their primary content container.
|
|
||||||
- **Unified Headers**: Every functional page MUST have an identical header structure:
|
|
||||||
- Icon inside a styled box (`p-4 bg-primary/10 rounded-[2rem] border border-primary/20`).
|
|
||||||
- Title: `text-3xl font-black text-white tracking-tight`.
|
|
||||||
- Subtitle: `text-xs text-slate-500 font-bold mt-1 tracking-wider`.
|
|
||||||
- **Typographic Integrity**:
|
|
||||||
- **NO UPPERCASE** (ALL CAPS) allowed anywhere in the UI (headers, labels, buttons, metadata).
|
|
||||||
- **NO ITALICS** in headers, titles, or active UI elements.
|
|
||||||
- Consistent font weight (`font-black`) for main headings.
|
|
||||||
- **Safety Actions**:
|
|
||||||
- The **Logout** button MUST be clearly differentiated (e.g., `text-rose-500`) and MUST always require a `window.confirm` before execution.
|
|
||||||
|
|
||||||
## 8. Audit & Deletion Policy
|
## END OF SESSION PROTOCOL
|
||||||
- **Immutability of Logs**: Deleting an `Item` MUST NOT delete its corresponding entries in the `AuditLog` table in the database.
|
End your final response on a separate line exactly with:
|
||||||
- **Disk Traceability**: Every item deletion MUST be logged to the disk file (`logs/backend.log`) with `USER[id]`, `ITEM[id]`, `Name`, and `PN`.
|
```
|
||||||
- **Explicit Confirmation**: Destructive actions (Delete Item from catalog) MUST always trigger a browser-native `window.confirm` before proceeding.
|
---
|
||||||
|
✓ Done.
|
||||||
|
```
|
||||||
## 5. End of Session Protocol
|
|
||||||
- Once a phase/task from `PLAN.md` is completed and verified, move that entry into `dev_docs/PLAN_HISTORY.md`.
|
|
||||||
- After finishing an entire job (including updating session state, architecture logs, and versioning), end your final response on a separate line exactly with:
|
|
||||||
```
|
|
||||||
---
|
|
||||||
✓ Done.
|
|
||||||
```
|
|
||||||
- Do not provide unnecessary verbatim summaries of the code.
|
|
||||||
|
|||||||
@@ -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).
|
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).
|
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.
|
Do not proceed with any task until you have analyzed these three files.
|
||||||
|
|||||||
12
Caddyfile
@@ -1,12 +0,0 @@
|
|||||||
# TFM aInventory - Caddy Self-Signed Internal Proxy
|
|
||||||
# This replaces the need for `local-ssl-proxy` in Node.
|
|
||||||
|
|
||||||
:3003 {
|
|
||||||
tls internal
|
|
||||||
reverse_proxy frontend:3000
|
|
||||||
}
|
|
||||||
|
|
||||||
:3002 {
|
|
||||||
tls internal
|
|
||||||
reverse_proxy backend:8000
|
|
||||||
}
|
|
||||||
@@ -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).
|
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).
|
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.
|
Do not proceed with any task until you have analyzed these three files.
|
||||||
|
|||||||
8
PLAN.md
@@ -3,11 +3,7 @@
|
|||||||
This document tracks the immediate implementation checklist. For overarching design and constraints, see [PROJECT_ARCHITECTURE.md](PROJECT_ARCHITECTURE.md).
|
This document tracks the immediate implementation checklist. For overarching design and constraints, see [PROJECT_ARCHITECTURE.md](PROJECT_ARCHITECTURE.md).
|
||||||
|
|
||||||
## Implementation Status
|
## Implementation Status
|
||||||
- [x] **Phase 1: Backend Foundation** (FastAPI, SQLite, Models).
|
- [ ] **Phase 8: Database Encryption** (Implementing SQLCipher for data-at-rest protection).
|
||||||
- [x] **Phase 2: Modular AI Integration** (Gemini support, v2 SDK).
|
- [ ] **Phase 9: Multi-Location Support** (Tracking inventory across different physical warehouses).
|
||||||
- [x] **Phase 3: AI Onboarding UI** (Validation Mask, Camera integration).
|
|
||||||
- [x] **Phase 4: Offline Synchronization** (Deduplicated Dexie -> SQL sync logic).
|
|
||||||
- [x] **Phase 5: Inventory Trash Management** (Waste/Discard/Damage workflow).
|
|
||||||
- [ ] **Phase 6: Audit Log Dashboard UI** (Visual historical interventions).
|
|
||||||
|
|
||||||
*(Note: Completed phases are periodically moved to `dev_docs/PLAN_HISTORY.md` according to AI_RULES)*
|
*(Note: Completed phases are periodically moved to `dev_docs/PLAN_HISTORY.md` according to AI_RULES)*
|
||||||
|
|||||||
@@ -16,27 +16,35 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
|
|||||||
|
|
||||||
### 2.2 Frontend (Web & PWA)
|
### 2.2 Frontend (Web & PWA)
|
||||||
- **Architecture:** Next.js 15+ (App Router)
|
- **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)
|
- **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)
|
- **Offline persistence:** Dexie.js (IndexedDB wrapper)
|
||||||
- **Scanner:** `html5-qrcode` (Client-side, offline-only)
|
- **Scanner:** `html5-qrcode` (Client-side, offline-only)
|
||||||
- **Sync:** Axios with bulk-sync idempotency (UUID-based)
|
- **Sync:** Axios with bulk-sync idempotency (UUID-based)
|
||||||
|
|
||||||
### 2.3 Operations & Tooling
|
### 2.3 Operations & Tooling
|
||||||
- **PWA Deployment:** `next-pwa` (Service Workers + Manifest.json)
|
- **PWA Deployment:** `next-pwa` (Service Workers + Manifest.json)
|
||||||
- **HTTPS Proxy:** `local-ssl-proxy` (Required for mobile camera access, Port 3003)
|
- **HTTPS Proxy:** `caddy` or `local-ssl-proxy` (Port 8909)
|
||||||
- **Servers:** Frontend (`npm run dev` on 3000), Backend (`./start_server.sh` on 8000)
|
- **Servers:** Frontend (Port 8907), Backend (Port 8906)
|
||||||
|
- **Configuration:** Centrally managed via root `inventory.env` (Network/CORS/API Keys) and `config/` directory (LDAP, Caddyfile).
|
||||||
|
|
||||||
## 3. Data Models & Entities
|
## 3. Data Models & Entities
|
||||||
- **Item:** Name, Category Group (Structured), Item Type (Specific), Quantity, Barcode, Part Number.
|
- **Item:** Name, Category Group (Structured), Item Type (Specific), Quantity, Barcode, Part Number, Box Label (Association).
|
||||||
- **Category:** Predefined groups for organizational structure.
|
- **Category:** Predefined groups for organizational structure.
|
||||||
- **Intervention:** Linked to a required items list.
|
- **Box/Container:** A generic grouping label (box_label) that links multiple items together for rapid multi-scanning.
|
||||||
- **Audit Log:** Immutable ledger detailing CRUD operations and stock fluctuations.
|
- **Audit Log:** Immutable ledger detailing CRUD operations and stock fluctuations, including point-in-time box associations.
|
||||||
|
|
||||||
## 4. Scanning & Optimization Strategy (Crucial)
|
## 4. Scanning & Optimization Strategy (Crucial)
|
||||||
### 4.1 AI Usage Policy
|
### 4.1 AI Usage Policy
|
||||||
- **Routine Operations (Check-in/Out):** Executes entirely on the local device unconditionally using `html5-qrcode` ($0 cost). No AI is allowed here.
|
- **Routine Operations (Check-in/Out):** Executes entirely on the local device unconditionally using `html5-qrcode` ($0 cost). No AI is allowed here.
|
||||||
- **New Item Onboarding (AI Label OCR):** Uses cloud AI (`gemini-2.0-flash`). The user takes a photo, AI extracts data based on strict templates.
|
- **New Item Onboarding (AI Label OCR):** Uses cloud AI (`gemini-2.0-flash`). The user takes a photo, AI extracts data based on strict templates.
|
||||||
|
- **AI Box Discovery Mode (v1.6.0):** Supports specialized `mode="box"` prompt that focuses exclusively on prominent container names/hand-written labels, ignoring technical spec noise.
|
||||||
- **Validation Mask:** AI-extracted data is NEVER saved directly. It is presented in a validation UI for human confirmation.
|
- **Validation Mask:** AI-extracted data is NEVER saved directly. It is presented in a validation UI for human confirmation.
|
||||||
|
|
||||||
### 4.2 Scanner Technical Specs
|
### 4.2 Scanner Technical Specs
|
||||||
@@ -48,6 +56,16 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
|
|||||||
- Noise Filtering: Ignores `< 3` chars, decimals, and dates.
|
- Noise Filtering: Ignores `< 3` chars, decimals, and dates.
|
||||||
- Scoring: Exact S/N (+500), Exact P/N (+200), Token match (+50), Category match (+20).
|
- Scoring: Exact S/N (+500), Exact P/N (+200), Token match (+50), Category match (+20).
|
||||||
- Threshold: Minimum **40 points** for auto-match without user intervention.
|
- Threshold: Minimum **40 points** for auto-match without user intervention.
|
||||||
|
- **Targeted Field Scanning (v1.6.0):** UI allows "locking" the scanner focus to a specific input field (e.g., `box_label`). The OCR result is then redirected to state without performing regular item lookup.
|
||||||
|
|
||||||
|
### 4.3 Box Labeling & Printing System (v1.5.0)
|
||||||
|
- **Local OCR Priority:** Before checking individual S/Ns, the matching engine searches for `box_label` tokens. If a box is identified:
|
||||||
|
- Single Match: Directly opens stock adjustment.
|
||||||
|
- Multi Match: Opens "Box Contents" selection interstitial.
|
||||||
|
- **Label Generation:** Native SVG-based Code 128 and QR generation (`lib/labels.ts`). Requires ZERO external libraries for maximum offline stability.
|
||||||
|
- **Printing Modes:**
|
||||||
|
- @media print: Hardcoded CSS styles for 62mm x 29mm label dimensions.
|
||||||
|
- Mobile Export: Canvas-to-PNG rasterization for sharing with Bluetooth printer roll apps.
|
||||||
|
|
||||||
|
|
||||||
## 5. Offline Sync Protocol
|
## 5. Offline Sync Protocol
|
||||||
@@ -59,3 +77,29 @@ To prevent data loss in basements or unstable networks:
|
|||||||
## 6. Automation & Versioning (`scripts/`)
|
## 6. Automation & Versioning (`scripts/`)
|
||||||
- **`scripts/save_version.py`**: Implements the `save-version` AI Command Shortcut. Increments `VERSION.json` patch version, commits all staged changes, creates a snapshot branch `v.X.Y.Z`, and calls `./export_prod.sh` to generate the production bundle. Always stays on the `dev` branch.
|
- **`scripts/save_version.py`**: Implements the `save-version` AI Command Shortcut. Increments `VERSION.json` patch version, commits all staged changes, creates a snapshot branch `v.X.Y.Z`, and calls `./export_prod.sh` to generate the production bundle. Always stays on the `dev` branch.
|
||||||
|
|
||||||
|
|
||||||
|
## 7. Security & Hardening (v1.4.0)
|
||||||
|
To ensure enterprise-grade protection, the following policies are enforced:
|
||||||
|
|
||||||
|
### 7.1 Access Control & RBAC
|
||||||
|
- **Strict Separation:** Operations are divided into `user` and `admin` roles.
|
||||||
|
- **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 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
|
||||||
|
- **Information Scrubbing:** Backend logs are configured to intercept and mask sensitive auth tokens or internal secrets (e.g., `JWT_SECRET_KEY`) during debug output.
|
||||||
|
- **Direct Bind LDAP:** Authentication uses direct user binding to the LDAP server, avoiding the need for a privileged service account with broad search permissions.
|
||||||
|
- **Cryptographic Credential Caching:** To support offline operations, the system caches a **PBKDF2-HMAC-SHA256 hash** of the user's Enterprise credentials upon successful online login. Plain text passwords are NEVER stored.
|
||||||
|
|
||||||
|
### 7.4 PWA Trust & Security
|
||||||
|
- **HTTPS Enforcement:** The system requires TLS (Port 8909) for camera access and secure token transmission.
|
||||||
|
- **Manifest Integrity:** A comprehensive `manifest.json` ensures the app is recognized as a trusted PWA on mobile platforms (iOS/Android).
|
||||||
|
7.5 Git Infrastructure Hardening (v1.7.0)
|
||||||
|
To ensure deployment stability on macOS environments with potentially broken developer tool links (`xcode-select` errors):
|
||||||
|
- **Direct Binary Mapping:** The system bypasses path resolution by using a hardcoded direct link to the Git binary in `.git_path` (`/Library/Developer/CommandLineTools/usr/bin/git`).
|
||||||
|
- **Persistence Mandate:** This path is protected by mandatory AI rules and must never be removed or modified to ensure `save-version` and automated deployment scripts remain functional.
|
||||||
|
|||||||
32
README.md
@@ -12,21 +12,21 @@ This project supports three distinct operational modes:
|
|||||||
Ideal for local development on macOS/Linux.
|
Ideal for local development on macOS/Linux.
|
||||||
* **Command:** `./start_server.sh`
|
* **Command:** `./start_server.sh`
|
||||||
* **Details:** Runs FastAPI (backend) and Next.js (frontend) in development mode. Uses `local-ssl-proxy` for HTTPS.
|
* **Details:** Runs FastAPI (backend) and Next.js (frontend) in development mode. Uses `local-ssl-proxy` for HTTPS.
|
||||||
* **Backend:** http://localhost:8000
|
* **Backend:** http://localhost:8916
|
||||||
* **Frontend:** https://localhost:3003
|
* **Frontend:** https://localhost:8919
|
||||||
|
|
||||||
### 2. 🐳 Docker Mode (Recommended for Production)
|
### 2. 🐳 Docker Mode (Recommended for Production)
|
||||||
Isolated and portable container stack.
|
Isolated and portable container stack.
|
||||||
* **Command:** `docker-compose up -d --build`
|
* **Command:** `docker-compose up -d --build`
|
||||||
* **Details:** Uses Caddy as a reverse proxy for HTTPS. Persistent data and logs are mapped to `./data` and `./logs`.
|
* **Details:** Uses Caddy as a reverse proxy for HTTPS. Persistent data and logs are mapped to `./data` and `./logs`.
|
||||||
* **Access:** https://localhost:3003
|
* **Access:** https://localhost:8909
|
||||||
|
|
||||||
### 3. 🐧 Standalone Linux Mode (Systemd)
|
### 3. 🐧 Standalone Linux Mode (Systemd)
|
||||||
Native Linux installation (Alma/Debian/Ubuntu) without Docker dependencies.
|
Native Linux installation (Alma/Debian/Ubuntu) without Docker dependencies.
|
||||||
* **Installation:** `sudo ./install_service.sh`
|
* **Installation:** `sudo ./install_service.sh`
|
||||||
* **Execution:** `sudo systemctl start inventory`
|
* **Execution:** `sudo systemctl start inventory`
|
||||||
* **Details:** Compiles the frontend for production and manages the entire stack as a system service.
|
* **Details:** Compiles the frontend for production and manages the entire stack as a system service.
|
||||||
* **Access:** https://<SERVER-IP>:3003
|
* **Access:** https://<SERVER-IP>:8909
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -34,14 +34,25 @@ Native Linux installation (Alma/Debian/Ubuntu) without Docker dependencies.
|
|||||||
To generate a clean production package and snapshot the current state:
|
To generate a clean production package and snapshot the current state:
|
||||||
1. Use the AI shortcut command: `save-version`.
|
1. Use the AI shortcut command: `save-version`.
|
||||||
2. Alternatively, run `./export_prod.sh` manually.
|
2. Alternatively, run `./export_prod.sh` manually.
|
||||||
3. A `.zip` archive will be created (e.g., `aInventory-PROD-v1.3.6.zip`).
|
3. A `.zip` archive will be created (e.g., `aInventory-PROD-v1.7.0.zip`).
|
||||||
4. A backup branch `v.1.3.x` will be created automatically.
|
4. A backup branch `v.1.3.x` will be created automatically.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🎨 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
|
## 🏗 Technical Overview
|
||||||
* **Backend:** FastAPI (Python 3.12+)
|
* **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.
|
* **Database:** SQLite (SQLAlchemy) with Dexie.js (IndexedDB) for client-side sync.
|
||||||
* **Proxy:** Caddy (Docker) or local-ssl-proxy (Standalone/Dev).
|
* **Proxy:** Caddy (Docker) or local-ssl-proxy (Standalone/Dev).
|
||||||
* **AI Engine:** Google Gemini (Generative AI SDK).
|
* **AI Engine:** Google Gemini (Generative AI SDK).
|
||||||
@@ -58,7 +69,8 @@ The application requires the following environment variables for production depl
|
|||||||
| Variable | Purpose | Example |
|
| Variable | Purpose | Example |
|
||||||
|----------|---------|---------|
|
|----------|---------|---------|
|
||||||
| **JWT_SECRET_KEY** | JWT token signing key (REQUIRED for production) | `openssl rand -hex 32` |
|
| **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` |
|
| **DATA_DIR** | SQLite database location | `/app/data` |
|
||||||
| **LOGS_DIR** | Application logs directory | `/app/logs` |
|
| **LOGS_DIR** | Application logs directory | `/app/logs` |
|
||||||
|
|
||||||
@@ -77,6 +89,12 @@ export ALLOWED_ORIGINS="https://your-domain.com"
|
|||||||
docker-compose up -d --build
|
docker-compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🌐 Network & Port Customization
|
||||||
|
The application uses a central configuration file for all network settings:
|
||||||
|
- **Location:** `config/network_config.env`
|
||||||
|
- **Purpose:** Change the `SERVER_IP` (default: `192.168.84.113`) and reserved ports (`8906-8909`).
|
||||||
|
- **Mechanism:** Startup scripts automatically sync these settings to the frontend and Docker environment.
|
||||||
|
|
||||||
For detailed security audit report, see [dev_docs/SECURITY_REPORT.md](dev_docs/SECURITY_REPORT.md).
|
For detailed security audit report, see [dev_docs/SECURITY_REPORT.md](dev_docs/SECURITY_REPORT.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ The application is a **Progressive Web App**, which means you don't need to down
|
|||||||
|
|
||||||
- **Default User:** On first installation, use `Admin` / `<initial-password>` (check your system administrator for the initial password).
|
- **Default User:** On first installation, use `Admin` / `<initial-password>` (check your system administrator for the initial password).
|
||||||
- **Change Password:** We recommend changing your password immediately from the Admin settings.
|
- **Change Password:** We recommend changing your password immediately from the Admin settings.
|
||||||
- **LDAP/Enterprise Login:** If your administrator has configured LDAP integration, you can log in with your company/domain account. The application will cache your password locally to allow offline access (e.g., in areas without signal like basements).
|
- **LDAP/Enterprise Login:** If your administrator has configured LDAP integration, you can log in with your company/domain account. The application will securely cache a **cryptographic hash** of your credentials (using PBKDF2) to allow offline access (e.g., in areas without signal like basements). **Note: Your actual password is NEVER stored in plain text on the local device.**
|
||||||
- **JWT Tokens:** Your login session is secured with JWT bearer tokens that expire after 8 hours. You will be automatically logged out when your token expires.
|
- **JWT Tokens:** Your login session is secured with JWT bearer tokens that expire after 8 hours. You will be automatically logged out when your token expires.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -31,16 +31,27 @@ The application supports two scanning modes:
|
|||||||
### Manual / Barcode Scanning
|
### Manual / Barcode Scanning
|
||||||
Scan an existing barcode to locate or update an item in your inventory.
|
Scan an existing barcode to locate or update an item in your inventory.
|
||||||
|
|
||||||
### Client-Side Label Scanning (OCR)
|
|
||||||
Point your device's camera at a product label. The scanner **automatically analyzes the label every 4 seconds** — no button press required. A countdown timer shows when the next scan will occur.
|
|
||||||
|
|
||||||
When text is detected:
|
|
||||||
1. A preview of the captured image appears.
|
|
||||||
2. Detected words are highlighted — tap the correct product name or serial number.
|
|
||||||
3. The selected text populates the relevant field automatically.
|
|
||||||
|
|
||||||
If no readable text is found, the scanner silently retries on the next cycle.
|
If no readable text is found, the scanner silently retries on the next cycle.
|
||||||
|
|
||||||
|
### Box & Container Scanning (NEW v1.6.0)
|
||||||
|
You can now manage containers more efficiently with two specialized methods:
|
||||||
|
|
||||||
|
- **AI Box Discovery**: When adding a new container through **AI Discovery**, use the **"Box / Container"** toggle. Gemini will focus exclusively on the container's name, ignoring technical noise on labels.
|
||||||
|
- **Targeted Field Scanning**: In the **Edit Item** modal, tap the small **Camera icon** next to the "Box / Container Label" field. The scanner will capture the next physical label directly into the text field.
|
||||||
|
- **Automatic Matching**: In the main scanner, scanning a box identifies all its contents. Scanning a box and then an item will suggest linking them together if they aren't already matched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏷️ Label Printing
|
||||||
|
Administrators and users can generate physical labels for boxes to ensure 100% accurate scanning.
|
||||||
|
## 🏷️ 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📂 Inventory Organization
|
## 📂 Inventory Organization
|
||||||
@@ -92,6 +103,11 @@ If your organization uses LDAP/Active Directory, administrators can:
|
|||||||
### Settings
|
### Settings
|
||||||
Access application settings from the **Admin** panel.
|
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:
|
||||||
|
- **`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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚨 Security Notices
|
## 🚨 Security Notices
|
||||||
@@ -133,5 +149,5 @@ For detailed technical documentation, see the [Project Architecture](../PROJECT_
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** v1.3.6
|
**Version:** v1.9.19
|
||||||
**Last Updated:** 2026-04-11
|
**Last Updated:** 2026-04-14
|
||||||
|
|||||||
12
VERSION.json
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.4.0",
|
|
||||||
"last_build": "2026-04-12-0940",
|
|
||||||
"commit": "HEAD",
|
|
||||||
"changelog": [
|
|
||||||
"v1.4.0: Final Audit Dashboard (Phase 6), LDAP UI Restoration, environment path fixes for macOS",
|
|
||||||
"v1.3.9: Maintenance and structural updates",
|
|
||||||
"v1.3.8: Remove .npx_cache/ and scratch/npm_cache/ from git tracking (3350 files purged from index)",
|
|
||||||
"v1.3.7: Security hardening \u2014 expanded .gitignore, removed ldap_config.json from tracking, added example files",
|
|
||||||
"v1.3.6: Scanner UI redesign (autonomous OCR, countdown), Item Type datalist, save-version automation"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
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,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.1.100:3000,https://192.168.1.100:3003
|
|
||||||
ALLOWED_ORIGINS=http://localhost:3000,https://localhost:3003
|
|
||||||
|
|
||||||
# --- Data Paths (overridden by start_server.sh / docker-compose) ---
|
|
||||||
# DATA_DIR=/absolute/path/to/data
|
|
||||||
# LOGS_DIR=/absolute/path/to/logs
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
# Install system dependencies required for python-ldap (needed by backend)
|
# Install system dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
libldap2-dev \
|
libldap2-dev \
|
||||||
libsasl2-dev \
|
libsasl2-dev \
|
||||||
|
gosu \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -26,14 +27,13 @@ COPY scripts ./scripts
|
|||||||
ENV DATA_DIR="/app/data"
|
ENV DATA_DIR="/app/data"
|
||||||
ENV LOGS_DIR="/app/logs"
|
ENV LOGS_DIR="/app/logs"
|
||||||
|
|
||||||
# Ensure the appuser can write to data and logs if we pre-create them,
|
# Pre-create directories and ensure they are writable
|
||||||
# although Docker volumes will handle ownership context.
|
|
||||||
RUN mkdir -p /app/data /app/logs && chown -R appuser:appuser /app
|
RUN mkdir -p /app/data /app/logs && chown -R appuser:appuser /app
|
||||||
|
|
||||||
# Make initialization scripts executable
|
# Make initialization scripts executable
|
||||||
RUN chmod +x /app/scripts/init_data.sh /app/backend/entrypoint.sh
|
RUN chmod +x /app/scripts/init_data.sh /app/backend/entrypoint.sh
|
||||||
|
|
||||||
USER appuser
|
EXPOSE 8000
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,50 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import io
|
import io
|
||||||
|
import logging
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from google import genai
|
from google import genai
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
|
||||||
|
log = logging.getLogger("ainventory")
|
||||||
|
|
||||||
def get_best_models():
|
def get_best_models():
|
||||||
# Using the exact models discovered via diagnostic
|
# Priority list based on aliases and future-gen models found in user logs
|
||||||
return ["gemini-2.0-flash", "gemini-2.5-flash"]
|
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):
|
def extract(image_bytes: bytes, prompt: str):
|
||||||
api_key = os.environ.get("GEMINI_API_KEY")
|
api_key = os.environ.get("GEMINI_API_KEY")
|
||||||
if not 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
|
return None
|
||||||
|
|
||||||
# Log partial key for safety debug
|
# Log partial key for safety debug
|
||||||
key_hint = f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else "too short"
|
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:
|
try:
|
||||||
# Initialize the NEW SDK Client forcing stable v1 API
|
# Switching to v1beta which is required for many experimental/new models
|
||||||
client = genai.Client(api_key=api_key, http_options={'api_version': 'v1'})
|
client = genai.Client(api_key=api_key, http_options={'api_version': 'v1beta'})
|
||||||
|
|
||||||
# DEBUG: List allowed models for this key
|
# DEBUG: List allowed models for this key
|
||||||
print("🔍 Checking available models for your key...")
|
log.debug("🔍 Checking available models for your key...")
|
||||||
try:
|
try:
|
||||||
for m in client.models.list():
|
for m in client.models.list():
|
||||||
print(f" - Found: {m.name}")
|
log.debug(f" - Found: {m.name}")
|
||||||
except Exception as list_e:
|
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()
|
models_to_try = get_best_models()
|
||||||
|
|
||||||
# Try models in order
|
# Try models in order
|
||||||
for model_name in models_to_try:
|
for model_name in models_to_try:
|
||||||
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)
|
# In the new SDK, we pass a list of parts (text string and image bytes)
|
||||||
response = client.models.generate_content(
|
response = client.models.generate_content(
|
||||||
@@ -48,10 +56,12 @@ def extract(image_bytes: bytes, prompt: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not response or not response.text:
|
if not response or not response.text:
|
||||||
|
log.warning(f"⚠️ Gemini {model_name} returned NO text.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
text = response.text.strip()
|
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
|
# Extract JSON block
|
||||||
if "```json" in text:
|
if "```json" in text:
|
||||||
@@ -62,10 +72,10 @@ def extract(image_bytes: bytes, prompt: str):
|
|||||||
return json.loads(text)
|
return json.loads(text)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Gemini {model_name} failed: {e}")
|
log.error(f"❌ Gemini {model_name} failed: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
except Exception as outer_e:
|
except Exception as outer_e:
|
||||||
print(f"❌ Gemini Client Init failed: {outer_e}")
|
log.error(f"❌ Gemini Client Init failed: {outer_e}")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,43 +1,132 @@
|
|||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from . import models
|
||||||
|
from .database import SessionLocal
|
||||||
from .ai import gemini, claude
|
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__))
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
dotenv_path = os.path.join(base_dir, ".env")
|
|
||||||
load_dotenv(dotenv_path)
|
|
||||||
|
|
||||||
def extract_label_info(image_bytes: bytes):
|
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.
|
Orchestrates extraction across multiple AI providers.
|
||||||
Order: Gemini (Flash/Pro) -> Claude (Haiku/Sonnet)
|
Modes: 'item' (full technical extraction), 'box' (container discovery)
|
||||||
"""
|
"""
|
||||||
prompt = """
|
db = SessionLocal()
|
||||||
Extract technical inventory information from this label image.
|
try:
|
||||||
|
if mode == "box":
|
||||||
CRITICAL INSTRUCTIONS:
|
prompt = """
|
||||||
1. Look at the most prominent text (usually top 1-2 rows). This is the product NAME and MODEL.
|
Identify the CONTAINER or BOX name from this image.
|
||||||
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').
|
Look for large, prominent, bold, or hand-written text that identifies a storage unit.
|
||||||
3. Separate the COLOR (e.g. Turquoise, Yellow, Black).
|
Ignore small technical details, quantities, or fine print.
|
||||||
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:
|
||||||
|
{
|
||||||
Return ONLY a valid JSON object:
|
"box_label": "The identified container name",
|
||||||
{
|
"name": "Same as box_label",
|
||||||
"name": "Full descriptive name from header",
|
"category": "Storage",
|
||||||
"part_number": "Unique identifier for fast scanning",
|
"description": "Brief description if useful",
|
||||||
"category": "Broad category",
|
"quantity": 1
|
||||||
"color": "Color if present",
|
}
|
||||||
"specs": "Brief tech specs list",
|
"""
|
||||||
"barcode": "Barcode value if visible",
|
else:
|
||||||
"quantity": 1
|
# 1. Try fetching from the configuration file first (SSOT)
|
||||||
}
|
prompt = prompt_mgr.get_prompt()
|
||||||
"""
|
|
||||||
|
if not prompt:
|
||||||
# 1. Try Gemini
|
# 2. Fallback to Database if file is missing
|
||||||
result = gemini.extract(image_bytes, prompt)
|
setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_extraction_prompt").first()
|
||||||
if result:
|
if setting:
|
||||||
return result
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 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)
|
# 2. Try Claude (Fallback)
|
||||||
result = claude.extract(image_bytes, prompt)
|
result = claude.extract(image_bytes, prompt)
|
||||||
|
|||||||
@@ -66,8 +66,7 @@ async def get_current_user(credentials = Depends(security)):
|
|||||||
token = credentials.credentials
|
token = credentials.credentials
|
||||||
import logging
|
import logging
|
||||||
log = logging.getLogger("ainventory")
|
log = logging.getLogger("ainventory")
|
||||||
log.debug(f"[AUTH] Validating token (first 20 chars): {token[:20] if token else 'None'}")
|
log.debug(f"[AUTH] Validating token (first 10 chars): {token[:10] if token else 'None'}")
|
||||||
log.debug(f"[AUTH] Using SECRET_KEY (first 10 chars): {SECRET_KEY[:10] if SECRET_KEY else 'None'}")
|
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
user_id: int = int(payload.get("sub")) # sub is stored as string, convert back to int
|
user_id: int = int(payload.get("sub")) # sub is stored as string, convert back to int
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"_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",
|
|
||||||
"use_tls": false,
|
|
||||||
"role_mappings": [
|
|
||||||
{
|
|
||||||
"group": "inventory_admins",
|
|
||||||
"role": "admin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "inventory_users",
|
|
||||||
"role": "user"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
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:
|
except Exception as e:
|
||||||
logger.error(f"[DB] Restore failed: {str(e)}")
|
logger.error(f"[DB] Restore failed: {str(e)}")
|
||||||
raise Exception(f"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)}")
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export LOGS_DIR="${LOGS_DIR:-/app/logs}"
|
|||||||
echo "🐳 [Docker] Running data initialization..."
|
echo "🐳 [Docker] Running data initialization..."
|
||||||
bash /app/scripts/init_data.sh
|
bash /app/scripts/init_data.sh
|
||||||
|
|
||||||
# Hand off to the application server
|
# Fix permissions for mounted volumes (which might be root-owned by the host)
|
||||||
echo "🐳 [Docker] Starting uvicorn..."
|
echo "🐳 [Docker] Fixing volume permissions..."
|
||||||
exec python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000
|
chown -R appuser:appuser "${DATA_DIR}" "${LOGS_DIR}"
|
||||||
|
|
||||||
|
# Hand off to the application server as non-root user
|
||||||
|
echo "🐳 [Docker] Starting uvicorn as appuser..."
|
||||||
|
exec gosu appuser python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000
|
||||||
|
|||||||
119
backend/main.py
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from . import config_loader # This triggers the automatic environment loading
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from slowapi import Limiter
|
from slowapi import Limiter
|
||||||
@@ -19,15 +20,55 @@ log.info("Database tables verified.")
|
|||||||
app = FastAPI(title="TFM aInventory API", version="1.1.0")
|
app = FastAPI(title="TFM aInventory API", version="1.1.0")
|
||||||
log.info("TFM aInventory API process started.")
|
log.info("TFM aInventory API process started.")
|
||||||
|
|
||||||
# [SECURITY FIX M-01] CORS: allow_origins=["*"] + allow_credentials=True is invalid per spec.
|
# [SECURITY FIX M-01] CORS Configuration
|
||||||
# Allowed origins are configured via ALLOWED_ORIGINS environment variable (comma-separated).
|
# We dynamically build allowed origins from environment variables to simplify deployment.
|
||||||
# Secure fallback: localhost only for development.
|
_raw_origins = os.environ.get("ALLOWED_ORIGINS", "")
|
||||||
_raw_origins = os.environ.get(
|
|
||||||
"ALLOWED_ORIGINS",
|
|
||||||
"http://localhost:3000,http://localhost:3002"
|
|
||||||
)
|
|
||||||
ALLOWED_ORIGINS = [o.strip() for o in _raw_origins.split(",") if o.strip()]
|
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)
|
# Add CORS middleware FIRST (before rate limiter)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@@ -54,6 +95,68 @@ def startup_event():
|
|||||||
scheduler.start()
|
scheduler.start()
|
||||||
sync_scheduler_config()
|
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("/")
|
@app.get("/")
|
||||||
def read_root():
|
def read_root():
|
||||||
return {"message": "Inventory API is running"}
|
return {"message": "Inventory API is running"}
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ class Category(Base):
|
|||||||
|
|
||||||
items = relationship("Item", back_populates="category_rel")
|
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):
|
class Item(Base):
|
||||||
__tablename__ = "items"
|
__tablename__ = "items"
|
||||||
|
|
||||||
@@ -36,11 +42,18 @@ class Item(Base):
|
|||||||
category_rel = relationship("Category", back_populates="items")
|
category_rel = relationship("Category", back_populates="items")
|
||||||
part_number = Column(String, index=True, nullable=True)
|
part_number = Column(String, index=True, nullable=True)
|
||||||
color = 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)
|
specs = Column(Text, nullable=True)
|
||||||
quantity = Column(Float, default=0.0)
|
quantity = Column(Float, default=0.0)
|
||||||
min_quantity = Column(Float, default=1.0)
|
min_quantity = Column(Float, default=1.0)
|
||||||
image_url = Column(String, nullable=True)
|
image_url = Column(String, nullable=True)
|
||||||
|
|
||||||
|
# Generic box/container association for multi-item OCR scanning
|
||||||
|
box_label = Column(String, index=True, nullable=True)
|
||||||
|
|
||||||
# Full AI metadata
|
# Full AI metadata
|
||||||
labels_data = Column(Text, nullable=True)
|
labels_data = Column(Text, nullable=True)
|
||||||
|
|
||||||
@@ -52,6 +65,10 @@ class AuditLog(Base):
|
|||||||
user_id = Column(Integer, ForeignKey("users.id"))
|
user_id = Column(Integer, ForeignKey("users.id"))
|
||||||
action = Column(String) # e.g., 'CHECK_IN', 'CHECK_OUT', 'CREATE_ITEM'
|
action = Column(String) # e.g., 'CHECK_IN', 'CHECK_OUT', 'CREATE_ITEM'
|
||||||
target_item_id = Column(Integer, nullable=True)
|
target_item_id = Column(Integer, nullable=True)
|
||||||
|
target_item_name = Column(String, nullable=True)
|
||||||
|
target_item_pn = Column(String, nullable=True)
|
||||||
|
target_item_barcode = Column(String, nullable=True)
|
||||||
|
target_snapshot = Column(Text, nullable=True) # Full JSON snapshot
|
||||||
quantity_change = Column(Float, nullable=True)
|
quantity_change = Column(Float, nullable=True)
|
||||||
uuid = Column(String, unique=True, index=True, nullable=True)
|
uuid = Column(String, unique=True, index=True, nullable=True)
|
||||||
details = Column(Text, nullable=True) # For reasons, sync notes, etc.
|
details = Column(Text, nullable=True) # For reasons, sync notes, etc.
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from .. import models, schemas, auth
|
|||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
from ..db_manager import DbManager
|
from ..db_manager import DbManager
|
||||||
from ..scheduler import sync_scheduler_config
|
from ..scheduler import sync_scheduler_config
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi import UploadFile, File
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/admin/db",
|
prefix="/admin/db",
|
||||||
@@ -103,3 +105,87 @@ def update_db_settings(
|
|||||||
# Re-trigger scheduler sync
|
# Re-trigger scheduler sync
|
||||||
sync_scheduler_config()
|
sync_scheduler_config()
|
||||||
return settings
|
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)
|
current_user: auth.TokenData = Depends(auth.get_current_user)
|
||||||
):
|
):
|
||||||
"""[C-01] List of categories — only for authenticated users."""
|
"""[C-01] List of categories — only for authenticated users."""
|
||||||
categories = db.query(models.Category).all()
|
return 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
|
|
||||||
|
|
||||||
@router.post("/", response_model=schemas.Category)
|
@router.post("/", response_model=schemas.Category)
|
||||||
def create_category(
|
def create_category(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File,
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from typing import List
|
from typing import List
|
||||||
|
import json
|
||||||
from slowapi import Limiter
|
from slowapi import Limiter
|
||||||
from slowapi.util import get_remote_address
|
from slowapi.util import get_remote_address
|
||||||
from .. import models, schemas, auth
|
from .. import models, schemas, auth
|
||||||
@@ -65,6 +66,7 @@ _MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
|
|||||||
async def extract_label(
|
async def extract_label(
|
||||||
request: Request,
|
request: Request,
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
mode: str = "item", # 'item' or 'box'
|
||||||
current_user: auth.TokenData = Depends(auth.get_current_user)
|
current_user: auth.TokenData = Depends(auth.get_current_user)
|
||||||
):
|
):
|
||||||
"""[C-01] Extract label from image — only for authenticated users. [H-02] Rate limit: 10 req/min per IP."""
|
"""[C-01] Extract label from image — only for authenticated users. [H-02] Rate limit: 10 req/min per IP."""
|
||||||
@@ -85,7 +87,7 @@ async def extract_label(
|
|||||||
detail="File exceeds 10MB limit."
|
detail="File exceeds 10MB limit."
|
||||||
)
|
)
|
||||||
|
|
||||||
result = extract_label_info(contents)
|
result = extract_label_info(contents, mode=mode)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@router.post("/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED)
|
@router.post("/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED)
|
||||||
@@ -95,10 +97,18 @@ def create_item(
|
|||||||
current_user: auth.TokenData = Depends(auth.get_current_user)
|
current_user: auth.TokenData = Depends(auth.get_current_user)
|
||||||
):
|
):
|
||||||
"""[C-01] Create item — only for authenticated users. [M-02] user_id from token."""
|
"""[C-01] Create item — only for authenticated users. [M-02] user_id from token."""
|
||||||
# Check if barcode exists
|
# [AUTO-PERSIST] Create Category/Color if not exists
|
||||||
db_item = db.query(models.Item).filter(models.Item.barcode == item.barcode).first()
|
if item.category:
|
||||||
if db_item:
|
cat = db.query(models.Category).filter(models.Category.name == item.category).first()
|
||||||
raise HTTPException(status_code=400, detail="Barcode already registered")
|
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_item = models.Item(**item.model_dump())
|
||||||
db.add(db_item)
|
db.add(db_item)
|
||||||
@@ -106,10 +116,27 @@ def create_item(
|
|||||||
db.refresh(db_item)
|
db.refresh(db_item)
|
||||||
|
|
||||||
# Audit log the creation — [M-02] user_id from token, not from body
|
# Audit log the creation — [M-02] user_id from token, not from body
|
||||||
|
# Capture full snapshot
|
||||||
|
item_snapshot = {
|
||||||
|
"barcode": db_item.barcode,
|
||||||
|
"name": db_item.name,
|
||||||
|
"category": db_item.category,
|
||||||
|
"type": db_item.type,
|
||||||
|
"part_number": db_item.part_number,
|
||||||
|
"color": db_item.color,
|
||||||
|
"specs": db_item.specs,
|
||||||
|
"box_label": db_item.box_label,
|
||||||
|
"image_url": db_item.image_url
|
||||||
|
}
|
||||||
|
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=current_user.sub,
|
user_id=current_user.sub,
|
||||||
action="CREATE_ITEM",
|
action="CREATE_ITEM",
|
||||||
target_item_id=db_item.id,
|
target_item_id=db_item.id,
|
||||||
|
target_item_name=db_item.name,
|
||||||
|
target_item_pn=db_item.part_number,
|
||||||
|
target_item_barcode=db_item.barcode,
|
||||||
|
target_snapshot=json.dumps(item_snapshot),
|
||||||
quantity_change=item.quantity
|
quantity_change=item.quantity
|
||||||
)
|
)
|
||||||
db.add(audit)
|
db.add(audit)
|
||||||
@@ -129,6 +156,19 @@ def update_item(
|
|||||||
if not db_item:
|
if not db_item:
|
||||||
raise HTTPException(status_code=404, detail="Item not found")
|
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)
|
update_data = item.model_dump(exclude_unset=True)
|
||||||
for key, value in update_data.items():
|
for key, value in update_data.items():
|
||||||
setattr(db_item, key, value)
|
setattr(db_item, key, value)
|
||||||
@@ -141,22 +181,46 @@ def update_item(
|
|||||||
def delete_item(
|
def delete_item(
|
||||||
item_id: int,
|
item_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: auth.TokenData = Depends(auth.get_current_user)
|
current_user: auth.TokenData = Depends(auth.get_current_admin)
|
||||||
):
|
):
|
||||||
"""[C-01] Delete item — only for authenticated users. InterventionItems are cleared; AuditLogs are KEPT for history."""
|
"""[C-01] Delete item — only for authenticated users. InterventionItems are cleared; AuditLogs are KEPT for history."""
|
||||||
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
|
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
|
||||||
if not db_item:
|
if not db_item:
|
||||||
raise HTTPException(status_code=404, detail="Item not found")
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
|
|
||||||
# [AUDIT] Log the deletion to disk file via logger.py
|
# [AUDIT] Log the deletion to database and disk
|
||||||
from ..logger import log
|
from ..logger import log
|
||||||
log.warning(f"USER[{current_user.sub}] DELETING ITEM: ID={item_id}, Name={db_item.name}, PN={db_item.part_number}")
|
log.warning(f"USER[{current_user.sub}] DELETING ITEM: ID={item_id}, Name={db_item.name}, PN={db_item.part_number}")
|
||||||
|
|
||||||
|
item_snapshot = {
|
||||||
|
"barcode": db_item.barcode,
|
||||||
|
"name": db_item.name,
|
||||||
|
"category": db_item.category,
|
||||||
|
"type": db_item.type,
|
||||||
|
"part_number": db_item.part_number,
|
||||||
|
"color": db_item.color,
|
||||||
|
"specs": db_item.specs,
|
||||||
|
"box_label": db_item.box_label,
|
||||||
|
"image_url": db_item.image_url,
|
||||||
|
"final_quantity": db_item.quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
audit = models.AuditLog(
|
||||||
|
user_id=current_user.sub,
|
||||||
|
action="DELETE_ITEM",
|
||||||
|
target_item_id=item_id,
|
||||||
|
target_item_name=db_item.name,
|
||||||
|
target_item_pn=db_item.part_number,
|
||||||
|
target_item_barcode=db_item.barcode,
|
||||||
|
target_snapshot=json.dumps(item_snapshot),
|
||||||
|
details=f"Final Quantity: {db_item.quantity}"
|
||||||
|
)
|
||||||
|
db.add(audit)
|
||||||
|
|
||||||
# [CLEANUP] Delete related InterventionItems to prevent foreign key issues
|
# [CLEANUP] Delete related InterventionItems to prevent foreign key issues
|
||||||
db.query(models.InterventionItem).filter(models.InterventionItem.item_id == item_id).delete()
|
db.query(models.InterventionItem).filter(models.InterventionItem.item_id == item_id).delete()
|
||||||
|
|
||||||
# Audit Logs in database are NOT deleted here to preserve history of actions
|
# Audit Logs in database are NOT deleted here to preserve history of actions
|
||||||
|
|
||||||
db.delete(db_item)
|
db.delete(db_item)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Item deleted successfully. History logs preserved."}
|
return {"message": "Item deleted successfully. History logs preserved."}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import List
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from .. import models, schemas, auth
|
from .. import models, schemas, auth
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
|
import json
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/operations",
|
prefix="/operations",
|
||||||
@@ -27,10 +28,21 @@ def check_in_item(
|
|||||||
item.quantity += op.quantity
|
item.quantity += op.quantity
|
||||||
|
|
||||||
# Create Mandatory Audit Log — [M-02] user_id from token
|
# Create Mandatory Audit Log — [M-02] user_id from token
|
||||||
|
item_snapshot = {
|
||||||
|
"barcode": item.barcode,
|
||||||
|
"name": item.name,
|
||||||
|
"category": item.category,
|
||||||
|
"part_number": item.part_number
|
||||||
|
}
|
||||||
|
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=current_user.sub,
|
user_id=current_user.sub,
|
||||||
action="CHECK_IN",
|
action="CHECK_IN",
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
|
target_item_name=item.name,
|
||||||
|
target_item_pn=item.part_number,
|
||||||
|
target_item_barcode=item.barcode,
|
||||||
|
target_snapshot=json.dumps(item_snapshot),
|
||||||
quantity_change=op.quantity
|
quantity_change=op.quantity
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,10 +72,21 @@ def check_out_item(
|
|||||||
item.quantity -= op.quantity
|
item.quantity -= op.quantity
|
||||||
|
|
||||||
# Create Mandatory Audit Log
|
# Create Mandatory Audit Log
|
||||||
|
item_snapshot = {
|
||||||
|
"barcode": item.barcode,
|
||||||
|
"name": item.name,
|
||||||
|
"category": item.category,
|
||||||
|
"part_number": item.part_number
|
||||||
|
}
|
||||||
|
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=current_user.sub,
|
user_id=current_user.sub,
|
||||||
action="CHECK_OUT",
|
action="CHECK_OUT",
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
|
target_item_name=item.name,
|
||||||
|
target_item_pn=item.part_number,
|
||||||
|
target_item_barcode=item.barcode,
|
||||||
|
target_snapshot=json.dumps(item_snapshot),
|
||||||
quantity_change=-op.quantity
|
quantity_change=-op.quantity
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,10 +116,21 @@ def trash_item(
|
|||||||
item.quantity -= op.quantity
|
item.quantity -= op.quantity
|
||||||
|
|
||||||
# Create Mandatory Audit Log with TRASH action and reason in details
|
# Create Mandatory Audit Log with TRASH action and reason in details
|
||||||
|
item_snapshot = {
|
||||||
|
"barcode": item.barcode,
|
||||||
|
"name": item.name,
|
||||||
|
"category": item.category,
|
||||||
|
"part_number": item.part_number
|
||||||
|
}
|
||||||
|
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=current_user.sub,
|
user_id=current_user.sub,
|
||||||
action="TRASH",
|
action="TRASH",
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
|
target_item_name=item.name,
|
||||||
|
target_item_pn=item.part_number,
|
||||||
|
target_item_barcode=item.barcode,
|
||||||
|
target_snapshot=json.dumps(item_snapshot),
|
||||||
quantity_change=-op.quantity,
|
quantity_change=-op.quantity,
|
||||||
details=op.reason
|
details=op.reason
|
||||||
)
|
)
|
||||||
@@ -130,10 +164,21 @@ def bulk_check_out(
|
|||||||
item.quantity -= op.quantity
|
item.quantity -= op.quantity
|
||||||
|
|
||||||
# Log individual audit for this item
|
# Log individual audit for this item
|
||||||
|
item_snapshot = {
|
||||||
|
"barcode": item.barcode,
|
||||||
|
"name": item.name,
|
||||||
|
"category": item.category,
|
||||||
|
"part_number": item.part_number
|
||||||
|
}
|
||||||
|
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=current_user.sub,
|
user_id=current_user.sub,
|
||||||
action="BULK_CHECK_OUT",
|
action="BULK_CHECK_OUT",
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
|
target_item_name=item.name,
|
||||||
|
target_item_pn=item.part_number,
|
||||||
|
target_item_barcode=item.barcode,
|
||||||
|
target_snapshot=json.dumps(item_snapshot),
|
||||||
quantity_change=-op.quantity
|
quantity_change=-op.quantity
|
||||||
)
|
)
|
||||||
db.add(audit)
|
db.add(audit)
|
||||||
@@ -182,10 +227,21 @@ def bulk_sync(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Log audit with original offline timestamp and UUID
|
# Log audit with original offline timestamp and UUID
|
||||||
|
item_snapshot = {
|
||||||
|
"barcode": item.barcode,
|
||||||
|
"name": item.name,
|
||||||
|
"category": item.category,
|
||||||
|
"part_number": item.part_number
|
||||||
|
}
|
||||||
|
|
||||||
audit = models.AuditLog(
|
audit = models.AuditLog(
|
||||||
user_id=current_user.sub,
|
user_id=current_user.sub,
|
||||||
action=op.type,
|
action=op.type,
|
||||||
target_item_id=item.id,
|
target_item_id=item.id,
|
||||||
|
target_item_name=item.name,
|
||||||
|
target_item_pn=item.part_number,
|
||||||
|
target_item_barcode=item.barcode,
|
||||||
|
target_snapshot=json.dumps(item_snapshot),
|
||||||
quantity_change=change,
|
quantity_change=change,
|
||||||
timestamp=op.timestamp,
|
timestamp=op.timestamp,
|
||||||
uuid=op.uuid,
|
uuid=op.uuid,
|
||||||
@@ -215,6 +271,10 @@ def get_logs(
|
|||||||
models.User.username,
|
models.User.username,
|
||||||
models.AuditLog.action,
|
models.AuditLog.action,
|
||||||
models.AuditLog.target_item_id,
|
models.AuditLog.target_item_id,
|
||||||
|
models.AuditLog.target_item_name,
|
||||||
|
models.AuditLog.target_item_pn,
|
||||||
|
models.AuditLog.target_item_barcode,
|
||||||
|
models.AuditLog.target_snapshot,
|
||||||
models.AuditLog.quantity_change,
|
models.AuditLog.quantity_change,
|
||||||
models.AuditLog.details
|
models.AuditLog.details
|
||||||
).join(models.User, models.AuditLog.user_id == models.User.id).order_by(models.AuditLog.timestamp.desc()).limit(limit).all()
|
).join(models.User, models.AuditLog.user_id == models.User.id).order_by(models.AuditLog.timestamp.desc()).limit(limit).all()
|
||||||
@@ -228,6 +288,10 @@ def get_logs(
|
|||||||
"username": l.username,
|
"username": l.username,
|
||||||
"action": l.action,
|
"action": l.action,
|
||||||
"target_item_id": l.target_item_id,
|
"target_item_id": l.target_item_id,
|
||||||
|
"target_item_name": l.target_item_name,
|
||||||
|
"target_item_pn": l.target_item_pn,
|
||||||
|
"target_item_barcode": l.target_item_barcode,
|
||||||
|
"target_snapshot": l.target_snapshot,
|
||||||
"quantity_change": l.quantity_change,
|
"quantity_change": l.quantity_change,
|
||||||
"details": l.details
|
"details": l.details
|
||||||
} for l in logs_with_users
|
} for l in logs_with_users
|
||||||
|
|||||||