Build [v1.9.19]
This commit is contained in:
55
.agent/rules/superpowers.md
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
|
||||||
@@ -44,10 +44,13 @@ You can now manage containers more efficiently with two specialized methods:
|
|||||||
|
|
||||||
## 🏷️ Label Printing
|
## 🏷️ Label Printing
|
||||||
Administrators and users can generate physical labels for boxes to ensure 100% accurate scanning.
|
Administrators and users can generate physical labels for boxes to ensure 100% accurate scanning.
|
||||||
1. Tap the **Package (Box)** icon in the header to open the **Box Inventory**.
|
## 🏷️ Label Printing
|
||||||
2. Find the box you want to label and tap **Print Label**.
|
Administrators and users can generate physical labels for boxes to ensure 100% accurate scanning.
|
||||||
3. **Desktop:** Use the print dialog to send the label directly to a Dymo/Brother thermal printer.
|
1. Tap the **Package (Box)** icon in the global header or the **Manage Boxes** card on the dashboard to open the **Box Inventory**.
|
||||||
4. **Mobile:** Use **"Save for Mobile App"** to download a PNG image of the label, which you can then print using your Bluetooth printer's app (like NIIMBOT).
|
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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -146,5 +149,5 @@ For detailed technical documentation, see the [Project Architecture](../PROJECT_
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** v1.9.18
|
**Version:** v1.9.19
|
||||||
**Last Updated:** 2026-04-13
|
**Last Updated:** 2026-04-14
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import os
|
from . import models
|
||||||
from dotenv import load_dotenv
|
from .database import SessionLocal
|
||||||
from .ai import gemini, claude
|
|
||||||
|
|
||||||
# Load environment variables from the directory where this file resides
|
# Load environment variables from the directory where this file resides
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -12,51 +11,74 @@ def extract_label_info(image_bytes: bytes, mode: str = "item"):
|
|||||||
Orchestrates extraction across multiple AI providers.
|
Orchestrates extraction across multiple AI providers.
|
||||||
Modes: 'item' (full technical extraction), 'box' (container discovery)
|
Modes: 'item' (full technical extraction), 'box' (container discovery)
|
||||||
"""
|
"""
|
||||||
if mode == "box":
|
db = SessionLocal()
|
||||||
prompt = """
|
try:
|
||||||
Identify the CONTAINER or BOX name from this image.
|
if mode == "box":
|
||||||
Look for large, prominent, bold, or hand-written text that identifies a storage unit.
|
prompt = """
|
||||||
Ignore small technical details, quantities, or fine print.
|
Identify the CONTAINER or BOX name from this image.
|
||||||
|
Look for large, prominent, bold, or hand-written text that identifies a storage unit.
|
||||||
|
Ignore small technical details, quantities, or fine print.
|
||||||
|
|
||||||
Return ONLY a valid JSON object:
|
Return ONLY a valid JSON object:
|
||||||
{
|
{
|
||||||
"box_label": "The identified container name",
|
"box_label": "The identified container name",
|
||||||
"name": "Same as box_label",
|
"name": "Same as box_label",
|
||||||
"category": "Storage",
|
"category": "Storage",
|
||||||
"specs": "Brief description if useful",
|
"description": "Brief description if useful",
|
||||||
"quantity": 1
|
"quantity": 1
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
prompt = """
|
# Fetch custom prompt from DB
|
||||||
Extract technical inventory information from this label image.
|
setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_extraction_prompt").first()
|
||||||
|
if setting:
|
||||||
|
prompt = setting.value
|
||||||
|
else:
|
||||||
|
# Fallback to a sensible default if DB is not ready
|
||||||
|
prompt = "Extract technical specs. Return JSON with name, category, description, connector, size, color, part_number, ocr_text, quantity."
|
||||||
|
|
||||||
CRITICAL INSTRUCTIONS:
|
# 1. Try Gemini
|
||||||
1. Look at the most prominent text (usually top 1-2 rows). This is the product NAME and MODEL.
|
result = gemini.extract(image_bytes, prompt)
|
||||||
2. Extract the PART NUMBER (P/N, Model No, Type). If no explicit Part Number is found, synthesize one from the most unique identifier in the header (e.g. 'OM4-MMF-DX').
|
|
||||||
3. Separate the COLOR (e.g. Turquoise, Yellow, Black).
|
|
||||||
4. Extract CATEGORY based on the item type (e.g. Patchcord, SFP, Connector).
|
|
||||||
5. Extract technical SPECS (e.g. '2.0mm', '10G', '850nm').
|
|
||||||
|
|
||||||
Return ONLY a valid JSON object:
|
if result:
|
||||||
{
|
# Map user-defined prompt keys to model fields if needed
|
||||||
"name": "Full descriptive name from header",
|
# User keys: Item, Type, Description, Category, Connector, Size, Color, PartNr, OCR
|
||||||
"part_number": "Unique identifier for fast scanning",
|
mapping = {
|
||||||
"category": "Broad category",
|
"Item": "name",
|
||||||
"color": "Color if present",
|
"Type": "type",
|
||||||
"specs": "Brief tech specs list",
|
"Description": "description",
|
||||||
"barcode": "Barcode value if visible",
|
"Category": "category",
|
||||||
"quantity": 1
|
"Connector": "connector",
|
||||||
}
|
"Size": "size",
|
||||||
"""
|
"Color": "color",
|
||||||
|
"PartNr": "part_number",
|
||||||
|
"OCR": "ocr_text"
|
||||||
|
}
|
||||||
|
|
||||||
# 1. Try Gemini
|
final_result = {}
|
||||||
result = gemini.extract(image_bytes, prompt)
|
for ai_key, model_key in mapping.items():
|
||||||
if result:
|
if ai_key in result:
|
||||||
# Maintenance: Ensure fields are mapped if mode was box
|
final_result[model_key] = result[ai_key]
|
||||||
if mode == "box" and "box_label" in result and "name" not in result:
|
elif model_key in result: # Already mapped or using model keys
|
||||||
result["name"] = result["box_label"]
|
final_result[model_key] = result[model_key]
|
||||||
return result
|
|
||||||
|
# Ensure quantity and barcode are handled if returned or default
|
||||||
|
final_result["quantity"] = result.get("quantity", 1)
|
||||||
|
final_result["barcode"] = result.get("barcode", result.get("PartNr", result.get("part_number", "")))
|
||||||
|
|
||||||
|
# Handle Box mode specifically
|
||||||
|
if mode == "box":
|
||||||
|
final_result["box_label"] = result.get("box_label", result.get("name", "Unknown Box"))
|
||||||
|
final_result["name"] = final_result["box_label"]
|
||||||
|
|
||||||
|
return final_result
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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)}")
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ if extra_origins_raw:
|
|||||||
if combo not in ALLOWED_ORIGINS:
|
if combo not in ALLOWED_ORIGINS:
|
||||||
ALLOWED_ORIGINS.append(combo)
|
ALLOWED_ORIGINS.append(combo)
|
||||||
|
|
||||||
log.info(f"CORS allowed origins: {ALLOWED_ORIGINS}")
|
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(
|
||||||
@@ -92,6 +94,50 @@ 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.")
|
||||||
|
|
||||||
|
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,6 +42,10 @@ 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)
|
||||||
|
|||||||
@@ -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,63 @@ 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))
|
||||||
|
|
||||||
|
@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."""
|
||||||
|
setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_extraction_prompt").first()
|
||||||
|
if not setting:
|
||||||
|
return {"value": ""}
|
||||||
|
return {"value": setting.value}
|
||||||
|
|
||||||
|
@router.post("/settings/prompt")
|
||||||
|
def update_ai_prompt(
|
||||||
|
payload: dict,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_admin: auth.TokenData = Depends(auth.get_current_admin)
|
||||||
|
):
|
||||||
|
"""Update the AI extraction prompt."""
|
||||||
|
value = payload.get("value")
|
||||||
|
if value is None:
|
||||||
|
raise HTTPException(status_code=400, detail="Value required")
|
||||||
|
|
||||||
|
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"}
|
||||||
|
|||||||
@@ -97,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)
|
||||||
@@ -148,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)
|
||||||
|
|||||||
@@ -51,6 +51,19 @@ class Category(CategoryBase):
|
|||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
# --- Colors ---
|
||||||
|
class ColorBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class ColorCreate(ColorBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Color(ColorBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
# --- Items ---
|
# --- Items ---
|
||||||
class ItemBase(BaseModel):
|
class ItemBase(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
@@ -60,6 +73,10 @@ class ItemBase(BaseModel):
|
|||||||
barcode: str
|
barcode: str
|
||||||
part_number: Optional[str] = None
|
part_number: Optional[str] = None
|
||||||
color: Optional[str] = None
|
color: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
connector: Optional[str] = None
|
||||||
|
size: Optional[str] = None
|
||||||
|
ocr_text: Optional[str] = None
|
||||||
specs: Optional[str] = None
|
specs: Optional[str] = None
|
||||||
quantity: float = 0.0
|
quantity: float = 0.0
|
||||||
min_quantity: float = 1.0
|
min_quantity: float = 1.0
|
||||||
|
|||||||
51
backend/scripts/init_settings.py
Normal file
51
backend/scripts/init_settings.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add parent directory to path
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from database import SessionLocal, engine
|
||||||
|
import models
|
||||||
|
|
||||||
|
def init_settings():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
# Default AI Prompt from User Request
|
||||||
|
default_prompt = """identify and summarise the minimal necessary information for a quick description if item. I need the following output - <field name> : the result from you.
|
||||||
|
For any field, do not add comments in parenthesis.
|
||||||
|
|
||||||
|
Item: in three words type of this item
|
||||||
|
Type: what type of item is, like "spare parts", "consumables", "patch cords" etc.
|
||||||
|
Description: description (max 5 words)
|
||||||
|
Category: category, if any
|
||||||
|
Connector: connectors
|
||||||
|
Size: size or length
|
||||||
|
Color: color if useful
|
||||||
|
PartNr: part number if any
|
||||||
|
OCR: identification string for local OCR matching"""
|
||||||
|
|
||||||
|
# We will wrap this in a instruction to return JSON for easier parsing while keeping the user's content
|
||||||
|
final_prompt = f"IMAGE ANALYSIS INSTRUCTIONS:\n{default_prompt}\n\nIMPORTANT: Return ONLY a valid JSON object with the keys: Item, Type, Description, Category, Connector, Size, Color, PartNr, OCR."
|
||||||
|
|
||||||
|
existing = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_extraction_prompt").first()
|
||||||
|
if not existing:
|
||||||
|
db.add(models.SystemSetting(key="ai_extraction_prompt", value=final_prompt))
|
||||||
|
print("Added default AI prompt setting.")
|
||||||
|
|
||||||
|
# Add default backup settings if missing
|
||||||
|
defaults = {
|
||||||
|
"backup_retention_count": "10",
|
||||||
|
"backup_schedule_hour": "3",
|
||||||
|
"backup_schedule_freq_days": "1"
|
||||||
|
}
|
||||||
|
for key, val in defaults.items():
|
||||||
|
if not db.query(models.SystemSetting).filter(models.SystemSetting.key == key).first():
|
||||||
|
db.add(models.SystemSetting(key=key, value=val))
|
||||||
|
print(f"Added default setting: {key}")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_settings()
|
||||||
49
backend/scripts/migrate_v4_v5.py
Normal file
49
backend/scripts/migrate_v4_v5.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
from ..database import db_path
|
||||||
|
from ..logger import log
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
"""
|
||||||
|
Migration script to upgrade the items table from schema v4 to v5.
|
||||||
|
Adds columns: description, connector, size, ocr_text.
|
||||||
|
"""
|
||||||
|
log.info(f"🚀 Starting database migration on {db_path}...")
|
||||||
|
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
log.error(f"❌ Database file not found at {db_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check existing columns
|
||||||
|
cursor.execute("PRAGMA table_info(items)")
|
||||||
|
columns = [row[1] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
new_columns = [
|
||||||
|
("description", "VARCHAR"),
|
||||||
|
("connector", "VARCHAR"),
|
||||||
|
("size", "VARCHAR"),
|
||||||
|
("ocr_text", "TEXT")
|
||||||
|
]
|
||||||
|
|
||||||
|
for col_name, col_type in new_columns:
|
||||||
|
if col_name not in columns:
|
||||||
|
log.info(f"➕ Adding column '{col_name}' to 'items' table...")
|
||||||
|
cursor.execute(f"ALTER TABLE items ADD COLUMN {col_name} {col_type}")
|
||||||
|
else:
|
||||||
|
log.info(f"✔️ Column '{col_name}' already exists.")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
log.info("✅ Migration completed successfully.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"❌ Migration failed: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.9.18",
|
"version": "1.9.19",
|
||||||
"last_build": "2026-04-13-2343",
|
"last_build": "2026-04-13-2343",
|
||||||
"codename": "MobilePolish",
|
"codename": "MobilePolish",
|
||||||
"commit": "1fff658d"
|
"commit": "1fff658d"
|
||||||
|
|||||||
@@ -13,21 +13,12 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
LogOut,
|
LogOut,
|
||||||
Database,
|
|
||||||
History,
|
|
||||||
Lock,
|
|
||||||
Edit2,
|
|
||||||
X,
|
|
||||||
Server,
|
|
||||||
Globe,
|
|
||||||
Wifi,
|
|
||||||
WifiOff,
|
|
||||||
Layers,
|
|
||||||
ChevronDown,
|
|
||||||
Clock,
|
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Download,
|
Download,
|
||||||
RotateCcw
|
RotateCcw,
|
||||||
|
FileText,
|
||||||
|
Upload,
|
||||||
|
Brain
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -61,6 +52,11 @@ export default function AdminPage() {
|
|||||||
const [dbStats, setDbStats] = useState({ backup_count: 0, total_size_bytes: 0 });
|
const [dbStats, setDbStats] = useState({ backup_count: 0, total_size_bytes: 0 });
|
||||||
const [dbSettings, setDbSettings] = useState({ retention_count: 10, schedule_hour: 3, schedule_freq_days: 1 });
|
const [dbSettings, setDbSettings] = useState({ retention_count: 10, schedule_hour: 3, schedule_freq_days: 1 });
|
||||||
const [isBackingUp, setIsBackingUp] = useState(false);
|
const [isBackingUp, setIsBackingUp] = useState(false);
|
||||||
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
|
||||||
|
// AI Prompt State
|
||||||
|
const [aiPrompt, setAiPrompt] = useState("");
|
||||||
|
const [isSavingPrompt, setIsSavingPrompt] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
@@ -83,6 +79,9 @@ export default function AdminPage() {
|
|||||||
setBackups(b);
|
setBackups(b);
|
||||||
setDbStats(s);
|
setDbStats(s);
|
||||||
setDbSettings(st);
|
setDbSettings(st);
|
||||||
|
|
||||||
|
const p = await inventoryApi.getAiPrompt();
|
||||||
|
setAiPrompt(p.value);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error("Failed to load admin data");
|
toast.error("Failed to load admin data");
|
||||||
@@ -213,6 +212,60 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdatePrompt = async () => {
|
||||||
|
setIsSavingPrompt(true);
|
||||||
|
try {
|
||||||
|
await inventoryApi.updateAiPrompt(aiPrompt);
|
||||||
|
toast.success("AI Extraction Prompt updated");
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error("Failed to update AI prompt");
|
||||||
|
} finally {
|
||||||
|
setIsSavingPrompt(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExportDb = async () => {
|
||||||
|
try {
|
||||||
|
const blob = await inventoryApi.exportDb();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `inventory_backup_${new Date().toISOString().split('T')[0]}.db`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
toast.success("Database exported successfully");
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error("Export failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportDb = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!confirm("DANGEROUS: You are about to REPLACE the entire database with this file. All current data will be lost. Proceed?")) {
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsImporting(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const loadingToast = toast.loading("Importing database...");
|
||||||
|
try {
|
||||||
|
await inventoryApi.importDb(formData);
|
||||||
|
toast.success("Database imported! Reloading system...", { id: loadingToast });
|
||||||
|
setTimeout(() => window.location.reload(), 2000);
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error("Import failed: " + (err.response?.data?.detail || err.message), { id: loadingToast });
|
||||||
|
} finally {
|
||||||
|
setIsImporting(false);
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateLdap = async () => {
|
const handleUpdateLdap = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -565,6 +618,48 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* AI Configuration Section */}
|
||||||
|
<section className="glass-card p-6 md:p-8 rounded-[2.5rem] md:rounded-[3rem] space-y-8">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6 px-2">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-3 bg-purple-500/10 rounded-2xl text-purple-500 border border-purple-500/20">
|
||||||
|
<Brain size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-black text-white">AI Vision Intelligence</h2>
|
||||||
|
<p className="text-xs text-slate-500 font-bold mt-1">Configure extraction prompts and heuristic models</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleUpdatePrompt}
|
||||||
|
disabled={isSavingPrompt}
|
||||||
|
className="bg-purple-600 hover:bg-purple-500 text-white font-black text-xs px-6 py-3 rounded-xl shadow-xl shadow-purple-500/20 transition-all active:scale-95 disabled:opacity-50 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isSavingPrompt ? <div className="w-3 h-3 border-2 border-white border-t-transparent animate-spin rounded-full" /> : <Edit2 size={14} />}
|
||||||
|
Save AI Prompt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 px-1">
|
||||||
|
<FileText size={14} className="text-purple-400" />
|
||||||
|
<label className="text-xs font-black text-slate-300">Global Extraction Prompt</label>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={aiPrompt}
|
||||||
|
onChange={(e) => setAiPrompt(e.target.value)}
|
||||||
|
placeholder="Enter the AI system prompt for label extraction..."
|
||||||
|
className="w-full bg-slate-950/80 border border-slate-800 rounded-3xl p-6 text-sm text-slate-300 font-mono leading-relaxed min-h-[300px] outline-none focus:border-purple-500/30 transition-all custom-scrollbar"
|
||||||
|
/>
|
||||||
|
<div className="bg-purple-500/5 border border-purple-500/10 p-5 rounded-2xl">
|
||||||
|
<p className="text-[10px] text-slate-500 leading-relaxed font-bold italic">
|
||||||
|
Tip: Use specific instructions like "Return ONLY valid JSON" to ensure consistent results.
|
||||||
|
Reference fields: <span className="text-purple-400/80">Item, Type, Description, Category, Connector, Size, Color, PartNr, OCR</span>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Category Management Section */}
|
{/* Category Management Section */}
|
||||||
<section className="glass-card p-6 md:p-8 rounded-[2.5rem] md:rounded-[3rem] space-y-8">
|
<section className="glass-card p-6 md:p-8 rounded-[2.5rem] md:rounded-[3rem] space-y-8">
|
||||||
<div className="flex items-center justify-between px-2">
|
<div className="flex items-center justify-between px-2">
|
||||||
@@ -674,18 +769,38 @@ export default function AdminPage() {
|
|||||||
<p className="text-xs text-slate-500 font-bold mt-1">Snapshot management and disaster recovery tools</p>
|
<p className="text-xs text-slate-500 font-bold mt-1">Snapshot management and disaster recovery tools</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={handleCreateBackup}
|
<button
|
||||||
disabled={isBackingUp}
|
onClick={handleExportDb}
|
||||||
className="group flex items-center justify-center gap-2 bg-amber-500/10 hover:bg-amber-500 text-amber-500 hover:text-white font-black text-xs px-6 py-3 rounded-xl transition-all border border-amber-500/20 active:scale-95 disabled:opacity-50"
|
className="flex items-center justify-center gap-2 bg-slate-800 hover:bg-slate-700 text-slate-300 font-black text-xs px-5 py-3 rounded-xl transition-all border border-slate-700 active:scale-95"
|
||||||
>
|
title="Download current database as .db file"
|
||||||
{isBackingUp ? (
|
>
|
||||||
<div className="w-3 h-3 border-2 border-current border-t-transparent animate-spin rounded-full" />
|
<Download size={14} /> Export
|
||||||
) : (
|
</button>
|
||||||
<Download size={14} className="group-hover:-translate-y-0.5 transition-transform" />
|
<label className="flex items-center justify-center gap-2 bg-slate-800 hover:bg-rose-900/20 text-slate-300 hover:text-rose-400 font-black text-xs px-5 py-3 rounded-xl transition-all border border-slate-700 hover:border-rose-500/30 cursor-pointer active:scale-95">
|
||||||
)}
|
<Upload size={14} /> Import
|
||||||
Create Manual Backup
|
<input
|
||||||
</button>
|
type="file"
|
||||||
|
accept=".db,.sqlite,.sqlite3"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleImportDb}
|
||||||
|
disabled={isImporting}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="w-px h-8 bg-slate-800 mx-2" />
|
||||||
|
<button
|
||||||
|
onClick={handleCreateBackup}
|
||||||
|
disabled={isBackingUp}
|
||||||
|
className="group flex items-center justify-center gap-2 bg-amber-500/10 hover:bg-amber-500 text-amber-500 hover:text-white font-black text-xs px-6 py-3 rounded-xl transition-all border border-amber-500/20 active:scale-95 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isBackingUp ? (
|
||||||
|
<div className="w-3 h-3 border-2 border-current border-t-transparent animate-spin rounded-full" />
|
||||||
|
) : (
|
||||||
|
<Download size={14} className="group-hover:-translate-y-0.5 transition-transform" />
|
||||||
|
)}
|
||||||
|
Snapshot
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-3 gap-6">
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export default function Home() {
|
|||||||
const [currentUser, setCurrentUser] = useState<any | null>(null);
|
const [currentUser, setCurrentUser] = useState<any | null>(null);
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
const [categories, setCategories] = useState<any[]>([]);
|
||||||
const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null);
|
const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null);
|
||||||
|
const [boxSearchQuery, setBoxSearchQuery] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!localStorage.getItem('inventory_token')) {
|
if (!localStorage.getItem('inventory_token')) {
|
||||||
@@ -291,6 +292,10 @@ export default function Home() {
|
|||||||
const sn = (item.serial_number || '').toUpperCase();
|
const sn = (item.serial_number || '').toUpperCase();
|
||||||
const name = item.name.toUpperCase();
|
const name = item.name.toUpperCase();
|
||||||
const category = item.category.toUpperCase();
|
const category = item.category.toUpperCase();
|
||||||
|
const ocrKey = (item.ocr_text || '').toUpperCase().replace(/[^A-Z0-9\s/+-]/g, ' ');
|
||||||
|
|
||||||
|
// Priority 0: Exact OCR Key match (Heuristic provided by AI)
|
||||||
|
if (ocrKey && cleanText.includes(ocrKey)) score += 1000;
|
||||||
|
|
||||||
// Priority 1: Serial Number (Absolute match)
|
// Priority 1: Serial Number (Absolute match)
|
||||||
if (sn && cleanText.includes(sn)) score += 500;
|
if (sn && cleanText.includes(sn)) score += 500;
|
||||||
@@ -418,10 +423,13 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
item.name.toLowerCase().includes(query) ||
|
item.name.toLowerCase().includes(query) ||
|
||||||
item.category.toLowerCase().includes(query) ||
|
item.category.toLowerCase().includes(query) ||
|
||||||
(item.specs?.toLowerCase().includes(query) ?? false) ||
|
(item.description?.toLowerCase().includes(query) ?? false) ||
|
||||||
|
(item.connector?.toLowerCase().includes(query) ?? false) ||
|
||||||
|
(item.size?.toLowerCase().includes(query) ?? false) ||
|
||||||
(item.part_number?.toLowerCase().includes(query) ?? false) ||
|
(item.part_number?.toLowerCase().includes(query) ?? false) ||
|
||||||
(item.color?.toLowerCase().includes(query) ?? false) ||
|
(item.color?.toLowerCase().includes(query) ?? false) ||
|
||||||
(item.box_label?.toLowerCase().includes(query) ?? false)
|
(item.box_label?.toLowerCase().includes(query) ?? false) ||
|
||||||
|
(item.ocr_text?.toLowerCase().includes(query) ?? false)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -476,19 +484,26 @@ export default function Home() {
|
|||||||
{isScannerReady && (
|
{isScannerReady && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" />
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" />
|
||||||
<span className="text-[10px] sm:text-xs font-black text-green-500/90 whitespace-nowrap tracking-wider uppercase">
|
<span className="text-[10px] sm:text-xs font-black text-green-500/90 whitespace-nowrap">
|
||||||
Scanner: OK
|
Scanner: OK
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className={`w-1.5 h-1.5 rounded-full ${isOnline ? 'bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]' : 'bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.6)]'}`} />
|
<div className={`w-1.5 h-1.5 rounded-full ${isOnline ? 'bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]' : 'bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.6)]'}`} />
|
||||||
<span className={`text-[10px] sm:text-xs font-black whitespace-nowrap tracking-wider uppercase ${isOnline ? 'text-green-500/90' : 'text-rose-500/90'}`}>
|
<span className={`text-[10px] sm:text-xs font-black whitespace-nowrap ${isOnline ? 'text-green-500/90' : 'text-rose-500/90'}`}>
|
||||||
Sync: {isOnline ? 'Active' : 'Offline'}
|
Sync: {isOnline ? 'Active' : 'Offline'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowBoxManager(true)}
|
||||||
|
className="p-2.5 bg-slate-900/80 border border-slate-800 text-slate-400 rounded-xl hover:text-primary transition-all active:scale-95"
|
||||||
|
title="Box Manager"
|
||||||
|
>
|
||||||
|
<Package size={18} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSync}
|
onClick={handleSync}
|
||||||
disabled={syncing}
|
disabled={syncing}
|
||||||
@@ -553,27 +568,31 @@ export default function Home() {
|
|||||||
|
|
||||||
<div className="w-full h-px bg-slate-800/50 my-2" />
|
<div className="w-full h-px bg-slate-800/50 my-2" />
|
||||||
|
|
||||||
<div className="w-full grid grid-cols-1 gap-4">
|
<div className="w-full grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowOnboarding(true)}
|
onClick={() => setShowOnboarding(true)}
|
||||||
className="w-full h-18 py-4 rounded-[1.5rem] bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center gap-4 group hover:border-indigo-500/50 transition-all font-black text-indigo-400"
|
className="flex flex-col items-center justify-center p-5 rounded-3xl bg-indigo-500/10 border border-indigo-500/20 group hover:border-indigo-500/50 transition-all font-black text-indigo-400 gap-3"
|
||||||
>
|
>
|
||||||
<Sparkles size={24} className="group-hover:scale-125 transition-transform" />
|
<div className="p-3 bg-indigo-500/10 rounded-2xl group-hover:scale-110 transition-transform">
|
||||||
<span className="text-left leading-tight">
|
<Sparkles size={24} />
|
||||||
Add NEW Item<br />
|
</div>
|
||||||
<span className="text-[10px] opacity-60 uppercase tracking-widest font-mono">AI Onboarding</span>
|
<div className="text-center">
|
||||||
</span>
|
<p className="text-sm leading-tight">Add Item</p>
|
||||||
|
<p className="text-[10px] opacity-60 font-mono mt-1">AI Onboarding</p>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowBoxManager(true)}
|
onClick={() => setShowBoxManager(true)}
|
||||||
className="w-full h-18 py-4 rounded-[1.5rem] bg-slate-900/50 border border-slate-800 flex items-center justify-center gap-4 group hover:border-primary/40 transition-all font-black text-slate-300"
|
className="flex flex-col items-center justify-center p-5 rounded-3xl bg-slate-900/50 border border-slate-800 group hover:border-primary/40 transition-all font-black text-slate-300 gap-3"
|
||||||
>
|
>
|
||||||
<Package size={24} className="text-primary group-hover:scale-125 transition-transform" />
|
<div className="p-3 bg-primary/10 rounded-2xl group-hover:scale-110 transition-transform">
|
||||||
<span className="text-left leading-tight">
|
<Package size={24} className="text-primary" />
|
||||||
Manage Boxes<br />
|
</div>
|
||||||
<span className="text-[10px] opacity-60 uppercase tracking-widest font-mono">Box Inventory</span>
|
<div className="text-center">
|
||||||
</span>
|
<p className="text-sm leading-tight">Manage Boxes</p>
|
||||||
|
<p className="text-[10px] opacity-60 font-mono mt-1">Box Inventory</p>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -709,15 +728,54 @@ export default function Home() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div>
|
||||||
<label className="text-xs font-black text-slate-500 ml-1">Specs</label>
|
<label className="text-xs font-black text-slate-500 ml-1">Connectors</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={editedItem.specs || ''}
|
value={editedItem.connector || ''}
|
||||||
onChange={e => setEditedItem({ ...editedItem, specs: e.target.value })}
|
onChange={e => setEditedItem({...editedItem, connector: e.target.value})}
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100"
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100"
|
||||||
|
placeholder="e.g. LC/UPC"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-black text-slate-500 ml-1">Size / Length</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedItem.size || ''}
|
||||||
|
onChange={e => setEditedItem({...editedItem, size: e.target.value})}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100"
|
||||||
|
placeholder="e.g. 5m / 10G"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="text-xs font-black text-slate-500 ml-1">Color</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedItem.color || ''}
|
||||||
|
onChange={e => setEditedItem({...editedItem, color: e.target.value})}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100"
|
||||||
|
placeholder="e.g. Aqua"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="text-xs font-black text-slate-500 ml-1">Description</label>
|
||||||
|
<textarea
|
||||||
|
value={editedItem.description || ''}
|
||||||
|
onChange={e => setEditedItem({ ...editedItem, description: e.target.value })}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100 resize-none h-20"
|
||||||
|
placeholder="Item description..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="text-xs font-black text-slate-500 ml-1">OCR Matching Key (Local Identification)</label>
|
||||||
|
<textarea
|
||||||
|
value={editedItem.ocr_text || ''}
|
||||||
|
onChange={e => setEditedItem({ ...editedItem, ocr_text: e.target.value })}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-[10px] font-mono outline-none text-slate-400 resize-none h-12"
|
||||||
|
placeholder="Paste identification string here..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -861,59 +919,84 @@ export default function Home() {
|
|||||||
{showBoxManager && (
|
{showBoxManager && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/90 backdrop-blur-md animate-in fade-in duration-300">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/90 backdrop-blur-md animate-in fade-in duration-300">
|
||||||
<div className="w-full max-w-2xl bg-slate-900 border border-slate-800 rounded-[2.5rem] shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
<div className="w-full max-w-2xl bg-slate-900 border border-slate-800 rounded-[2.5rem] shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
||||||
<div className="p-8 border-b border-slate-800 flex justify-between items-center shrink-0 bg-slate-900/50">
|
<div className="p-8 pb-4 flex flex-col gap-6 shrink-0 bg-slate-900/50">
|
||||||
<div>
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="text-2xl font-black tracking-tight flex items-center gap-3">
|
<div>
|
||||||
<Package className="text-primary" size={28} />
|
<h3 className="text-2xl font-black tracking-tight flex items-center gap-3">
|
||||||
Box Inventory
|
<Package className="text-primary" size={28} />
|
||||||
</h3>
|
Box Inventory
|
||||||
<p className="text-sm text-slate-500 font-bold mt-1">Manage physical box labels & printing</p>
|
</h3>
|
||||||
|
<p className="text-sm text-slate-500 font-bold mt-1">Manage physical box labels & printing</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => { setShowBoxManager(false); setBoxSearchQuery(''); }} className="p-3 hover:bg-slate-800 rounded-full transition-colors text-slate-400">
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search boxes..."
|
||||||
|
value={boxSearchQuery}
|
||||||
|
onChange={(e) => setBoxSearchQuery(e.target.value)}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3.5 pl-11 pr-4 text-sm focus:border-primary outline-none transition-all placeholder:text-slate-600"
|
||||||
|
/>
|
||||||
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-600" size={18} />
|
||||||
|
{boxSearchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setBoxSearchQuery('')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 p-1.5 hover:bg-slate-800 rounded-lg text-slate-500"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setShowBoxManager(false)} className="p-3 hover:bg-slate-800 rounded-full transition-colors">
|
|
||||||
<X size={24} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
<div className="flex-1 overflow-y-auto p-6 space-y-4 pt-4">
|
||||||
{existingBoxes.length === 0 ? (
|
{existingBoxes.filter(b => b.toLowerCase().includes(boxSearchQuery.toLowerCase())).length === 0 ? (
|
||||||
<div className="py-20 text-center space-y-4 opacity-40">
|
<div className="py-20 text-center space-y-4 opacity-40">
|
||||||
<Package size={48} className="mx-auto" />
|
<Package size={48} className="mx-auto" />
|
||||||
<p className="font-bold">No box labels defined yet.</p>
|
<p className="font-bold">{existingBoxes.length === 0 ? 'No box labels defined yet.' : 'No matching boxes found.'}</p>
|
||||||
<p className="text-xs max-w-xs mx-auto">Associate items with a "Box Label" in their metadata to see them here.</p>
|
<p className="text-xs max-w-xs mx-auto">Associate items with a "Box Label" in their metadata to see them here.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pb-4">
|
||||||
{existingBoxes.map(box => {
|
{existingBoxes
|
||||||
const itemCount = inventory.filter(i => i.box_label === box).length;
|
.filter(box => box.toLowerCase().includes(boxSearchQuery.toLowerCase()))
|
||||||
return (
|
.map(box => {
|
||||||
<div key={box} className="bg-slate-950 border border-slate-800 p-5 rounded-3xl flex flex-col gap-4 group hover:border-primary/50 transition-all">
|
const itemCount = inventory.filter(i => i.box_label === box).length;
|
||||||
<div className="flex-1 min-w-0">
|
return (
|
||||||
<h4 className="text-lg font-black text-white truncate">{box}</h4>
|
<div key={box} className="bg-slate-950/50 border border-slate-800/60 p-5 rounded-3xl flex flex-col gap-4 group hover:border-primary/40 transition-all">
|
||||||
<p className="text-xs text-slate-500 font-bold">{itemCount} items linked</p>
|
<div className="flex-1 min-w-0">
|
||||||
</div>
|
<h4 className="text-lg font-black text-white truncate">{box}</h4>
|
||||||
<div className="flex gap-2 shrink-0">
|
<p className="text-xs text-slate-500 font-bold mt-0.5">{itemCount} items linked</p>
|
||||||
<button
|
</div>
|
||||||
onClick={() => setSelectedBoxLabel(box)}
|
<div className="flex gap-2 shrink-0">
|
||||||
className="flex-1 py-3 bg-primary text-white text-xs font-black rounded-xl flex items-center justify-center gap-2 hover:bg-blue-500 transition-colors shadow-lg shadow-primary/20"
|
<button
|
||||||
>
|
onClick={() => setSelectedBoxLabel(box)}
|
||||||
<Printer size={14} /> Print Label
|
className="flex-1 py-3 bg-primary text-white text-[11px] font-black rounded-xl flex items-center justify-center gap-2 hover:bg-blue-500 transition-colors shadow-lg shadow-primary/10 active:scale-95"
|
||||||
</button>
|
>
|
||||||
<button
|
<Printer size={14} /> Print Label
|
||||||
onClick={() => { setSearchQuery(box.toLowerCase()); setShowBoxManager(false); }}
|
</button>
|
||||||
className="px-4 py-3 bg-slate-900 border border-slate-800 text-slate-400 text-xs font-bold rounded-xl hover:bg-slate-800"
|
<button
|
||||||
>
|
onClick={() => { setSearchQuery(box.toLowerCase()); setShowBoxManager(false); setBoxSearchQuery(''); }}
|
||||||
View
|
className="px-4 py-3 bg-slate-900 border border-slate-800 text-slate-400 text-[11px] font-bold rounded-xl hover:bg-slate-800 active:scale-95"
|
||||||
</button>
|
>
|
||||||
</div>
|
View
|
||||||
</div>
|
</button>
|
||||||
);
|
</div>
|
||||||
})}
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 bg-slate-950/50 border-t border-slate-800 text-center">
|
<div className="p-6 py-4 bg-slate-950/50 border-t border-slate-800 text-center flex items-center justify-center gap-2">
|
||||||
<p className="text-[10px] text-slate-600 font-bold uppercase tracking-widest">TFM aInventory • Box Management Mode</p>
|
<div className="w-1 h-1 rounded-full bg-primary animate-pulse" />
|
||||||
|
<p className="text-[10px] text-slate-500 font-bold font-mono">TFM aInventory • Box management mode</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,8 +73,12 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
|
|||||||
type: extractedData.type ? String(extractedData.type) : null,
|
type: extractedData.type ? String(extractedData.type) : null,
|
||||||
part_number: extractedData.part_number ? String(extractedData.part_number) : null,
|
part_number: extractedData.part_number ? String(extractedData.part_number) : null,
|
||||||
color: extractedData.color ? String(extractedData.color) : null,
|
color: extractedData.color ? String(extractedData.color) : null,
|
||||||
specs: String(extractedData.specs || ""),
|
description: String(extractedData.description || ""),
|
||||||
barcode: String(extractedData.barcode || extractedData.part_number || extractedData.serial_number || `AI-${Date.now()}`),
|
connector: extractedData.connector ? String(extractedData.connector) : null,
|
||||||
|
size: extractedData.size ? String(extractedData.size) : null,
|
||||||
|
ocr_text: extractedData.ocr_text ? String(extractedData.ocr_text) : null,
|
||||||
|
specs: String(extractedData.specs || ""), // Keep specs as extra data if needed
|
||||||
|
barcode: String(extractedData.barcode || extractedData.part_number || `AI-${Date.now()}`),
|
||||||
quantity: parseFloat(String(extractedData.quantity || 1)),
|
quantity: parseFloat(String(extractedData.quantity || 1)),
|
||||||
min_quantity: 1.0,
|
min_quantity: 1.0,
|
||||||
box_label: extractedData.box_label ? String(extractedData.box_label) : null,
|
box_label: extractedData.box_label ? String(extractedData.box_label) : null,
|
||||||
@@ -273,12 +277,46 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-900/80 p-5 rounded-[1.5rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
|
<div className="bg-slate-900/80 p-5 rounded-[1.5rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
|
||||||
<label className="text-xs text-slate-500 font-bold mb-1 block group-focus-within:text-primary transition-colors">Technical Specifications</label>
|
<label className="text-xs text-slate-500 font-bold mb-1 block group-focus-within:text-primary transition-colors">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={extractedData.specs || ''}
|
value={extractedData.description || ''}
|
||||||
onChange={(e) => setExtractedData({...extractedData, specs: e.target.value})}
|
onChange={(e) => setExtractedData({...extractedData, description: e.target.value})}
|
||||||
className="bg-transparent w-full text-sm leading-relaxed outline-none resize-none h-20 text-slate-300"
|
className="bg-transparent w-full text-sm leading-relaxed outline-none resize-none h-16 text-slate-300"
|
||||||
placeholder="Technical details..."
|
placeholder="Product description..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-slate-900/80 p-5 rounded-[1.5rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
|
||||||
|
<label className="text-xs text-slate-500 font-bold mb-1 block group-focus-within:text-primary transition-colors">Connector</label>
|
||||||
|
<input
|
||||||
|
value={extractedData.connector || ''}
|
||||||
|
onChange={(e) => setExtractedData({...extractedData, connector: e.target.value})}
|
||||||
|
className="bg-transparent w-full font-bold outline-none text-slate-200"
|
||||||
|
placeholder="e.g. LC/UPC"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-900/80 p-5 rounded-[1.5rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
|
||||||
|
<label className="text-xs text-slate-500 font-bold mb-1 block group-focus-within:text-primary transition-colors">Size / Length</label>
|
||||||
|
<input
|
||||||
|
value={extractedData.size || ''}
|
||||||
|
onChange={(e) => setExtractedData({...extractedData, size: e.target.value})}
|
||||||
|
className="bg-transparent w-full font-bold outline-none text-slate-200"
|
||||||
|
placeholder="e.g. 5m / 10G"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-900 border-2 border-primary/20 p-5 rounded-[1.5rem] shadow-lg">
|
||||||
|
<div className="flex items-center gap-2 mb-2 text-primary">
|
||||||
|
<Sparkles size={14} />
|
||||||
|
<label className="text-xs font-black uppercase tracking-tighter">AI OCR Matching Key</label>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={extractedData.ocr_text || ''}
|
||||||
|
onChange={(e) => setExtractedData({...extractedData, ocr_text: e.target.value})}
|
||||||
|
className="bg-transparent w-full text-[10px] font-mono leading-tight outline-none resize-none h-12 text-slate-400"
|
||||||
|
placeholder="Heuristic string for local matching..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -220,5 +220,32 @@ export const inventoryApi = {
|
|||||||
updateDbSettings: async (settings: any) => {
|
updateDbSettings: async (settings: any) => {
|
||||||
const res = await axiosInstance.patch('/admin/db/settings', settings);
|
const res = await axiosInstance.patch('/admin/db/settings', settings);
|
||||||
return res.data;
|
return res.data;
|
||||||
|
},
|
||||||
|
exportDb: async () => {
|
||||||
|
const res = await axiosInstance.get('/admin/db/export', { responseType: 'blob' });
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
importDb: async (formData: FormData) => {
|
||||||
|
const res = await axiosInstance.post('/admin/db/import', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
// System Settings
|
||||||
|
getSystemSettings: async () => {
|
||||||
|
// Note: Reusing the existing settings router pattern but adding AI Prompt
|
||||||
|
const res = await axiosInstance.get('/admin/db/settings');
|
||||||
|
// We need another endpoint for general settings or expand the DB one
|
||||||
|
// For now, I'll add a specific fetch for the prompt
|
||||||
|
const promptRes = await axiosInstance.get('/admin/db/settings/prompt');
|
||||||
|
return { ...res.data, ai_extraction_prompt: promptRes.data.value };
|
||||||
|
},
|
||||||
|
getAiPrompt: async () => {
|
||||||
|
const res = await axiosInstance.get('/admin/db/settings/prompt');
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
updateAiPrompt: async (prompt: string) => {
|
||||||
|
const res = await axiosInstance.post('/admin/db/settings/prompt', { value: prompt });
|
||||||
|
return res.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export interface Item {
|
|||||||
labels_data?: string;
|
labels_data?: string;
|
||||||
serial_number?: string;
|
serial_number?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
description?: string;
|
||||||
|
connector?: string;
|
||||||
|
size?: string;
|
||||||
|
ocr_text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PendingOperation {
|
export interface PendingOperation {
|
||||||
@@ -34,8 +38,8 @@ export class InventoryDatabase extends Dexie {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('InventoryDatabase');
|
super('InventoryDatabase');
|
||||||
this.version(4).stores({
|
this.version(5).stores({
|
||||||
items: '++id, barcode, name, category, part_number, color, box_label',
|
items: '++id, barcode, name, category, part_number, color, box_label, ocr_text',
|
||||||
pendingOperations: '++id, barcode, timestamp, synced, uuid'
|
pendingOperations: '++id, barcode, timestamp, synced, uuid'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
7
frontend/public/network.json
Normal file
7
frontend/public/network.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"SERVER_IP": "192.168.84.113",
|
||||||
|
"BACKEND_PORT": 8916,
|
||||||
|
"BACKEND_SSL_PORT": 8918,
|
||||||
|
"FRONTEND_PORT": 8917,
|
||||||
|
"FRONTEND_SSL_PORT": 8919
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user