Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6feea5ee6 | ||
|
|
3d41579ad3 | ||
|
|
d3a558352d | ||
|
|
aaf33880c4 | ||
|
|
b396b6d3c9 | ||
|
|
3ff9b70e4c |
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
project/web/index/new/config/local.php
|
||||
project/web/index/new/logs/*.log
|
||||
project/web/index/new/logs/*.txt
|
||||
project/sql/private/
|
||||
9
AGENTS.local.md.example
Normal file
9
AGENTS.local.md.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# AGENTS.local.md (optional example)
|
||||
|
||||
Use this file for project-specific rules only.
|
||||
|
||||
## Example
|
||||
- App dir: `project/`
|
||||
- Deploy target: staging Kubernetes cluster
|
||||
- Extra rule: DB changes require `work/artifacts/<id>/db.md`
|
||||
- Extra rule: `scripts/verify.local.sh` must run smoke tests
|
||||
47
AGENTS.md
47
AGENTS.md
@@ -1,19 +1,56 @@
|
||||
# AGENTS.md — Entry point del framework
|
||||
# AGENTS.md — Entry point del template ARNES
|
||||
|
||||
Este repositorio es un **template genérico** para cualquier proyecto nuevo o en curso.
|
||||
|
||||
## Arranque obligatorio
|
||||
1. Leer `work/current.md`.
|
||||
2. Leer `backlog/features.json` y seleccionar **una** feature `pending`.
|
||||
3. Ejecutar `./scripts/verify.sh`.
|
||||
4. Seguir `harness/workflow.stages.yml` y `harness/agents.matrix.yml`.
|
||||
1. Usar ARNES dentro de un repo de proyecto real, no dentro del repo fuente de ARNES.
|
||||
2. Si es primer uso en proyecto: ejecutar `./scripts/start.sh`.
|
||||
3. Leer `work/current.md`.
|
||||
4. Leer `backlog/features.json` y seleccionar **una** feature `pending`.
|
||||
5. Ejecutar `./scripts/verify.sh`.
|
||||
6. Mostrar estado runtime: `python3 scripts/agent_status.py show`.
|
||||
7. Seguir `harness/workflow.stages.yml` y `harness/agents.matrix.yml`.
|
||||
|
||||
## Ticket creation policy
|
||||
- Tickets are created by `leader` (or `triager`) only.
|
||||
- Use: `python3 scripts/new_ticket.py`
|
||||
- Ticket language: **English caveman**.
|
||||
- Internal orders/handoffs: **English caveman**.
|
||||
|
||||
## Estado visible del arnés
|
||||
- Estado runtime: `work/runtime-status.json`.
|
||||
- Mostrar: `python3 scripts/agent_status.py show`.
|
||||
- Actualizar transición:
|
||||
- `python3 scripts/agent_status.py set --feature-id F-123 --stage build --agent implementer --action "Implementando" --state running --next-agent reviewer --waiting-for "work/artifacts/F-123/implementer.md"`
|
||||
- Cerrar/idle:
|
||||
- `python3 scripts/agent_status.py reset`
|
||||
|
||||
## Reglas duras
|
||||
- Una sola feature en `in_progress`.
|
||||
- Ningún agente pasa código por chat: todo va a `work/artifacts/<feature_id>/`.
|
||||
- `implementer` nunca marca `done`.
|
||||
- `done` requiere gates aprobados: `reviewer`, `security`, `qa`.
|
||||
- `done` requiere evidencia de `documenter`: `work/artifacts/<feature_id>/documenter.md`.
|
||||
- `done` requiere publish final con commit+push del ticket: `work/artifacts/<feature_id>/publish.json`.
|
||||
- Si `verify.sh` falla, no se cierra la feature.
|
||||
|
||||
## Modelo por tarea (token-aware)
|
||||
- Use smallest model that fits task.
|
||||
- Routing config: `harness/models.profiles.yml`
|
||||
- Rules: `harness/policies/model-routing.md`
|
||||
|
||||
## Git publish por ticket
|
||||
- Al terminar una feature/ticket, `leader` debe ejecutar:
|
||||
- `python3 scripts/publish_ticket.py --feature-id F-123`
|
||||
- Esto crea commit + push del ticket y deja evidencia en `work/artifacts/<feature_id>/publish.json`.
|
||||
|
||||
## Extensión por proyecto (overlay)
|
||||
- Opcional: `AGENTS.local.md` para reglas específicas del proyecto actual.
|
||||
- Opcional: `scripts/verify.local.sh` para checks de dominio.
|
||||
- El core de ARNES debe seguir siendo agnóstico.
|
||||
|
||||
## Reentrada (context loss)
|
||||
- Releer `work/current.md` y artefactos de la feature activa.
|
||||
- Ejecutar `./scripts/verify.sh`.
|
||||
- Mostrar `python3 scripts/agent_status.py show`.
|
||||
- Continuar desde “Próximo paso”.
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
# CHECKPOINTS
|
||||
|
||||
## C1 — Estructura
|
||||
- [ ] Existe `harness/`, `spec/`, `backlog/`, `work/`, `scripts/`, `platforms/`.
|
||||
- [ ] Existe `project/`, `harness/`, `spec/`, `backlog/`, `work/`, `scripts/`, `platforms/`.
|
||||
- [ ] `project/README.md` existe como placeholder mínimo.
|
||||
|
||||
## C2 — Estado
|
||||
- [ ] Máximo una feature en `in_progress`.
|
||||
- [ ] Estados válidos en backlog.
|
||||
- [ ] Tipos de ticket válidos en backlog.
|
||||
- [ ] `work/runtime-status.json` válido y visible con `scripts/agent_status.py`.
|
||||
|
||||
## C3 — Gates
|
||||
- [ ] Toda feature `done` tiene `reviewer.json` aprobado.
|
||||
- [ ] Toda feature `done` tiene `security.json` aprobado.
|
||||
- [ ] Toda feature `done` tiene `qa.json` aprobado.
|
||||
- [ ] Toda feature `done` tiene `leader-close.json` válido.
|
||||
- [ ] Toda feature `done` tiene `documenter.md`.
|
||||
- [ ] Toda feature `done` tiene `publish.json` con commit+push del ticket.
|
||||
|
||||
## C4 — Verificación
|
||||
- [ ] `./scripts/verify.sh` termina en OK.
|
||||
|
||||
41
HOWTO-FEATURE.md
Normal file
41
HOWTO-FEATURE.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# HOWTO-FEATURE — Crear una feature con SDD y BDD
|
||||
|
||||
## Flujo corto
|
||||
1. Crear ticket en backlog (`python3 scripts/new_ticket.py`)
|
||||
2. `design` (architect)
|
||||
3. `build` (implementer)
|
||||
4. `review/security/qa`
|
||||
5. `documentation_gate`
|
||||
6. `close`
|
||||
7. `publish` (`python3 scripts/publish_ticket.py --feature-id F-001`)
|
||||
|
||||
## Artefactos esperados
|
||||
- `work/artifacts/<feature_id>/triage.md` (opcional)
|
||||
- `work/artifacts/<feature_id>/architect.md` (opcional)
|
||||
- `work/artifacts/<feature_id>/implementer.md`
|
||||
- `work/artifacts/<feature_id>/reviewer.json`
|
||||
- `work/artifacts/<feature_id>/security.json`
|
||||
- `work/artifacts/<feature_id>/qa.json`
|
||||
- `work/artifacts/<feature_id>/documenter.md`
|
||||
- `work/artifacts/<feature_id>/leader-close.json`
|
||||
- `work/artifacts/<feature_id>/publish.json`
|
||||
|
||||
## Ticket style
|
||||
- English caveman
|
||||
- short title
|
||||
- short acceptance bullets
|
||||
- clear scope in/out
|
||||
|
||||
## BDD notes
|
||||
- Put `.feature` files in `spec/bdd/features/`
|
||||
- Put steps in `features/steps/`
|
||||
- Use tags like `@F-001`, `@smoke`, `@regression`
|
||||
|
||||
## Close rule
|
||||
Feature can be `done` only if:
|
||||
- review approved
|
||||
- security approved
|
||||
- qa approved
|
||||
- documenter evidence exists
|
||||
- publish evidence exists (`publish.json`)
|
||||
- `./scripts/verify.sh` is green
|
||||
183
HOWTO.md
183
HOWTO.md
@@ -1,145 +1,78 @@
|
||||
# HOWTO — Cómo usar ARNES Framework
|
||||
# HOWTO (breve) — iniciar ARNES en proyecto nuevo o ya empezado
|
||||
|
||||
Guía rápida para arrancar proyectos nuevos usando este framework.
|
||||
|
||||
---
|
||||
|
||||
## Fórmula base (siempre igual)
|
||||
|
||||
1. **Crear repo nuevo**
|
||||
2. **Copiar ARNES Framework dentro del repo**
|
||||
3. **Configurar spec + backlog**
|
||||
4. **Ejecutar verificación**
|
||||
5. **Empezar implementación por features (una a la vez)**
|
||||
|
||||
---
|
||||
|
||||
## 1) Crear repo
|
||||
## 1) Proyecto nuevo (greenfield)
|
||||
|
||||
```bash
|
||||
mkdir mi-proyecto
|
||||
cd mi-proyecto
|
||||
mkdir mi-proyecto && cd mi-proyecto
|
||||
git init
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2) Copiar framework
|
||||
|
||||
Desde tu copia local de ARNES:
|
||||
|
||||
```bash
|
||||
cp -R /ruta/a/arnes/* .
|
||||
cp -R /ruta/a/arnes/.[!.]* . 2>/dev/null || true
|
||||
```
|
||||
|
||||
> Si usas plantilla remota, clónala y copia su contenido al repo nuevo.
|
||||
|
||||
---
|
||||
|
||||
## 3) Personalizar proyecto
|
||||
|
||||
Edita mínimo:
|
||||
|
||||
- `README.md` (contexto del proyecto)
|
||||
- `spec/product.md` (qué construir)
|
||||
- `spec/tech.md` (stack y límites técnicos)
|
||||
- `spec/acceptance.md` (criterios de aceptación)
|
||||
- `backlog/features.json` (features iniciales en `pending`)
|
||||
- `harness/agents.matrix.yml` (roles/permisos)
|
||||
- `harness/workflow.stages.yml` (flujo y gates)
|
||||
|
||||
---
|
||||
|
||||
## 4) Elegir plataforma (pi.dev u opencode)
|
||||
|
||||
Usa el adaptador correspondiente:
|
||||
|
||||
- `platforms/pi/`
|
||||
- `platforms/opencode/`
|
||||
|
||||
El núcleo del framework no cambia; solo cambian prompts/hooks/permisos de plataforma.
|
||||
|
||||
---
|
||||
|
||||
## 5) Inicializar estado de trabajo
|
||||
|
||||
Verifica que existan y estén limpios:
|
||||
|
||||
- `work/current.md`
|
||||
- `work/history.md`
|
||||
- `work/artifacts/`
|
||||
|
||||
Pon solo **1 feature activa** (`in_progress`) como máximo.
|
||||
|
||||
---
|
||||
|
||||
## 6) Ejecutar verificación inicial
|
||||
|
||||
```bash
|
||||
# instalar/copiAR ARNES dentro de este repo de proyecto
|
||||
/path/to/arnes/scripts/install_into_repo.sh .
|
||||
./scripts/start.sh
|
||||
./scripts/verify.sh
|
||||
python3 scripts/agent_status.py show
|
||||
```
|
||||
|
||||
Si falla, **no empezar implementación** hasta dejar todo en verde.
|
||||
Después:
|
||||
- Mete tu código dentro de `project/` (o indica otra ruta en el wizard).
|
||||
- Edita `backlog/features.json` (`project`, `description`).
|
||||
- Crea tu primera feature `pending` (puedes usar `starter-pack/backlog.features.bootstrap.json`).
|
||||
- Empieza el ciclo SDD (una feature a la vez).
|
||||
|
||||
---
|
||||
|
||||
## 7) Ciclo operativo por feature
|
||||
## 2) Proyecto ya empezado (brownfield)
|
||||
|
||||
Orden obligatorio:
|
||||
Copia al repo existente solo el core ARNES y coloca el código real en `project/` (o usa otra ruta al lanzar el wizard). Recomendado:
|
||||
|
||||
1. `leader` orquesta
|
||||
2. `architect` define/ajusta diseño
|
||||
3. `implementer` implementa + tests
|
||||
4. `reviewer` gate técnico
|
||||
5. `security` gate seguridad
|
||||
6. `qa` gate funcional
|
||||
7. `leader` cierra si todo está aprobado
|
||||
```bash
|
||||
/path/to/arnes/scripts/install_into_repo.sh .
|
||||
```
|
||||
|
||||
Reglas clave:
|
||||
- una feature a la vez
|
||||
- evidencia en disco (`work/artifacts/<feature>/...`)
|
||||
- nadie marca `done` si falta un gate
|
||||
Contenido core:
|
||||
- `harness/`
|
||||
- `spec/`
|
||||
- `backlog/`
|
||||
- `work/`
|
||||
- `scripts/`
|
||||
- `platforms/`
|
||||
- `AGENTS.md`, `CHECKPOINTS.md`
|
||||
|
||||
Luego ejecuta:
|
||||
|
||||
```bash
|
||||
./scripts/start.sh
|
||||
./scripts/verify.sh
|
||||
python3 scripts/agent_status.py show
|
||||
```
|
||||
|
||||
Y añade checks del dominio en:
|
||||
- `scripts/verify.local.sh` (opcional)
|
||||
|
||||
---
|
||||
|
||||
## 8) Cierre de feature
|
||||
Crear ticket nuevo (leader/triager, EN caveman):
|
||||
```bash
|
||||
python3 scripts/new_ticket.py
|
||||
```
|
||||
|
||||
Antes de pasar a `done`:
|
||||
Tipos soportados:
|
||||
- `feature`
|
||||
- `fix`
|
||||
- `bug`
|
||||
- `chore`
|
||||
|
||||
- `verify.sh` en verde
|
||||
- review aprobado
|
||||
- security aprobado
|
||||
- qa aprobado
|
||||
- resumen en `work/history.md`
|
||||
Al final del ticket:
|
||||
```bash
|
||||
python3 scripts/publish_ticket.py --feature-id F-001
|
||||
```
|
||||
|
||||
---
|
||||
Modelo por tarea:
|
||||
- Config base en `harness/models.profiles.yml`
|
||||
- Reglas en `harness/policies/model-routing.md`
|
||||
|
||||
## 9) Manejo de pérdida de contexto (memoria)
|
||||
|
||||
Si una sesión se corta:
|
||||
|
||||
1. leer `work/current.md`
|
||||
2. revisar `backlog/features.json`
|
||||
3. abrir artefactos de la feature activa
|
||||
4. ejecutar `./scripts/verify.sh`
|
||||
5. continuar desde “Próximo paso”
|
||||
|
||||
---
|
||||
|
||||
## 10) Checklist rápido de arranque
|
||||
|
||||
- [ ] Repo creado
|
||||
- [ ] Framework copiado
|
||||
- [ ] Specs escritas
|
||||
- [ ] Backlog definido
|
||||
- [ ] Matriz de agentes configurada
|
||||
- [ ] Workflow de stages configurado
|
||||
- [ ] Verificación inicial OK
|
||||
- [ ] Primera feature en `pending`
|
||||
|
||||
---
|
||||
|
||||
## Comando mental (resumen)
|
||||
|
||||
**Crear repo → copiar framework → definir spec/backlog → verificar → ejecutar pipeline de 6 agentes con gates obligatorios.**
|
||||
## Reglas operativas mínimas
|
||||
- Máximo una feature en `in_progress`.
|
||||
- `done` requiere gates `review/security/qa` aprobados.
|
||||
- `done` requiere publish final con commit+push del ticket.
|
||||
- Evidencia siempre en `work/artifacts/<feature_id>/`.
|
||||
- Si `verify.sh` falla, no se cierra la feature.
|
||||
|
||||
32
Makefile
Normal file
32
Makefile
Normal file
@@ -0,0 +1,32 @@
|
||||
.PHONY: verify start ticket publish install clean help
|
||||
|
||||
verify:
|
||||
./scripts/verify.sh
|
||||
|
||||
start:
|
||||
./scripts/start.sh
|
||||
|
||||
ticket:
|
||||
python3 scripts/new_ticket.py
|
||||
|
||||
publish:
|
||||
@test -n "$(FEATURE_ID)" || (echo "Use: make publish FEATURE_ID=F-001" && exit 1)
|
||||
python3 scripts/publish_ticket.py --feature-id $(FEATURE_ID)
|
||||
|
||||
install:
|
||||
@test -n "$(TARGET)" || (echo "Use: make install TARGET=/path/to/project-repo" && exit 1)
|
||||
./scripts/install_into_repo.sh $(TARGET)
|
||||
|
||||
clean:
|
||||
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
|
||||
help:
|
||||
@echo "ARNES template - commands:"
|
||||
@echo ""
|
||||
@echo " make verify - Verify harness core"
|
||||
@echo " make start - First-run wizard"
|
||||
@echo " make ticket - Create ticket (EN caveman)"
|
||||
@echo " make publish FEATURE_ID=.. - Commit and push one ticket"
|
||||
@echo " make install TARGET=.. - Install ARNES into external repo"
|
||||
@echo " make clean - Clean cache files"
|
||||
150
README.md
150
README.md
@@ -1,6 +1,9 @@
|
||||
# ARNES Framework (agnóstico) — Diseño v0.1
|
||||
|
||||
Framework para construir aplicaciones con agentes autónomos, con control estricto de calidad, seguridad y trazabilidad.
|
||||
|
||||
Convención recomendada: el código real del proyecto vive dentro de `project/`.
|
||||
Cada proyecto real debe vivir en **su propio repo git**, distinto del repo fuente de ARNES.
|
||||
Compatible por diseño con **pi.dev** y **opencode** mediante adaptadores.
|
||||
|
||||
---
|
||||
@@ -25,43 +28,49 @@ Permitir que agentes implementen features de forma autónoma **sin perder contro
|
||||
|
||||
---
|
||||
|
||||
## Matriz de agentes (6)
|
||||
## Matriz de agentes (8)
|
||||
|
||||
1. **leader**
|
||||
- Orquesta etapas y handoffs.
|
||||
- No implementa código de producto.
|
||||
- Da órdenes internas en English caveman.
|
||||
|
||||
2. **architect**
|
||||
2. **triager**
|
||||
- Convierte requests en tickets claros.
|
||||
- Escribe tickets en English caveman.
|
||||
|
||||
3. **architect**
|
||||
- Define/ajusta diseño técnico y contratos.
|
||||
- Puede editar documentación y diseño.
|
||||
|
||||
3. **implementer**
|
||||
- Implementa una sola feature + tests.
|
||||
4. **implementer**
|
||||
- Implementa feature + tests.
|
||||
- No puede aprobar ni cerrar.
|
||||
|
||||
4. **reviewer**
|
||||
- Revisión técnica vs arquitectura/convenios.
|
||||
- No edita código, solo aprueba/rechaza.
|
||||
5. **reviewer**
|
||||
- Gate técnico.
|
||||
|
||||
5. **security**
|
||||
- Gate de seguridad: secretos, dependencias, SAST básico, hardening checks.
|
||||
- No edita código.
|
||||
6. **security**
|
||||
- Gate de seguridad.
|
||||
|
||||
6. **qa**
|
||||
- Gate de calidad funcional: aceptación, integración/E2E, regresión.
|
||||
- No edita código.
|
||||
7. **qa**
|
||||
- Gate funcional.
|
||||
|
||||
8. **documenter**
|
||||
- Documenta fix/feature/bug y actualiza docs.
|
||||
|
||||
---
|
||||
|
||||
## Flujo de trabajo (pipeline)
|
||||
|
||||
1. `intake` (leader)
|
||||
2. `design` (architect)
|
||||
3. `build` (implementer)
|
||||
4. `review_gate` (reviewer) ✅
|
||||
5. `security_gate` (security) ✅
|
||||
6. `qa_gate` (qa) ✅
|
||||
7. `close` (leader)
|
||||
1. `triage_translate` (leader/triager)
|
||||
2. `intake` (leader)
|
||||
3. `design` (architect)
|
||||
4. `build` (implementer)
|
||||
5. `review_gate` (reviewer) ✅
|
||||
6. `security_gate` (security) ✅
|
||||
7. `qa_gate` (qa) ✅
|
||||
8. `documentation_gate` (documenter) ✅
|
||||
9. `close` (leader)
|
||||
10. `publish` (leader) ✅
|
||||
|
||||
**Regla:** no hay `done` si cualquier gate falla.
|
||||
|
||||
@@ -77,9 +86,11 @@ Permitir que agentes implementen features de forma autónoma **sin perder contro
|
||||
### Evidencia obligatoria por etapa
|
||||
Cada agente escribe artefactos en disco:
|
||||
- `work/artifacts/<feature>/implementer.md`
|
||||
- `work/artifacts/<feature>/reviewer.md`
|
||||
- `work/artifacts/<feature>/security.md`
|
||||
- `work/artifacts/<feature>/qa.md`
|
||||
- `work/artifacts/<feature>/reviewer.json`
|
||||
- `work/artifacts/<feature>/security.json`
|
||||
- `work/artifacts/<feature>/qa.json`
|
||||
- `work/artifacts/<feature>/leader-close.json`
|
||||
- `work/artifacts/<feature>/publish.json`
|
||||
|
||||
Respuesta de agente siempre: `done -> <ruta>` o `blocked -> <ruta>`.
|
||||
|
||||
@@ -98,33 +109,59 @@ Respuesta de agente siempre: `done -> <ruta>` o `blocked -> <ruta>`.
|
||||
|
||||
```text
|
||||
.
|
||||
├── project/ # código real del proyecto
|
||||
│ └── README.md
|
||||
├── README.md
|
||||
├── AGENTS.md
|
||||
├── CHECKPOINTS.md
|
||||
├── harness/
|
||||
│ ├── agents.matrix.yml
|
||||
│ ├── workflow.stages.yml
|
||||
│ ├── models.profiles.yml
|
||||
│ ├── policies/
|
||||
│ │ ├── security.md
|
||||
│ │ ├── quality.md
|
||||
│ │ └── governance.md
|
||||
│ └── contracts/
|
||||
│ ├── handoff.md
|
||||
│ └── evidence.schema.json
|
||||
├── spec/
|
||||
│ ├── product.md
|
||||
│ ├── tech.md
|
||||
│ └── acceptance.md
|
||||
│ ├── acceptance.md
|
||||
│ ├── sdd/
|
||||
│ └── bdd/
|
||||
├── backlog/
|
||||
│ └── features.json
|
||||
├── work/
|
||||
│ ├── current.md
|
||||
│ ├── history.md
|
||||
│ ├── runtime-status.json
|
||||
│ └── artifacts/
|
||||
└── scripts/
|
||||
└── verify.sh
|
||||
├── scripts/
|
||||
│ ├── start.sh
|
||||
│ ├── new_ticket.py
|
||||
│ ├── agent_status.py
|
||||
│ └── verify.sh
|
||||
├── defaults/
|
||||
│ └── flask-skeleton/
|
||||
└── platforms/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estado runtime visible
|
||||
|
||||
- Estado en tiempo real: `work/runtime-status.json`
|
||||
- CLI: `python3 scripts/agent_status.py show|set|reset`
|
||||
|
||||
## Overlays por proyecto (sin contaminar el core)
|
||||
|
||||
- Reglas locales: `AGENTS.local.md` (opcional)
|
||||
- Checks locales: `scripts/verify.local.sh` (opcional)
|
||||
- El template base sigue agnóstico.
|
||||
|
||||
## Lenguaje y modelos
|
||||
|
||||
- Política de lenguaje: `harness/policies/language.md` (English caveman interno)
|
||||
- Routing de modelos: `harness/models.profiles.yml`
|
||||
- Reglas de routing: `harness/policies/model-routing.md`
|
||||
|
||||
## Manejo de pérdidas de memoria (context loss)
|
||||
|
||||
Sí: el framework está diseñado para eso.
|
||||
@@ -161,10 +198,47 @@ El núcleo no cambia; solo el adaptador.
|
||||
|
||||
---
|
||||
|
||||
## Inicio rápido
|
||||
|
||||
- Instalar ARNES en repo externo: `./scripts/install_into_repo.sh /path/to/project-repo`
|
||||
- Ejecuta wizard: `./scripts/start.sh`
|
||||
- Crear ticket: `python3 scripts/new_ticket.py`
|
||||
- Publicar ticket: `python3 scripts/publish_ticket.py --feature-id F-001`
|
||||
- Guía breve: `HOWTO.md`
|
||||
- Starter pack: `starter-pack/README.md`
|
||||
- Adaptación del template: `TEMPLATE.md`
|
||||
- Índice de docs: `docs/README.md`
|
||||
- Layout del repo: `docs/repository-layout.md`
|
||||
- Referencia de scripts: `docs/scripts-reference.md`
|
||||
- Manual Skeleton (uso + mejoras): `docs/skeleton-manual.md`
|
||||
|
||||
## Tipos de tarea / ticket
|
||||
|
||||
`python3 scripts/new_ticket.py` soporta estos tipos:
|
||||
|
||||
- `feature`: nueva capacidad
|
||||
- `fix`: corrección de comportamiento roto
|
||||
- `bug`: incidencia reportada o defecto claro
|
||||
- `chore`: trabajo interno, refactor, setup, mantenimiento
|
||||
|
||||
Además guarda campos estructurados:
|
||||
- `problem`
|
||||
- `goal`
|
||||
- `scope_in`
|
||||
- `scope_out`
|
||||
- `priority`
|
||||
- `risk`
|
||||
- `acceptance`
|
||||
|
||||
Convención recomendada:
|
||||
- usar `feature` para trabajo nuevo visible
|
||||
- usar `fix` o `bug` para reparación
|
||||
- usar `chore` para cambios internos sin valor funcional directo
|
||||
|
||||
## Próximos pasos sugeridos
|
||||
|
||||
1. Definir `agents.matrix.yml` completo (permisos exactos por rutas).
|
||||
2. Definir `workflow.stages.yml` con transiciones válidas.
|
||||
3. Diseñar `features.json` con estados y criterios de aceptación.
|
||||
4. Especificar `scripts/verify.sh` (lint/test/security/qa gates).
|
||||
5. Crear adaptadores `platforms/pi` y `platforms/opencode`.
|
||||
1. Instalar/copiar ARNES en un repo de proyecto real distinto del repo fuente.
|
||||
2. Definir el backlog inicial del proyecto real.
|
||||
3. Configurar overlay opcional (`AGENTS.local.md`, `scripts/verify.local.sh`).
|
||||
4. Ejecutar `./scripts/verify.sh` y `python3 scripts/agent_status.py show`.
|
||||
5. Empezar la primera feature `pending` con pipeline completo y terminar con commit+push del ticket.
|
||||
|
||||
37
TEMPLATE.md
Normal file
37
TEMPLATE.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# TEMPLATE.md — Cómo adaptar ARNES a cualquier proyecto
|
||||
|
||||
## 1) Clonar y renombrar contexto
|
||||
- Pon el código real dentro de `project/` (o elige otra ruta en `./scripts/start.sh`).
|
||||
- Ajusta `backlog/features.json` (`project`, `description`).
|
||||
- Crea primeras features reales en `features[]`.
|
||||
|
||||
## 2) Reglas específicas (sin tocar core)
|
||||
- Opcional: crea `AGENTS.local.md` con reglas del dominio.
|
||||
- Opcional: crea `scripts/verify.local.sh` con checks propios del stack.
|
||||
- Mantén tickets y órdenes internas en English caveman (`harness/policies/language.md`).
|
||||
- Usa tipos de ticket consistentes: `feature`, `fix`, `bug`, `chore`.
|
||||
- Ajusta routing de modelos por rol/tarea en `harness/models.profiles.yml`.
|
||||
|
||||
## 3) Flujo estándar
|
||||
0. Instalar ARNES en repo externo: `./scripts/install_into_repo.sh /path/to/project-repo`
|
||||
1. `./scripts/start.sh` (primer uso)
|
||||
2. `python3 scripts/new_ticket.py` (leader/triager)
|
||||
3. `python3 scripts/agent_status.py show`
|
||||
4. Seleccionar 1 feature `pending` y pasarla a `in_progress`
|
||||
5. Implementar con artefactos en `work/artifacts/<feature_id>/`
|
||||
6. Cerrar con gates `review/security/qa` + `documenter` aprobados
|
||||
7. Publicar ticket: `python3 scripts/publish_ticket.py --feature-id F-001`
|
||||
8. `python3 scripts/agent_status.py reset`
|
||||
|
||||
## 4) Contrato de cierre
|
||||
- `status=done` exige:
|
||||
- `reviewer.json` APPROVED
|
||||
- `security.json` APPROVED
|
||||
- `qa.json` APPROVED
|
||||
- `leader-close.json` APPROVED
|
||||
- `publish.json` PUBLISHED
|
||||
- `./scripts/verify.sh` OK
|
||||
|
||||
## 5) Principio de template
|
||||
- El core ARNES es agnóstico.
|
||||
- Todo lo específico de proyecto vive en overlays (`AGENTS.local.md`, `verify.local.sh`, docs propias).
|
||||
@@ -1,24 +1,156 @@
|
||||
{
|
||||
"project": "nuevo-proyecto",
|
||||
"description": "Backlog inicial del proyecto",
|
||||
"project": "template-project",
|
||||
"description": "Template ARNES agnóstico para cualquier proyecto",
|
||||
"rules": {
|
||||
"one_feature_at_a_time": true,
|
||||
"require_review_gate": true,
|
||||
"require_security_gate": true,
|
||||
"require_qa_gate": true,
|
||||
"valid_status": ["pending", "in_progress", "blocked", "done"]
|
||||
"valid_status": [
|
||||
"pending",
|
||||
"in_progress",
|
||||
"blocked",
|
||||
"done"
|
||||
],
|
||||
"valid_types": [
|
||||
"feature",
|
||||
"fix",
|
||||
"bug",
|
||||
"chore"
|
||||
]
|
||||
},
|
||||
"template_feature_schema": {
|
||||
"id": "F-001",
|
||||
"type": "feature",
|
||||
"title": "Short ticket title",
|
||||
"problem": "Need change",
|
||||
"goal": "Make flow better",
|
||||
"scope_in": [
|
||||
"Core flow"
|
||||
],
|
||||
"scope_out": [
|
||||
"No redesign"
|
||||
],
|
||||
"priority": "med",
|
||||
"risk": "low",
|
||||
"description": "Problem: ... Goal: ... Scope IN: ... Scope OUT: ... Type: ... Priority: ... Risk: ...",
|
||||
"acceptance": [
|
||||
"Flow works end to end",
|
||||
"No break old behavior"
|
||||
],
|
||||
"status": "pending",
|
||||
"created_at": "YYYY-MM-DD",
|
||||
"gates": {
|
||||
"review": false,
|
||||
"security": false,
|
||||
"qa": false
|
||||
}
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"id": "F-001",
|
||||
"title": "Definir estructura inicial",
|
||||
"description": "Bootstrap del proyecto con estructura base.",
|
||||
"acceptance": [
|
||||
"Estructura base creada",
|
||||
"Tests o checks iniciales definidos",
|
||||
"Artefactos de gate configurados"
|
||||
"type": "feature",
|
||||
"title": "Document and move legacy PHP app into ARNES project layout",
|
||||
"problem": "Legacy PHP app lives in temporary folder and has no ARNES design record",
|
||||
"goal": "Create SDD record and move code and SQL into stable project layout",
|
||||
"scope_in": [
|
||||
"SDD docs",
|
||||
"ADR for layout",
|
||||
"move project/new to project/web/index/new",
|
||||
"move SQL dump to project/sql"
|
||||
],
|
||||
"status": "pending"
|
||||
"scope_out": [
|
||||
"No functional refactor",
|
||||
"No production deploy",
|
||||
"No OpenAI or auth rewrite yet"
|
||||
],
|
||||
"priority": "high",
|
||||
"risk": "med",
|
||||
"description": "Problem: Legacy PHP app lives in temporary folder and has no ARNES design record. Goal: Create SDD record and move code and SQL into stable project layout. Scope IN: SDD docs, ADR for layout, move project/new to project/web/index/new, move SQL dump to project/sql. Scope OUT: No functional refactor, No production deploy, No OpenAI or auth rewrite yet. Type: feature. Priority: high. Risk: med.",
|
||||
"acceptance": [
|
||||
"SDD docs exist and explain current legacy app structure",
|
||||
"ADR records why code moves under project/web and SQL under project/sql",
|
||||
"Legacy code is moved with same contents and no file loss",
|
||||
"SQL dump is kept as local development baseline in project/sql",
|
||||
"verify.sh is green"
|
||||
],
|
||||
"status": "blocked",
|
||||
"created_at": "2026-05-25",
|
||||
"gates": {
|
||||
"review": false,
|
||||
"security": false,
|
||||
"qa": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "F-002",
|
||||
"type": "fix",
|
||||
"title": "Remove secrets and externalize config",
|
||||
"problem": "Secrets live in repo and prod URLs live in code",
|
||||
"goal": "Move secrets and config out of source files",
|
||||
"scope_in": [
|
||||
"config loader",
|
||||
"replace hardcoded DB and OpenAI values",
|
||||
"centralize base URLs and external endpoints",
|
||||
"setup docs"
|
||||
],
|
||||
"scope_out": [
|
||||
"No business logic refactor",
|
||||
"No deploy automation",
|
||||
"No auth redesign"
|
||||
],
|
||||
"priority": "high",
|
||||
"risk": "high",
|
||||
"description": "Problem: Secrets live in repo and prod URLs live in code. Goal: Move secrets and config out of source files. Scope IN: config loader, replace hardcoded DB and OpenAI values, centralize base URLs and external endpoints, setup docs. Scope OUT: No business logic refactor, No deploy automation, No auth redesign. Type: fix. Priority: high. Risk: high.",
|
||||
"acceptance": [
|
||||
"No hard-coded API or DB secrets stay in versioned PHP files",
|
||||
"Config values load from one local config source",
|
||||
"Prod URLs and external endpoints are configurable",
|
||||
"Legacy pages still point to valid local config keys after change",
|
||||
"verify.sh is green"
|
||||
],
|
||||
"status": "done",
|
||||
"created_at": "2026-05-25",
|
||||
"gates": {
|
||||
"review": false,
|
||||
"security": false,
|
||||
"qa": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "F-003",
|
||||
"type": "fix",
|
||||
"title": "Sanitize SQL dump for safe dev use",
|
||||
"problem": "Repo keeps production-like SQL dump with sensitive data risk",
|
||||
"goal": "Keep dev database baseline without sensitive live data in repo",
|
||||
"scope_in": [
|
||||
"review dump scope",
|
||||
"define safe replacement strategy",
|
||||
"remove or redact sensitive data",
|
||||
"document local data handling"
|
||||
],
|
||||
"scope_out": [
|
||||
"No app logic change",
|
||||
"No production DB changes",
|
||||
"No schema redesign"
|
||||
],
|
||||
"priority": "high",
|
||||
"risk": "high",
|
||||
"description": "Problem: Repo keeps production-like SQL dump with sensitive data risk. Goal: Keep dev database baseline without sensitive live data in repo. Scope IN: review dump scope, define safe replacement strategy, remove or redact sensitive data, document local data handling. Scope OUT: No app logic change, No production DB changes, No schema redesign. Type: fix. Priority: high. Risk: high.",
|
||||
"acceptance": [
|
||||
"Repo no longer stores raw sensitive production-like SQL dump as current dev baseline",
|
||||
"Safe dev data handling is documented",
|
||||
"Replacement dump or import path keeps local development possible",
|
||||
"Security risk note for SQL data is addressed",
|
||||
"verify.sh is green"
|
||||
],
|
||||
"status": "done",
|
||||
"created_at": "2026-05-25",
|
||||
"gates": {
|
||||
"review": false,
|
||||
"security": false,
|
||||
"qa": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
17
defaults/flask-skeleton/README.md
Normal file
17
defaults/flask-skeleton/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Default UI assets (Flask + Skeleton)
|
||||
|
||||
Estos archivos se usan como **default** cuando el `scripts/start.sh` configure stack por defecto:
|
||||
- Python/Flask
|
||||
- MariaDB
|
||||
- Skeleton CSS
|
||||
|
||||
## Origen
|
||||
Copiados desde:
|
||||
- `~/git/Skeleton-2.0.4/css/normalize.css`
|
||||
- `~/git/Skeleton-2.0.4/css/skeleton.css`
|
||||
- `~/git/Skeleton-2.0.4/images/favicon.png`
|
||||
|
||||
## Ubicación de destino recomendada en proyecto
|
||||
- `static/css/normalize.css`
|
||||
- `static/css/skeleton.css`
|
||||
- `static/images/favicon.png`
|
||||
12
defaults/flask-skeleton/UPSTREAM-NOTES.md
Normal file
12
defaults/flask-skeleton/UPSTREAM-NOTES.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Upstream notes — Skeleton
|
||||
|
||||
Repositorio revisado: `https://github.com/getskeleton/Skeleton`
|
||||
Versión base usada: `2.0.4` (2014)
|
||||
|
||||
Archivos copiados al template:
|
||||
- `css/normalize.css`
|
||||
- `css/skeleton.css`
|
||||
- `images/favicon.png`
|
||||
|
||||
Referencia rápida de uso y mejoras:
|
||||
- `docs/skeleton-manual.md`
|
||||
427
defaults/flask-skeleton/static/css/normalize.css
vendored
Normal file
427
defaults/flask-skeleton/static/css/normalize.css
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
||||
418
defaults/flask-skeleton/static/css/skeleton.css
vendored
Normal file
418
defaults/flask-skeleton/static/css/skeleton.css
vendored
Normal file
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* Skeleton V2.0.4
|
||||
* Copyright 2014, Dave Gamache
|
||||
* www.getskeleton.com
|
||||
* Free to use under the MIT license.
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
* 12/29/2014
|
||||
*/
|
||||
|
||||
|
||||
/* Table of contents
|
||||
––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||
- Grid
|
||||
- Base Styles
|
||||
- Typography
|
||||
- Links
|
||||
- Buttons
|
||||
- Forms
|
||||
- Lists
|
||||
- Code
|
||||
- Tables
|
||||
- Spacing
|
||||
- Utilities
|
||||
- Clearing
|
||||
- Media Queries
|
||||
*/
|
||||
|
||||
|
||||
/* Grid
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box; }
|
||||
.column,
|
||||
.columns {
|
||||
width: 100%;
|
||||
float: left;
|
||||
box-sizing: border-box; }
|
||||
|
||||
/* For devices larger than 400px */
|
||||
@media (min-width: 400px) {
|
||||
.container {
|
||||
width: 85%;
|
||||
padding: 0; }
|
||||
}
|
||||
|
||||
/* For devices larger than 550px */
|
||||
@media (min-width: 550px) {
|
||||
.container {
|
||||
width: 80%; }
|
||||
.column,
|
||||
.columns {
|
||||
margin-left: 4%; }
|
||||
.column:first-child,
|
||||
.columns:first-child {
|
||||
margin-left: 0; }
|
||||
|
||||
.one.column,
|
||||
.one.columns { width: 4.66666666667%; }
|
||||
.two.columns { width: 13.3333333333%; }
|
||||
.three.columns { width: 22%; }
|
||||
.four.columns { width: 30.6666666667%; }
|
||||
.five.columns { width: 39.3333333333%; }
|
||||
.six.columns { width: 48%; }
|
||||
.seven.columns { width: 56.6666666667%; }
|
||||
.eight.columns { width: 65.3333333333%; }
|
||||
.nine.columns { width: 74.0%; }
|
||||
.ten.columns { width: 82.6666666667%; }
|
||||
.eleven.columns { width: 91.3333333333%; }
|
||||
.twelve.columns { width: 100%; margin-left: 0; }
|
||||
|
||||
.one-third.column { width: 30.6666666667%; }
|
||||
.two-thirds.column { width: 65.3333333333%; }
|
||||
|
||||
.one-half.column { width: 48%; }
|
||||
|
||||
/* Offsets */
|
||||
.offset-by-one.column,
|
||||
.offset-by-one.columns { margin-left: 8.66666666667%; }
|
||||
.offset-by-two.column,
|
||||
.offset-by-two.columns { margin-left: 17.3333333333%; }
|
||||
.offset-by-three.column,
|
||||
.offset-by-three.columns { margin-left: 26%; }
|
||||
.offset-by-four.column,
|
||||
.offset-by-four.columns { margin-left: 34.6666666667%; }
|
||||
.offset-by-five.column,
|
||||
.offset-by-five.columns { margin-left: 43.3333333333%; }
|
||||
.offset-by-six.column,
|
||||
.offset-by-six.columns { margin-left: 52%; }
|
||||
.offset-by-seven.column,
|
||||
.offset-by-seven.columns { margin-left: 60.6666666667%; }
|
||||
.offset-by-eight.column,
|
||||
.offset-by-eight.columns { margin-left: 69.3333333333%; }
|
||||
.offset-by-nine.column,
|
||||
.offset-by-nine.columns { margin-left: 78.0%; }
|
||||
.offset-by-ten.column,
|
||||
.offset-by-ten.columns { margin-left: 86.6666666667%; }
|
||||
.offset-by-eleven.column,
|
||||
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
|
||||
|
||||
.offset-by-one-third.column,
|
||||
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
|
||||
.offset-by-two-thirds.column,
|
||||
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
|
||||
|
||||
.offset-by-one-half.column,
|
||||
.offset-by-one-half.columns { margin-left: 52%; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Base Styles
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
/* NOTE
|
||||
html is set to 62.5% so that all the REM measurements throughout Skeleton
|
||||
are based on 10px sizing. So basically 1.5rem = 15px :) */
|
||||
html {
|
||||
font-size: 62.5%; }
|
||||
body {
|
||||
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #222; }
|
||||
|
||||
|
||||
/* Typography
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 300; }
|
||||
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
|
||||
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
|
||||
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
|
||||
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
|
||||
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
|
||||
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
|
||||
|
||||
/* Larger than phablet */
|
||||
@media (min-width: 550px) {
|
||||
h1 { font-size: 5.0rem; }
|
||||
h2 { font-size: 4.2rem; }
|
||||
h3 { font-size: 3.6rem; }
|
||||
h4 { font-size: 3.0rem; }
|
||||
h5 { font-size: 2.4rem; }
|
||||
h6 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0; }
|
||||
|
||||
|
||||
/* Links
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
a {
|
||||
color: #1EAEDB; }
|
||||
a:hover {
|
||||
color: #0FA0CE; }
|
||||
|
||||
|
||||
/* Buttons
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.button,
|
||||
button,
|
||||
input[type="submit"],
|
||||
input[type="reset"],
|
||||
input[type="button"] {
|
||||
display: inline-block;
|
||||
height: 38px;
|
||||
padding: 0 30px;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 38px;
|
||||
letter-spacing: .1rem;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #bbb;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box; }
|
||||
.button:hover,
|
||||
button:hover,
|
||||
input[type="submit"]:hover,
|
||||
input[type="reset"]:hover,
|
||||
input[type="button"]:hover,
|
||||
.button:focus,
|
||||
button:focus,
|
||||
input[type="submit"]:focus,
|
||||
input[type="reset"]:focus,
|
||||
input[type="button"]:focus {
|
||||
color: #333;
|
||||
border-color: #888;
|
||||
outline: 0; }
|
||||
.button.button-primary,
|
||||
button.button-primary,
|
||||
input[type="submit"].button-primary,
|
||||
input[type="reset"].button-primary,
|
||||
input[type="button"].button-primary {
|
||||
color: #FFF;
|
||||
background-color: #33C3F0;
|
||||
border-color: #33C3F0; }
|
||||
.button.button-primary:hover,
|
||||
button.button-primary:hover,
|
||||
input[type="submit"].button-primary:hover,
|
||||
input[type="reset"].button-primary:hover,
|
||||
input[type="button"].button-primary:hover,
|
||||
.button.button-primary:focus,
|
||||
button.button-primary:focus,
|
||||
input[type="submit"].button-primary:focus,
|
||||
input[type="reset"].button-primary:focus,
|
||||
input[type="button"].button-primary:focus {
|
||||
color: #FFF;
|
||||
background-color: #1EAEDB;
|
||||
border-color: #1EAEDB; }
|
||||
|
||||
|
||||
/* Forms
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="search"],
|
||||
input[type="text"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
textarea,
|
||||
select {
|
||||
height: 38px;
|
||||
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
|
||||
background-color: #fff;
|
||||
border: 1px solid #D1D1D1;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box; }
|
||||
/* Removes awkward default styles on some inputs for iOS */
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="search"],
|
||||
input[type="text"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none; }
|
||||
textarea {
|
||||
min-height: 65px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px; }
|
||||
input[type="email"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="search"]:focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="tel"]:focus,
|
||||
input[type="url"]:focus,
|
||||
input[type="password"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border: 1px solid #33C3F0;
|
||||
outline: 0; }
|
||||
label,
|
||||
legend {
|
||||
display: block;
|
||||
margin-bottom: .5rem;
|
||||
font-weight: 600; }
|
||||
fieldset {
|
||||
padding: 0;
|
||||
border-width: 0; }
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
display: inline; }
|
||||
label > .label-body {
|
||||
display: inline-block;
|
||||
margin-left: .5rem;
|
||||
font-weight: normal; }
|
||||
|
||||
|
||||
/* Lists
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
ul {
|
||||
list-style: circle inside; }
|
||||
ol {
|
||||
list-style: decimal inside; }
|
||||
ol, ul {
|
||||
padding-left: 0;
|
||||
margin-top: 0; }
|
||||
ul ul,
|
||||
ul ol,
|
||||
ol ol,
|
||||
ol ul {
|
||||
margin: 1.5rem 0 1.5rem 3rem;
|
||||
font-size: 90%; }
|
||||
li {
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
|
||||
/* Code
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
code {
|
||||
padding: .2rem .5rem;
|
||||
margin: 0 .2rem;
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
background: #F1F1F1;
|
||||
border: 1px solid #E1E1E1;
|
||||
border-radius: 4px; }
|
||||
pre > code {
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre; }
|
||||
|
||||
|
||||
/* Tables
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
th,
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #E1E1E1; }
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 0; }
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 0; }
|
||||
|
||||
|
||||
/* Spacing
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
button,
|
||||
.button {
|
||||
margin-bottom: 1rem; }
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
fieldset {
|
||||
margin-bottom: 1.5rem; }
|
||||
pre,
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
table,
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
form {
|
||||
margin-bottom: 2.5rem; }
|
||||
|
||||
|
||||
/* Utilities
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.u-full-width {
|
||||
width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.u-max-full-width {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.u-pull-right {
|
||||
float: right; }
|
||||
.u-pull-left {
|
||||
float: left; }
|
||||
|
||||
|
||||
/* Misc
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
hr {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3.5rem;
|
||||
border-width: 0;
|
||||
border-top: 1px solid #E1E1E1; }
|
||||
|
||||
|
||||
/* Clearing
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
|
||||
/* Self Clearing Goodness */
|
||||
.container:after,
|
||||
.row:after,
|
||||
.u-cf {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
|
||||
|
||||
/* Media Queries
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
/*
|
||||
Note: The best way to structure the use of media queries is to create the queries
|
||||
near the relevant code. For example, if you wanted to change the styles for buttons
|
||||
on small devices, paste the mobile query code up in the buttons section and style it
|
||||
there.
|
||||
*/
|
||||
|
||||
|
||||
/* Larger than mobile */
|
||||
@media (min-width: 400px) {}
|
||||
|
||||
/* Larger than phablet (also point when grid becomes active) */
|
||||
@media (min-width: 550px) {}
|
||||
|
||||
/* Larger than tablet */
|
||||
@media (min-width: 750px) {}
|
||||
|
||||
/* Larger than desktop */
|
||||
@media (min-width: 1000px) {}
|
||||
|
||||
/* Larger than Desktop HD */
|
||||
@media (min-width: 1200px) {}
|
||||
BIN
defaults/flask-skeleton/static/images/favicon.png
Normal file
BIN
defaults/flask-skeleton/static/images/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
17
docs/README.md
Normal file
17
docs/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Docs index
|
||||
|
||||
## Core docs
|
||||
- `../README.md` — framework overview
|
||||
- `../HOWTO.md` — quick start
|
||||
- `../HOWTO-FEATURE.md` — feature workflow
|
||||
- `../TEMPLATE.md` — how to adapt ARNES
|
||||
|
||||
## Reference docs
|
||||
- `repository-layout.md` — repo structure and separation rules
|
||||
- `scripts-reference.md` — start/verify/ticket/publish/install scripts
|
||||
- `skeleton-manual.md` — default Skeleton UI notes
|
||||
|
||||
## Harness source of truth
|
||||
- `../harness/agents.matrix.yml` — roles and edit boundaries
|
||||
- `../harness/workflow.stages.yml` — ordered workflow stages
|
||||
- `../harness/policies/` — governance, security, quality, language, model routing
|
||||
40
docs/repository-layout.md
Normal file
40
docs/repository-layout.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Repository layout
|
||||
|
||||
## Core idea
|
||||
- ARNES core lives at repository root once installed into a project repo.
|
||||
- The source repo of ARNES is not the repo where product work should happen.
|
||||
- Real project code lives in `project/` by default.
|
||||
- Project-specific rules live in overlays, not in core files.
|
||||
|
||||
## Main directories
|
||||
- `project/` — real app code
|
||||
- `backlog/` — ticket list and feature state
|
||||
- `work/` — runtime state, history, artifacts
|
||||
- `harness/` — workflow, roles, policies, contracts
|
||||
- `spec/` — product, tech, acceptance, SDD, BDD source docs
|
||||
- `features/` — optional executable BDD runner assets
|
||||
- `scripts/` — start, verify, ticket creation, publish, install, runtime status
|
||||
- `platforms/` — platform adapters (pi, opencode)
|
||||
- `defaults/` — optional starter assets
|
||||
- `tests/` — self-tests for the ARNES source repo only
|
||||
|
||||
## Recommended separation
|
||||
- Core ARNES should stay generic.
|
||||
- Domain checks go in `scripts/verify.local.sh`.
|
||||
- Domain rules go in `AGENTS.local.md`.
|
||||
- Real code should not be mixed into `harness/`, `work/`, `backlog/`, or `spec/`.
|
||||
- Source-repo self-tests under `tests/` are not part of installed project repos.
|
||||
|
||||
## Default project shape
|
||||
```text
|
||||
project/
|
||||
├── README.md
|
||||
├── templates/
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ ├── js/
|
||||
│ └── images/
|
||||
└── ...
|
||||
```
|
||||
|
||||
This shape is only a default. The wizard can target another app directory if needed.
|
||||
69
docs/scripts-reference.md
Normal file
69
docs/scripts-reference.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Scripts reference
|
||||
|
||||
## `./scripts/install_into_repo.sh /path/to/project-repo`
|
||||
Copies ARNES core into a different project repo.
|
||||
|
||||
What it does:
|
||||
- refuses to install into the ARNES source repo itself
|
||||
- initializes git repo at target if missing
|
||||
- copies ARNES core files into target repo
|
||||
|
||||
## `./scripts/start.sh`
|
||||
Interactive bootstrap wizard.
|
||||
|
||||
What it does:
|
||||
- asks project metadata
|
||||
- chooses default app directory (`project/` by default)
|
||||
- defaults Python/Flask projects to `python3 -m unittest discover -s project/tests -v`
|
||||
- seeds a minimal bootstrap smoke test under `project/tests/` for Python/Flask
|
||||
- writes `harness/project.config.json`
|
||||
- creates `scripts/verify.local.sh`
|
||||
- can seed one bootstrap ticket
|
||||
- resets runtime status
|
||||
|
||||
## `./scripts/verify.sh`
|
||||
Core harness verification.
|
||||
|
||||
What it checks:
|
||||
- required core files exist
|
||||
- project is inside a git repo
|
||||
- warns if no git remote exists
|
||||
- backlog JSON is valid
|
||||
- only one feature is `in_progress`
|
||||
- done features have all required artifacts, including publish evidence
|
||||
- runtime status JSON is valid
|
||||
- source-repo self-tests run automatically if `tests/` exists
|
||||
- optional local overlay runs if present
|
||||
|
||||
## `python3 scripts/new_ticket.py`
|
||||
Interactive ticket creator.
|
||||
|
||||
Writes one new backlog entry with:
|
||||
- `type`
|
||||
- `title`
|
||||
- `problem`
|
||||
- `goal`
|
||||
- `scope_in`
|
||||
- `scope_out`
|
||||
- `priority`
|
||||
- `risk`
|
||||
- `acceptance`
|
||||
|
||||
## `python3 scripts/publish_ticket.py --feature-id F-001`
|
||||
Final publish step for one ticket.
|
||||
|
||||
What it does:
|
||||
- validates git repo, remote, and git identity
|
||||
- writes `work/artifacts/<feature_id>/publish.json`
|
||||
- creates one commit for the ticket
|
||||
- pushes the branch to remote
|
||||
|
||||
## `python3 scripts/agent_status.py`
|
||||
Runtime status helper.
|
||||
|
||||
Commands:
|
||||
- `show`
|
||||
- `set`
|
||||
- `reset`
|
||||
|
||||
The `set` command validates stage and agent names against harness files.
|
||||
107
docs/skeleton-manual.md
Normal file
107
docs/skeleton-manual.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Manual de uso — Skeleton 2.0.4 (para ARNES template)
|
||||
|
||||
> Fuente revisada: `https://github.com/getskeleton/Skeleton` (v2.0.4, 2014).
|
||||
|
||||
## 1) Qué es y cuándo usarlo
|
||||
Skeleton es un boilerplate CSS ligero (no framework UI completo). Ideal para:
|
||||
- paneles internos
|
||||
- CRUDs
|
||||
- prototipos rápidos
|
||||
- apps server-rendered (Flask + Jinja)
|
||||
|
||||
No ideal para:
|
||||
- diseño de componentes complejos
|
||||
- design systems avanzados
|
||||
- apps con alta complejidad visual
|
||||
|
||||
## 2) Archivos base
|
||||
En este template se incluyen en:
|
||||
- `defaults/flask-skeleton/static/css/normalize.css`
|
||||
- `defaults/flask-skeleton/static/css/skeleton.css`
|
||||
- `defaults/flask-skeleton/static/images/favicon.png`
|
||||
|
||||
Uso recomendado en HTML:
|
||||
```html
|
||||
<link rel="stylesheet" href="/static/css/normalize.css">
|
||||
<link rel="stylesheet" href="/static/css/skeleton.css">
|
||||
```
|
||||
|
||||
## 3) Grid (12 columnas)
|
||||
Estructura:
|
||||
```html
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="six columns">...</div>
|
||||
<div class="six columns">...</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Clases comunes:
|
||||
- `one` ... `twelve columns`
|
||||
- `one-half column`
|
||||
- `one-third column`
|
||||
- `two-thirds column`
|
||||
|
||||
Offsets:
|
||||
- `offset-by-one ... offset-by-eleven`
|
||||
|
||||
Breakpoints relevantes:
|
||||
- `min-width: 400px`
|
||||
- `min-width: 550px` (aquí se activa grid multi-columna)
|
||||
- `750px`, `1000px`, `1200px`
|
||||
|
||||
## 4) Utilidades clave
|
||||
- `u-full-width` → ancho 100%
|
||||
- `u-max-full-width` → max-width 100%
|
||||
- `u-pull-right`, `u-pull-left`
|
||||
- `u-cf` (clear float)
|
||||
|
||||
## 5) Formularios y botones
|
||||
Inputs/select/textarea ya traen estilo base.
|
||||
Patrones recomendados:
|
||||
- siempre usar `u-full-width` en formularios de app
|
||||
- usar `.button` y `.button.button-primary`
|
||||
- mantener labels visibles (`label` + `for`)
|
||||
|
||||
## 6) Tablas
|
||||
Skeleton trae estilo mínimo. Para tablas largas:
|
||||
- envolver en `.table-responsive` propia del proyecto
|
||||
- controlar overflow horizontal en móviles
|
||||
|
||||
## 7) Limitaciones conocidas (por antigüedad)
|
||||
- Sistema basado en **floats** (no flex/grid nativo)
|
||||
- Tipografía por defecto antigua (Raleway de Google Fonts vía URL legacy)
|
||||
- No incluye componentes modernos (modal, tabs, toast, etc.)
|
||||
- No incluye tokens de diseño ni theming avanzado
|
||||
|
||||
## 8) Mejoras recomendadas en proyectos nuevos
|
||||
Mantener Skeleton como base, pero añadir capa moderna propia:
|
||||
|
||||
1. **Layout helper CSS local**
|
||||
- utilidades flex (`.d-flex`, `.justify-between`, etc.)
|
||||
- spacing consistente (`.mb-1`, `.mb-2`, ...)
|
||||
|
||||
2. **Responsive wrappers**
|
||||
- `.table-responsive`
|
||||
- patrones mobile-first para filtros/toolbar
|
||||
|
||||
3. **Componentes mínimos reutilizables**
|
||||
- modal base
|
||||
- badges
|
||||
- pagination bar
|
||||
- alertas/confirmaciones
|
||||
|
||||
4. **Accesibilidad**
|
||||
- foco visible
|
||||
- contraste de colores
|
||||
- labels/aria en acciones icon-only
|
||||
|
||||
5. **No tocar upstream directamente**
|
||||
- dejar `skeleton.css` y `normalize.css` sin modificar
|
||||
- personalización en `custom.css` del proyecto
|
||||
|
||||
## 9) Regla de mantenimiento para ARNES
|
||||
- Skeleton se trata como dependencia estática base.
|
||||
- Cualquier override va en CSS del proyecto.
|
||||
- Si un proyecto requiere UI compleja, considerar migración progresiva a capa de componentes propia.
|
||||
11
features/README.md
Normal file
11
features/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Executable BDD assets
|
||||
|
||||
This directory is for executable BDD helpers.
|
||||
|
||||
Recommended split:
|
||||
- `spec/bdd/features/` = source-of-truth scenarios in Gherkin
|
||||
- `features/steps/` = executable step definitions and runner config
|
||||
- `features/behave.ini` = Behave runner config
|
||||
|
||||
Keep feature text in `spec/bdd/features/`.
|
||||
Keep runner-specific code in `features/`.
|
||||
8
features/behave.ini
Normal file
8
features/behave.ini
Normal file
@@ -0,0 +1,8 @@
|
||||
[behave]
|
||||
paths = features/
|
||||
format = pretty
|
||||
|
||||
# Examples:
|
||||
# behave features/
|
||||
# behave features/ --tags @smoke
|
||||
# behave features/ --tags ~@slow
|
||||
0
features/steps/.gitkeep
Normal file
0
features/steps/.gitkeep
Normal file
48
features/steps/common/README.md
Normal file
48
features/steps/common/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Common Steps
|
||||
|
||||
Steps reutilizables para múltiples features.
|
||||
|
||||
## Navigation
|
||||
|
||||
```python
|
||||
@given('el usuario está en la página principal')
|
||||
def step_at_home_page(context):
|
||||
context.current_page = "home"
|
||||
|
||||
@when('el usuario hace clic en el elemento de menú "{menu_item}"')
|
||||
def step_click_menu(context, menu_item):
|
||||
context.menu_clicked = menu_item
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
@given('la conexión a internet está disponible')
|
||||
def step_internet_available(context):
|
||||
context.internet_available = True
|
||||
|
||||
@given('el servidor no responde')
|
||||
def step_server_down(context):
|
||||
context.server_responding = False
|
||||
|
||||
@then('el sistema muestra toast "{message}"')
|
||||
def step_show_toast(context, message):
|
||||
context.toast_message = message
|
||||
```
|
||||
|
||||
## User Session
|
||||
|
||||
```python
|
||||
@given('el usuario tiene sesión activa')
|
||||
def step_user_logged_in(context):
|
||||
context.user_logged_in = True
|
||||
context.token = "valid_token"
|
||||
|
||||
@then('el sistema muestra indicador de carga')
|
||||
def step_show_loading(context):
|
||||
context.showing_loading = True
|
||||
|
||||
@then('después de timeout muestra error "{message}"')
|
||||
def step_timeout_error(context, message):
|
||||
assert context.timeout_error == message
|
||||
```
|
||||
@@ -2,29 +2,44 @@ version: 1
|
||||
|
||||
roles:
|
||||
leader:
|
||||
can_edit: ["work/", "backlog/", "spec/", "harness/"]
|
||||
cannot_edit: ["src/", "tests/"]
|
||||
emoji: "🧭"
|
||||
can_edit: ["work/", "backlog/", "spec/", "harness/", "AGENTS.md", "CHECKPOINTS.md"]
|
||||
cannot_edit: ["project/", "tests/"]
|
||||
responsibilities:
|
||||
- plan
|
||||
- orchestrate
|
||||
- enforce_gates
|
||||
- publish_ticket_changes
|
||||
- close_feature
|
||||
- issue_orders_in_english_caveman
|
||||
|
||||
triager:
|
||||
emoji: "🧩"
|
||||
can_edit: ["backlog/", "work/artifacts/", "spec/"]
|
||||
cannot_edit: ["project/", "tests/", "backlog/features.json:status=done"]
|
||||
responsibilities:
|
||||
- normalize_requests
|
||||
- create_tickets_in_english_caveman
|
||||
- define_scope_acceptance
|
||||
|
||||
architect:
|
||||
emoji: "🏗️"
|
||||
can_edit: ["spec/", "harness/contracts/", "docs/"]
|
||||
cannot_edit: ["src/", "tests/", "backlog/features.json:status"]
|
||||
cannot_edit: ["project/", "tests/", "backlog/features.json:status"]
|
||||
responsibilities:
|
||||
- design
|
||||
- update_contracts
|
||||
|
||||
implementer:
|
||||
can_edit: ["src/", "tests/", "work/artifacts/"]
|
||||
emoji: "🛠️"
|
||||
can_edit: ["project/", "tests/", "work/artifacts/"]
|
||||
cannot_edit:
|
||||
- "backlog/features.json:done"
|
||||
- "work/history.md"
|
||||
- "work/artifacts/*/reviewer.json"
|
||||
- "work/artifacts/*/security.json"
|
||||
- "work/artifacts/*/qa.json"
|
||||
- "work/artifacts/*/publish.json"
|
||||
- "work/artifacts/*/leader-close.json"
|
||||
responsibilities:
|
||||
- implement_feature
|
||||
@@ -32,15 +47,17 @@ roles:
|
||||
- produce_implementer_evidence
|
||||
|
||||
reviewer:
|
||||
emoji: "🔍"
|
||||
can_edit: ["work/artifacts/"]
|
||||
cannot_edit: ["src/", "tests/", "backlog/"]
|
||||
cannot_edit: ["project/", "tests/", "backlog/"]
|
||||
responsibilities:
|
||||
- technical_review
|
||||
- emit_reviewer_verdict
|
||||
|
||||
security:
|
||||
emoji: "🔒"
|
||||
can_edit: ["work/artifacts/"]
|
||||
cannot_edit: ["src/", "tests/", "backlog/"]
|
||||
cannot_edit: ["project/", "tests/", "backlog/"]
|
||||
responsibilities:
|
||||
- sast
|
||||
- dependency_review
|
||||
@@ -48,16 +65,27 @@ roles:
|
||||
- emit_security_verdict
|
||||
|
||||
qa:
|
||||
emoji: "🧪"
|
||||
can_edit: ["work/artifacts/"]
|
||||
cannot_edit: ["src/", "tests/", "backlog/"]
|
||||
cannot_edit: ["project/", "tests/", "backlog/"]
|
||||
responsibilities:
|
||||
- acceptance_traceability
|
||||
- integration_e2e_checks
|
||||
- regression_checks
|
||||
- emit_qa_verdict
|
||||
|
||||
documenter:
|
||||
emoji: "📚"
|
||||
can_edit: ["docs/", "spec/", "README.md", "HOWTO.md", "work/artifacts/"]
|
||||
cannot_edit: ["project/", "tests/", "backlog/features.json:status"]
|
||||
responsibilities:
|
||||
- document_feature_changes
|
||||
- update_user_docs
|
||||
- emit_documenter_summary
|
||||
|
||||
anti_cheat:
|
||||
- "Implementer cannot promote feature to done"
|
||||
- "Done requires reviewer/security/qa approved artifacts"
|
||||
- "Done requires documenter evidence"
|
||||
- "Leader close requires verify.sh success"
|
||||
- "Evidence must be on disk; chat-only claims are invalid"
|
||||
|
||||
51
harness/models.profiles.yml
Normal file
51
harness/models.profiles.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
version: 1
|
||||
|
||||
policy:
|
||||
goal: "Use smallest model that can do task well"
|
||||
fallback_order: ["tiny", "small", "medium", "large"]
|
||||
|
||||
profiles:
|
||||
tiny:
|
||||
use_for:
|
||||
- status updates
|
||||
- file moves
|
||||
- boilerplate JSON
|
||||
- simple docs formatting
|
||||
small:
|
||||
use_for:
|
||||
- triage ticket drafting
|
||||
- reviewer/security/qa short verdicts
|
||||
- changelog/doc updates
|
||||
- refactors with low logic risk
|
||||
medium:
|
||||
use_for:
|
||||
- architecture decisions
|
||||
- non-trivial implementation
|
||||
- multi-file integration changes
|
||||
large:
|
||||
use_for:
|
||||
- complex debugging
|
||||
- deep root-cause analysis
|
||||
- migrations with high risk
|
||||
- ambiguous requirements
|
||||
|
||||
role_defaults:
|
||||
leader: small
|
||||
triager: small
|
||||
architect: medium
|
||||
implementer: medium
|
||||
reviewer: small
|
||||
security: small
|
||||
qa: small
|
||||
documenter: tiny
|
||||
|
||||
stage_overrides:
|
||||
triage_translate: small
|
||||
intake: small
|
||||
design: medium
|
||||
build: medium
|
||||
review_gate: small
|
||||
security_gate: small
|
||||
qa_gate: small
|
||||
documentation_gate: tiny
|
||||
close: small
|
||||
22
harness/policies/language.md
Normal file
22
harness/policies/language.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Policy: Language and style
|
||||
|
||||
## Internal language
|
||||
- Internal artifacts, tickets, and leader orders must be in **English**.
|
||||
- User chat can be in any language.
|
||||
|
||||
## Style mode: Caveman English
|
||||
- Short words.
|
||||
- Short lines.
|
||||
- One idea per line.
|
||||
- No fluff.
|
||||
- No long intros.
|
||||
- Prefer bullets.
|
||||
|
||||
## Ticket writing rules
|
||||
- Title: 4–10 words.
|
||||
- Acceptance: 3–6 bullets max.
|
||||
- Keep scope explicit (in/out).
|
||||
- Use active verbs: Fix, Add, Move, Remove, Validate.
|
||||
|
||||
## Runtime action rules
|
||||
- `agent_status.action` should be concise (<= 60 chars).
|
||||
24
harness/policies/model-routing.md
Normal file
24
harness/policies/model-routing.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Policy: Model routing
|
||||
|
||||
Use model by task complexity, not by habit.
|
||||
|
||||
## Core rule
|
||||
- Start small.
|
||||
- Escalate only when blocked or quality poor.
|
||||
|
||||
## Escalation triggers
|
||||
- Repeated failed attempts.
|
||||
- Ambiguous requirements.
|
||||
- Cross-module side effects.
|
||||
- Security-critical code paths.
|
||||
|
||||
## De-escalation triggers
|
||||
- Routine CRUD edits.
|
||||
- Mechanical refactors.
|
||||
- Artifact writing.
|
||||
- Status/timeline updates.
|
||||
|
||||
## Required behavior
|
||||
- Record chosen model class in artifact header when work is non-trivial.
|
||||
- Keep outputs concise to reduce token burn.
|
||||
- If `harness/project.config.json` has `model_mode=lean`, prefer tiny/small whenever possible.
|
||||
@@ -4,6 +4,15 @@ feature_states:
|
||||
allowed: [pending, in_progress, blocked, done]
|
||||
|
||||
stages:
|
||||
- name: triage_translate
|
||||
owner: leader
|
||||
optional: true
|
||||
input:
|
||||
- backlog/features.json
|
||||
- work/current.md
|
||||
output:
|
||||
- work/artifacts/<feature_id>/triage.md
|
||||
|
||||
- name: intake
|
||||
owner: leader
|
||||
input:
|
||||
@@ -41,6 +50,12 @@ stages:
|
||||
output:
|
||||
- work/artifacts/<feature_id>/qa.json
|
||||
|
||||
- name: documentation_gate
|
||||
owner: documenter
|
||||
required: true
|
||||
output:
|
||||
- work/artifacts/<feature_id>/documenter.md
|
||||
|
||||
- name: close
|
||||
owner: leader
|
||||
required: true
|
||||
@@ -48,8 +63,16 @@ stages:
|
||||
- work/artifacts/<feature_id>/leader-close.json
|
||||
- work/history.md
|
||||
|
||||
- name: publish
|
||||
owner: leader
|
||||
required: true
|
||||
output:
|
||||
- work/artifacts/<feature_id>/publish.json
|
||||
|
||||
close_requirements:
|
||||
- reviewer.json.verdict == "APPROVED"
|
||||
- security.json.verdict == "APPROVED"
|
||||
- qa.json.verdict == "APPROVED"
|
||||
- documenter.md exists
|
||||
- publish.json.verdict == "PUBLISHED"
|
||||
- scripts/verify.sh exit_code == 0
|
||||
|
||||
11
project/README.md
Normal file
11
project/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Project code lives here
|
||||
|
||||
Current project layout:
|
||||
|
||||
- `project/web/index/new/` — legacy PHP web module copied from production
|
||||
- `project/web/index/new/config/local.example.php` — versioned local config template
|
||||
- `project/web/index/new/config/local.php` — ignored local config with real values
|
||||
- `project/sql/db-25052026.sql` — sanitized local development SQL baseline
|
||||
- `project/sql/private/` — optional ignored path for private raw dumps
|
||||
|
||||
ARNES core stays outside this folder.
|
||||
25
project/sql/README.md
Normal file
25
project/sql/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# SQL baselines for local development
|
||||
|
||||
## Tracked baseline
|
||||
- `db-25052026.sql`
|
||||
- Purpose: safe local baseline for the legacy PHP module
|
||||
- Content: schema and synthetic seed data only
|
||||
- Safe for commit and push
|
||||
|
||||
## Private local data
|
||||
- If you need a private raw snapshot, keep it outside git.
|
||||
- Recommended local ignored path: `project/sql/private/`
|
||||
- Do not commit raw customer, order, or production-like data back to this repo.
|
||||
|
||||
## Local import
|
||||
Example:
|
||||
|
||||
```bash
|
||||
mysql -u root -p < project/sql/db-25052026.sql
|
||||
```
|
||||
|
||||
The sanitized baseline includes the tables used by:
|
||||
- `index.php`
|
||||
- `productos_bulk_update.php`
|
||||
- `productos_modificados.php`
|
||||
- `worker_bulk.php`
|
||||
170
project/sql/db-25052026.sql
Normal file
170
project/sql/db-25052026.sql
Normal file
@@ -0,0 +1,170 @@
|
||||
-- Sanitized development baseline for legacy PHP module
|
||||
-- Synthetic data only. Safe for local development and version control.
|
||||
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `legacy_module_dev` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE `legacy_module_dev`;
|
||||
|
||||
DROP TABLE IF EXISTS `oc_product_queue`;
|
||||
DROP TABLE IF EXISTS `oc_product_description`;
|
||||
DROP TABLE IF EXISTS `oc_product`;
|
||||
DROP TABLE IF EXISTS `oc_attribute_description`;
|
||||
DROP TABLE IF EXISTS `oc_tax_class`;
|
||||
DROP TABLE IF EXISTS `oc_url_alias`;
|
||||
DROP TABLE IF EXISTS `oc_manufacturer_to_store`;
|
||||
DROP TABLE IF EXISTS `oc_manufacturer`;
|
||||
DROP TABLE IF EXISTS `oc_category_to_store`;
|
||||
DROP TABLE IF EXISTS `oc_category_description`;
|
||||
DROP TABLE IF EXISTS `oc_category`;
|
||||
|
||||
CREATE TABLE `oc_category` (
|
||||
`category_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`category_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_category_description` (
|
||||
`category_id` int(11) NOT NULL,
|
||||
`language_id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`category_id`,`language_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_category_to_store` (
|
||||
`category_id` int(11) NOT NULL,
|
||||
`store_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`category_id`,`store_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_manufacturer` (
|
||||
`manufacturer_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(64) NOT NULL,
|
||||
`image` varchar(255) DEFAULT NULL,
|
||||
`sort_order` int(3) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`manufacturer_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_manufacturer_to_store` (
|
||||
`manufacturer_id` int(11) NOT NULL,
|
||||
`store_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`manufacturer_id`,`store_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_url_alias` (
|
||||
`url_alias_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`query` varchar(255) NOT NULL,
|
||||
`keyword` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`url_alias_id`),
|
||||
UNIQUE KEY `uq_keyword` (`keyword`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_tax_class` (
|
||||
`tax_class_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(64) NOT NULL,
|
||||
PRIMARY KEY (`tax_class_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_attribute_description` (
|
||||
`attribute_id` int(11) NOT NULL,
|
||||
`language_id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`attribute_id`,`language_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_product` (
|
||||
`product_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`ean` varchar(14) NOT NULL DEFAULT '',
|
||||
`image` varchar(255) DEFAULT NULL,
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`product_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_product_description` (
|
||||
`product_id` int(11) NOT NULL,
|
||||
`language_id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`description` text NOT NULL,
|
||||
`meta_description` varchar(255) NOT NULL DEFAULT '',
|
||||
`meta_keyword` varchar(255) NOT NULL DEFAULT '',
|
||||
`tag` text NOT NULL,
|
||||
`u_title` varchar(255) NOT NULL DEFAULT '',
|
||||
`u_h1` varchar(255) NOT NULL DEFAULT '',
|
||||
`u_h2` varchar(255) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`product_id`,`language_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `oc_product_queue` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`product_id` int(11) NOT NULL,
|
||||
`processed` tinyint(1) DEFAULT 0,
|
||||
`processed_at` datetime DEFAULT NULL,
|
||||
`result_es` tinyint(1) DEFAULT 0,
|
||||
`result_en` tinyint(1) DEFAULT 0,
|
||||
`needs_verify` tinyint(1) DEFAULT 0,
|
||||
`log` text DEFAULT NULL,
|
||||
`created_at` datetime DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_product_id` (`product_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `oc_category` (`category_id`) VALUES
|
||||
(1),
|
||||
(2),
|
||||
(3);
|
||||
|
||||
INSERT INTO `oc_category_description` (`category_id`, `language_id`, `name`) VALUES
|
||||
(1, 4, 'Despensa ecológica'),
|
||||
(2, 4, 'Suplementos naturales'),
|
||||
(3, 4, 'Cuidado personal');
|
||||
|
||||
INSERT INTO `oc_category_to_store` (`category_id`, `store_id`) VALUES
|
||||
(1, 0),
|
||||
(2, 0),
|
||||
(3, 0);
|
||||
|
||||
INSERT INTO `oc_manufacturer` (`manufacturer_id`, `name`, `image`, `sort_order`) VALUES
|
||||
(1, 'Herbolario Demo', '', 0),
|
||||
(2, 'Bio Sample', '', 0),
|
||||
(3, 'Natural Test Labs', '', 0);
|
||||
|
||||
INSERT INTO `oc_manufacturer_to_store` (`manufacturer_id`, `store_id`) VALUES
|
||||
(1, 0),
|
||||
(2, 0),
|
||||
(3, 0);
|
||||
|
||||
INSERT INTO `oc_url_alias` (`url_alias_id`, `query`, `keyword`) VALUES
|
||||
(1, 'manufacturer_id=1', 'herbolario-demo'),
|
||||
(2, 'manufacturer_id=2', 'bio-sample'),
|
||||
(3, 'manufacturer_id=3', 'natural-test-labs');
|
||||
|
||||
INSERT INTO `oc_tax_class` (`tax_class_id`, `title`) VALUES
|
||||
(1, 'IVA 4%'),
|
||||
(2, 'IVA 10%'),
|
||||
(3, 'IVA 21%');
|
||||
|
||||
INSERT INTO `oc_attribute_description` (`attribute_id`, `language_id`, `name`) VALUES
|
||||
(1, 4, 'bio'),
|
||||
(2, 4, 'vegano'),
|
||||
(3, 4, 'sin-gluten'),
|
||||
(4, 4, 'sin-azucar'),
|
||||
(5, 4, 'rico-en-fibra'),
|
||||
(6, 4, 'sin-lactosa'),
|
||||
(7, 4, 'cruelty-free'),
|
||||
(8, 4, 'apto-infantil');
|
||||
|
||||
INSERT INTO `oc_product` (`product_id`, `ean`, `image`, `status`) VALUES
|
||||
(1001, '8437000000011', 'catalog/demo/te-verde-demo.jpg', 1),
|
||||
(1002, '8437000000028', 'catalog/demo/magnesio-demo.jpg', 1),
|
||||
(1003, '8437000000035', 'catalog/demo/jabon-demo.jpg', 1);
|
||||
|
||||
INSERT INTO `oc_product_description` (`product_id`, `language_id`, `name`, `description`, `meta_description`, `meta_keyword`, `tag`, `u_title`, `u_h1`, `u_h2`) VALUES
|
||||
(1001, 4, 'Té verde ecológico demo', '<p>Infusión demo para pruebas locales.</p>', 'Producto demo para entorno local.', '', '', 'Té verde ecológico demo', 'Té verde ecológico demo', 'Beneficios del té verde demo'),
|
||||
(1001, 1, 'Demo organic green tea', '<p>Demo infusion for local development.</p>', 'Demo product for local environment.', '', '', 'Demo organic green tea', 'Demo organic green tea', 'Benefits of demo green tea'),
|
||||
(1002, 4, 'Magnesio natural demo', '<p>Complemento demo para pruebas locales.</p>', 'Suplemento demo para entorno local.', '', '', 'Magnesio natural demo', 'Magnesio natural demo', 'Beneficios del magnesio demo'),
|
||||
(1002, 1, 'Demo natural magnesium', '<p>Demo supplement for local development.</p>', 'Demo supplement for local environment.', '', '', 'Demo natural magnesium', 'Demo natural magnesium', 'Benefits of demo magnesium'),
|
||||
(1003, 4, 'Jabón suave demo', '<p>Producto cosmético demo para pruebas locales.</p>', 'Cosmético demo para entorno local.', '', '', 'Jabón suave demo', 'Jabón suave demo', 'Cuidado suave demo'),
|
||||
(1003, 1, 'Demo gentle soap', '<p>Demo cosmetic product for local development.</p>', 'Demo cosmetic for local environment.', '', '', 'Demo gentle soap', 'Demo gentle soap', 'Gentle care demo');
|
||||
|
||||
COMMIT;
|
||||
14
project/web/index/new/README.md
Normal file
14
project/web/index/new/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Legacy PHP module
|
||||
|
||||
Important paths:
|
||||
- `bootstrap.php` — shared config loader
|
||||
- `config/local.example.php` — copy template
|
||||
- `config/local.php` — real local values, ignored by git
|
||||
- `db/conn.php` — shared DB connection helper
|
||||
- `worker_bulk.php` — CLI worker
|
||||
|
||||
Setup:
|
||||
1. Review `config/README.md`.
|
||||
2. Fill `config/local.php` with local values.
|
||||
3. Import `project/sql/db-25052026.sql` into local MariaDB if needed.
|
||||
4. See `project/sql/README.md` for safe SQL baseline and private data handling.
|
||||
140
project/web/index/new/bootstrap.php
Normal file
140
project/web/index/new/bootstrap.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
if (defined('LEGACY_MODULE_BOOTSTRAPPED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
define('LEGACY_MODULE_BOOTSTRAPPED', true);
|
||||
define('LEGACY_MODULE_ROOT', __DIR__);
|
||||
define('LEGACY_MODULE_CONFIG_DIR', LEGACY_MODULE_ROOT . '/config');
|
||||
define('LEGACY_MODULE_LOCAL_CONFIG', LEGACY_MODULE_CONFIG_DIR . '/local.php');
|
||||
define('LEGACY_MODULE_EXAMPLE_CONFIG', LEGACY_MODULE_CONFIG_DIR . '/local.example.php');
|
||||
|
||||
function legacy_default_config() {
|
||||
return [
|
||||
'db' => [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 3306,
|
||||
'database' => '',
|
||||
'user' => '',
|
||||
'password' => '',
|
||||
'charset' => 'utf8',
|
||||
],
|
||||
'openai' => [
|
||||
'api_key' => '',
|
||||
'model' => 'gpt-4o-mini',
|
||||
'endpoint' => 'https://api.openai.com/v1/chat/completions',
|
||||
],
|
||||
'store' => [
|
||||
'name' => 'Natural - Mercado de Vida',
|
||||
'language_es' => 4,
|
||||
'language_en' => 1,
|
||||
'image_base_url' => 'https://example.local/image/',
|
||||
'product_base_url' => 'https://example.local/index.php?route=product/product&product_id=',
|
||||
],
|
||||
'routes' => [
|
||||
'login_url' => '../login.php',
|
||||
'success_url' => 'https://example.local/producto-nuevo/success.php',
|
||||
],
|
||||
'security' => [
|
||||
'form_password_hash' => '',
|
||||
],
|
||||
'paths' => [
|
||||
'log_dir' => LEGACY_MODULE_ROOT . '/logs',
|
||||
'worker_log' => LEGACY_MODULE_ROOT . '/logs/worker.log',
|
||||
'prompt_en' => LEGACY_MODULE_ROOT . '/inc/prompt_en.md',
|
||||
'prompt_es' => LEGACY_MODULE_ROOT . '/inc/prompt_es.md',
|
||||
],
|
||||
'worker' => [
|
||||
'batch_size' => 20,
|
||||
'min_html_length' => 500,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function legacy_deep_merge(array $base, array $override) {
|
||||
foreach ($override as $key => $value) {
|
||||
if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
|
||||
$base[$key] = legacy_deep_merge($base[$key], $value);
|
||||
continue;
|
||||
}
|
||||
$base[$key] = $value;
|
||||
}
|
||||
return $base;
|
||||
}
|
||||
|
||||
function legacy_config_file_data($path) {
|
||||
if (!is_file($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = require $path;
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
function legacy_normalize_config(array $config) {
|
||||
foreach (['image_base_url', 'product_base_url'] as $key) {
|
||||
if (isset($config['store'][$key])) {
|
||||
$config['store'][$key] = trim((string)$config['store'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['routes']['login_url'])) {
|
||||
$config['routes']['login_url'] = trim((string)$config['routes']['login_url']);
|
||||
}
|
||||
|
||||
if (isset($config['routes']['success_url'])) {
|
||||
$config['routes']['success_url'] = trim((string)$config['routes']['success_url']);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
$legacy_config = legacy_default_config();
|
||||
$legacy_config_source = LEGACY_MODULE_EXAMPLE_CONFIG;
|
||||
|
||||
if (is_file(LEGACY_MODULE_LOCAL_CONFIG)) {
|
||||
$legacy_config = legacy_deep_merge($legacy_config, legacy_config_file_data(LEGACY_MODULE_LOCAL_CONFIG));
|
||||
$legacy_config_source = LEGACY_MODULE_LOCAL_CONFIG;
|
||||
} elseif (is_file(LEGACY_MODULE_EXAMPLE_CONFIG)) {
|
||||
$legacy_config = legacy_deep_merge($legacy_config, legacy_config_file_data(LEGACY_MODULE_EXAMPLE_CONFIG));
|
||||
}
|
||||
|
||||
$legacy_config = legacy_normalize_config($legacy_config);
|
||||
|
||||
function legacy_config_all() {
|
||||
global $legacy_config;
|
||||
return $legacy_config;
|
||||
}
|
||||
|
||||
function legacy_config($path, $default = null) {
|
||||
$value = legacy_config_all();
|
||||
foreach (explode('.', $path) as $segment) {
|
||||
if (!is_array($value) || !array_key_exists($segment, $value)) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$segment];
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
function legacy_config_source() {
|
||||
global $legacy_config_source;
|
||||
return $legacy_config_source;
|
||||
}
|
||||
|
||||
function legacy_new_mysqli() {
|
||||
$host = legacy_config('db.host', '127.0.0.1');
|
||||
$user = legacy_config('db.user', '');
|
||||
$password = legacy_config('db.password', '');
|
||||
$database = legacy_config('db.database', '');
|
||||
$port = (int) legacy_config('db.port', 3306);
|
||||
$charset = legacy_config('db.charset', 'utf8');
|
||||
|
||||
$db = new mysqli($host, $user, $password, $database, $port);
|
||||
if (!$db->connect_errno && $charset) {
|
||||
$db->set_charset($charset);
|
||||
}
|
||||
|
||||
return $db;
|
||||
}
|
||||
17
project/web/index/new/config/README.md
Normal file
17
project/web/index/new/config/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Local config setup
|
||||
|
||||
1. Copy `local.example.php` to `local.php`.
|
||||
2. Fill real local DB, OpenAI, and URL values in `local.php`.
|
||||
3. Keep `local.php` out of git.
|
||||
|
||||
Config keys used by the legacy module:
|
||||
- `db.*`
|
||||
- `openai.*`
|
||||
- `store.*`
|
||||
- `routes.*`
|
||||
- `security.form_password_hash`
|
||||
- `worker.*`
|
||||
|
||||
The module loads:
|
||||
- real local values from `config/local.php`
|
||||
- safe fallback values from `config/local.example.php`
|
||||
35
project/web/index/new/config/local.example.php
Normal file
35
project/web/index/new/config/local.example.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'db' => [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 3306,
|
||||
'database' => 'CHANGE_ME_DB_NAME',
|
||||
'user' => 'CHANGE_ME_DB_USER',
|
||||
'password' => 'CHANGE_ME_DB_PASSWORD',
|
||||
'charset' => 'utf8',
|
||||
],
|
||||
'openai' => [
|
||||
'api_key' => 'CHANGE_ME_OPENAI_API_KEY',
|
||||
'model' => 'gpt-4o-mini',
|
||||
'endpoint' => 'https://api.openai.com/v1/chat/completions',
|
||||
],
|
||||
'store' => [
|
||||
'name' => 'Natural - Mercado de Vida',
|
||||
'language_es' => 4,
|
||||
'language_en' => 1,
|
||||
'image_base_url' => 'https://example.local/image/',
|
||||
'product_base_url' => 'https://example.local/index.php?route=product/product&product_id=',
|
||||
],
|
||||
'routes' => [
|
||||
'login_url' => '../login.php',
|
||||
'success_url' => 'https://example.local/producto-nuevo/success.php',
|
||||
],
|
||||
'security' => [
|
||||
'form_password_hash' => 'CHANGE_ME_FORM_PASSWORD_HASH',
|
||||
],
|
||||
'worker' => [
|
||||
'batch_size' => 20,
|
||||
'min_html_length' => 500,
|
||||
],
|
||||
];
|
||||
396
project/web/index/new/css/custom.css
Normal file
396
project/web/index/new/css/custom.css
Normal file
@@ -0,0 +1,396 @@
|
||||
option:disabled {
|
||||
font-weight: bolder;
|
||||
color: #FF5722;
|
||||
}
|
||||
|
||||
#atrib_zone, #edit_zone {
|
||||
border: #ccc 3px dashed;
|
||||
padding: 15px 15px 0px 15px;
|
||||
}
|
||||
|
||||
.ck-editor__editable_inline {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
input[name=modelo], input[name=url] {
|
||||
pointer-events: none;
|
||||
background: whitesmoke;
|
||||
color: #FF5722;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
#imgPreview {
|
||||
text-align: center;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.infoText {
|
||||
font-size: small;
|
||||
font-weight: bolder;
|
||||
color: #FF5722;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.uploadImg {
|
||||
border: 1px solid #ccc;
|
||||
/*display: inline-block;*/
|
||||
padding: 20px 12px 6px 12px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
height: 37px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#imgElement {
|
||||
max-width: 170px;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
#crearProducto {
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
#loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ia_link {
|
||||
/* display: none; */
|
||||
background-color: #FFC107;
|
||||
border-color: #FFC107;
|
||||
}
|
||||
|
||||
/* Definimos la animación de parpadeo */
|
||||
@keyframes blink {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Aplicamos la animación al elemento */
|
||||
#ia_link {
|
||||
animation: blink 2s ease-in-out infinite;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Agregamos un efecto de transición suave */
|
||||
#ia_link:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Natural - Mercado de Vida
|
||||
Panel de administración SEO y descripciones
|
||||
Complemento para Skeleton Boilerplate
|
||||
============================================================ */
|
||||
|
||||
/* ---------- Variables ---------- */
|
||||
:root {
|
||||
--color-primary: #0074D9;
|
||||
--color-success: #28a745;
|
||||
--color-danger: #dc3545;
|
||||
--color-warning: #ffae00;
|
||||
--color-light: #f4f4f4;
|
||||
--color-muted: #666;
|
||||
}
|
||||
|
||||
/* ---------- Contenedores ---------- */
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 0 50px;
|
||||
}
|
||||
|
||||
/* ---------- Tablas ---------- */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 3px rgba(0,0,0,0.05);
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--color-light);
|
||||
text-align: left;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
/* ---------- Imágenes ---------- */
|
||||
td img {
|
||||
width: 140px;
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
margin: 0 auto 8px;
|
||||
}
|
||||
|
||||
/* ---------- Textos de idiomas ---------- */
|
||||
.lang-title {
|
||||
font-weight: bold;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.lang-section {
|
||||
line-height: 1.5em;
|
||||
font-size: 1rem;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
/* ---------- Badges ---------- */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: .85rem;
|
||||
color: #fff;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.badge.ok {
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.badge.miss {
|
||||
background: var(--color-danger);
|
||||
}
|
||||
|
||||
.badge.warn {
|
||||
background: var(--color-warning);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* ---------- Botones ---------- */
|
||||
.button-primary,
|
||||
button.button-primary {
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Botón de verificación manual */
|
||||
.toggle-btn {
|
||||
background: var(--color-warning);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 6px;
|
||||
transition: all .2s ease-in-out;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: var(--color-success);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Toolbar superior */
|
||||
.toolbar {
|
||||
margin: 10px 0 15px;
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ---------- Caja de ID y estado ---------- */
|
||||
.idbox {
|
||||
font-size: .9rem;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.idbox small {
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-actions a {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
background: #f5f8fb;
|
||||
border: 1px solid #d9e3ec;
|
||||
border-radius: 6px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.table-description {
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.log-panel {
|
||||
margin-top: 20px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.log-panel__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.log-panel__status {
|
||||
font-size: 0.95rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.log-panel__body {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.45;
|
||||
background: #111;
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.pagination-row {
|
||||
margin-top: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagination-row .button {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.pagination-row .button.disabled {
|
||||
pointer-events: none;
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
.pagination-row .pagination-meta {
|
||||
margin: 0 12px;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
/* ---------- Loader ---------- */
|
||||
.loader-container {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 5px solid #eee;
|
||||
border-top: 5px solid var(--color-primary);
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
animation: fadeText 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeText {
|
||||
0%,100% { opacity: 0.2; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ---------- Paginación ---------- */
|
||||
.pagination {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.pagination a {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
margin: 0 4px;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pagination a.disabled {
|
||||
background: #ccc;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pagination span {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (max-width: 768px) {
|
||||
td img { width: 100px; }
|
||||
table, th, td { font-size: 1.3rem; }
|
||||
.toolbar { text-align: center; }
|
||||
.lang-section { font-size: 1.3rem;
|
||||
text-align: justify;}
|
||||
}
|
||||
|
||||
.last {
|
||||
margin: 10px 0px;
|
||||
}
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.checkbox-label .label-body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Alineación vertical suave y estética Skeleton */
|
||||
#missing {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
label[for="missing"] {
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
color: #555;
|
||||
}
|
||||
39
project/web/index/new/css/modal.css
Normal file
39
project/web/index/new/css/modal.css
Normal file
@@ -0,0 +1,39 @@
|
||||
/* The Modal (background) */
|
||||
.modal {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1; /* Sit on top */
|
||||
padding-top: 100px; /* Location of the box */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0,0,0); /* Fallback color */
|
||||
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
max-width: 750px;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
/* The Close Button */
|
||||
.close {
|
||||
color: #aaaaaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
427
project/web/index/new/css/normalize.css
vendored
Normal file
427
project/web/index/new/css/normalize.css
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
||||
427
project/web/index/new/css/skeleton.css
vendored
Normal file
427
project/web/index/new/css/skeleton.css
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
* Skeleton V2.0.4
|
||||
* Copyright 2014, Dave Gamache
|
||||
* www.getskeleton.com
|
||||
* Free to use under the MIT license.
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
* 12/29/2014
|
||||
*/
|
||||
|
||||
|
||||
/* Table of contents
|
||||
––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||
- Grid
|
||||
- Base Styles
|
||||
- Typography
|
||||
- Links
|
||||
- Buttons
|
||||
- Forms
|
||||
- Lists
|
||||
- Code
|
||||
- Tables
|
||||
- Spacing
|
||||
- Utilities
|
||||
- Clearing
|
||||
- Media Queries
|
||||
*/
|
||||
|
||||
|
||||
/* Grid
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box; }
|
||||
.column,
|
||||
.columns {
|
||||
width: 100%;
|
||||
float: left;
|
||||
box-sizing: border-box; }
|
||||
|
||||
/* For devices larger than 400px */
|
||||
@media (min-width: 400px) {
|
||||
.container {
|
||||
width: 85%;
|
||||
padding: 0; }
|
||||
}
|
||||
|
||||
/* For devices larger than 550px */
|
||||
@media (min-width: 550px) {
|
||||
.container {
|
||||
width: 80%; }
|
||||
.column,
|
||||
.columns {
|
||||
margin-left: 4%; }
|
||||
.column:first-child,
|
||||
.columns:first-child {
|
||||
margin-left: 0; }
|
||||
|
||||
.one.column,
|
||||
.one.columns { width: 4.66666666667%; }
|
||||
.two.columns { width: 13.3333333333%; }
|
||||
.three.columns { width: 22%; }
|
||||
.four.columns { width: 30.6666666667%; }
|
||||
.five.columns { width: 39.3333333333%; }
|
||||
.six.columns { width: 48%; }
|
||||
.seven.columns { width: 56.6666666667%; }
|
||||
.eight.columns { width: 65.3333333333%; }
|
||||
.nine.columns { width: 74.0%; }
|
||||
.ten.columns { width: 82.6666666667%; }
|
||||
.eleven.columns { width: 91.3333333333%; }
|
||||
.twelve.columns { width: 100%; margin-left: 0; }
|
||||
|
||||
.one-third.column { width: 30.6666666667%; }
|
||||
.two-thirds.column { width: 65.3333333333%; }
|
||||
|
||||
.one-half.column { width: 48%; }
|
||||
|
||||
/* Offsets */
|
||||
.offset-by-one.column,
|
||||
.offset-by-one.columns { margin-left: 8.66666666667%; }
|
||||
.offset-by-two.column,
|
||||
.offset-by-two.columns { margin-left: 17.3333333333%; }
|
||||
.offset-by-three.column,
|
||||
.offset-by-three.columns { margin-left: 26%; }
|
||||
.offset-by-four.column,
|
||||
.offset-by-four.columns { margin-left: 34.6666666667%; }
|
||||
.offset-by-five.column,
|
||||
.offset-by-five.columns { margin-left: 43.3333333333%; }
|
||||
.offset-by-six.column,
|
||||
.offset-by-six.columns { margin-left: 52%; }
|
||||
.offset-by-seven.column,
|
||||
.offset-by-seven.columns { margin-left: 60.6666666667%; }
|
||||
.offset-by-eight.column,
|
||||
.offset-by-eight.columns { margin-left: 69.3333333333%; }
|
||||
.offset-by-nine.column,
|
||||
.offset-by-nine.columns { margin-left: 78.0%; }
|
||||
.offset-by-ten.column,
|
||||
.offset-by-ten.columns { margin-left: 86.6666666667%; }
|
||||
.offset-by-eleven.column,
|
||||
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
|
||||
|
||||
.offset-by-one-third.column,
|
||||
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
|
||||
.offset-by-two-thirds.column,
|
||||
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
|
||||
|
||||
.offset-by-one-half.column,
|
||||
.offset-by-one-half.columns { margin-left: 52%; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Base Styles
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
/* NOTE
|
||||
html is set to 62.5% so that all the REM measurements throughout Skeleton
|
||||
are based on 10px sizing. So basically 1.5rem = 15px :) */
|
||||
html {
|
||||
font-size: 62.5%; }
|
||||
body {
|
||||
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #222; }
|
||||
|
||||
|
||||
/* Typography
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 300; }
|
||||
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
|
||||
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
|
||||
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
|
||||
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
|
||||
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
|
||||
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
|
||||
|
||||
/* Larger than phablet */
|
||||
@media (min-width: 550px) {
|
||||
h1 { font-size: 5.0rem; }
|
||||
h2 { font-size: 4.2rem; }
|
||||
h3 { font-size: 3.6rem; }
|
||||
h4 { font-size: 3.0rem; }
|
||||
h5 { font-size: 2.4rem; }
|
||||
h6 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0; }
|
||||
|
||||
|
||||
/* Links
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
a {
|
||||
color: #1EAEDB; }
|
||||
a:hover {
|
||||
color: #0FA0CE; }
|
||||
|
||||
|
||||
/* Buttons
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.button,
|
||||
button,
|
||||
input[type="submit"],
|
||||
input[type="reset"],
|
||||
input[type="button"] {
|
||||
display: inline-block;
|
||||
height: 38px;
|
||||
padding: 0 30px;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 38px;
|
||||
letter-spacing: .1rem;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #bbb;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box; }
|
||||
.button:hover,
|
||||
button:hover,
|
||||
input[type="submit"]:hover,
|
||||
input[type="reset"]:hover,
|
||||
input[type="button"]:hover,
|
||||
.button:focus,
|
||||
button:focus,
|
||||
input[type="submit"]:focus,
|
||||
input[type="reset"]:focus,
|
||||
input[type="button"]:focus {
|
||||
color: #333;
|
||||
border-color: #888;
|
||||
outline: 0; }
|
||||
.button.button-primary,
|
||||
button.button-primary,
|
||||
input[type="submit"].button-primary,
|
||||
input[type="reset"].button-primary,
|
||||
input[type="button"].button-primary {
|
||||
color: #FFF;
|
||||
background-color: #70ad47;
|
||||
border-color: #70ad47; }
|
||||
.button.button-primary:hover,
|
||||
button.button-primary:hover,
|
||||
input[type="submit"].button-primary:hover,
|
||||
input[type="reset"].button-primary:hover,
|
||||
input[type="button"].button-primary:hover,
|
||||
.button.button-primary:focus,
|
||||
button.button-primary:focus,
|
||||
input[type="submit"].button-primary:focus,
|
||||
input[type="reset"].button-primary:focus,
|
||||
input[type="button"].button-primary:focus {
|
||||
color: #FFF;
|
||||
background-color: #70ad47de;
|
||||
border-color: #70ad47de; }
|
||||
|
||||
|
||||
/* Forms
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="date"],
|
||||
input[type="search"],
|
||||
input[type="text"],
|
||||
input[type="color"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
input[type="file"],
|
||||
textarea,
|
||||
select {
|
||||
height: 38px;
|
||||
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
|
||||
background-color: #fff;
|
||||
border: 1px solid #D1D1D1;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box; }
|
||||
/* Removes awkward default styles on some inputs for iOS */
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="date"],
|
||||
input[type="search"],
|
||||
input[type="text"],
|
||||
input[type="color"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
input[type="file"],
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none; }
|
||||
textarea {
|
||||
min-height: 65px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px; }
|
||||
input[type="email"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="date"]:focus,
|
||||
input[type="search"]:focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="color"]:focus,
|
||||
input[type="tel"]:focus,
|
||||
input[type="url"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="file"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border: 1px solid #70ad47;
|
||||
outline: 0; }
|
||||
label,
|
||||
legend {
|
||||
display: block;
|
||||
margin-bottom: .5rem;
|
||||
font-weight: 600; }
|
||||
fieldset {
|
||||
padding: 0;
|
||||
border-width: 0; }
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
display: inline; }
|
||||
label > .label-body {
|
||||
display: inline-block;
|
||||
margin-left: .5rem;
|
||||
font-weight: normal; }
|
||||
|
||||
|
||||
/* Lists
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
ul {
|
||||
list-style: circle inside; }
|
||||
ol {
|
||||
list-style: decimal inside; }
|
||||
ol, ul {
|
||||
padding-left: 0;
|
||||
margin-top: 0; }
|
||||
ul ul,
|
||||
ul ol,
|
||||
ol ol,
|
||||
ol ul {
|
||||
margin: 1.5rem 0 1.5rem 3rem;
|
||||
font-size: 90%; }
|
||||
li {
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
|
||||
/* Code
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
code {
|
||||
padding: .2rem .5rem;
|
||||
margin: 0 .2rem;
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
background: #F1F1F1;
|
||||
border: 1px solid #E1E1E1;
|
||||
border-radius: 4px; }
|
||||
pre > code {
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre; }
|
||||
|
||||
|
||||
/* Tables
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
th,
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #E1E1E1; }
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 0; }
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 0; }
|
||||
|
||||
|
||||
/* Spacing
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
button,
|
||||
.button {
|
||||
margin-bottom: 1rem; }
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
fieldset {
|
||||
margin-bottom: 1.5rem; }
|
||||
pre,
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
table,
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
form {
|
||||
margin-bottom: 2.5rem; }
|
||||
|
||||
|
||||
/* Utilities
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.u-full-width {
|
||||
width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.u-max-full-width {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.u-pull-right {
|
||||
float: right; }
|
||||
.u-pull-left {
|
||||
float: left; }
|
||||
|
||||
|
||||
/* Misc
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
hr {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3.5rem;
|
||||
border-width: 0;
|
||||
border-top: 1px solid #E1E1E1; }
|
||||
|
||||
|
||||
/* Clearing
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
|
||||
/* Self Clearing Goodness */
|
||||
.container:after,
|
||||
.row:after,
|
||||
.u-cf {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
|
||||
|
||||
/* Media Queries
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
/*
|
||||
Note: The best way to structure the use of media queries is to create the queries
|
||||
near the relevant code. For example, if you wanted to change the styles for buttons
|
||||
on small devices, paste the mobile query code up in the buttons section and style it
|
||||
there.
|
||||
*/
|
||||
|
||||
|
||||
/* Larger than mobile */
|
||||
@media (min-width: 400px) {}
|
||||
|
||||
/* Larger than phablet (also point when grid becomes active) */
|
||||
@media (min-width: 550px) {}
|
||||
|
||||
/* Larger than tablet */
|
||||
@media (min-width: 750px) {}
|
||||
|
||||
/* Larger than desktop */
|
||||
@media (min-width: 1000px) {}
|
||||
|
||||
/* Larger than Desktop HD */
|
||||
@media (min-width: 1200px) {}
|
||||
187
project/web/index/new/css/slugify.css
Normal file
187
project/web/index/new/css/slugify.css
Normal file
@@ -0,0 +1,187 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
|
||||
@import url('https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');
|
||||
*{ margin: 0; padding: 0;}
|
||||
body{
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-smoothing: antialiased;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 15px;
|
||||
background: #eee;
|
||||
}
|
||||
.intro{
|
||||
background: #fff;
|
||||
padding: 60px 30px;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
.intro h1 {
|
||||
font-size: 18pt;
|
||||
padding-bottom: 15px;
|
||||
|
||||
}
|
||||
.intro p{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action{
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
a.btn {
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
border: 2px solid #666;
|
||||
padding: 10px 15px;
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
a.btn:hover{
|
||||
background: #666;
|
||||
color: #fff;
|
||||
transition: .3s;
|
||||
-webkit-transition: .3s;
|
||||
}
|
||||
.btn:before{
|
||||
font-family: FontAwesome;
|
||||
font-weight: normal;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.github:before{content: "\f09b"}
|
||||
.down:before{content: "\f019"}
|
||||
.back:before{content:"\f112"}
|
||||
.credit{
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
font-size: 9pt;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-top: 40px;
|
||||
|
||||
}
|
||||
.credit span:before{
|
||||
font-family: FontAwesome;
|
||||
color: #e41b17;
|
||||
content: "\f004";
|
||||
|
||||
|
||||
}
|
||||
.credit a{
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
.credit a:hover{
|
||||
color: #1DBF73;
|
||||
}
|
||||
.credit a:hover:after{
|
||||
font-family: FontAwesome;
|
||||
content: "\f08e";
|
||||
font-size: 9pt;
|
||||
position: absolute;
|
||||
margin: 3px;
|
||||
}
|
||||
main{
|
||||
background: #fff;
|
||||
padding:: 20px;
|
||||
|
||||
}
|
||||
|
||||
article li{
|
||||
color: #444;
|
||||
font-size: 15px;
|
||||
margin-left: 33px;
|
||||
line-height: 1.5;
|
||||
padding: 5px;
|
||||
}
|
||||
article h1,
|
||||
article h2,
|
||||
article h3,
|
||||
article h4,
|
||||
article p{
|
||||
padding: 14px;
|
||||
color: #333;
|
||||
}
|
||||
article p{
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 720px){
|
||||
main{
|
||||
max-width: 720px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.set-overlayer,
|
||||
.set-glass,
|
||||
.set-sticky {
|
||||
cursor: pointer;
|
||||
height: 45px;
|
||||
line-height: 45px;
|
||||
padding: 0 15px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
.set-overlayer:after,
|
||||
.set-glass:after,
|
||||
.to-active:after,
|
||||
.set-sticky:after {
|
||||
font-family: FontAwesome;
|
||||
font-size: 18pt;
|
||||
position: relative;
|
||||
float: right;
|
||||
}
|
||||
.set-overlayer:after,
|
||||
.set-glass:after,
|
||||
.set-sticky:after {
|
||||
content: "\f204";
|
||||
transition: .6s;
|
||||
}
|
||||
|
||||
.to-active:after {
|
||||
content: "\f205";
|
||||
color: #008080;
|
||||
transition: .6s;
|
||||
}
|
||||
.set-overlayer,
|
||||
.set-glass,
|
||||
.set-sticky,
|
||||
.source,
|
||||
.theme-tray {
|
||||
margin: 10px;
|
||||
background: #f2f2f2;
|
||||
border-radius: 5px;
|
||||
border: 2px solid #f1f1f1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* Syntax Highlighter*/
|
||||
|
||||
pre.prettyprint {
|
||||
padding: 15px !important;
|
||||
margin: 10px;
|
||||
border: 0 !important;
|
||||
background: #f2f2f2;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.source {
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
code{
|
||||
border:1px solid #ddd;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
11
project/web/index/new/db/conn.php
Normal file
11
project/web/index/new/db/conn.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__) . '/bootstrap.php';
|
||||
|
||||
$con = legacy_new_mysqli();
|
||||
|
||||
if ($con->connect_errno) {
|
||||
echo 'Failed to connect to MySQL.';
|
||||
}
|
||||
|
||||
?>
|
||||
269
project/web/index/new/describe.php
Executable file
269
project/web/index/new/describe.php
Executable file
@@ -0,0 +1,269 @@
|
||||
<?php include ('./inc/header.php'); ?>
|
||||
|
||||
<?php
|
||||
ini_set('max_execution_time', 120);
|
||||
set_time_limit(120);
|
||||
|
||||
$producto = "";
|
||||
$ean = "";
|
||||
$autoGenerar = false;
|
||||
$formato = isset($_POST['formato']) ? $_POST['formato'] : 'plano';
|
||||
|
||||
// Si viene por URL
|
||||
if (isset($_GET['name']) && !empty($_GET['name'])) {
|
||||
$producto = urldecode($_GET['name']);
|
||||
$autoGenerar = true;
|
||||
}
|
||||
|
||||
// Si viene por POST
|
||||
if (isset($_POST['prompt'])) {
|
||||
$producto = trim($_POST['prompt']);
|
||||
$autoGenerar = true;
|
||||
}
|
||||
|
||||
// Detectar EAN (8 a 13 dígitos seguidos)
|
||||
if (preg_match('/\b\d{8,13}\b/', $producto, $matches)) {
|
||||
$ean = $matches[0];
|
||||
}
|
||||
|
||||
if ($autoGenerar && !empty($producto)) {
|
||||
|
||||
// Prompt según formato seleccionado
|
||||
if ($formato === 'plano') {
|
||||
$prompt = "
|
||||
Eres un redactor SEO experto en productos naturales, ecológicos y saludables.
|
||||
Genera una descripción en texto plano sin formato ni negritas ni guiones, lista para pegar en CKEditor.
|
||||
|
||||
Sobre el producto: \"$producto\"
|
||||
|
||||
Instrucciones:
|
||||
1. Prioriza la información del fabricante o distribuidor oficial.
|
||||
2. Si el producto tiene un código EAN, utilízalo para obtener información nutricional en OpenFoodFacts.
|
||||
3. No menciones ni enlaces fuentes externas.
|
||||
4. No inventes datos no verificables.
|
||||
|
||||
Estructura del texto:
|
||||
Descripción:
|
||||
Ingredientes:
|
||||
Información nutricional (por 100 g):
|
||||
Beneficios para la salud:
|
||||
Por qué deberías probarlo:
|
||||
Keywords:
|
||||
|
||||
Reglas:
|
||||
- No uses HTML, JSON ni emojis.
|
||||
- No repitas el nombre del producto en exceso.
|
||||
";
|
||||
} else {
|
||||
$prompt = "
|
||||
Eres un redactor SEO especializado en productos naturales y saludables.
|
||||
Genera una descripción optimizada, estructurada y lista para publicación web.
|
||||
|
||||
Producto: \"$producto\"
|
||||
|
||||
Prioriza datos del fabricante o distribuidor oficial, y si el producto tiene un código EAN, usa OpenFoodFacts para complementar la información nutricional.
|
||||
No incluyas ni menciones enlaces externos, ni nombres de tiendas online o marketplaces.
|
||||
|
||||
Estructura del texto:
|
||||
|
||||
### Descripción
|
||||
Breve texto atractivo sobre origen, uso y beneficios.
|
||||
|
||||
### Ingredientes
|
||||
Lista completa y verificada.
|
||||
|
||||
### Información nutricional (por 100 g)
|
||||
Calorías, grasas, hidratos, azúcares, proteínas, fibra y sal. Indica si provienen del fabricante o fuente pública.
|
||||
|
||||
### Beneficios para la salud
|
||||
Texto breve explicando las propiedades y usos.
|
||||
|
||||
### Por qué deberías probarlo
|
||||
Cierre aspiracional, natural y coherente.
|
||||
|
||||
### Keywords
|
||||
Lista de términos relevantes separados por comas.
|
||||
|
||||
Reglas:
|
||||
- No uses HTML ni JSON.
|
||||
- No uses emojis.
|
||||
- No menciones fuentes ni enlaces externos.
|
||||
";
|
||||
}
|
||||
|
||||
$respuesta = obtener_respuesta($prompt);
|
||||
|
||||
// Detectar enlaces válidos (solo OpenFoodFacts o fabricante)
|
||||
$fuentes = [];
|
||||
|
||||
// --- Verificar si el EAN existe realmente en OpenFoodFacts ---
|
||||
if (!empty($ean)) {
|
||||
$url_off = "https://world.openfoodfacts.org/product/$ean";
|
||||
$headers = @get_headers($url_off);
|
||||
if ($headers && strpos($headers[0], '200') !== false) {
|
||||
$fuentes[] = "Información verificada en OpenFoodFacts: $url_off";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Verificar si existe ficha oficial del fabricante (ejemplo: Terpenic Labs) ---
|
||||
$producto_slug = strtolower(str_replace(' ', '-', $producto));
|
||||
$url_fabricante = "https://www.terpenic.com/product-page/" . urlencode($producto_slug);
|
||||
$headers = @get_headers($url_fabricante);
|
||||
if ($headers && strpos($headers[0], '200') !== false) {
|
||||
$fuentes[] = "Ficha oficial del fabricante: $url_fabricante";
|
||||
}
|
||||
|
||||
// --- Agregar los enlaces válidos al final del texto (copiable) ---
|
||||
if (!empty($fuentes)) {
|
||||
$respuesta .= "\n\n" . implode("\n", $fuentes);
|
||||
}
|
||||
}
|
||||
|
||||
function obtener_respuesta($prompt) {
|
||||
$apiKey = trim((string) legacy_config('openai.api_key', ''));
|
||||
$model = legacy_config('openai.model', 'gpt-4o-mini');
|
||||
$endpoint = legacy_config('openai.endpoint', 'https://api.openai.com/v1/chat/completions');
|
||||
|
||||
if ($apiKey === '' || strpos($apiKey, 'CHANGE_ME_') === 0) {
|
||||
return "⚠️ Configura openai.api_key en config/local.php.";
|
||||
}
|
||||
|
||||
$ch = curl_init($endpoint);
|
||||
$data = array(
|
||||
'model' => $model,
|
||||
'messages' => array(
|
||||
array('role' => 'system', 'content' => 'Eres un redactor SEO experto en e-commerce.'),
|
||||
array('role' => 'user', 'content' => $prompt)
|
||||
),
|
||||
'temperature' => 0.6,
|
||||
'max_tokens' => 1200
|
||||
);
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $apiKey
|
||||
));
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
|
||||
|
||||
$result = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
return "⚠️ Error de conexión: " . curl_error($ch);
|
||||
}
|
||||
|
||||
$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($http_status !== 200) {
|
||||
return "⚠️ Error HTTP $http_status — el servidor no respondió correctamente.";
|
||||
}
|
||||
|
||||
$response = json_decode($result, true);
|
||||
|
||||
if (isset($response['choices'][0]['message']['content'])) {
|
||||
return $response['choices'][0]['message']['content'];
|
||||
} else {
|
||||
return "⚠️ No se pudo generar respuesta. Detalle:\n" . json_encode($response, JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<style>
|
||||
.loader-container {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.loader {
|
||||
border: 5px solid #f3f3f3;
|
||||
border-top: 5px solid #0074D9;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { 0% { transform: rotate(0deg);} 100% { transform: rotate(360deg);} }
|
||||
.loading-text {
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
color: #333;
|
||||
animation: fadeText 1.5s infinite;
|
||||
}
|
||||
@keyframes fadeText {
|
||||
0%,100% {opacity: 0.2;} 50% {opacity: 1;}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<form method="POST" onsubmit="showLoader()">
|
||||
<div class="row" style="margin-top: 20px">
|
||||
<div class="ten columns">
|
||||
<input class="u-full-width" type="text" name="prompt" id="prompt"
|
||||
placeholder="Ingresa el nombre del producto"
|
||||
value="<?php echo htmlspecialchars($producto); ?>">
|
||||
</div>
|
||||
<div class="two columns">
|
||||
<button class="button button-primary u-full-width" type="submit">Generar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px;">
|
||||
<label>
|
||||
<input type="checkbox" name="formato" value="formato" <?php echo ($formato==='formato')?'checked':''; ?>>
|
||||
<span>Con formato para SEO</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="loader" class="loader-container">
|
||||
<div class="loader"></div>
|
||||
<div class="loading-text">Generando contenido...</div>
|
||||
</div>
|
||||
|
||||
<?php if ($autoGenerar && !isset($respuesta)) { ?>
|
||||
<script>document.addEventListener("DOMContentLoaded",()=>{document.getElementById("loader").style.display="flex";});</script>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (isset($respuesta)) { ?>
|
||||
<script>document.addEventListener("DOMContentLoaded",()=>{document.getElementById("loader").style.display="none";});</script>
|
||||
|
||||
<div class="row" style="margin-top:20px;">
|
||||
<div class="twelve columns">
|
||||
<pre id="texto-copiar" style="background:#f9f9f9;padding:15px;border-radius:6px;white-space:pre-wrap;font-size:1rem;"><?php echo htmlspecialchars($respuesta); ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px;">
|
||||
<div class="two columns">
|
||||
<button class="button button-primary u-full-width" onclick="refreshPage()">
|
||||
<i class="fa fa-refresh fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ten columns">
|
||||
<button class="u-full-width" onclick="copiarTexto()">Copiar texto</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showLoader(){document.getElementById("loader").style.display="flex";}
|
||||
function copiarTexto(){
|
||||
var e=document.getElementById("texto-copiar");
|
||||
var t=e.innerText;
|
||||
navigator.clipboard.writeText(t);
|
||||
e.style.backgroundColor="#ffffa0";
|
||||
setTimeout(()=>{e.style.backgroundColor="";alert("Texto copiado al portapapeles");},500);
|
||||
}
|
||||
function refreshPage(){location.reload();}
|
||||
</script>
|
||||
|
||||
<?php include ('./inc/footer.php'); ?>
|
||||
|
||||
BIN
project/web/index/new/images/nopic.png
Normal file
BIN
project/web/index/new/images/nopic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
project/web/index/new/images/rikrdo-white.jpg
Normal file
BIN
project/web/index/new/images/rikrdo-white.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
27
project/web/index/new/inc/atributos.php
Normal file
27
project/web/index/new/inc/atributos.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<fieldset>
|
||||
<?php
|
||||
$atributos = mysqli_query($con," SELECT `oc_attribute_description`.`attribute_id` , `oc_attribute_description`.`language_id` , `oc_attribute_description`.`name` FROM `oc_attribute_description` WHERE `oc_attribute_description`.`language_id` = '4' ORDER BY `oc_attribute_description`.`name`");
|
||||
|
||||
$grupo = 0;
|
||||
$row_count = mysqli_num_rows($atributos);
|
||||
|
||||
//echo $row_count;
|
||||
|
||||
while ($row = mysqli_fetch_assoc($atributos))
|
||||
{
|
||||
if ($grupo == 0) {
|
||||
|
||||
echo '<div class="three columns">';
|
||||
}
|
||||
|
||||
echo '<input type="checkbox" name="atributos[]" value="'. $row['attribute_id'] .'" > ' . ucwords(str_replace("-"," ", $row['name'])) . ' <br>';
|
||||
$grupo++;
|
||||
|
||||
if ($grupo == 4) {
|
||||
|
||||
echo '</div>';
|
||||
$grupo = 0;
|
||||
}
|
||||
}
|
||||
?>
|
||||
</fieldset>
|
||||
184
project/web/index/new/inc/footer.php
Normal file
184
project/web/index/new/inc/footer.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<!-- SCRIPTS JS
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
|
||||
<!-- EDITOR CKEDITOR
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
|
||||
<script src="https://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdn.ckeditor.com/ckeditor5/22.0.0/classic/ckeditor.js"></script>
|
||||
|
||||
<script>
|
||||
ClassicEditor
|
||||
.create( document.querySelector( '#editor' ) )
|
||||
.then( editor => {
|
||||
console.log( editor );
|
||||
} )
|
||||
.catch( error => {
|
||||
console.error( error );
|
||||
} );
|
||||
</script>
|
||||
|
||||
<!-- SEO URL
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<script src="./js/slugify.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$('#slug,#slug-span').slugify('#slug-source');
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- CONVIERTE TEXTO A MAYÚSCULAS
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
|
||||
<script>
|
||||
$(".text-uppercase").keyup(function () {
|
||||
this.value = this.value.toLocaleUpperCase();
|
||||
this.value = this.value.replace(/['"]+/g, '');
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- CONVIERTE TEXTO A MINÚSCULAS
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<script>
|
||||
$(".text-lowercase").keyup(function () {
|
||||
this.value = this.value.toLocaleLowerCase();
|
||||
this.value = this.value.replace(/['"]+/g, '');
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- AJAX - REFRESCA LISTA DE MARCAS
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<script>
|
||||
function refreshBrand()
|
||||
{
|
||||
$.ajax({
|
||||
url: './inc/marcas.php',
|
||||
type: 'post',
|
||||
success: function(data) {
|
||||
$('.newbrand').html(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- VENTANA MODAL CREAR NUEVA MARCA
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
|
||||
<script>
|
||||
// GET THE MODAL
|
||||
var modal = document.getElementById("marcanueva");
|
||||
|
||||
// GET THE BUTTON THAT OPENS THE MODAL
|
||||
var btn = document.getElementById("newbrand");
|
||||
|
||||
// GET THE <SPAN> ELEMENT THAT CLOSES THE MODAL
|
||||
var span = document.getElementsByClassName("close")[0];
|
||||
|
||||
// WHEN THE USER CLICKS THE BUTTON, OPEN THE MODAL
|
||||
btn.onclick = function() {
|
||||
modal.style.display = "block";
|
||||
}
|
||||
|
||||
// WHEN THE USER CLICKS ON <SPAN> (X), CLOSE THE MODAL
|
||||
span.onclick = function() {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
// WHEN THE USER CLICKS ANYWHERE OUTSIDE OF THE MODAL, CLOSE IT
|
||||
window.onclick = function(event) {
|
||||
if (event.target == modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// WHEN THE USER CLICKS THE BUTTON <GUARDAR>, CLOSE THE MODAL
|
||||
function cerrarModal(idModal) {
|
||||
var idModal = idModal;
|
||||
var modala = document.getElementById(idModal);
|
||||
modala.style.display = "none";
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- AJAX - CREAR NUEVA MARCA
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
|
||||
<script>
|
||||
function createBrand()
|
||||
{
|
||||
var nombreMarca= $("#nombreMarca").val();
|
||||
|
||||
$.ajax({
|
||||
url: './inc/newmarca.php',
|
||||
type: 'post',
|
||||
data: "nombreMarca=" + nombreMarca,
|
||||
beforeSend: function(){
|
||||
$('#guardarMarca').hide();
|
||||
$('#loading').show();
|
||||
},
|
||||
complete: function(){
|
||||
$('#loading').hide();
|
||||
$('#guardarMarca').show();
|
||||
cerrarModal('marcanueva');
|
||||
},
|
||||
success: function(data) {
|
||||
refreshBrand();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ENTER - STOP SUBMITTING FORM
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
|
||||
<script>
|
||||
document.getElementById("newproduct").onkeypress = function(e) {
|
||||
var key = e.charCode || e.keyCode || 0;
|
||||
if (key == 13) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- INPUT 'DATE' PARA SAFARI
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/webshim/1.12.4/extras/modernizr-custom.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/webshim/1.12.4/polyfiller.js"></script>
|
||||
|
||||
<script>
|
||||
webshims.setOptions('waitReady', false);
|
||||
webshims.setOptions('forms-ext', {type: 'date'});
|
||||
webshims.setOptions('forms-ext', {type: 'time'});
|
||||
webshims.polyfill('forms forms-ext');
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function setLink() {
|
||||
var nameValue = document.getElementById("slug-source").value;
|
||||
var brandSelect = document.getElementById("marca");
|
||||
var brandValue = brandSelect.options[brandSelect.selectedIndex].text;
|
||||
var url = "./describe.php?name=" + encodeURIComponent(nameValue) + " " + encodeURIComponent(brandValue);
|
||||
document.getElementById("ia_link").href = url;
|
||||
}
|
||||
|
||||
function validateSelect() {
|
||||
var brandSelect = document.getElementById("marca");
|
||||
var ia_link = document.getElementById("ia_link");
|
||||
if (brandSelect.value) {
|
||||
ia_link.style.display = "block";
|
||||
var element = document.getElementById("url_div");
|
||||
element.className = "ten columns";
|
||||
} else {
|
||||
ia_link.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!-- FIN SCRIPTS JS
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
49
project/web/index/new/inc/header.php
Normal file
49
project/web/index/new/inc/header.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__) . '/bootstrap.php';
|
||||
|
||||
session_start();
|
||||
$_SESSION['after_login'] = $_SERVER['REQUEST_URI'];
|
||||
$_SESSION['acceso'] = TRUE;
|
||||
if (empty($_SESSION['logged'])) {
|
||||
header('Location: ' . legacy_config('routes.login_url', '../login.php'));
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/db/conn.php';
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
|
||||
<!-- Basic Page Needs
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<meta charset="utf-8">
|
||||
<title>Nuevo Producto</title>
|
||||
<meta name="description" content="Carga rápida de productos - Natural - Mercado de Vida">
|
||||
<meta name="author" content="rikrdo.es">
|
||||
|
||||
<!-- Mobile Specific Metas
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- FONT
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
|
||||
|
||||
<!-- CSS
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<link rel="stylesheet" href="./css/normalize.css?<?php echo filemtime('./css/normalize.css') ?>" type="text/css">
|
||||
<link rel="stylesheet" href="./css/skeleton.css?<?php echo filemtime('./css/skeleton.css') ?>" type="text/css">
|
||||
<link rel="stylesheet" href="./css/modal.css?<?php echo filemtime('./css/modal.css') ?>" type="text/css">
|
||||
<link rel="stylesheet" href="./css/custom.css?<?php echo filemtime('./css/custom.css') ?>" type="text/css">
|
||||
|
||||
<!-- Favicon
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<link rel="icon" type="image/png" href="../images/favicon.png">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
12
project/web/index/new/inc/marcas.php
Normal file
12
project/web/index/new/inc/marcas.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php require ('../db/conn.php');?>
|
||||
|
||||
<select name="marca" class="u-full-width" id="marca" required>
|
||||
<option value="" selected disabled>MARCA</option>
|
||||
<?php
|
||||
$marcas = mysqli_query($con," SELECT `oc_manufacturer`.`manufacturer_id` , `oc_manufacturer`.`name` FROM `oc_manufacturer` ORDER BY `oc_manufacturer`.`name`");
|
||||
while ($row = mysqli_fetch_assoc($marcas))
|
||||
{
|
||||
echo '<option value="'. $row['manufacturer_id'] .'" >' . $row['name'] . ' </option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
31
project/web/index/new/inc/newmarca.php
Normal file
31
project/web/index/new/inc/newmarca.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
require ('../db/conn.php');
|
||||
|
||||
$nombreMarca=trim($_POST['nombreMarca']);
|
||||
|
||||
setlocale(LC_ALL, 'en_US.UTF8'); // NECESARIO PARA CONVERTIR CARACTERES ESPECIALES AL ALFABETO INGLÉS
|
||||
|
||||
$urlMarca = htmlspecialchars($nombreMarca);
|
||||
$urlMarca = html_entity_decode($urlMarca);
|
||||
$urlMarca = strip_tags($urlMarca);
|
||||
$urlMarca = preg_replace("/[^a-z0-9 ]/i", " ", iconv('UTF-8', 'ASCII//TRANSLIT', $urlMarca)); // ELIMINA CARACTERES ESPECIALES
|
||||
$urlMarca = preg_replace("/\s+/", " ", $urlMarca); // QUITA DOBLE ESPACIO
|
||||
$urlMarca = trim($urlMarca); // QUITA ESPACIO AL INICIO Y AL FINAL
|
||||
$urlMarca = strtolower(str_replace(" ", "-", $urlMarca)); // REEMPLAZA ESPACIO POR GUION
|
||||
|
||||
$insert_manufacturer = "INSERT INTO `oc_manufacturer` (`manufacturer_id`,`name`, `image`, `sort_order`) VALUES (NULL , '". $nombreMarca ."', '', 0)";
|
||||
|
||||
if (mysqli_query($con, $insert_manufacturer))
|
||||
{
|
||||
$last_id = mysqli_insert_id($con);
|
||||
mysqli_query($con, "INSERT INTO `oc_manufacturer_to_store` (`manufacturer_id`, `store_id`) VALUES (". $last_id .", 0)");
|
||||
mysqli_query($con, "INSERT INTO `oc_url_alias` (`url_alias_id`, `query`, `keyword`)
|
||||
VALUES (NULL , 'manufacturer_id=" . $last_id . "' , '" . $urlMarca . "')");
|
||||
}
|
||||
else
|
||||
{
|
||||
echo "<code>Error: " . $insert_manufacturer . "</code><br>" . mysqli_error($con);
|
||||
}
|
||||
|
||||
mysqli_close($con);
|
||||
?>
|
||||
50
project/web/index/new/inc/prompt_en.md
Normal file
50
project/web/index/new/inc/prompt_en.md
Normal file
@@ -0,0 +1,50 @@
|
||||
You are an expert SEO copywriter specialized in natural, organic, and healthy products.
|
||||
Produce an HTML-formatted description that feels authoritative and practical.
|
||||
Use **bold** text and lists when helpful; keep emojis limited to the section labels provided below.
|
||||
|
||||
Product name: "$producto"
|
||||
EAN (if any): "$ean"
|
||||
|
||||
General rules:
|
||||
- Prioritize information from the official manufacturer or distributor.
|
||||
You may consult these sites only as silent references (never mention or link them): nutritienda.com, veritas.es, naturitas.es, iherb.com, dietisur.es, openfoodfacts.org.
|
||||
- Do not invent unverifiable facts. When data is missing, state that it is pending confirmation from the manufacturer.
|
||||
- Only reference external sources if they are the official manufacturer or (when appropriate) OpenFoodFacts.
|
||||
- Avoid `<h1>`, `<h2>`, `<h3>` tags. Stick to `<p>`, `<b>`, `<ul>`, `<li>`, `<h4>`, `<i>`, `<br>`.
|
||||
- Never wrap the answer in Markdown code fences (` ``` `) or any kind of code block; return plain HTML only.
|
||||
- Keep the tone informative, natural, and aligned with *Natural – Mercado de Vida*.
|
||||
- When an EAN is provided, query the OpenFoodFacts API `https://world.openfoodfacts.net/api/v2/product/{EAN}`.
|
||||
* If the response indicates the product is missing or lacks meaningful fields (status ≠ 1 or no name/ingredients/nutrition), do **not** include the OpenFoodFacts link and note that information is pending verification.
|
||||
* Only add the public UI link (`https://world.openfoodfacts.org/product/{EAN}`) when the API returns substantial data for that product.
|
||||
|
||||
Determine the product category before writing:
|
||||
1. **Edible products / beverages / dietary supplements:** include ingredients and nutritional information if available.
|
||||
2. **Topical cosmetics or hygiene items (creams, soaps, etc.):** mention key ingredients or active compounds only when they are typically disclosed; otherwise explain that the composition is pending verification. No nutritional data.
|
||||
3. **Household products (cleaners, detergents, etc.):** focus on functional ingredients or key features; no nutritional information.
|
||||
4. **Accessories, utensils, equipment, grooming tools (e.g., combs, bottles, blenders):** skip ingredients and nutritional information entirely. Highlight materials, design details, usage tips, and care instructions.
|
||||
5. If the product type does not fit any of the above, use judgment and omit sections that are clearly irrelevant.
|
||||
|
||||
Section guidelines (adapt as needed based on the category analysis):
|
||||
|
||||
🪴 **Description:**
|
||||
Concise overview of purpose, origin, and main benefits. Always include this section.
|
||||
|
||||
🌿 **Ingredients / Key components:**
|
||||
- Include only when the product category reasonably has a composition list (foods, supplements, cosmetics, household consumables).
|
||||
- If data is missing, note that it is pending verification.
|
||||
- For accessories or tools, replace this section with **Key Features** describing materials or build qualities.
|
||||
|
||||
🍎 **Nutritional information (per 100 g / per serving):**
|
||||
- Provide full table (calories, fats, carbohydrates, sugars, proteins, fiber, salt) only for edible items or supplements when data exists.
|
||||
- If unavailable, state it is pending verification.
|
||||
- Omit this section entirely for non-ingestible products.
|
||||
|
||||
💚 **Health benefits / Functional benefits:**
|
||||
Explain how the product supports wellbeing, personal care, or practical use. Adjust wording to fit the category.
|
||||
|
||||
✨ **Why you should try it / Usage tips / Care instructions:**
|
||||
Close with an aspirational or practical paragraph encouraging its use, tailored to the product type.
|
||||
|
||||
OpenFoodFacts link:
|
||||
If the item is edible or a supplement **and** the EAN is available, add a final sentence linking to the corresponding OpenFoodFacts page. Do not add this link for non-food items.
|
||||
|
||||
49
project/web/index/new/inc/prompt_es.md
Normal file
49
project/web/index/new/inc/prompt_es.md
Normal file
@@ -0,0 +1,49 @@
|
||||
Eres un redactor SEO experto en productos naturales, ecológicos y saludables.
|
||||
Genera una descripción en HTML que resulte rigurosa y útil para la tienda.
|
||||
|
||||
Nombre del producto: "$producto"
|
||||
EAN (si existe): "$ean"
|
||||
|
||||
Reglas generales:
|
||||
- Prioriza la información del fabricante o distribuidor oficial.
|
||||
Puedes consultar estas webs solo como referencia interna (no las cites ni enlaces): nutritienda.com, veritas.es, naturitas.es, iherb.com, dietisur.es, openfoodfacts.org.
|
||||
- No inventes datos que no puedas respaldar. Si la información falta, indícalo como pendiente de verificación en la web del fabricante.
|
||||
- Solo menciona fuentes externas si son el fabricante u OpenFoodFacts cuando corresponda.
|
||||
- Emplea únicamente etiquetas HTML sencillas: `<p>`, `<b>`, `<ul>`, `<li>`, `<h4>`, `<i>`, `<br>`. Evita `<h1>`, `<h2>`, `<h3>`.
|
||||
- No devuelvas el contenido dentro de bloques de código ni fences Markdown (` ``` `); responde únicamente con HTML plano.
|
||||
- Estilo profesional, cercano y acorde a *Natural – Mercado de Vida*.
|
||||
- Cuando exista EAN, consulta la API `https://world.openfoodfacts.net/api/v2/product/{EAN}`.
|
||||
* Si la respuesta indica que el producto no existe o no aporta datos relevantes (status ≠ 1 o sin nombre/ingredientes/nutrición), no añadas el enlace a OpenFoodFacts e indica que la información está pendiente.
|
||||
* Solo agrega el enlace público (`https://world.openfoodfacts.org/product/{EAN}`) cuando la API devuelva información sustancial del producto.
|
||||
|
||||
Antes de redactar, identifica el tipo de producto:
|
||||
1. **Alimentos, bebidas o complementos alimenticios:** incluye ingredientes y tabla nutricional si están disponibles.
|
||||
2. **Cosmética o higiene (cremas, champús, jabones, etc.):** menciona ingredientes o activos principales solo si suelen publicarse; de lo contrario, explica que la composición está pendiente de verificación. No aportes información nutricional.
|
||||
3. **Limpieza del hogar y detergentes:** describe ingredientes funcionales o características clave; no incluyas datos nutricionales.
|
||||
4. **Accesorios, utensilios, equipamiento, herramientas de cuidado personal (ej. peines, botellas, batidoras):** omite ingredientes e información nutricional. En su lugar, destaca materiales, diseño, uso y mantenimiento.
|
||||
5. Si el producto no encaja en las categorías anteriores, aplica criterio y elimina las secciones que no sean pertinentes.
|
||||
|
||||
Guía de secciones (adáptalas según lo anterior):
|
||||
|
||||
🪴 **Descripción:**
|
||||
Resumen breve sobre finalidad, origen y beneficios principales. Siempre debe aparecer.
|
||||
|
||||
🌿 **Ingredientes / Componentes clave / Características:**
|
||||
- Usa “Ingredientes” solo cuando el producto realmente tenga una lista de composición.
|
||||
- Para accesorios o herramientas, cambia el título por “Características principales” y describe materiales, tecnología o acabados.
|
||||
- Si no hay datos confirmados, indica que están pendientes de verificación.
|
||||
|
||||
🍎 **Información nutricional (por 100 g o por dosis):**
|
||||
- Solo para alimentos o complementos cuando haya datos.
|
||||
- Si la información no está disponible, menciona que falta confirmación.
|
||||
- Elimina por completo esta sección cuando el producto no sea ingerible.
|
||||
|
||||
💚 **Beneficios para la salud / Beneficios funcionales:**
|
||||
Explica cómo contribuye al bienestar, cuidado personal o utilidad práctica según el caso.
|
||||
|
||||
✨ **Por qué deberías probarlo / Consejos de uso / Mantenimiento:**
|
||||
Cierre inspirador o práctico que anime a utilizarlo, adaptado al tipo de producto.
|
||||
|
||||
Enlace a OpenFoodFacts:
|
||||
Si es un producto comestible o suplemento y el EAN está disponible, añade al final una frase con enlace a su ficha en OpenFoodFacts. No incluyas este enlace para artículos no alimentarios.
|
||||
|
||||
190
project/web/index/new/index.php
Normal file
190
project/web/index/new/index.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php include ('./inc/header.php'); ?>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- FORMULARIO
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
|
||||
<form enctype="multipart/form-data" action="<?php echo htmlspecialchars(legacy_config('routes.success_url', '')); ?>" name = "newproduct" id = "newproduct" method = "POST">
|
||||
|
||||
<input name="pwd" type="hidden" value="<?php echo htmlspecialchars(legacy_config('security.form_password_hash', '')); ?>">
|
||||
|
||||
<!-- CODIGO, SEO URL
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div class="row" style = "margin-top: 20px">
|
||||
<div class="ten columns" id = "url_div">
|
||||
<span class="slug-ouput"> <b><input type ="text" name="url" value="" placeholder="SEO URL" class="u-full-width" id="slug" tabindex="-1" /></b></span>
|
||||
</div>
|
||||
<div class="two columns">
|
||||
<a id="ia_link" href="" class="button button-primary u-full-width" target="_blank"><i class="fa fa-lightbulb-o fa-lg" aria-hidden="true"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NOMBRE DEL PRODUCTO
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div class="row" >
|
||||
<div class="nine columns">
|
||||
<label for="slug-source">Nombre del Artículo: </label>
|
||||
<input type ="text" name="nombre" value="" class="u-full-width text-uppercase" id="slug-source" oninput="setLink()" required />
|
||||
</div>
|
||||
<div class="three columns">
|
||||
<label for="slug-source">EAN: </label>
|
||||
<input type ="text" name="ean" value="" class="u-full-width" id="ean" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CATEGORIAS, MARCA
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div class="row">
|
||||
<div class="six columns">
|
||||
<select name="categoria" class="u-full-width" id="categoria" required >
|
||||
<option value="" selected disabled>CATEGORÍA</option>
|
||||
<?php
|
||||
$categorias = mysqli_query($con," SELECT * FROM `oc_category`, `oc_category_description`, `oc_category_to_store` WHERE `oc_category`.`category_id` = `oc_category_description`.`category_id` AND `oc_category_description`.`category_id`= `oc_category_to_store`.`category_id` AND `oc_category_description`.`language_id` = 4 ORDER BY `oc_category_description`.`name` ASC");
|
||||
while ($row = mysqli_fetch_assoc($categorias))
|
||||
{
|
||||
echo '<option value="'. $row['category_id'] .'" >' . $row['name'] . ' </option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="four columns newbrand">
|
||||
<select name="marca" class="u-full-width" id="marca" onchange="validateSelect(),setLink()" required>
|
||||
<option value="" selected disabled>MARCA</option>
|
||||
<?php
|
||||
$marcas = mysqli_query($con," SELECT `oc_manufacturer`.`manufacturer_id` , `oc_manufacturer`.`name` FROM `oc_manufacturer` ORDER BY `oc_manufacturer`.`name`");
|
||||
while ($row = mysqli_fetch_assoc($marcas))
|
||||
{
|
||||
echo '<option value="'. $row['manufacturer_id'] .'" >' . $row['name'] . ' </option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="two columns">
|
||||
<!-- Abre Ventana Modal -->
|
||||
<span class="button button-primary u-full-width" id="newbrand"><i class="fa fa-pencil fa-lg" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PVP, COSTE, IVA
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div class="row">
|
||||
<div class="four columns">
|
||||
<label for="pvp">PVP: </label>
|
||||
<input type ="number" name="pvp" value="" class="u-full-width" id="pvp" step="any" required />
|
||||
</div>
|
||||
<div class="four columns">
|
||||
<label for="coste">Coste: </label>
|
||||
<input type ="number" name="coste" value="" class="u-full-width" id="coste" step="any" required />
|
||||
</div>
|
||||
<div class="four columns">
|
||||
<label for="iva">IVA: </label>
|
||||
<select name="iva" class="u-full-width" id="iva" required >
|
||||
<?php
|
||||
$iva = mysqli_query($con," SELECT `tax_class_id` , `title` FROM `oc_tax_class` ORDER BY `title` ASC");
|
||||
while ($row = mysqli_fetch_assoc($iva))
|
||||
{
|
||||
echo '<option value="'. $row['tax_class_id'] .'" >' . substr($row['title'], 4) . ' </option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CANTIDAD, CADUCIDAD, PESO
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div class="row">
|
||||
<div class="four columns">
|
||||
<label for="cantidad">Cantidad: </label>
|
||||
<input type ="number" name="cantidad" value="" class="u-full-width" id="cantidad" required>
|
||||
</div>
|
||||
<div class="four columns">
|
||||
<label for="caducidad">Caducidad: </label>
|
||||
<input type ="date" name="caducidad" value="" min="<?php echo date('Y-m-d');?>" class="u-full-width" id="caducidad" required />
|
||||
</div>
|
||||
<div class="four columns">
|
||||
<label for="peso">Peso: </label>
|
||||
<select name="peso" class="u-full-width" id="peso" required>
|
||||
<option value="0.100">100gr</option>
|
||||
<option value="0.250" selected>250gr</option>
|
||||
<option value="0.500">500gr</option>
|
||||
<option value="0.750">750gr</option>
|
||||
<option value="1.000">1kg</option>
|
||||
<option value="1.500">1,5kg</option>
|
||||
<option value="2.000">2kg</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FIELDSET ATRIBUTOS
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div class="row" id="atrib_zone">
|
||||
<div class="twelve columns">
|
||||
<?php include ('inc/atributos.php'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDITOR JS
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div class="row" style = "margin-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<textarea name="editor" id="editor" placeholder="Descripcion del producto..." class="u-full-width"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARGAR IMAGEN X URL
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div class="row" style = "margin-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<input type ="text" name="image_path" id="image_path" class="u-full-width" placeholder="Pegar URL de la Imagen" onchange="document.getElementById('imgElement').src = $('#image_path').val()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UPLOAD IMAGEN LOCAL
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div class="row" style = "margin-top: 20px">
|
||||
<div class="four columns" id="imgPreview">
|
||||
<img id="imgElement" alt="Imagen de Producto" src="./images/nopic.png" />
|
||||
</div>
|
||||
<div class="four columns" >
|
||||
<p class="infoText">* Formatos soportados JPG y PNG</p>
|
||||
<label for="imagen" class="uploadImg">
|
||||
<i class="fa fa-cloud-upload"> </i> Cargar Imágen
|
||||
</label>
|
||||
<input type="file" class="u-full-width" name="imagen" id="imagen" onchange="document.getElementById('imgElement').src = window.URL.createObjectURL(this.files[0])">
|
||||
</div>
|
||||
<div class="four columns" >
|
||||
<button type="submit" class="button-primary u-full-width" name="submit" id="crearProducto" >Guardar Producto</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- VENTANA MODAL 'NUEVA MARCA'
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
|
||||
<div id="marcanueva" class="modal">
|
||||
<!-- CONTENIDO VENTANA MODAL -->
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<form enctype="multipart/form-data" action="inc/newmarca.php" method = "POST">
|
||||
<div class="row" >
|
||||
<div class="twelve columns">
|
||||
<input type ="text" name="nombreMarca" value="" class="u-full-width text-uppercase" id="nombreMarca" onfocus="this.value=''" placeholder="Nombre de la Marca" required />
|
||||
</div>
|
||||
<div class="modalfoot" >
|
||||
<button onclick="createBrand();return false" class="button button-primary u-full-width"id="guardarMarca" >Guardar</button>
|
||||
<button class="button button-primary u-full-width" id="loading" disabled><i class="fa fa-spinner fa-spin"></i> Guardando...</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php
|
||||
mysqli_close($con);
|
||||
include ('./inc/footer.php');
|
||||
?>
|
||||
613
project/web/index/new/js/slugify.js
Normal file
613
project/web/index/new/js/slugify.js
Normal file
@@ -0,0 +1,613 @@
|
||||
/*! Slugify - v0.1.0 - 2013-05-22
|
||||
* https://github.com/madflow/jquery-slugify
|
||||
* Copyright (c) 2013 madflow; Licensed MIT */
|
||||
;(function($) {
|
||||
|
||||
$.fn.slugify = function(source, options) {
|
||||
return this.each(function() {
|
||||
var $target = $(this),
|
||||
$source = $(source);
|
||||
|
||||
$target.on('keyup change',function() {
|
||||
if($target.val() !== '' && $target.val() !== undefined) {
|
||||
$target.data('locked', true);
|
||||
} else {
|
||||
$target.data('locked', false);
|
||||
}
|
||||
});
|
||||
|
||||
$source.on('keyup change',function() {
|
||||
if( true === $target.data('locked')) {return;}
|
||||
if($target.is('input') || $target.is('textarea')) {
|
||||
$target.val($.slugify($source.val(), options));
|
||||
} else {
|
||||
$target.text($.slugify($source.val(), options));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Static method.
|
||||
$.slugify = function(sourceString, options) {
|
||||
// Override default options with passed-in options.
|
||||
options = $.extend({}, $.slugify.options, options);
|
||||
sourceString = $.trim(sourceString); // Trim
|
||||
sourceString = sourceString.toLowerCase(); // Lower Case
|
||||
$.each(options.replaceMap, function(key, value) { // Special char map
|
||||
sourceString = sourceString.replace(new RegExp(key, 'g'), value || options.invalid);
|
||||
});
|
||||
return sourceString
|
||||
.replace(/\s+/g, options.whitespace) // Replace whitespace characters
|
||||
.replace(new RegExp('[^a-z0-9 '+ options.whitespace +']', 'g'), options.invalid); // Replace invalid characters
|
||||
};
|
||||
|
||||
// Default options
|
||||
$.slugify.options = {
|
||||
whitespace: '-',
|
||||
invalid: '',
|
||||
replaceMap: {
|
||||
'á': 'a',
|
||||
'Ã ': 'a',
|
||||
'â': 'a',
|
||||
'ä': 'ae',
|
||||
'ã': 'a',
|
||||
'æ': 'ae',
|
||||
'ç': 'c',
|
||||
'é': 'e',
|
||||
'è': 'e',
|
||||
'ê': 'e',
|
||||
'ë': 'e',
|
||||
'ẽ': 'e',
|
||||
'Ã': 'i',
|
||||
'ì': 'i',
|
||||
'î': 'i',
|
||||
'ï': 'i',
|
||||
'Ä©': 'i',
|
||||
'ó': 'o',
|
||||
'ò': 'o',
|
||||
'ô': 'o',
|
||||
'ö': 'oe',
|
||||
'õ': 'o',
|
||||
'Å“': 'oe',
|
||||
'ß': 'ss',
|
||||
'ú': 'u',
|
||||
'ù': 'u',
|
||||
'û': 'u',
|
||||
'ü': 'ue',
|
||||
'Å©': 'u',
|
||||
'ă': 'a',
|
||||
'ắ': 'a',
|
||||
'ằ': 'a',
|
||||
'ẵ': 'a',
|
||||
'ẳ': 'a',
|
||||
'ấ': 'a',
|
||||
'ầ': 'a',
|
||||
'ẫ': 'a',
|
||||
'ẩ': 'a',
|
||||
'ÇŽ': 'a',
|
||||
'Ã¥': 'a',
|
||||
'Ç»': 'a',
|
||||
'ÇŸ': 'a',
|
||||
'ȧ': 'a',
|
||||
'Ç¡': 'a',
|
||||
'Ä…': 'a',
|
||||
'Ä': 'a',
|
||||
'ả': 'a',
|
||||
'È': 'a',
|
||||
'ȃ': 'a',
|
||||
'ạ': 'a',
|
||||
'ặ': 'a',
|
||||
'áº': 'a',
|
||||
'á¸': 'a',
|
||||
'â±¥': 'a',
|
||||
'á¶': 'a',
|
||||
'É': 'a',
|
||||
'É‘': 'a',
|
||||
'ḃ': 'b',
|
||||
'ḅ': 'b',
|
||||
'ḇ': 'b',
|
||||
'Æ€': 'b',
|
||||
'É“': 'b',
|
||||
'ƃ': 'b',
|
||||
'ᵬ': 'b',
|
||||
'á¶€': 'b',
|
||||
'þ': 'b',
|
||||
'ć': 'c',
|
||||
'ĉ': 'c',
|
||||
'Ä': 'c',
|
||||
'Ä‹': 'c',
|
||||
'ḉ': 'c',
|
||||
'ȼ': 'c',
|
||||
'ƈ': 'c',
|
||||
'É•': 'c',
|
||||
'Ä': 'd',
|
||||
'ḋ': 'd',
|
||||
'ḑ': 'd',
|
||||
'á¸': 'd',
|
||||
'ḓ': 'd',
|
||||
'á¸': 'd',
|
||||
'Ä‘': 'd',
|
||||
'É–': 'd',
|
||||
'É—': 'd',
|
||||
'ƌ': 'd',
|
||||
'áµ': 'd',
|
||||
'á¶': 'd',
|
||||
'á¶‘': 'd',
|
||||
'È¡': 'd',
|
||||
'∂': 'd',
|
||||
'Ä•': 'e',
|
||||
'ế': 'e',
|
||||
'á»': 'e',
|
||||
'á»…': 'e',
|
||||
'ể': 'e',
|
||||
'Ä›': 'e',
|
||||
'Ä—': 'e',
|
||||
'È©': 'e',
|
||||
'á¸': 'e',
|
||||
'Ä™': 'e',
|
||||
'Ä“': 'e',
|
||||
'ḗ': 'e',
|
||||
'ḕ': 'e',
|
||||
'ẻ': 'e',
|
||||
'È…': 'e',
|
||||
'ȇ': 'e',
|
||||
'ẹ': 'e',
|
||||
'ệ': 'e',
|
||||
'ḙ': 'e',
|
||||
'ḛ': 'e',
|
||||
'ɇ': 'e',
|
||||
'á¶’': 'e',
|
||||
'ḟ': 'f',
|
||||
'Æ’': 'f',
|
||||
'áµ®': 'f',
|
||||
'á¶‚': 'f',
|
||||
'ǵ': 'g',
|
||||
'ÄŸ': 'g',
|
||||
'Ä': 'g',
|
||||
'ǧ': 'g',
|
||||
'Ä¡': 'g',
|
||||
'Ä£': 'g',
|
||||
'ḡ': 'g',
|
||||
'Ç¥': 'g',
|
||||
'É ': 'g',
|
||||
'ᶃ': 'g',
|
||||
'Ä¥': 'h',
|
||||
'ÈŸ': 'h',
|
||||
'ḧ': 'h',
|
||||
'ḣ': 'h',
|
||||
'ḩ': 'h',
|
||||
'ḥ': 'h',
|
||||
'ḫ': 'h',
|
||||
'ẖ': 'h',
|
||||
'ħ': 'h',
|
||||
'ⱨ': 'h',
|
||||
'Ä': 'i',
|
||||
'Ç': 'i',
|
||||
'ḯ': 'i',
|
||||
'į': 'i',
|
||||
'Ä«': 'i',
|
||||
'ỉ': 'i',
|
||||
'ȉ': 'i',
|
||||
'È‹': 'i',
|
||||
'ị': 'i',
|
||||
'á¸': 'i',
|
||||
'ɨ': 'i',
|
||||
'áµ»': 'i',
|
||||
'á¶–': 'i',
|
||||
'i': 'i',
|
||||
'ı': 'i',
|
||||
'ĵ': 'j',
|
||||
'ɉ': 'j',
|
||||
'ǰ': 'j',
|
||||
'È·': 'j',
|
||||
'Ê': 'j',
|
||||
'ÉŸ': 'j',
|
||||
'Ê„': 'j',
|
||||
'ḱ': 'k',
|
||||
'Ç©': 'k',
|
||||
'Ä·': 'k',
|
||||
'ḳ': 'k',
|
||||
'ḵ': 'k',
|
||||
'Æ™': 'k',
|
||||
'ⱪ': 'k',
|
||||
'á¶„': 'k',
|
||||
'ĺ': 'l',
|
||||
'ľ': 'l',
|
||||
'ļ': 'l',
|
||||
'ḷ': 'l',
|
||||
'ḹ': 'l',
|
||||
'ḽ': 'l',
|
||||
'ḻ': 'l',
|
||||
'Å‚': 'l',
|
||||
'Å€': 'l',
|
||||
'Æš': 'l',
|
||||
'ⱡ': 'l',
|
||||
'É«': 'l',
|
||||
'ɬ': 'l',
|
||||
'á¶…': 'l',
|
||||
'É': 'l',
|
||||
'È´': 'l',
|
||||
'ḿ': 'm',
|
||||
'á¹': 'm',
|
||||
'ṃ': 'm',
|
||||
'ᵯ': 'm',
|
||||
'ᶆ': 'm',
|
||||
'ɱ': 'm',
|
||||
|
||||
'Å„': 'n',
|
||||
'ǹ': 'n',
|
||||
'ň': 'n',
|
||||
'ñ': 'n',
|
||||
'á¹…': 'n',
|
||||
'ņ': 'n',
|
||||
'ṇ': 'n',
|
||||
'ṋ': 'n',
|
||||
'ṉ': 'n',
|
||||
'n̈': 'n',
|
||||
'ɲ': 'n',
|
||||
'Æž': 'n',
|
||||
'Å‹': 'n',
|
||||
'áµ°': 'n',
|
||||
'ᶇ': 'n',
|
||||
'ɳ': 'n',
|
||||
'ȵ': 'n',
|
||||
'Å': 'o',
|
||||
'ố': 'o',
|
||||
'ồ': 'o',
|
||||
'á»—': 'o',
|
||||
'ổ': 'o',
|
||||
'Ç’': 'o',
|
||||
'È«': 'o',
|
||||
'Å‘': 'o',
|
||||
'á¹': 'o',
|
||||
'á¹': 'o',
|
||||
'È': 'o',
|
||||
'ȯ': 'o',
|
||||
'͘o͘': 'o',
|
||||
'ȱ': 'o',
|
||||
'ø': 'o',
|
||||
'Ç¿': 'o',
|
||||
'Ç«': 'o',
|
||||
'Ç': 'o',
|
||||
'Å': 'o',
|
||||
'ṓ': 'o',
|
||||
'ṑ': 'o',
|
||||
'á»': 'o',
|
||||
'È': 'o',
|
||||
'È': 'o',
|
||||
'Æ¡': 'o',
|
||||
'á»›': 'o',
|
||||
'á»': 'o',
|
||||
'ỡ': 'o',
|
||||
'ở': 'o',
|
||||
'ợ': 'o',
|
||||
'á»': 'o',
|
||||
'á»™': 'o',
|
||||
'ɵ': 'o',
|
||||
'É”': 'o',
|
||||
'ṕ': 'p',
|
||||
'á¹—': 'p',
|
||||
'áµ½': 'p',
|
||||
'Æ¥': 'p',
|
||||
'p̃': 'p',
|
||||
'áµ±': 'p',
|
||||
'ᶈ': 'p',
|
||||
'É‹': 'q',
|
||||
'Æ£': 'q',
|
||||
'Ê ': 'q',
|
||||
'Å•': 'r',
|
||||
'Å™': 'r',
|
||||
'á¹™': 'r',
|
||||
'Å—': 'r',
|
||||
'È‘': 'r',
|
||||
'È“': 'r',
|
||||
'á¹›': 'r',
|
||||
'á¹': 'r',
|
||||
'ṟ': 'r',
|
||||
'É': 'r',
|
||||
'ɽ': 'r',
|
||||
'áµ²': 'r',
|
||||
'ᶉ': 'r',
|
||||
'ɼ': 'r',
|
||||
'ɾ': 'r',
|
||||
'áµ³': 'r',
|
||||
'Å›': 's',
|
||||
'á¹¥': 's',
|
||||
'Å': 's',
|
||||
'Å¡': 's',
|
||||
'á¹§': 's',
|
||||
'ṡẛ': 's',
|
||||
'ÅŸ': 's',
|
||||
'á¹£': 's',
|
||||
'ṩ': 's',
|
||||
'È™': 's',
|
||||
's̩': 's',
|
||||
'áµ´': 's',
|
||||
'á¶Š': 's',
|
||||
'Ê‚': 's',
|
||||
'È¿': 's',
|
||||
'Å¥': 't',
|
||||
'ṫ': 't',
|
||||
'Å£': 't',
|
||||
'á¹': 't',
|
||||
'È›': 't',
|
||||
'á¹±': 't',
|
||||
'ṯ': 't',
|
||||
'ŧ': 't',
|
||||
'ⱦ': 't',
|
||||
'Æ': 't',
|
||||
'ʈ': 't',
|
||||
'̈ẗ': 't',
|
||||
'áµµ': 't',
|
||||
'Æ«': 't',
|
||||
'ȶ': 't',
|
||||
'Å': 'u',
|
||||
'Ç”': 'u',
|
||||
'ů': 'u',
|
||||
'ǘ': 'u',
|
||||
'ǜ': 'u',
|
||||
'Çš': 'u',
|
||||
'Ç–': 'u',
|
||||
'ű': 'u',
|
||||
'á¹¹': 'u',
|
||||
'ų': 'u',
|
||||
'Å«': 'u',
|
||||
'á¹»': 'u',
|
||||
'á»§': 'u',
|
||||
'È•': 'u',
|
||||
'È—': 'u',
|
||||
'ư': 'u',
|
||||
'ứ': 'u',
|
||||
'ừ': 'u',
|
||||
'ữ': 'u',
|
||||
'á»': 'u',
|
||||
'á»±': 'u',
|
||||
'ụ': 'u',
|
||||
'á¹³': 'u',
|
||||
'á¹·': 'u',
|
||||
'á¹µ': 'u',
|
||||
'ʉ': 'u',
|
||||
'áµ¾': 'u',
|
||||
'á¶™': 'u',
|
||||
'á¹½': 'v',
|
||||
'ṿ': 'v',
|
||||
'Ê‹': 'v',
|
||||
'ᶌ': 'v',
|
||||
'â±´': 'v',
|
||||
'ẃ': 'w',
|
||||
'áº': 'w',
|
||||
'ŵ': 'w',
|
||||
'ẅ': 'w',
|
||||
'ẇ': 'w',
|
||||
'ẉ': 'w',
|
||||
'ẘ': 'w',
|
||||
'áº': 'x',
|
||||
'ẋ': 'x',
|
||||
'á¶': 'x',
|
||||
'ý': 'y',
|
||||
'ỳ': 'y',
|
||||
'Å·': 'y',
|
||||
'ẙ': 'y',
|
||||
'ÿ': 'y',
|
||||
'ỹ': 'y',
|
||||
'áº': 'y',
|
||||
'ȳ': 'y',
|
||||
'á»·': 'y',
|
||||
'ỵ': 'y',
|
||||
'É': 'y',
|
||||
'Æ´': 'y',
|
||||
'Ê': 'y',
|
||||
'ź': 'z',
|
||||
'ẑ': 'z',
|
||||
'ž': 'z',
|
||||
'ż': 'z',
|
||||
'ẓ': 'z',
|
||||
'ẕ': 'z',
|
||||
'ƶ': 'z',
|
||||
'È¥': 'z',
|
||||
'ⱬ': 'z',
|
||||
'áµ¶': 'z',
|
||||
'á¶Ž': 'z',
|
||||
'Ê': 'z',
|
||||
'Ê‘': 'z',
|
||||
'É€': 'z',
|
||||
'α': 'a',
|
||||
'β': 'b',
|
||||
'γ': 'g',
|
||||
'É£': 'g',
|
||||
'δ': 'd',
|
||||
'ð': 'd',
|
||||
'ε': 'e',
|
||||
'ζ': 'z',
|
||||
'η': 'i',
|
||||
'θ': 'th',
|
||||
'ι': 'i',
|
||||
'κ': 'k',
|
||||
'λ': 'l',
|
||||
'μ': 'm',
|
||||
'µ': 'm',
|
||||
'ν': 'n',
|
||||
'ξ': 'x',
|
||||
'ο': 'o',
|
||||
'Ï€': 'p',
|
||||
'Ï': 'r',
|
||||
'σ': 's',
|
||||
'Ï‚': 's',
|
||||
'Ï„': 't',
|
||||
'Ï…': 'u',
|
||||
'φ': 'f',
|
||||
'χ': 'ch',
|
||||
'ψ': 'ps',
|
||||
'ω': 'o',
|
||||
'á¾³': 'a',
|
||||
'ά': 'a',
|
||||
'á½°': 'a',
|
||||
'á¾´': 'a',
|
||||
'á¾²': 'a',
|
||||
'á¾¶': 'a',
|
||||
'á¾·': 'a',
|
||||
'á¼€': 'a',
|
||||
'á¾€': 'a',
|
||||
'ἄ': 'a',
|
||||
'ᾄ': 'a',
|
||||
'ἂ': 'a',
|
||||
'ᾂ': 'a',
|
||||
'ἆ': 'a',
|
||||
'ᾆ': 'a',
|
||||
'á¼': 'a',
|
||||
'á¾': 'a',
|
||||
'á¼…': 'a',
|
||||
'á¾…': 'a',
|
||||
'ἃ': 'a',
|
||||
'ᾃ': 'a',
|
||||
'ἇ': 'a',
|
||||
'ᾇ': 'a',
|
||||
'á¾±': 'a',
|
||||
'á¾°': 'a',
|
||||
'Î': 'e',
|
||||
'á½²': 'e',
|
||||
'á¼': 'e',
|
||||
'á¼”': 'e',
|
||||
'á¼’': 'e',
|
||||
'ἑ': 'e',
|
||||
'ἕ': 'e',
|
||||
'ἓ': 'e',
|
||||
'ῃ': 'i',
|
||||
'ή': 'i',
|
||||
'á½´': 'i',
|
||||
'á¿„': 'i',
|
||||
'á¿‚': 'i',
|
||||
'ῆ': 'i',
|
||||
'ῇ': 'i',
|
||||
'á¼ ': 'i',
|
||||
'á¾': 'i',
|
||||
'ἤ': 'i',
|
||||
'á¾”': 'i',
|
||||
'á¼¢': 'i',
|
||||
'á¾’': 'i',
|
||||
'ἦ': 'i',
|
||||
'á¾–': 'i',
|
||||
'ἡ': 'i',
|
||||
'ᾑ': 'i',
|
||||
'á¼¥': 'i',
|
||||
'ᾕ': 'i',
|
||||
'á¼£': 'i',
|
||||
'ᾓ': 'i',
|
||||
'á¼§': 'i',
|
||||
'á¾—': 'i',
|
||||
'ί': 'i',
|
||||
'á½¶': 'i',
|
||||
'á¿–': 'i',
|
||||
'á¼°': 'i',
|
||||
'á¼´': 'i',
|
||||
'á¼²': 'i',
|
||||
'á¼¶': 'i',
|
||||
'á¼±': 'i',
|
||||
'á¼µ': 'i',
|
||||
'á¼³': 'i',
|
||||
'á¼·': 'i',
|
||||
'ÏŠ': 'i',
|
||||
'Î': 'i',
|
||||
'á¿’': 'i',
|
||||
'á¿—': 'i',
|
||||
'á¿‘': 'i',
|
||||
'á¿': 'i',
|
||||
'ό': 'o',
|
||||
'ὸ': 'o',
|
||||
'á½€': 'o',
|
||||
'ὄ': 'o',
|
||||
'ὂ': 'o',
|
||||
'á½': 'o',
|
||||
'á½…': 'o',
|
||||
'ὃ': 'o',
|
||||
'Ï': 'u',
|
||||
'ὺ': 'u',
|
||||
'ῦ': 'u',
|
||||
'á½': 'u',
|
||||
'á½”': 'u',
|
||||
'á½’': 'u',
|
||||
'á½–': 'u',
|
||||
'ὑ': 'u',
|
||||
'ὕ': 'u',
|
||||
'ὓ': 'u',
|
||||
'á½—': 'u',
|
||||
'Ï‹': 'u',
|
||||
'ΰ': 'u',
|
||||
'á¿¢': 'u',
|
||||
'á¿§': 'u',
|
||||
'á¿¡': 'u',
|
||||
'á¿ ': 'u',
|
||||
'ῳ': 'o',
|
||||
'ÏŽ': 'o',
|
||||
'á¿´': 'o',
|
||||
'á½¼': 'o',
|
||||
'ῲ': 'o',
|
||||
'á¿¶': 'o',
|
||||
'á¿·': 'o',
|
||||
'á½ ': 'o',
|
||||
'á¾ ': 'o',
|
||||
'ὤ': 'o',
|
||||
'ᾤ': 'o',
|
||||
'á½¢': 'o',
|
||||
'á¾¢': 'o',
|
||||
'ὦ': 'o',
|
||||
'ᾦ': 'o',
|
||||
'ὡ': 'o',
|
||||
'ᾡ': 'o',
|
||||
'á½¥': 'o',
|
||||
'á¾¥': 'o',
|
||||
'á½£': 'o',
|
||||
'á¾£': 'o',
|
||||
'á½§': 'o',
|
||||
'á¾§': 'o',
|
||||
'ῤ': 'r',
|
||||
'á¿¥': 'r',
|
||||
'а': 'a',
|
||||
'б': 'b',
|
||||
'в': 'v',
|
||||
'г': 'g',
|
||||
'д': 'd',
|
||||
'е': 'e',
|
||||
'Ñ‘': 'e',
|
||||
'ж': 'zh',
|
||||
'з': 'z',
|
||||
'и': 'i',
|
||||
'й': 'j',
|
||||
'к': 'k',
|
||||
'л': 'l',
|
||||
'м': 'm',
|
||||
'н': 'n',
|
||||
'о': 'o',
|
||||
'п': 'p',
|
||||
'Ñ€': 'r',
|
||||
'Ñ': 'n',
|
||||
'Ñ‚': 't',
|
||||
'у': 'u',
|
||||
'Ñ„': 'f',
|
||||
'Ñ…': 'h',
|
||||
'ц': 'ts',
|
||||
'ч': 'ch',
|
||||
'ш': 'sh',
|
||||
'щ': 'sh',
|
||||
'ÑŠ': '',
|
||||
'Ñ‹': 'i',
|
||||
'ь': '',
|
||||
'Ñ': 'n',
|
||||
'ÑŽ': 'yu',
|
||||
'Ñ': 'ya',
|
||||
'Ñ–': 'j',
|
||||
'ѳ': 'f',
|
||||
'Ñ£': 'e',
|
||||
'ѵ': 'i',
|
||||
'Ñ•': 'z',
|
||||
'ѯ': 'ks',
|
||||
'ѱ': 'ps',
|
||||
'Ñ¡': 'o',
|
||||
'Ñ«': 'yu',
|
||||
'ѧ': 'ya',
|
||||
'Ñ': 'yu',
|
||||
'Ñ©': 'ya'
|
||||
}
|
||||
};
|
||||
|
||||
}(jQuery));
|
||||
269
project/web/index/new/productos_bulk_update.php
Executable file
269
project/web/index/new/productos_bulk_update.php
Executable file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
ini_set('max_execution_time', 120);
|
||||
set_time_limit(120);
|
||||
|
||||
$LANG_ES = (int) legacy_config('store.language_es', 4);
|
||||
$IMG_BASE = legacy_config('store.image_base_url', 'https://example.local/image/');
|
||||
$LOG_PATH = legacy_config('paths.worker_log', __DIR__ . '/logs/worker.log');
|
||||
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'log_tail') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$limit = isset($_GET['limit']) ? max(10, min(200, (int)$_GET['limit'])) : 80;
|
||||
if (!file_exists($LOG_PATH)) {
|
||||
echo json_encode(['ok' => false, 'message' => 'Aún no hay registros.']);
|
||||
} else {
|
||||
$lines = file($LOG_PATH, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$tail = array_slice($lines, -$limit);
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'updated' => date('H:i:s'),
|
||||
'log' => implode("\n", $tail)
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'show_log') {
|
||||
include('./inc/header.php');
|
||||
$logEndpoint = 'productos_bulk_update.php?action=log_tail&limit=120';
|
||||
echo "<div class='container'>
|
||||
<h3>📊 Progreso del worker</h3>
|
||||
<p>Puedes dejar esta ventana abierta para monitorear la cola en tiempo real.</p>
|
||||
<div class='row'>
|
||||
<div class='twelve columns'>
|
||||
<div class='log-panel'>
|
||||
<div class='log-panel__header'>
|
||||
<strong>Últimas entradas</strong>
|
||||
<span id='log-status' class='log-panel__status'>Actualizando...</span>
|
||||
</div>
|
||||
<pre id='log-viewer' class='log-panel__body'>Cargando log...</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='row'>
|
||||
<div class='twelve columns'>
|
||||
<button onclick=\"window.location.href='productos_bulk_update.php'\" class='button last'>← Volver al listado</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const viewer = document.getElementById('log-viewer');
|
||||
const status = document.getElementById('log-status');
|
||||
const endpoint = '$logEndpoint';
|
||||
const refresh = () => {
|
||||
fetch(endpoint + '&_=' + Date.now(), {cache:'no-store'})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.ok) {
|
||||
viewer.textContent = data.message || 'Sin datos disponibles.';
|
||||
status.textContent = 'Sin registros';
|
||||
return;
|
||||
}
|
||||
viewer.textContent = data.log || 'Sin registros recientes.';
|
||||
status.textContent = 'Actualizado ' + (data.updated || '');
|
||||
viewer.scrollTop = viewer.scrollHeight;
|
||||
})
|
||||
.catch(() => {
|
||||
status.textContent = 'Error al actualizar';
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
setInterval(refresh, 5000);
|
||||
})();
|
||||
</script>";
|
||||
include('./inc/footer.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
include('./inc/header.php');
|
||||
|
||||
$db = legacy_new_mysqli();
|
||||
if ($db->connect_errno) die("❌ Error DB: " . $db->connect_error);
|
||||
|
||||
/* ============================================
|
||||
🧾 Insertar productos seleccionados a la cola
|
||||
============================================ */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['productos'])) {
|
||||
$productos = $_POST['productos'];
|
||||
$inserted = 0;
|
||||
foreach ($productos as $pid) {
|
||||
$pid = (int)$pid;
|
||||
$db->query("INSERT IGNORE INTO oc_product_queue (product_id) VALUES ($pid)");
|
||||
if ($db->affected_rows > 0) {
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
$logEndpoint = 'productos_bulk_update.php?action=log_tail&limit=120';
|
||||
echo "<div class='container'>
|
||||
<h3>✅ $inserted productos añadidos a la cola.</h3>
|
||||
<p>El worker los procesará automáticamente por orden de llegada. Puedes seguir el progreso en tiempo real debajo.</p>
|
||||
<div class='row'>
|
||||
<div class='twelve columns'>
|
||||
<div class='log-panel'>
|
||||
<div class='log-panel__header'>
|
||||
<strong>📊 Log del worker (últimas entradas)</strong>
|
||||
<span id='log-status' class='log-panel__status'>Actualizando...</span>
|
||||
</div>
|
||||
<pre id='log-viewer' class='log-panel__body'>Cargando log...</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='row'>
|
||||
<div class='twelve columns'>
|
||||
<button class='button button-primary' onclick=\"window.location.href='productos_bulk_update.php'\">← Volver</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const viewer = document.getElementById('log-viewer');
|
||||
const status = document.getElementById('log-status');
|
||||
const endpoint = '$logEndpoint';
|
||||
const refresh = () => {
|
||||
fetch(endpoint + '&_=' + Date.now(), {cache: 'no-store'})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.ok) {
|
||||
viewer.textContent = data.message || 'Sin datos disponibles.';
|
||||
status.textContent = 'Sin registros';
|
||||
return;
|
||||
}
|
||||
viewer.textContent = data.log || 'Sin registros recientes.';
|
||||
status.textContent = 'Actualizado ' + (data.updated || '');
|
||||
viewer.scrollTop = viewer.scrollHeight;
|
||||
})
|
||||
.catch(() => {
|
||||
status.textContent = 'Error al actualizar';
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
setInterval(refresh, 5000);
|
||||
})();
|
||||
</script>";
|
||||
include('./inc/footer.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
📋 Listado de productos activos no procesados
|
||||
============================================ */
|
||||
$per_page = isset($_GET['per_page']) ? max(10, min(200, (int)$_GET['per_page'])) : 50;
|
||||
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
||||
|
||||
$sql_total = "
|
||||
SELECT COUNT(*) AS total
|
||||
FROM oc_product p
|
||||
WHERE p.status = 1
|
||||
AND p.product_id NOT IN (SELECT product_id FROM oc_product_queue)";
|
||||
$res_total = $db->query($sql_total);
|
||||
$total_rows = $res_total ? (int)$res_total->fetch_assoc()['total'] : 0;
|
||||
$total_pages = max(1, (int)ceil($total_rows / $per_page));
|
||||
if ($page > $total_pages) {
|
||||
$page = $total_pages;
|
||||
}
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
$q = "
|
||||
SELECT p.product_id, p.image, d1.name, LEFT(d1.description, 250) AS descripcion
|
||||
FROM oc_product p
|
||||
LEFT JOIN oc_product_description d1
|
||||
ON p.product_id=d1.product_id AND d1.language_id=$LANG_ES
|
||||
WHERE p.status = 1
|
||||
AND p.product_id NOT IN (SELECT product_id FROM oc_product_queue)
|
||||
ORDER BY p.product_id ASC
|
||||
LIMIT $per_page OFFSET $offset";
|
||||
$res = $db->query($q);
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h3>🧾 Productos pendientes de optimización SEO</h3>
|
||||
<div class="row">
|
||||
<div class="eight columns">
|
||||
<p>Total pendientes: <strong><?php echo $total_rows; ?></strong><br>
|
||||
Página <strong><?php echo $page; ?></strong> de <strong><?php echo $total_pages; ?></strong></p>
|
||||
</div>
|
||||
<div class="four columns">
|
||||
<form method="GET" class="row u-cf">
|
||||
<div class="six columns">
|
||||
<label for="per_page">products</label>
|
||||
</div>
|
||||
<div class="six columns">
|
||||
<select class="u-full-width" name="per_page" id="per_page" onchange="this.form.submit()">
|
||||
<?php foreach ([25, 50, 100, 150, 200] as $opt): ?>
|
||||
<option value="<?php echo $opt; ?>" <?php echo $opt==$per_page?'selected':''; ?>><?php echo $opt; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" name="page" value="1">
|
||||
<noscript><div class="four columns"><button type="submit" class="button">Actualizar</button></div></noscript>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($total_rows === 0): ?>
|
||||
<div class="row">
|
||||
<div class="twelve columns alert-box">
|
||||
<p>No hay productos pendientes de encolar.</p>
|
||||
<button type="button" class="button" onclick="window.location.href='productos_bulk_update.php?action=show_log'">
|
||||
Ver progreso del worker
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<table class="u-full-width">
|
||||
<thead>
|
||||
<tr><th></th><th>ID</th><th>Imagen</th><th>Nombre</th><th>Descripción</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php while($r=$res->fetch_assoc()){
|
||||
$img=!empty($r['image'])?$IMG_BASE.htmlspecialchars($r['image']):"https://via.placeholder.com/80x80?text=No+Image"; ?>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="productos[]" value="<?php echo $r['product_id']; ?>" checked></td>
|
||||
<td><?php echo $r['product_id']; ?></td>
|
||||
<td><img src="<?php echo $img; ?>" alt=""></td>
|
||||
<td><?php echo htmlspecialchars($r['name']); ?></td>
|
||||
<td><div class="table-description"><?php echo nl2br(strip_tags($r['descripcion'])); ?></div></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<button type="submit" class="button button-primary">➕ Encolar seleccionados</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="current_page" value="<?php echo $page; ?>">
|
||||
<input type="hidden" name="current_per_page" value="<?php echo $per_page; ?>">
|
||||
</form>
|
||||
|
||||
<?php
|
||||
$queryBase = function($targetPage) use ($per_page) {
|
||||
return 'productos_bulk_update.php?page=' . $targetPage . '&per_page=' . $per_page;
|
||||
};
|
||||
?>
|
||||
<div class="row pagination-row">
|
||||
<div class="twelve columns">
|
||||
<?php if($page > 1): ?>
|
||||
<a class="button" href="<?php echo $queryBase($page-1); ?>">← Página anterior</a>
|
||||
<?php else: ?>
|
||||
<span class="button disabled">← Página anterior</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="pagination-meta">Página <?php echo $page; ?> de <?php echo $total_pages; ?></span>
|
||||
|
||||
<?php if($page < $total_pages): ?>
|
||||
<a class="button" href="<?php echo $queryBase($page+1); ?>">Página siguiente →</a>
|
||||
<?php else: ?>
|
||||
<span class="button disabled">Página siguiente →</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('./inc/footer.php'); ?>
|
||||
284
project/web/index/new/productos_modificados.php
Normal file
284
project/web/index/new/productos_modificados.php
Normal file
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
/* ============================
|
||||
productos_modificados.php
|
||||
============================ */
|
||||
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
$LANG_ES = (int) legacy_config('store.language_es', 4);
|
||||
$LANG_EN = (int) legacy_config('store.language_en', 1);
|
||||
$IMG_BASE = legacy_config('store.image_base_url', 'https://example.local/image/');
|
||||
$PRODUCT_BASE = legacy_config('store.product_base_url', 'https://example.local/index.php?route=product/product&product_id=');
|
||||
|
||||
/* ---- Conexión BD ---- */
|
||||
$db = legacy_new_mysqli();
|
||||
if ($db->connect_errno) {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
http_response_code(500);
|
||||
echo "DB Error: " . $db->connect_error;
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ---- AJAX ---- */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Toggle individual
|
||||
if (isset($_POST['pid']) && isset($_POST['toggle'])) {
|
||||
$pid = (int)$_POST['pid'];
|
||||
$state = (int)($_POST['toggle'] ? 1 : 0);
|
||||
$ok = $db->query("UPDATE oc_product_queue SET needs_verify=$state WHERE product_id=$pid");
|
||||
echo json_encode(['ok'=>$ok, 'state'=>$state]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Auto-marcar faltantes
|
||||
if (isset($_POST['autoMark'])) {
|
||||
$ok = $db->query("UPDATE oc_product_queue
|
||||
SET needs_verify=1
|
||||
WHERE processed=1 AND
|
||||
(product_id IN (
|
||||
SELECT p.product_id FROM oc_product p
|
||||
LEFT JOIN oc_product_description d1 ON p.product_id=d1.product_id AND d1.language_id=$LANG_ES
|
||||
LEFT JOIN oc_product_description d2 ON p.product_id=d2.product_id AND d2.language_id=$LANG_EN
|
||||
WHERE (d1.description IS NULL OR d1.description='')
|
||||
OR (d2.description IS NULL OR d2.description='')
|
||||
))");
|
||||
echo json_encode(['ok'=>$ok]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 🔁 Reprocesar Needs Verify
|
||||
if (isset($_POST['reprocessNeeds'])) {
|
||||
$ok = $db->query("UPDATE oc_product_queue
|
||||
SET processed=0, log=NULL
|
||||
WHERE needs_verify=1");
|
||||
echo json_encode(['ok'=>$ok]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Vista ---- */
|
||||
include('./inc/header.php'); // aquí ya se incluye custom.css
|
||||
|
||||
/* ---- Paginación ---- */
|
||||
$per_page = 50;
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
$search = isset($_GET['q']) ? trim($_GET['q']) : '';
|
||||
$filter_missing = isset($_GET['missing']) ? (int)$_GET['missing'] : 0;
|
||||
|
||||
$conditions = ["q.processed=1"];
|
||||
if ($search !== '') {
|
||||
$safe = $db->real_escape_string($search);
|
||||
$conditions[] = "(d1.name LIKE '%$safe%' OR d2.name LIKE '%$safe%')";
|
||||
}
|
||||
if ($filter_missing) {
|
||||
$conditions[] = "(
|
||||
(d1.description IS NULL OR d1.description='')
|
||||
OR (d2.description IS NULL OR d2.description='')
|
||||
OR (d1.meta_description IS NULL OR d1.meta_description='')
|
||||
OR (d2.meta_description IS NULL OR d2.meta_description='')
|
||||
)";
|
||||
}
|
||||
$where = implode(' AND ', $conditions);
|
||||
|
||||
$res_total = $db->query("SELECT COUNT(*) AS t
|
||||
FROM oc_product_queue q
|
||||
JOIN oc_product p ON q.product_id=p.product_id
|
||||
LEFT JOIN oc_product_description d1 ON p.product_id=d1.product_id AND d1.language_id=$LANG_ES
|
||||
LEFT JOIN oc_product_description d2 ON p.product_id=d2.product_id AND d2.language_id=$LANG_EN
|
||||
WHERE $where");
|
||||
$total = $res_total ? (int)$res_total->fetch_assoc()['t'] : 0;
|
||||
$total_pages = max(1, ceil($total / $per_page));
|
||||
|
||||
$sql = "
|
||||
SELECT p.product_id,p.image,
|
||||
d1.description AS descripcion_es,
|
||||
d2.description AS descripcion_en,
|
||||
d1.name AS nombre_es,
|
||||
d2.name AS nombre_en,
|
||||
q.processed_at,q.needs_verify
|
||||
FROM oc_product_queue q
|
||||
JOIN oc_product p ON q.product_id=p.product_id
|
||||
LEFT JOIN oc_product_description d1 ON p.product_id=d1.product_id AND d1.language_id=$LANG_ES
|
||||
LEFT JOIN oc_product_description d2 ON p.product_id=d2.product_id AND d2.language_id=$LANG_EN
|
||||
WHERE $where
|
||||
ORDER BY q.processed_at DESC
|
||||
LIMIT $per_page OFFSET $offset";
|
||||
$res = $db->query($sql);
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h3>🧾 Productos modificados</h3>
|
||||
<p>Total: <b><?php echo $total; ?></b> — Página <b><?php echo $page; ?></b> de <b><?php echo $total_pages; ?></b></p>
|
||||
|
||||
<form method="GET">
|
||||
<div class="row" style="margin-bottom: 0;">
|
||||
<div class="nine columns">
|
||||
<input type="text"
|
||||
id="search-name"
|
||||
name="q"
|
||||
value="<?php echo htmlspecialchars($search); ?>"
|
||||
placeholder="Filtrar por nombre..."
|
||||
class="u-full-width">
|
||||
</div>
|
||||
<div class="three columns">
|
||||
<label for="missing" class="u-pull-right" style="margin-top:8px;">
|
||||
<input type="checkbox"
|
||||
id="missing"
|
||||
name="missing"
|
||||
value="1"
|
||||
<?php echo $filter_missing ? 'checked' : ''; ?>>
|
||||
Missing
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="six columns">
|
||||
<button type="submit" class="button button-primary u-full-width">Aplicar</button>
|
||||
</div>
|
||||
<div class="six columns">
|
||||
<a href="productos_modificados.php" class="button u-full-width">Limpiar</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<div class="row toolbar">
|
||||
<div class="six columns">
|
||||
<button class="button u-full-width" onclick="autoMark()" type="button">⚙️ Auto-marcar faltantes</button>
|
||||
</div>
|
||||
<div class="six columns">
|
||||
<button class="button u-full-width" onclick="reprocessNeeds()" type="button">🔁 Reprocesar Needs Verify</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="u-full-width">
|
||||
<tr><th>Imagen</th><th>Descripciones</th></tr>
|
||||
<?php while($r=$res->fetch_assoc()):
|
||||
$img = !empty($r['image']) ? $IMG_BASE.htmlspecialchars($r['image']) : "https://via.placeholder.com/160";
|
||||
$url = $PRODUCT_BASE.$r['product_id'];
|
||||
$fecha = $r['processed_at']?date('d/m/Y H:i',strtotime($r['processed_at'])):'-';
|
||||
$has_en = !empty(trim($r['descripcion_en']));
|
||||
$has_es = !empty(trim($r['descripcion_es']));
|
||||
$flag_html = $r['needs_verify']
|
||||
? '<span class="badge warn">⚠️ Needs verify</span>'
|
||||
: '<span class="badge ok">✔ Verified</span>';
|
||||
$btn_text = $r['needs_verify'] ? '✅ Mark OK' : '⚠️ Mark Needs Verify';
|
||||
$btn_cls = $r['needs_verify'] ? 'toggle-btn active' : 'toggle-btn';
|
||||
?>
|
||||
<tr>
|
||||
<td rowspan="2" style="text-align:center;width:180px;">
|
||||
<a href="<?php echo $url;?>" target="_blank"><img src="<?php echo $img;?>" alt=""></a>
|
||||
<div class="idbox">
|
||||
ID <?php echo $r['product_id'];?><br>
|
||||
<small><?php echo htmlspecialchars($r['nombre_es'] ?: $r['nombre_en'] ?: ''); ?></small><br>
|
||||
<small><?php echo $fecha;?></small><br>
|
||||
<div id="flag-<?php echo $r['product_id'];?>"><?php echo $flag_html;?></div>
|
||||
<button class="<?php echo $btn_cls;?>" id="btn-<?php echo $r['product_id'];?>"
|
||||
onclick="toggleVerify(<?php echo $r['product_id'];?>)">
|
||||
<?php echo $btn_text;?>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="lang-title">🇬🇧 English
|
||||
<span class="badge <?php echo $has_en?'ok':'miss';?>"><?php echo $has_en?'OK':'Missing';?></span>
|
||||
</div>
|
||||
<div class="lang-section"><?php echo $has_en?$r['descripcion_en']:'<i>No data</i>';?></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="lang-title">🇪🇸 Español
|
||||
<span class="badge <?php echo $has_es?'ok':'miss';?>"><?php echo $has_es?'OK':'Missing';?></span>
|
||||
</div>
|
||||
<div class="lang-section"><?php echo $has_es?$r['descripcion_es']:'<i>No data</i>';?></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endwhile;?>
|
||||
</table>
|
||||
|
||||
<?php
|
||||
$queryBase = function($targetPage) use ($search, $filter_missing) {
|
||||
$params = ['page'=>$targetPage];
|
||||
if ($search !== '') $params['q'] = $search;
|
||||
if ($filter_missing) $params['missing'] = 1;
|
||||
return 'productos_modificados.php?' . http_build_query($params);
|
||||
};
|
||||
?>
|
||||
<div class="pagination">
|
||||
<?php if($page>1):?>
|
||||
<a href="<?php echo $queryBase($page-1);?>">← Prev</a>
|
||||
<?php else:?>
|
||||
<a class="disabled">← Prev</a>
|
||||
<?php endif;?>
|
||||
<span>Página <?php echo $page;?> de <?php echo $total_pages;?></span>
|
||||
<?php if($page<$total_pages):?>
|
||||
<a href="<?php echo $queryBase($page+1);?>">Next →</a>
|
||||
<?php else:?>
|
||||
<a class="disabled">Next →</a>
|
||||
<?php endif;?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleVerify(pid){
|
||||
const btn = document.getElementById('btn-'+pid);
|
||||
const flag = document.getElementById('flag-'+pid);
|
||||
const active = btn.classList.contains('active');
|
||||
const next = active ? 0 : 1;
|
||||
const params = new URLSearchParams({pid:pid, toggle:next});
|
||||
fetch(window.location.pathname+window.location.search,{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body:params.toString()
|
||||
}).then(r=>r.json()).then(j=>{
|
||||
if(!j.ok){alert('Error al actualizar');return;}
|
||||
if(j.state===1){
|
||||
btn.classList.add('active');
|
||||
btn.textContent='✅ Mark OK';
|
||||
flag.innerHTML='<span class="badge warn">⚠️ Needs verify</span>';
|
||||
}else{
|
||||
btn.classList.remove('active');
|
||||
btn.textContent='⚠️ Mark Needs Verify';
|
||||
flag.innerHTML='<span class="badge ok">✔ Verified</span>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function autoMark(){
|
||||
if(!confirm('¿Marcar automáticamente como "Need verification" todos los productos con descripciones faltantes?'))return;
|
||||
fetch('',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body:'autoMark=1'
|
||||
}).then(r=>r.json()).then(j=>{
|
||||
if(j.ok){alert('Productos con descripciones faltantes marcados como Need Verify.');location.reload();}
|
||||
else alert('Error al marcar.');
|
||||
});
|
||||
}
|
||||
|
||||
function reprocessNeeds(){
|
||||
if(!confirm('¿Reprocesar todos los productos marcados como "Needs Verify"?'))return;
|
||||
fetch('',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body:'reprocessNeeds=1'
|
||||
})
|
||||
.then(r=>r.json())
|
||||
.then(j=>{
|
||||
if(j.ok){
|
||||
alert('Los productos "Needs Verify" han sido marcados como pendientes para reprocesar. El worker los regenerará.');
|
||||
location.reload();
|
||||
} else alert('Error al actualizar.');
|
||||
})
|
||||
.catch(()=>alert('Error de red.'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php include('./inc/footer.php'); ?>
|
||||
|
||||
255
project/web/index/new/worker_bulk.php
Executable file
255
project/web/index/new/worker_bulk.php
Executable file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// worker_bulk.php — doble prompt: inglés y español independientes
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
|
||||
date_default_timezone_set('Europe/Madrid');
|
||||
mb_internal_encoding('UTF-8');
|
||||
@ini_set('max_execution_time', '0');
|
||||
@set_time_limit(0);
|
||||
|
||||
/* === CONFIG === */
|
||||
$OPENAI_API_KEY = trim((string) legacy_config('openai.api_key', ''));
|
||||
$OPENAI_MODEL = legacy_config('openai.model', 'gpt-4o-mini');
|
||||
$OPENAI_ENDPOINT = legacy_config('openai.endpoint', 'https://api.openai.com/v1/chat/completions');
|
||||
|
||||
$LANG_ES = (int) legacy_config('store.language_es', 4);
|
||||
$LANG_EN = (int) legacy_config('store.language_en', 1);
|
||||
$STORE_NAME = legacy_config('store.name', 'Natural - Mercado de Vida');
|
||||
|
||||
$LOG_FILE = legacy_config('paths.worker_log', __DIR__ . '/logs/worker.log');
|
||||
$PROMPT_EN_FILE = legacy_config('paths.prompt_en', __DIR__ . '/inc/prompt_en.md');
|
||||
$PROMPT_ES_FILE = legacy_config('paths.prompt_es', __DIR__ . '/inc/prompt_es.md');
|
||||
$BATCH_SIZE = (int) legacy_config('worker.batch_size', 20);
|
||||
$MIN_HTML_LENGTH = (int) legacy_config('worker.min_html_length', 500);
|
||||
|
||||
$SHARD_TOTAL = 1;
|
||||
$SHARD_INDEX = 0;
|
||||
|
||||
if (PHP_SAPI === 'cli' && isset($argv)) {
|
||||
foreach ($argv as $arg) {
|
||||
if (strpos($arg, '--shards=') === 0) {
|
||||
$value = (int)substr($arg, 9);
|
||||
if ($value > 0) $SHARD_TOTAL = min($value, 16); // evita saturar en exceso
|
||||
} elseif (strpos($arg, '--shard=') === 0) {
|
||||
$value = (int)substr($arg, 8);
|
||||
if ($value >= 0) $SHARD_INDEX = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($SHARD_INDEX >= $SHARD_TOTAL) {
|
||||
$SHARD_INDEX = $SHARD_TOTAL - 1;
|
||||
}
|
||||
if ($SHARD_INDEX < 0) $SHARD_INDEX = 0;
|
||||
|
||||
/* === FUNCIONES === */
|
||||
function log_msg($msg) {
|
||||
global $LOG_FILE;
|
||||
$time = date('Y-m-d H:i:s');
|
||||
file_put_contents($LOG_FILE, "[$time] $msg\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
function obtener_respuesta($prompt, $key, $model, $max_tokens = 2000, $retries = 3) {
|
||||
$endpoint = legacy_config('openai.endpoint', 'https://api.openai.com/v1/chat/completions');
|
||||
|
||||
if ($key === '' || strpos($key, 'CHANGE_ME_') === 0) {
|
||||
log_msg('❌ Missing openai.api_key in config/local.php');
|
||||
return '';
|
||||
}
|
||||
|
||||
for ($i = 1; $i <= $retries; $i++) {
|
||||
$ch = curl_init($endpoint);
|
||||
$data = [
|
||||
'model' => $model,
|
||||
'messages' => [['role' => 'user', 'content' => $prompt]],
|
||||
'temperature' => 0.6,
|
||||
'max_tokens' => $max_tokens
|
||||
];
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . trim($key)
|
||||
],
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($data),
|
||||
CURLOPT_TIMEOUT => 180
|
||||
]);
|
||||
$result = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($err) { log_msg("⚠️ cURL error ($i/$retries): $err"); sleep(2); continue; }
|
||||
if ($http !== 200) { log_msg("⚠️ HTTP $http on attempt $i"); sleep(3); continue; }
|
||||
|
||||
$json = json_decode($result, true);
|
||||
$txt = $json['choices'][0]['message']['content'] ?? '';
|
||||
if ($txt && mb_strlen(trim($txt)) > 50) return trim($txt);
|
||||
|
||||
log_msg("⚠️ Empty response attempt $i");
|
||||
sleep(2);
|
||||
}
|
||||
log_msg("❌ No response after $retries attempts");
|
||||
return '';
|
||||
}
|
||||
|
||||
function limpiar_html($t) {
|
||||
if (!$t) return '';
|
||||
|
||||
// 🔧 Quita fences Markdown (```html ... ```)
|
||||
$t = preg_replace('/^```[a-zA-Z]*\s*/m', '', $t);
|
||||
$t = preg_replace('/```$/m', '', $t);
|
||||
$t = preg_replace('/```[\s\S]*?```/', '', $t);
|
||||
|
||||
// Quita h1/h2 pero conserva contenido
|
||||
$t = preg_replace('/<\/?h1[^>]*>/i', '', $t);
|
||||
$t = preg_replace('/<\/?h2[^>]*>/i', '', $t);
|
||||
|
||||
// Convierte div y section a <p>
|
||||
$t = preg_replace('/<\s*div[^>]*>/i', '<p>', $t);
|
||||
$t = preg_replace('/<\s*\/div\s*>/i', '</p>', $t);
|
||||
$t = preg_replace('/<\s*section[^>]*>/i', '<p>', $t);
|
||||
$t = preg_replace('/<\s*\/section\s*>/i', '</p>', $t);
|
||||
|
||||
// Quita scripts y estilos
|
||||
$t = preg_replace('/<script.*?<\/script>/is', '', $t);
|
||||
$t = preg_replace('/<style.*?<\/style>/is', '', $t);
|
||||
|
||||
// Quita markdown residual
|
||||
$t = str_replace('```', '', $t);
|
||||
|
||||
// Limpieza de espacios
|
||||
$t = preg_replace('/[ \t]+/', ' ', $t);
|
||||
$t = preg_replace('/\n{2,}/', "\n", $t);
|
||||
return trim($t);
|
||||
}
|
||||
|
||||
/* Elimina emojis y normaliza espacios */
|
||||
function sanitize_for_db($text) {
|
||||
if ($text === null || $text === '') return '';
|
||||
$text = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $text);
|
||||
$text = preg_replace('/\s+/', ' ', $text);
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
function sentence_case($text) {
|
||||
if (empty($text)) return '';
|
||||
$text = trim(mb_strtolower($text, 'UTF-8'));
|
||||
$first = mb_strtoupper(mb_substr($text, 0, 1, 'UTF-8'), 'UTF-8');
|
||||
return $first . mb_substr($text, 1, null, 'UTF-8');
|
||||
}
|
||||
|
||||
/* === DB === */
|
||||
$db = legacy_new_mysqli();
|
||||
if ($db->connect_errno) { log_msg('❌ DB: ' . $db->connect_error); exit; }
|
||||
|
||||
/* === Prompt base === */
|
||||
if (!file_exists($PROMPT_EN_FILE) || !file_exists($PROMPT_ES_FILE)) {
|
||||
log_msg("❌ Missing prompt files.");
|
||||
exit;
|
||||
}
|
||||
$PROMPT_EN = file_get_contents($PROMPT_EN_FILE);
|
||||
$PROMPT_ES = file_get_contents($PROMPT_ES_FILE);
|
||||
if (trim($PROMPT_EN) === '' || trim($PROMPT_ES) === '') {
|
||||
log_msg("❌ Empty prompt files.");
|
||||
exit;
|
||||
}
|
||||
|
||||
/* === Worker === */
|
||||
$shardLabel = $SHARD_TOTAL > 1 ? " | shard {$SHARD_INDEX}/{$SHARD_TOTAL}" : '';
|
||||
log_msg("🚀 Worker iniciado (modo doble prompt, batch={$BATCH_SIZE}{$shardLabel})");
|
||||
|
||||
$shardFilter = $SHARD_TOTAL > 1 ? " AND MOD(id, {$SHARD_TOTAL}) = {$SHARD_INDEX}" : '';
|
||||
$q = $db->query("SELECT * FROM oc_product_queue WHERE processed=0{$shardFilter} ORDER BY id ASC LIMIT $BATCH_SIZE");
|
||||
if (!$q || $q->num_rows === 0) { log_msg("⏸️ Cola vacía."); exit; }
|
||||
|
||||
while ($row = $q->fetch_assoc()) {
|
||||
$pid = (int)$row['product_id'];
|
||||
log_msg("🔄 Procesando producto $pid...");
|
||||
|
||||
$r = $db->query("
|
||||
SELECT p.ean, d.name
|
||||
FROM oc_product p
|
||||
LEFT JOIN oc_product_description d ON p.product_id=d.product_id AND d.language_id=$LANG_ES
|
||||
WHERE p.product_id=$pid
|
||||
");
|
||||
if (!$r || !$prod = $r->fetch_assoc()) {
|
||||
log_msg("⚠️ Producto $pid no encontrado");
|
||||
$db->query("UPDATE oc_product_queue SET processed=1, log='No encontrado' WHERE product_id=$pid");
|
||||
continue;
|
||||
}
|
||||
|
||||
$producto = $prod['name'];
|
||||
$ean = $prod['ean'];
|
||||
|
||||
// === Prompts personalizados ===
|
||||
$prompt_en = str_replace(['$producto', '$ean'], [$producto, $ean], $PROMPT_EN);
|
||||
$prompt_es = str_replace(['$producto', '$ean'], [$producto, $ean], $PROMPT_ES);
|
||||
|
||||
// === Generar EN ===
|
||||
$raw_en = obtener_respuesta($prompt_en, $OPENAI_API_KEY, $OPENAI_MODEL, 2200);
|
||||
file_put_contents(__DIR__ . "/logs/raw_openai_en_$pid.txt", $raw_en);
|
||||
|
||||
$clean_en = limpiar_html($raw_en);
|
||||
$html_en = sanitize_for_db($clean_en);
|
||||
$meta_en = sanitize_for_db(mb_substr(strip_tags($clean_en), 0, 255, 'UTF-8'));
|
||||
|
||||
// === Generar ES ===
|
||||
$raw_es = obtener_respuesta($prompt_es, $OPENAI_API_KEY, $OPENAI_MODEL, 2200);
|
||||
file_put_contents(__DIR__ . "/logs/raw_openai_es_$pid.txt", $raw_es);
|
||||
|
||||
$clean_es = limpiar_html($raw_es);
|
||||
$html_es = sanitize_for_db($clean_es);
|
||||
$meta_es = sanitize_for_db(mb_substr(strip_tags($clean_es), 0, 255, 'UTF-8'));
|
||||
|
||||
// === Longitud de contenido ===
|
||||
$len_en = mb_strlen($html_en);
|
||||
$len_es = mb_strlen($html_es);
|
||||
file_put_contents(__DIR__ . "/logs/html_debug_$pid.txt",
|
||||
"EN ($len_en):\n$html_en\n\nES ($len_es):\n$html_es"
|
||||
);
|
||||
|
||||
if ($len_en < $MIN_HTML_LENGTH || $len_es < $MIN_HTML_LENGTH) {
|
||||
log_msg("❌ Texto demasiado corto (EN=$len_en / ES=$len_es) PID $pid");
|
||||
$db->query("UPDATE oc_product_queue
|
||||
SET processed=1, processed_at=NOW(), result_en=0, result_es=0, needs_verify=1, log='Texto corto'
|
||||
WHERE product_id=$pid");
|
||||
continue;
|
||||
}
|
||||
|
||||
// === Guardar ===
|
||||
$u_title_en = sentence_case("$producto | $STORE_NAME");
|
||||
$u_h1_en = $producto;
|
||||
$u_h2_en = sentence_case("benefits and properties of $producto");
|
||||
|
||||
$u_title_es = sentence_case("comprar $producto | $STORE_NAME");
|
||||
$u_h1_es = $producto;
|
||||
$u_h2_es = sentence_case("propiedades y beneficios de $producto");
|
||||
|
||||
$stmt = $db->prepare("UPDATE oc_product_description
|
||||
SET description=?, meta_description=?, u_title=?, u_h1=?, u_h2=?
|
||||
WHERE product_id=? AND language_id=?");
|
||||
$stmt->bind_param('ssssssi', $html_en, $meta_en, $u_title_en, $u_h1_en, $u_h2_en, $pid, $LANG_EN);
|
||||
if (!$stmt->execute()) log_msg("❌ Error EN $pid: " . $stmt->error);
|
||||
$stmt->close();
|
||||
|
||||
$stmt = $db->prepare("UPDATE oc_product_description
|
||||
SET description=?, meta_description=?, u_title=?, u_h1=?, u_h2=?
|
||||
WHERE product_id=? AND language_id=?");
|
||||
$stmt->bind_param('ssssssi', $html_es, $meta_es, $u_title_es, $u_h1_es, $u_h2_es, $pid, $LANG_ES);
|
||||
if (!$stmt->execute()) log_msg("❌ Error ES $pid: " . $stmt->error);
|
||||
$stmt->close();
|
||||
|
||||
$db->query("UPDATE oc_product_queue
|
||||
SET processed=1, processed_at=NOW(), result_en=1, result_es=1, needs_verify=0, log='OK doble prompt'
|
||||
WHERE product_id=$pid");
|
||||
|
||||
log_msg("✅ $pid completado EN/ES (len EN=$len_en | ES=$len_es)");
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
log_msg("🏁 Worker finalizado.");
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
# Template core has no hard runtime deps.
|
||||
# Add project-specific dependencies after running ./scripts/start.sh
|
||||
280
scripts/agent_status.py
Executable file
280
scripts/agent_status.py
Executable file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
STATUS_PATH = ROOT / 'work' / 'runtime-status.json'
|
||||
MATRIX_PATH = ROOT / 'harness' / 'agents.matrix.yml'
|
||||
WORKFLOW_PATH = ROOT / 'harness' / 'workflow.stages.yml'
|
||||
ARTIFACTS_DIR = ROOT / 'work' / 'artifacts'
|
||||
VALID_RUNTIME_STATES = {'idle', 'waiting', 'running', 'blocked', 'done'}
|
||||
|
||||
DEFAULT_EMOJIS = {
|
||||
'leader': '🧭',
|
||||
'triager': '🧩',
|
||||
'architect': '🏗️',
|
||||
'implementer': '🛠️',
|
||||
'reviewer': '🔍',
|
||||
'security': '🔒',
|
||||
'qa': '🧪',
|
||||
'documenter': '📚',
|
||||
}
|
||||
|
||||
GATE_FILES = {
|
||||
'reviewer': 'reviewer.json',
|
||||
'security': 'security.json',
|
||||
'qa': 'qa.json',
|
||||
'documenter': 'documenter.md',
|
||||
'publish': 'publish.json',
|
||||
'leader': 'leader-close.json',
|
||||
}
|
||||
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
|
||||
|
||||
|
||||
def load_json(path: Path, default=None):
|
||||
if not path.exists():
|
||||
return default
|
||||
return json.loads(path.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def save_json(path: Path, payload):
|
||||
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
|
||||
|
||||
def load_role_emojis():
|
||||
emojis = dict(DEFAULT_EMOJIS)
|
||||
if not MATRIX_PATH.exists():
|
||||
return emojis
|
||||
current_role = None
|
||||
for line in MATRIX_PATH.read_text(encoding='utf-8').splitlines():
|
||||
match_role = re.match(r'^ ([a-z_]+):\s*$', line)
|
||||
if match_role:
|
||||
current_role = match_role.group(1)
|
||||
continue
|
||||
match_emoji = re.match(r'^\s{4}emoji:\s*["\']?(.*?)["\']?\s*$', line)
|
||||
if match_emoji and current_role:
|
||||
emojis[current_role] = match_emoji.group(1)
|
||||
return emojis
|
||||
|
||||
|
||||
def load_roles():
|
||||
roles = []
|
||||
if not MATRIX_PATH.exists():
|
||||
return roles
|
||||
for line in MATRIX_PATH.read_text(encoding='utf-8').splitlines():
|
||||
match_role = re.match(r'^ ([a-z_]+):\s*$', line)
|
||||
if match_role:
|
||||
roles.append(match_role.group(1))
|
||||
return roles
|
||||
|
||||
|
||||
def load_stage_names():
|
||||
stages = []
|
||||
if not WORKFLOW_PATH.exists():
|
||||
return stages
|
||||
for line in WORKFLOW_PATH.read_text(encoding='utf-8').splitlines():
|
||||
match_stage = re.match(r'^ - name:\s*([a-z_]+)\s*$', line)
|
||||
if match_stage:
|
||||
stages.append(match_stage.group(1))
|
||||
return stages
|
||||
|
||||
|
||||
def default_status():
|
||||
return {
|
||||
'feature_id': None,
|
||||
'stage': 'idle',
|
||||
'agent': 'leader',
|
||||
'action': 'Sin ejecución activa',
|
||||
'state': 'waiting',
|
||||
'next_agent': 'leader',
|
||||
'waiting_for': 'Seleccionar una feature pending y actualizar este estado',
|
||||
'updated_at': now_iso(),
|
||||
'timeline': [],
|
||||
}
|
||||
|
||||
|
||||
def load_status():
|
||||
status = load_json(STATUS_PATH, default_status())
|
||||
base = default_status()
|
||||
for key, value in base.items():
|
||||
status.setdefault(key, value)
|
||||
if not isinstance(status.get('timeline'), list):
|
||||
status['timeline'] = []
|
||||
return status
|
||||
|
||||
|
||||
def gate_status(feature_id):
|
||||
gates = {}
|
||||
if not feature_id:
|
||||
return gates
|
||||
feature_dir = ARTIFACTS_DIR / feature_id
|
||||
for gate, filename in GATE_FILES.items():
|
||||
path = feature_dir / filename
|
||||
if not path.exists():
|
||||
gates[gate] = 'pending'
|
||||
continue
|
||||
if gate == 'documenter':
|
||||
gates[gate] = 'approved'
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding='utf-8'))
|
||||
wanted = 'PUBLISHED' if gate == 'publish' else 'APPROVED'
|
||||
gates[gate] = 'approved' if payload.get('verdict') == wanted else 'present'
|
||||
except Exception:
|
||||
gates[gate] = 'invalid'
|
||||
return gates
|
||||
|
||||
|
||||
def render_gate(gate, state, emojis):
|
||||
icon = {
|
||||
'approved': '✅',
|
||||
'pending': '⏳',
|
||||
'present': '⚠️',
|
||||
'invalid': '❌',
|
||||
}.get(state, '•')
|
||||
label = {
|
||||
'leader': 'close',
|
||||
'documenter': 'docs',
|
||||
'publish': 'publish',
|
||||
}.get(gate, gate)
|
||||
return f"{icon} {emojis.get(gate, '•')} {label}: {state.upper()}"
|
||||
|
||||
|
||||
def validate_runtime_args(args):
|
||||
roles = set(load_roles()) or set(DEFAULT_EMOJIS)
|
||||
stages = set(load_stage_names()) | {'idle'}
|
||||
|
||||
if args.agent is not None and args.agent not in roles:
|
||||
raise SystemExit(f"Invalid agent: {args.agent}. Allowed: {', '.join(sorted(roles))}")
|
||||
if args.next_agent is not None and args.next_agent not in roles:
|
||||
raise SystemExit(f"Invalid next-agent: {args.next_agent}. Allowed: {', '.join(sorted(roles))}")
|
||||
if args.stage is not None and args.stage not in stages:
|
||||
raise SystemExit(f"Invalid stage: {args.stage}. Allowed: {', '.join(sorted(stages))}")
|
||||
if args.state is not None and args.state not in VALID_RUNTIME_STATES:
|
||||
raise SystemExit(f"Invalid state: {args.state}. Allowed: {', '.join(sorted(VALID_RUNTIME_STATES))}")
|
||||
|
||||
|
||||
def show_status():
|
||||
status = load_status()
|
||||
emojis = load_role_emojis()
|
||||
feature_id = status.get('feature_id') or '—'
|
||||
current_agent = status.get('agent', 'leader')
|
||||
next_agent = status.get('next_agent') or '—'
|
||||
gates = gate_status(status.get('feature_id'))
|
||||
|
||||
print('╔══════════════════════════════════════════════════════════════╗')
|
||||
print('║ ARNES · Runtime Status ║')
|
||||
print('╚══════════════════════════════════════════════════════════════╝')
|
||||
print(f"Feature activa : {feature_id}")
|
||||
print(f"Stage actual : {status.get('stage', '—')}")
|
||||
print(f"Agente actual : {emojis.get(current_agent, '•')} {current_agent}")
|
||||
print(f"Acción : {status.get('action', '—')}")
|
||||
print(f"Estado : {status.get('state', '—')}")
|
||||
print(f"Siguiente : {emojis.get(next_agent, '•')} {next_agent}")
|
||||
print(f"Esperando : {status.get('waiting_for', '—')}")
|
||||
print(f"Actualizado : {status.get('updated_at', '—')}")
|
||||
print()
|
||||
print('Gates')
|
||||
if gates:
|
||||
for gate in ['reviewer', 'security', 'qa', 'documenter', 'publish', 'leader']:
|
||||
print(f" {render_gate(gate, gates.get(gate, 'pending'), emojis)}")
|
||||
else:
|
||||
print(' — Sin feature activa —')
|
||||
print()
|
||||
print('Timeline')
|
||||
timeline = status.get('timeline', [])[-8:]
|
||||
if not timeline:
|
||||
print(' — Sin eventos —')
|
||||
return
|
||||
for item in timeline:
|
||||
agent = item.get('agent', 'leader')
|
||||
emoji = emojis.get(agent, '•')
|
||||
ts = item.get('ts', '—')
|
||||
stage = item.get('stage', '—')
|
||||
state = item.get('state', '—')
|
||||
message = item.get('message', '')
|
||||
print(f" - {ts} · {emoji} {agent} · {stage} · {state} · {message}")
|
||||
|
||||
|
||||
def set_status(args):
|
||||
validate_runtime_args(args)
|
||||
status = load_status()
|
||||
if args.feature_id is not None:
|
||||
status['feature_id'] = args.feature_id or None
|
||||
if args.stage is not None:
|
||||
status['stage'] = args.stage
|
||||
if args.agent is not None:
|
||||
status['agent'] = args.agent
|
||||
if args.action is not None:
|
||||
status['action'] = args.action
|
||||
if args.state is not None:
|
||||
status['state'] = args.state
|
||||
if args.next_agent is not None:
|
||||
status['next_agent'] = args.next_agent
|
||||
if args.waiting_for is not None:
|
||||
status['waiting_for'] = args.waiting_for
|
||||
|
||||
status['updated_at'] = now_iso()
|
||||
event_message = args.note or status.get('action') or 'Estado actualizado'
|
||||
status['timeline'].append({
|
||||
'ts': status['updated_at'],
|
||||
'agent': status.get('agent', 'leader'),
|
||||
'stage': status.get('stage', '—'),
|
||||
'state': status.get('state', '—'),
|
||||
'message': event_message,
|
||||
})
|
||||
status['timeline'] = status['timeline'][-20:]
|
||||
save_json(STATUS_PATH, status)
|
||||
show_status()
|
||||
|
||||
|
||||
def reset_status(_args):
|
||||
status = default_status()
|
||||
status['updated_at'] = now_iso()
|
||||
save_json(STATUS_PATH, status)
|
||||
show_status()
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(description='Renderiza y actualiza el estado visible de ARNES.')
|
||||
sub = parser.add_subparsers(dest='command', required=True)
|
||||
|
||||
sub.add_parser('show', help='Muestra el panel visible de estado')
|
||||
|
||||
set_parser = sub.add_parser('set', help='Actualiza el estado runtime y añade evento a timeline')
|
||||
set_parser.add_argument('--feature-id')
|
||||
set_parser.add_argument('--stage')
|
||||
set_parser.add_argument('--agent')
|
||||
set_parser.add_argument('--action')
|
||||
set_parser.add_argument('--state')
|
||||
set_parser.add_argument('--next-agent')
|
||||
set_parser.add_argument('--waiting-for')
|
||||
set_parser.add_argument('--note')
|
||||
|
||||
sub.add_parser('reset', help='Resetea el estado runtime a idle')
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
if args.command == 'show':
|
||||
show_status()
|
||||
elif args.command == 'set':
|
||||
set_status(args)
|
||||
elif args.command == 'reset':
|
||||
reset_status(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
66
scripts/install_into_repo.sh
Executable file
66
scripts/install_into_repo.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SRC_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
TARGET_INPUT="${1:-}"
|
||||
|
||||
if [ -z "$TARGET_INPUT" ]; then
|
||||
echo "Usage: ./scripts/install_into_repo.sh /path/to/target-repo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$TARGET_INPUT"
|
||||
TARGET_ROOT="$(cd "$TARGET_INPUT" && pwd)"
|
||||
|
||||
if [ "$TARGET_ROOT" = "$SRC_ROOT" ]; then
|
||||
echo "Refusing to install ARNES into its own source repository. Use a different project repo."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git -C "$TARGET_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "No git repo detected at target. Initializing git repository..."
|
||||
git -C "$TARGET_ROOT" init >/dev/null
|
||||
fi
|
||||
|
||||
copy_item() {
|
||||
local item="$1"
|
||||
if [ -d "$SRC_ROOT/$item" ]; then
|
||||
mkdir -p "$TARGET_ROOT/$item"
|
||||
cp -R "$SRC_ROOT/$item"/. "$TARGET_ROOT/$item"/
|
||||
else
|
||||
cp "$SRC_ROOT/$item" "$TARGET_ROOT/$item"
|
||||
fi
|
||||
}
|
||||
|
||||
ITEMS=(
|
||||
AGENTS.md
|
||||
AGENTS.local.md.example
|
||||
CHECKPOINTS.md
|
||||
HOWTO.md
|
||||
HOWTO-FEATURE.md
|
||||
README.md
|
||||
TEMPLATE.md
|
||||
Makefile
|
||||
requirements.txt
|
||||
backlog
|
||||
defaults
|
||||
docs
|
||||
features
|
||||
harness
|
||||
platforms
|
||||
project
|
||||
scripts
|
||||
spec
|
||||
starter-pack
|
||||
work
|
||||
)
|
||||
|
||||
for item in "${ITEMS[@]}"; do
|
||||
copy_item "$item"
|
||||
done
|
||||
|
||||
echo "Installed ARNES core into: $TARGET_ROOT"
|
||||
echo "Next steps:"
|
||||
echo " cd $TARGET_ROOT"
|
||||
echo " ./scripts/start.sh"
|
||||
echo " ./scripts/verify.sh"
|
||||
102
scripts/new_ticket.py
Executable file
102
scripts/new_ticket.py
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKLOG = ROOT / 'backlog' / 'features.json'
|
||||
TYPE_CHOICES = ('feature', 'fix', 'bug', 'chore')
|
||||
LEVEL_CHOICES = ('low', 'med', 'high')
|
||||
|
||||
|
||||
def ask(prompt, default=''):
|
||||
value = input(f"{prompt}{' [' + default + ']' if default else ''}: ").strip()
|
||||
return value if value else default
|
||||
|
||||
|
||||
def ask_choice(prompt, choices, default):
|
||||
while True:
|
||||
value = ask(prompt, default).lower()
|
||||
if value in choices:
|
||||
return value
|
||||
print(f"Invalid value. Use one of: {', '.join(choices)}")
|
||||
|
||||
|
||||
def ask_list(prompt, default_csv=''):
|
||||
raw = ask(prompt, default_csv)
|
||||
return [item.strip() for item in raw.split(',') if item.strip()]
|
||||
|
||||
|
||||
def next_id(features):
|
||||
nums = []
|
||||
for feature in features:
|
||||
fid = str(feature.get('id', ''))
|
||||
if fid.startswith('F-') and fid[2:].isdigit():
|
||||
nums.append(int(fid[2:]))
|
||||
return f"F-{(max(nums) + 1) if nums else 1:03d}"
|
||||
|
||||
|
||||
def main():
|
||||
data = json.loads(BACKLOG.read_text(encoding='utf-8'))
|
||||
features = data.get('features', [])
|
||||
|
||||
print('Create ticket (English caveman style).')
|
||||
ticket_type = ask_choice('Type (feature/fix/bug/chore)', TYPE_CHOICES, 'feature')
|
||||
title = ask('Title (short EN)', f'{ticket_type.capitalize()} TODO')
|
||||
problem = ask('Problem (short EN)', 'Need change')
|
||||
goal = ask('Goal (short EN)', 'Make flow better')
|
||||
scope_in = ask_list('Scope IN (comma list EN)', 'Core flow')
|
||||
scope_out = ask_list('Scope OUT (comma list EN)', 'No redesign')
|
||||
risk = ask_choice('Risk (low/med/high)', LEVEL_CHOICES, 'low')
|
||||
priority = ask_choice('Priority (low/med/high)', LEVEL_CHOICES, 'med')
|
||||
|
||||
print('Acceptance bullets (EN caveman). Empty line to end.')
|
||||
acceptance = []
|
||||
while True:
|
||||
line = input('- ').strip()
|
||||
if not line:
|
||||
break
|
||||
acceptance.append(line)
|
||||
|
||||
if not acceptance:
|
||||
acceptance = [
|
||||
'Flow works end to end',
|
||||
'No break old behavior',
|
||||
'verify.sh is green',
|
||||
]
|
||||
|
||||
fid = next_id(features)
|
||||
desc = (
|
||||
f"Problem: {problem}. "
|
||||
f"Goal: {goal}. "
|
||||
f"Scope IN: {', '.join(scope_in) or 'none'}. "
|
||||
f"Scope OUT: {', '.join(scope_out) or 'none'}. "
|
||||
f"Type: {ticket_type}. Priority: {priority}. Risk: {risk}."
|
||||
)
|
||||
|
||||
features.append({
|
||||
'id': fid,
|
||||
'type': ticket_type,
|
||||
'title': title,
|
||||
'problem': problem,
|
||||
'goal': goal,
|
||||
'scope_in': scope_in,
|
||||
'scope_out': scope_out,
|
||||
'priority': priority,
|
||||
'risk': risk,
|
||||
'description': desc,
|
||||
'acceptance': acceptance,
|
||||
'status': 'pending',
|
||||
'created_at': str(date.today()),
|
||||
'gates': {'review': False, 'security': False, 'qa': False},
|
||||
})
|
||||
|
||||
data['features'] = features
|
||||
rules = data.setdefault('rules', {})
|
||||
rules.setdefault('valid_types', list(TYPE_CHOICES))
|
||||
BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
print(f'Created {fid}: {title}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
133
scripts/publish_ticket.py
Executable file
133
scripts/publish_ticket.py
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKLOG = ROOT / 'backlog' / 'features.json'
|
||||
ARTIFACTS = ROOT / 'work' / 'artifacts'
|
||||
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
|
||||
|
||||
|
||||
def run_git(*args, check=True):
|
||||
result = subprocess.run(
|
||||
['git', *args],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
msg = result.stderr.strip() or result.stdout.strip() or f'git command failed: {args}'
|
||||
raise SystemExit(msg)
|
||||
return result
|
||||
|
||||
|
||||
def ensure_git_repo():
|
||||
result = run_git('rev-parse', '--is-inside-work-tree', check=False)
|
||||
if result.returncode != 0 or result.stdout.strip() != 'true':
|
||||
raise SystemExit('This project is not inside a git repository. Run git init or clone a repo first.')
|
||||
|
||||
|
||||
def load_backlog():
|
||||
return json.loads(BACKLOG.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def find_feature(feature_id):
|
||||
data = load_backlog()
|
||||
for feature in data.get('features', []):
|
||||
if str(feature.get('id')) == feature_id:
|
||||
return feature
|
||||
raise SystemExit(f'Feature not found in backlog: {feature_id}')
|
||||
|
||||
|
||||
def current_branch():
|
||||
branch = run_git('symbolic-ref', '--quiet', '--short', 'HEAD', check=False).stdout.strip()
|
||||
if not branch:
|
||||
branch = run_git('rev-parse', '--abbrev-ref', 'HEAD', check=False).stdout.strip()
|
||||
if not branch or branch == 'HEAD':
|
||||
raise SystemExit('Detached HEAD is not supported for publish. Checkout a branch first.')
|
||||
return branch
|
||||
|
||||
|
||||
def ensure_remote(remote):
|
||||
remotes = [line.strip() for line in run_git('remote').stdout.splitlines() if line.strip()]
|
||||
if remote not in remotes:
|
||||
raise SystemExit(f'Remote not found: {remote}. Add it first with git remote add {remote} <url>.')
|
||||
|
||||
|
||||
def status_porcelain():
|
||||
return run_git('status', '--porcelain').stdout.strip()
|
||||
|
||||
|
||||
def default_commit_message(feature):
|
||||
feature_id = feature['id']
|
||||
ticket_type = feature.get('type')
|
||||
title = str(feature.get('title', '')).strip()
|
||||
if ticket_type:
|
||||
return f'{feature_id} {ticket_type}: {title}'
|
||||
return f'{feature_id}: {title}'
|
||||
|
||||
|
||||
def write_publish_artifact(feature_id, payload):
|
||||
feature_dir = ARTIFACTS / feature_id
|
||||
feature_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = feature_dir / 'publish.json'
|
||||
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
return path
|
||||
|
||||
|
||||
def ensure_git_identity():
|
||||
name = run_git('config', 'user.name', check=False).stdout.strip()
|
||||
email = run_git('config', 'user.email', check=False).stdout.strip()
|
||||
if not name or not email:
|
||||
raise SystemExit('Missing git identity. Configure git user.name and user.email before publish.')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Commit and push one ticket, then write publish.json evidence.')
|
||||
parser.add_argument('--feature-id', required=True)
|
||||
parser.add_argument('--remote', default='origin')
|
||||
parser.add_argument('--branch', default='')
|
||||
parser.add_argument('--commit-message', default='')
|
||||
args = parser.parse_args()
|
||||
|
||||
ensure_git_repo()
|
||||
ensure_git_identity()
|
||||
feature = find_feature(args.feature_id)
|
||||
remote = args.remote.strip() or 'origin'
|
||||
branch = args.branch.strip() or current_branch()
|
||||
ensure_remote(remote)
|
||||
|
||||
if not status_porcelain():
|
||||
raise SystemExit('No git changes to publish. Nothing to commit.')
|
||||
|
||||
commit_message = args.commit_message.strip() or default_commit_message(feature)
|
||||
payload = {
|
||||
'agent': 'leader',
|
||||
'verdict': 'PUBLISHED',
|
||||
'feature_id': args.feature_id,
|
||||
'branch': branch,
|
||||
'remote': remote,
|
||||
'message': commit_message,
|
||||
'pushed': True,
|
||||
'published_at': now_iso(),
|
||||
'note': 'This artifact is committed inside the publish commit for this ticket.'
|
||||
}
|
||||
artifact_path = write_publish_artifact(args.feature_id, payload)
|
||||
|
||||
run_git('add', '-A')
|
||||
if not status_porcelain():
|
||||
raise SystemExit('No staged git changes after git add -A. Nothing to commit.')
|
||||
|
||||
run_git('commit', '-m', commit_message)
|
||||
run_git('push', remote, branch)
|
||||
print(f'done -> {artifact_path}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
215
scripts/start.sh
Executable file
215
scripts/start.sh
Executable file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
ask() {
|
||||
local prompt="$1"; local def="${2:-}"; local val
|
||||
if [ -n "$def" ]; then
|
||||
read -r -p "$prompt [$def]: " val || true
|
||||
echo "${val:-$def}"
|
||||
else
|
||||
read -r -p "$prompt: " val || true
|
||||
echo "$val"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== ARNES start wizard ==="
|
||||
echo "Mode: use this template in a new repo or copy core ARNES into an existing repo."
|
||||
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "No git repo detected. Initializing local git repository..."
|
||||
git init >/dev/null
|
||||
fi
|
||||
|
||||
PROJECT_NAME="$(ask 'Project name' 'my-project')"
|
||||
PROJECT_DESC="$(ask 'Project description' 'Project using ARNES template')"
|
||||
APP_DIR="$(ask 'App directory (relative)' 'project')"
|
||||
|
||||
STACK_CHOICE="$(ask 'Stack preset (1=default Flask+MariaDB+Skeleton, 2=custom)' '1')"
|
||||
if [ "$STACK_CHOICE" = "2" ]; then
|
||||
BACKEND="$(ask 'Backend stack' 'python/flask')"
|
||||
DB="$(ask 'Database' 'mariadb')"
|
||||
CSSFW="$(ask 'CSS framework' 'skeleton')"
|
||||
else
|
||||
BACKEND="python/flask"
|
||||
DB="mariadb"
|
||||
CSSFW="skeleton"
|
||||
fi
|
||||
|
||||
DEFAULT_TEST_CMD="echo TODO-set-test-command"
|
||||
if [ "$BACKEND" = "python/flask" ]; then
|
||||
DEFAULT_TEST_CMD="python3 -m unittest discover -s $APP_DIR/tests -v"
|
||||
fi
|
||||
|
||||
TEST_CMD="$(ask 'Test command' "$DEFAULT_TEST_CMD")"
|
||||
LINT_CMD="$(ask 'Lint command (optional)' '')"
|
||||
MODEL_MODE="$(ask 'Model mode (lean/balanced/power)' 'lean')"
|
||||
ADD_BOOTSTRAP="$(ask 'Create bootstrap ticket F-001 now? (y/n)' 'y')"
|
||||
|
||||
mkdir -p "$APP_DIR"
|
||||
[ -f "$APP_DIR/README.md" ] || cat > "$APP_DIR/README.md" <<EOF
|
||||
# Project code
|
||||
|
||||
This directory holds the real project code.
|
||||
Configured by ARNES start wizard.
|
||||
EOF
|
||||
|
||||
if [ "$BACKEND" = "python/flask" ]; then
|
||||
mkdir -p "$APP_DIR/templates" "$APP_DIR/static/js" "$APP_DIR/static/css" "$APP_DIR/static/images" "$APP_DIR/tests"
|
||||
[ -f "$APP_DIR/tests/test_bootstrap.py" ] || cat > "$APP_DIR/tests/test_bootstrap.py" <<'PY'
|
||||
import unittest
|
||||
|
||||
|
||||
class BootstrapSmokeTest(unittest.TestCase):
|
||||
def test_bootstrap(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
PY
|
||||
fi
|
||||
|
||||
if [ "$CSSFW" = "skeleton" ]; then
|
||||
mkdir -p "$APP_DIR/static/css" "$APP_DIR/static/images"
|
||||
[ -f "$APP_DIR/static/css/normalize.css" ] || cp defaults/flask-skeleton/static/css/normalize.css "$APP_DIR/static/css/normalize.css"
|
||||
[ -f "$APP_DIR/static/css/skeleton.css" ] || cp defaults/flask-skeleton/static/css/skeleton.css "$APP_DIR/static/css/skeleton.css"
|
||||
[ -f "$APP_DIR/static/images/favicon.png" ] || cp defaults/flask-skeleton/static/images/favicon.png "$APP_DIR/static/images/favicon.png"
|
||||
fi
|
||||
|
||||
cat > harness/project.config.json <<JSON
|
||||
{
|
||||
"project_name": "$PROJECT_NAME",
|
||||
"project_description": "$PROJECT_DESC",
|
||||
"app_dir": "$APP_DIR",
|
||||
"stack": {
|
||||
"backend": "$BACKEND",
|
||||
"database": "$DB",
|
||||
"css": "$CSSFW"
|
||||
},
|
||||
"commands": {
|
||||
"test": "$TEST_CMD",
|
||||
"lint": "$LINT_CMD"
|
||||
},
|
||||
"model_mode": "$MODEL_MODE"
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > scripts/verify.local.sh <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if [ ! -f "harness/project.config.json" ]; then
|
||||
echo "[LOCAL] missing harness/project.config.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APP_DIR=$(python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
cfg=json.loads(Path('harness/project.config.json').read_text())
|
||||
print(cfg.get('app_dir','project'))
|
||||
PY
|
||||
)
|
||||
TEST_CMD=$(python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
cfg=json.loads(Path('harness/project.config.json').read_text())
|
||||
print(cfg.get('commands',{}).get('test',''))
|
||||
PY
|
||||
)
|
||||
LINT_CMD=$(python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
cfg=json.loads(Path('harness/project.config.json').read_text())
|
||||
print(cfg.get('commands',{}).get('lint',''))
|
||||
PY
|
||||
)
|
||||
|
||||
if [ ! -d "$APP_DIR" ]; then
|
||||
echo "[LOCAL] app dir not found: $APP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[LOCAL] app dir OK: $APP_DIR"
|
||||
|
||||
if [ -n "$LINT_CMD" ]; then
|
||||
echo "[LOCAL] lint: $LINT_CMD"
|
||||
bash -lc "$LINT_CMD"
|
||||
fi
|
||||
|
||||
if [ -n "$TEST_CMD" ]; then
|
||||
echo "[LOCAL] test: $TEST_CMD"
|
||||
bash -lc "$TEST_CMD"
|
||||
fi
|
||||
|
||||
echo "[LOCAL] OK"
|
||||
SH
|
||||
chmod +x scripts/verify.local.sh
|
||||
|
||||
python3 - <<PY
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import date
|
||||
|
||||
b = Path('backlog/features.json')
|
||||
data = json.loads(b.read_text(encoding='utf-8'))
|
||||
data['project'] = '$PROJECT_NAME'
|
||||
data['description'] = '$PROJECT_DESC'
|
||||
rules = data.setdefault('rules', {})
|
||||
rules.setdefault('valid_types', ['feature', 'fix', 'bug', 'chore'])
|
||||
features = data.get('features', [])
|
||||
|
||||
if '$ADD_BOOTSTRAP'.lower().startswith('y') and not features:
|
||||
features.append({
|
||||
'id': 'F-001',
|
||||
'type': 'chore',
|
||||
'title': 'Bootstrap ARNES on project',
|
||||
'problem': 'Need base workflow and control',
|
||||
'goal': 'Make ARNES ready on this repo',
|
||||
'scope_in': ['Harness setup', 'Runtime status', 'First verify cycle'],
|
||||
'scope_out': ['Business feature work', 'Product redesign'],
|
||||
'priority': 'med',
|
||||
'risk': 'low',
|
||||
'description': 'Problem: Need base workflow and control. Goal: Make ARNES ready on this repo. Scope IN: Harness setup, Runtime status, First verify cycle. Scope OUT: Business feature work, Product redesign. Type: chore. Priority: med. Risk: low.',
|
||||
'acceptance': ['verify.sh is green', 'runtime status works', 'first feature closes with gates'],
|
||||
'status': 'pending',
|
||||
'created_at': str(date.today()),
|
||||
'gates': {'review': False, 'security': False, 'qa': False}
|
||||
})
|
||||
|
||||
data['features'] = features
|
||||
b.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
PY
|
||||
|
||||
cat > work/current.md <<EOF
|
||||
# Current session
|
||||
|
||||
- Feature in progress: _none_
|
||||
- Orchestrator: _leader_
|
||||
|
||||
## Plan
|
||||
- Pick one pending feature.
|
||||
- Run ./scripts/verify.sh
|
||||
- Set runtime status.
|
||||
|
||||
## Next step
|
||||
- Use python3 scripts/new_ticket.py to create first real ticket.
|
||||
EOF
|
||||
|
||||
python3 scripts/agent_status.py reset >/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "Done. Project configured."
|
||||
echo "- Config: harness/project.config.json"
|
||||
echo "- Local checks: scripts/verify.local.sh"
|
||||
echo "- Ticket tool: python3 scripts/new_ticket.py"
|
||||
echo "- Publish tool: python3 scripts/publish_ticket.py --feature-id F-001"
|
||||
echo "- Verify: ./scripts/verify.sh"
|
||||
echo "- Runtime: python3 scripts/agent_status.py show"
|
||||
echo "- Reminder: configure git remote before final publish if missing"
|
||||
13
scripts/verify.local.sh.example
Normal file
13
scripts/verify.local.sh.example
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Ejemplo de overlay local por proyecto.
|
||||
# Copiar a scripts/verify.local.sh y adaptar.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "[LOCAL] checks específicos del proyecto"
|
||||
# Ejemplos:
|
||||
# alembic check
|
||||
# pytest -m smoke -q
|
||||
# npm run lint
|
||||
|
||||
echo "[LOCAL] OK"
|
||||
@@ -12,23 +12,49 @@ fail() { printf "${RED}[FAIL]${NC} %s\n" "$1"; }
|
||||
|
||||
EXIT_CODE=0
|
||||
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
|
||||
echo "── 1) Verificando estructura base ─────────────────────"
|
||||
required=(
|
||||
"AGENTS.md"
|
||||
"CHECKPOINTS.md"
|
||||
"README.md"
|
||||
"HOWTO.md"
|
||||
"TEMPLATE.md"
|
||||
"docs/README.md"
|
||||
"docs/repository-layout.md"
|
||||
"docs/scripts-reference.md"
|
||||
"harness/agents.matrix.yml"
|
||||
"harness/workflow.stages.yml"
|
||||
"harness/policies/governance.md"
|
||||
"harness/policies/security.md"
|
||||
"harness/policies/quality.md"
|
||||
"harness/policies/language.md"
|
||||
"harness/policies/model-routing.md"
|
||||
"harness/models.profiles.yml"
|
||||
"harness/contracts/handoff.md"
|
||||
"harness/contracts/evidence.schema.json"
|
||||
"spec/product.md"
|
||||
"spec/tech.md"
|
||||
"spec/acceptance.md"
|
||||
"spec/bdd/README.md"
|
||||
"spec/bdd/features/README.md"
|
||||
"spec/sdd/README.md"
|
||||
"spec/sdd/components/README.md"
|
||||
"spec/sdd/decisions/README.md"
|
||||
"features/README.md"
|
||||
"project/README.md"
|
||||
"backlog/features.json"
|
||||
"work/current.md"
|
||||
"work/history.md"
|
||||
"work/runtime-status.json"
|
||||
"scripts/agent_status.py"
|
||||
"scripts/new_ticket.py"
|
||||
"scripts/publish_ticket.py"
|
||||
"scripts/install_into_repo.sh"
|
||||
"scripts/start.sh"
|
||||
"platforms/pi/README.md"
|
||||
"platforms/opencode/README.md"
|
||||
)
|
||||
|
||||
for f in "${required[@]}"; do
|
||||
@@ -40,6 +66,20 @@ for f in "${required[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "── 1.5) Validando git repo ───────────────────────────"
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
ok "Git repo detectado"
|
||||
if git remote | grep -q .; then
|
||||
ok "Git remote configurado"
|
||||
else
|
||||
warn "Sin git remote configurado (publish requerirá remote)"
|
||||
fi
|
||||
else
|
||||
fail "Este proyecto debe vivir dentro de un git repo"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── 2) Validando backlog + gates ───────────────────────"
|
||||
python3 - <<'PY'
|
||||
@@ -49,6 +89,7 @@ import sys
|
||||
|
||||
root = pathlib.Path('.')
|
||||
path = root / 'backlog' / 'features.json'
|
||||
level_choices = {'low', 'med', 'high'}
|
||||
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding='utf-8'))
|
||||
@@ -56,12 +97,19 @@ except Exception as e:
|
||||
print(f"[FAIL] backlog/features.json inválido: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
valid = set(data.get('rules', {}).get('valid_status', ["pending", "in_progress", "blocked", "done"]))
|
||||
rules = data.get('rules', {})
|
||||
valid_status = set(rules.get('valid_status', ["pending", "in_progress", "blocked", "done"]))
|
||||
valid_types = set(rules.get('valid_types', ["feature", "fix", "bug", "chore"]))
|
||||
features = data.get('features', [])
|
||||
if not isinstance(features, list):
|
||||
print('[FAIL] features debe ser una lista')
|
||||
sys.exit(1)
|
||||
|
||||
ids = [str(f.get('id', '')).strip() for f in features]
|
||||
if len(ids) != len(set(ids)):
|
||||
print('[FAIL] Hay IDs de feature duplicados en backlog/features.json')
|
||||
sys.exit(1)
|
||||
|
||||
in_progress = [f for f in features if f.get('status') == 'in_progress']
|
||||
if len(in_progress) > 1:
|
||||
print(f"[FAIL] Hay {len(in_progress)} features in_progress (máximo 1)")
|
||||
@@ -70,25 +118,65 @@ if len(in_progress) > 1:
|
||||
for f in features:
|
||||
fid = str(f.get('id', '')).strip()
|
||||
status = f.get('status')
|
||||
if status not in valid:
|
||||
title = str(f.get('title', '')).strip()
|
||||
acceptance = f.get('acceptance')
|
||||
gates = f.get('gates', {})
|
||||
|
||||
if not fid:
|
||||
print('[FAIL] Hay una feature sin id')
|
||||
sys.exit(1)
|
||||
if not title:
|
||||
print(f"[FAIL] Feature {fid} sin title")
|
||||
sys.exit(1)
|
||||
if status not in valid_status:
|
||||
print(f"[FAIL] Estado inválido en feature {fid}: {status}")
|
||||
sys.exit(1)
|
||||
if not isinstance(acceptance, list) or not acceptance or any(not str(item).strip() for item in acceptance):
|
||||
print(f"[FAIL] Feature {fid} debe tener acceptance como lista no vacía")
|
||||
sys.exit(1)
|
||||
|
||||
ticket_type = f.get('type')
|
||||
if ticket_type is not None and ticket_type not in valid_types:
|
||||
print(f"[FAIL] Feature {fid} tiene type inválido: {ticket_type}")
|
||||
sys.exit(1)
|
||||
|
||||
for field in ('priority', 'risk'):
|
||||
value = f.get(field)
|
||||
if value is not None and value not in level_choices:
|
||||
print(f"[FAIL] Feature {fid} tiene {field} inválido: {value}")
|
||||
sys.exit(1)
|
||||
|
||||
for field in ('scope_in', 'scope_out'):
|
||||
value = f.get(field)
|
||||
if value is not None:
|
||||
if not isinstance(value, list) or any(not str(item).strip() for item in value):
|
||||
print(f"[FAIL] Feature {fid} tiene {field} inválido")
|
||||
sys.exit(1)
|
||||
|
||||
if gates:
|
||||
for gate_name in ('review', 'security', 'qa'):
|
||||
gate_value = gates.get(gate_name)
|
||||
if not isinstance(gate_value, bool):
|
||||
print(f"[FAIL] Feature {fid} tiene gates.{gate_name} inválido")
|
||||
sys.exit(1)
|
||||
|
||||
if status == 'done':
|
||||
d = root / 'work' / 'artifacts' / fid
|
||||
req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.json']
|
||||
req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.json', 'documenter.md', 'publish.json']
|
||||
missing = [name for name in req if not (d / name).is_file()]
|
||||
if missing:
|
||||
print(f"[FAIL] Feature {fid} done sin artefactos: {', '.join(missing)}")
|
||||
sys.exit(1)
|
||||
|
||||
expected = {
|
||||
'reviewer.json': 'reviewer',
|
||||
'security.json': 'security',
|
||||
'qa.json': 'qa',
|
||||
'leader-close.json': 'leader',
|
||||
'reviewer.json': ('reviewer', 'APPROVED'),
|
||||
'security.json': ('security', 'APPROVED'),
|
||||
'qa.json': ('qa', 'APPROVED'),
|
||||
'leader-close.json': ('leader', 'APPROVED'),
|
||||
'publish.json': ('leader', 'PUBLISHED'),
|
||||
}
|
||||
for filename, agent in expected.items():
|
||||
for filename, rule in expected.items():
|
||||
agent, verdict = rule
|
||||
try:
|
||||
obj = json.loads((d / filename).read_text(encoding='utf-8'))
|
||||
except Exception as e:
|
||||
@@ -98,16 +186,43 @@ for f in features:
|
||||
if obj.get('agent') != agent:
|
||||
print(f"[FAIL] {fid}/{filename} agent debe ser '{agent}'")
|
||||
sys.exit(1)
|
||||
if obj.get('verdict') != 'APPROVED':
|
||||
print(f"[FAIL] {fid}/{filename} no está APPROVED")
|
||||
if obj.get('verdict') != verdict:
|
||||
print(f"[FAIL] {fid}/{filename} no está {verdict}")
|
||||
sys.exit(1)
|
||||
if filename == 'publish.json' and obj.get('pushed') is not True:
|
||||
print(f"[FAIL] {fid}/{filename} debe tener pushed=true")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[OK] backlog válido ({len(features)} features)")
|
||||
PY
|
||||
if [ $? -ne 0 ]; then EXIT_CODE=1; fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
path = pathlib.Path('work/runtime-status.json')
|
||||
required = ['feature_id', 'stage', 'agent', 'action', 'state', 'next_agent', 'waiting_for', 'updated_at', 'timeline']
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding='utf-8'))
|
||||
except Exception as e:
|
||||
print(f"[FAIL] work/runtime-status.json inválido: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
missing = [key for key in required if key not in data]
|
||||
if missing:
|
||||
print(f"[FAIL] work/runtime-status.json incompleto: {', '.join(missing)}")
|
||||
sys.exit(1)
|
||||
if not isinstance(data.get('timeline'), list):
|
||||
print('[FAIL] work/runtime-status.json timeline debe ser una lista')
|
||||
sys.exit(1)
|
||||
print('[OK] runtime-status válido')
|
||||
PY
|
||||
if [ $? -ne 0 ]; then EXIT_CODE=1; fi
|
||||
|
||||
echo ""
|
||||
echo "── 3) Verificación de tests/build (opcional auto-detect) ─"
|
||||
echo "── 3) Verificación de tests/build (auto-detect) ───────"
|
||||
if [ -f "Makefile" ] && grep -qE '^test:' Makefile; then
|
||||
if make test; then ok "make test OK"; else fail "make test falló"; EXIT_CODE=1; fi
|
||||
elif [ -f "package.json" ]; then
|
||||
@@ -127,9 +242,24 @@ else
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── 4) Resumen ─────────────────────────────────────────"
|
||||
echo "── 4) Overlay local opcional ─────────────────────────"
|
||||
if [ -x "scripts/verify.local.sh" ]; then
|
||||
if ./scripts/verify.local.sh; then
|
||||
ok "verify.local.sh OK"
|
||||
else
|
||||
fail "verify.local.sh falló"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
elif [ -f "scripts/verify.local.sh" ]; then
|
||||
warn "scripts/verify.local.sh existe pero no es ejecutable"
|
||||
else
|
||||
warn "Sin overlay local (scripts/verify.local.sh)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── 5) Resumen ─────────────────────────────────────────"
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
ok "Harness verificado. Puedes trabajar."
|
||||
ok "Harness verificado. Template listo para adaptar a cualquier proyecto."
|
||||
else
|
||||
fail "Harness NO verificado. Corrige antes de continuar."
|
||||
fi
|
||||
|
||||
@@ -1,9 +1,54 @@
|
||||
# Acceptance Criteria
|
||||
# Acceptance Spec
|
||||
|
||||
Define criterios verificables por feature.
|
||||
## F-001 — Document and move legacy PHP app into ARNES project layout
|
||||
|
||||
Formato recomendado:
|
||||
- Feature ID:
|
||||
- Escenario:
|
||||
- Given / When / Then:
|
||||
- Evidencia esperada (test/comando):
|
||||
### Acceptance criteria
|
||||
- Legacy PHP app structure is documented in SDD files.
|
||||
- Repo layout decision is recorded in one ADR.
|
||||
- Legacy code moves from `project/new` to `project/web/index/new` with no file loss.
|
||||
- SQL dump moves from `project/db-25052026.sql` to `project/sql/db-25052026.sql`.
|
||||
- `./scripts/verify.sh` stays green after the move.
|
||||
|
||||
### Evidence targets
|
||||
- `spec/sdd/architecture.md`
|
||||
- `spec/sdd/components/*.md`
|
||||
- `spec/sdd/decisions/001-store-legacy-app-under-project-web.md`
|
||||
- `spec/bdd/features/layout/legacy-app-layout.feature`
|
||||
- `work/artifacts/F-001/architect.md`
|
||||
|
||||
## F-002 — Remove secrets and externalize config
|
||||
|
||||
### Acceptance criteria
|
||||
- No hard-coded API or DB secrets stay in versioned PHP files.
|
||||
- Config values load from one local config source for the legacy module.
|
||||
- Production URLs and external endpoints are configurable.
|
||||
- Legacy PHP entry points use config helper keys instead of inline values.
|
||||
- `./scripts/verify.sh` stays green after the change.
|
||||
|
||||
### Evidence targets
|
||||
- `project/web/index/new/bootstrap.php`
|
||||
- `project/web/index/new/config/local.example.php`
|
||||
- `project/web/index/new/config/README.md`
|
||||
- `spec/sdd/components/legacy-config-loader.md`
|
||||
- `spec/sdd/decisions/002-use-local-config-loader-for-legacy-module.md`
|
||||
- `spec/bdd/features/config/legacy-config.feature`
|
||||
- `work/artifacts/F-002/architect.md`
|
||||
- `work/artifacts/F-002/implementer.md`
|
||||
|
||||
## F-003 — Sanitize SQL dump for safe dev use
|
||||
|
||||
### Acceptance criteria
|
||||
- Repo no longer stores the raw production-like SQL dump as the active development baseline.
|
||||
- Tracked SQL baseline contains only safe synthetic or non-sensitive data for local module work.
|
||||
- Safe local data handling is documented.
|
||||
- Local development remains possible through the sanitized baseline and docs.
|
||||
- `./scripts/verify.sh` stays green after the change.
|
||||
|
||||
### Evidence targets
|
||||
- `project/sql/db-25052026.sql`
|
||||
- `project/sql/README.md`
|
||||
- `spec/sdd/components/development-data-baseline.md`
|
||||
- `spec/sdd/decisions/003-replace-raw-sql-with-sanitized-dev-baseline.md`
|
||||
- `spec/bdd/features/data/sanitized-sql-baseline.feature`
|
||||
- `work/artifacts/F-003/architect.md`
|
||||
- `work/artifacts/F-003/implementer.md`
|
||||
|
||||
110
spec/bdd/README.md
Normal file
110
spec/bdd/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# BDD — Behavior Driven Development
|
||||
|
||||
## Índice
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [Step Definitions](#step-definitions)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Este directorio contiene las especificaciones BDD fuente en formato Gherkin.
|
||||
|
||||
Separación recomendada:
|
||||
- `spec/bdd/features/` = source-of-truth de escenarios
|
||||
- `features/` = assets ejecutables del runner (steps, config)
|
||||
|
||||
### naming conventions
|
||||
|
||||
```text
|
||||
spec/bdd/features/
|
||||
├── <domain>/
|
||||
│ ├── <feature-name>.feature
|
||||
│ └── <feature-name>.feature
|
||||
└── common/
|
||||
└── <common-feature>.feature
|
||||
```
|
||||
|
||||
### tags permitidos
|
||||
|
||||
| Tag | Uso |
|
||||
|-----|-----|
|
||||
| `@F-XXX` | Link a feature del backlog |
|
||||
| `@smoke` | Tests críticos (siempre ejecutar) |
|
||||
| `@regression` | Tests de regresión |
|
||||
| `@integration` | Tests de integración |
|
||||
| `@e2e` | End-to-end tests |
|
||||
| `@unit` | Tests unitarios |
|
||||
| `@api` | Tests de API |
|
||||
| `@ui` | Tests de interfaz |
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
Ver `spec/bdd/features/` para los archivos `.feature`.
|
||||
|
||||
---
|
||||
|
||||
## Step Definitions
|
||||
|
||||
Los step definitions deben estar en:
|
||||
- Python: `features/steps/*.py`
|
||||
- JS/TS: `features/step_definitions/*.ts`
|
||||
- Go: `features/steps/*.go`
|
||||
|
||||
### Template (Python/Behave)
|
||||
|
||||
```python
|
||||
"""Steps para login feature."""
|
||||
from behave import given, when, then
|
||||
|
||||
@given('un usuario registrado con email "{email}" y password "{password}"')
|
||||
def step_registered_user(context, email, password):
|
||||
"""Crea usuario de prueba."""
|
||||
pass
|
||||
|
||||
@when('el usuario ingresa su email "{email}"')
|
||||
def step_enter_email(context, email):
|
||||
"""Ingresa email en el formulario."""
|
||||
pass
|
||||
|
||||
@when('ingresa password "{password}"')
|
||||
def step_enter_password(context, password):
|
||||
"""Ingresa password."""
|
||||
pass
|
||||
|
||||
@then('el sistema muestra mensaje de error "{message}"')
|
||||
def step_show_error(context, message):
|
||||
"""Verifica mensaje de error."""
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ejecutar Tests
|
||||
|
||||
### Python (Behave)
|
||||
```bash
|
||||
behave features/
|
||||
behave features/ --tags @smoke
|
||||
behave features/ --tags ~@slow # exclude
|
||||
```
|
||||
|
||||
### Node.js (Cucumber)
|
||||
```bash
|
||||
npx cucumber-js features/
|
||||
npx cucumber-js features/ --tags "@smoke and @F-001"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist de Feature
|
||||
|
||||
- [ ] Feature documentado en Gherkin
|
||||
- [ ] Todos los scenarios tienen Given/When/Then
|
||||
- [ ] Tags `@F-XXX` presentes
|
||||
- [ ] Step definitions implementados
|
||||
- [ ] Tests ejecutables
|
||||
0
spec/bdd/features/.gitkeep
Normal file
0
spec/bdd/features/.gitkeep
Normal file
12
spec/bdd/features/README.md
Normal file
12
spec/bdd/features/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# BDD feature files
|
||||
|
||||
Put Gherkin `.feature` files here.
|
||||
|
||||
Example:
|
||||
- `spec/bdd/features/checkout/purchase.feature`
|
||||
- `spec/bdd/features/common/error-handling.feature`
|
||||
|
||||
Use tags like:
|
||||
- `@F-001`
|
||||
- `@smoke`
|
||||
- `@regression`
|
||||
20
spec/bdd/features/config/legacy-config.feature
Normal file
20
spec/bdd/features/config/legacy-config.feature
Normal file
@@ -0,0 +1,20 @@
|
||||
@F-002 @smoke @security @regression
|
||||
Feature: Legacy module reads config from one local source
|
||||
|
||||
As a maintainer
|
||||
I want secrets and URLs outside tracked PHP files
|
||||
So I can run the legacy module without storing sensitive values in source
|
||||
|
||||
Scenario: Entry points use shared config helper
|
||||
Given the legacy PHP module has multiple web and CLI entry points
|
||||
When feature F-002 is applied
|
||||
Then tracked PHP files do not contain hard-coded DB credentials
|
||||
And tracked PHP files do not contain hard-coded OpenAI credentials
|
||||
And DB and route values are loaded through a shared config helper
|
||||
|
||||
Scenario: Local config shape is documented
|
||||
Given a maintainer needs to set local credentials
|
||||
When feature F-002 is applied
|
||||
Then the repo contains a versioned local config example
|
||||
And the repo ignores the real local config file
|
||||
And setup notes explain how to create the local config
|
||||
18
spec/bdd/features/data/sanitized-sql-baseline.feature
Normal file
18
spec/bdd/features/data/sanitized-sql-baseline.feature
Normal file
@@ -0,0 +1,18 @@
|
||||
@F-003 @smoke @security @regression
|
||||
Feature: Safe SQL baseline exists for legacy module development
|
||||
|
||||
As a maintainer
|
||||
I want a tracked SQL baseline without sensitive live data
|
||||
So I can develop locally without keeping a raw production snapshot in git
|
||||
|
||||
Scenario: Tracked SQL baseline is sanitized
|
||||
Given the repo contains one tracked SQL baseline for the legacy module
|
||||
When feature F-003 is applied
|
||||
Then the tracked SQL baseline does not contain customer or live order snapshot data
|
||||
And the baseline contains only safe schema and synthetic seed data needed for local module work
|
||||
|
||||
Scenario: Local private data handling is documented
|
||||
Given a maintainer may still need a private raw dump outside git
|
||||
When feature F-003 is applied
|
||||
Then the repo documents where private local data should live
|
||||
And the tracked SQL baseline remains safe for commit and push
|
||||
25
spec/bdd/features/layout/legacy-app-layout.feature
Normal file
25
spec/bdd/features/layout/legacy-app-layout.feature
Normal file
@@ -0,0 +1,25 @@
|
||||
@F-001 @smoke @regression
|
||||
Feature: Legacy app lives in stable ARNES layout
|
||||
|
||||
As a maintainer
|
||||
I want the copied legacy PHP app in a stable repo path
|
||||
So I can trace design and change code safely
|
||||
|
||||
Scenario: Web module path is explicit
|
||||
Given the repo contains legacy PHP product module files
|
||||
When feature F-001 is applied
|
||||
Then the module lives under "project/web/index/new"
|
||||
And the old temporary path "project/new" is removed
|
||||
|
||||
Scenario: Development SQL baseline is explicit
|
||||
Given the repo contains one SQL dump for local development
|
||||
When feature F-001 is applied
|
||||
Then the dump lives under "project/sql/db-25052026.sql"
|
||||
And the dump is referenced by SDD docs as development baseline only
|
||||
|
||||
Scenario: Design trace exists for the move
|
||||
Given feature F-001 is in progress
|
||||
When the design stage is complete
|
||||
Then SDD architecture docs exist for the legacy app
|
||||
And one ADR records the repo layout move
|
||||
And architect evidence exists under "work/artifacts/F-001/architect.md"
|
||||
@@ -1,15 +1,72 @@
|
||||
# Product Spec
|
||||
|
||||
## Problema
|
||||
Describe el problema de negocio.
|
||||
## Problem
|
||||
Legacy PHP app lives in temporary path `project/new`.
|
||||
SQL dump lives mixed with app code.
|
||||
There is no ARNES design record for this code.
|
||||
This makes next change work risky and hard to trace.
|
||||
|
||||
## Objetivo
|
||||
Define el resultado esperado del producto.
|
||||
## Objective
|
||||
Put legacy app in stable ARNES project layout.
|
||||
Keep same code and same behavior for now.
|
||||
Make next work easy to trace, review, and test.
|
||||
|
||||
## Usuarios
|
||||
- Usuario principal:
|
||||
- Usuario secundario:
|
||||
## Users
|
||||
- Primary user: maintainer of legacy PHP app
|
||||
- Secondary user: architect, implementer, reviewer, qa
|
||||
|
||||
## Alcance v1
|
||||
## Scope v1
|
||||
- In scope:
|
||||
- document current legacy app structure
|
||||
- define target repo layout
|
||||
- move app code to `project/web/index/new`
|
||||
- move SQL dump to `project/sql/db-25052026.sql`
|
||||
- Out of scope:
|
||||
- auth rewrite
|
||||
- OpenAI secret cleanup
|
||||
- production deploy
|
||||
- feature refactor
|
||||
|
||||
## F-002 — Remove secrets and externalize config
|
||||
|
||||
### Problem
|
||||
Legacy PHP files still contain API keys, DB credentials, and production URLs.
|
||||
This blocks security approval and makes local setup unsafe.
|
||||
|
||||
### Objective
|
||||
Load config from one local source outside versioned code.
|
||||
Keep page behavior the same while removing hard-coded secrets from tracked PHP files.
|
||||
|
||||
### Scope
|
||||
- In scope:
|
||||
- one config loader for legacy module
|
||||
- one local config file shape for DB, OpenAI, URLs, and endpoints
|
||||
- replace hard-coded values in tracked PHP files
|
||||
- setup notes for local config
|
||||
- Out of scope:
|
||||
- auth redesign
|
||||
- worker refactor beyond config use
|
||||
- deploy automation
|
||||
|
||||
## F-003 — Sanitize SQL dump for safe dev use
|
||||
|
||||
### Problem
|
||||
Current SQL dump in repo looks like a production snapshot.
|
||||
It contains sensitive and production-like data.
|
||||
This is unsafe as a tracked development baseline.
|
||||
|
||||
### Objective
|
||||
Replace the raw dump in the working tree with a safe development baseline.
|
||||
Keep local development possible for the legacy PHP module.
|
||||
Document how to handle private data outside git.
|
||||
|
||||
### Scope
|
||||
- In scope:
|
||||
- define safe SQL baseline strategy
|
||||
- replace current tracked dump with sanitized development dump
|
||||
- document private local dump handling
|
||||
- keep module development possible with synthetic seed data
|
||||
- Out of scope:
|
||||
- production database changes
|
||||
- app logic changes
|
||||
- full OpenCart dataset preservation
|
||||
|
||||
315
spec/sdd-bdd-guide.md
Normal file
315
spec/sdd-bdd-guide.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# SDD/BDD Guide — System Design Document & Behavior Driven Development
|
||||
|
||||
Guía para crear y mantener SDD (System Design Document) y BDD (Behavior Driven Development) specs dentro del framework ARNES.
|
||||
|
||||
---
|
||||
|
||||
## 📐 Propósito
|
||||
|
||||
- **SDD**: Documenta el diseño técnico del sistema (arquitectura, componentes, decisiones).
|
||||
- **BDD**: Documenta el comportamiento esperado desde la perspectiva del usuario/negocio.
|
||||
|
||||
Ambos alimentan el pipeline de agentes y se versionan junto con el código.
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Relación con ARNES
|
||||
|
||||
```
|
||||
spec/
|
||||
├── product.md # Qué construir (negocio)
|
||||
├── tech.md # Stack y decisiones técnicas
|
||||
├── acceptance.md # Criterios de aceptación (BDD light)
|
||||
├── sdd/ # System Design Document
|
||||
│ ├── README.md
|
||||
│ ├── architecture.md
|
||||
│ ├── components/
|
||||
│ └── decisions/
|
||||
└── bdd/ # Behavior Driven Development source-of-truth
|
||||
├── README.md
|
||||
└── features/
|
||||
|
||||
features/ # optional executable BDD runner assets
|
||||
├── behave.ini
|
||||
└── steps/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 SDD — System Design Document
|
||||
|
||||
### Objetivos
|
||||
1. Definir arquitectura del sistema
|
||||
2. Documentar componentes y sus responsabilidades
|
||||
3. Registrar decisiones técnicas (ADRs)
|
||||
4. Servir como fuente de verdad para `architect` y `implementer`
|
||||
|
||||
### Estructura de un SDD
|
||||
|
||||
```
|
||||
spec/sdd/
|
||||
├── README.md # Índice y overview
|
||||
├── architecture.md # Vista general (contexto, capas)
|
||||
├── components/ # Componentes individuales
|
||||
│ ├── component-name.md
|
||||
│ └── ...
|
||||
└── decisions/ # Architecture Decision Records
|
||||
├── 001-decision-title.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Template: component.md
|
||||
|
||||
```markdown
|
||||
# Componente: <Nombre>
|
||||
|
||||
## Responsabilidad
|
||||
Descripción clara de qué hace este componente.
|
||||
|
||||
## Interfaces
|
||||
- **Entrada**: API, eventos, mensajes
|
||||
- **Salida**: Respuestas, side effects
|
||||
|
||||
## Dependencias
|
||||
- Servicio A (tipo de dependencia)
|
||||
- Base de datos B
|
||||
|
||||
## Límites
|
||||
- Qué NO hace este componente
|
||||
- Restricciones conocidas
|
||||
|
||||
## Criterios de éxito
|
||||
- [ ] Mecanismo de verificación
|
||||
- [ ] Métrica de performance
|
||||
```
|
||||
|
||||
### Template: ADR (Architecture Decision Record)
|
||||
|
||||
```markdown
|
||||
# ADR-XXX: <Título>
|
||||
|
||||
## Estado
|
||||
Aceptado | Deprecado | Propuesto
|
||||
|
||||
## Contexto
|
||||
Problema que motiva esta decisión.
|
||||
|
||||
## Decisión
|
||||
Qué se decidió y por qué.
|
||||
|
||||
## Consecuencias
|
||||
- ✅ Positivos
|
||||
- ❌ Negativos
|
||||
|
||||
## Alternativas consideradas
|
||||
1. Opción A - razón de descarte
|
||||
2. Opción B - razón de descarte
|
||||
|
||||
## Fecha
|
||||
YYYY-MM-DD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 BDD — Behavior Driven Development
|
||||
|
||||
### Objetivos
|
||||
1. Definir comportamiento del sistema en lenguaje de negocio
|
||||
2. Crear trazabilidad entre requisitos y tests
|
||||
3. Servir como especificación ejecutable para `implementer` y `qa`
|
||||
|
||||
### Formato: Gherkin
|
||||
|
||||
Usar sintaxis Gherkin para todos los features:
|
||||
|
||||
```gherkin
|
||||
Feature: <Nombre del feature>
|
||||
|
||||
Como <actor>
|
||||
Quiero <acción>
|
||||
Para <beneficio>
|
||||
|
||||
Scenario: <escenario positivo>
|
||||
Given <contexto inicial>
|
||||
And <más contexto>
|
||||
When <acción del usuario>
|
||||
And <otra acción>
|
||||
Then <resultado esperado>
|
||||
And <otro resultado>
|
||||
|
||||
Scenario: <escenario negativo>
|
||||
Given <contexto>
|
||||
When <acción que falla>
|
||||
Then <error esperado>
|
||||
```
|
||||
|
||||
### Estructura de un Feature BDD
|
||||
|
||||
```
|
||||
spec/bdd/features/
|
||||
├── README.md
|
||||
├── auth/
|
||||
│ ├── login.feature
|
||||
│ └── registration.feature
|
||||
├── checkout/
|
||||
│ └── purchase.feature
|
||||
└── common/
|
||||
└── error-handling.feature
|
||||
|
||||
features/
|
||||
├── behave.ini
|
||||
└── steps/
|
||||
└── login_steps.py
|
||||
```
|
||||
|
||||
### Tags para trazabilidad
|
||||
|
||||
```gherkin
|
||||
@F-001 @auth @smoke
|
||||
Feature: Inicio de sesión
|
||||
|
||||
@regression @slow
|
||||
Scenario: Login con credenciales válidas
|
||||
...
|
||||
```
|
||||
|
||||
Tags disponibles:
|
||||
- `@F-XXX` — Link a feature ID del backlog
|
||||
- `@smoke` — Test crítico (ejecutar siempre)
|
||||
- `@regression` — Tests de regresión
|
||||
- `@integration` — Tests de integración
|
||||
- `@e2e` — End-to-end tests
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Flujo de trabajo SDD/BDD en ARNES
|
||||
|
||||
### Stage: design (architect)
|
||||
|
||||
1. **Crear/actualizar SDD**
|
||||
- Definir componentes nuevos
|
||||
- Documentar decisiones técnicas
|
||||
- Crear ADRs cuando haya cambios
|
||||
|
||||
2. **Crear/actualizar BDD**
|
||||
- Traducir requisitos de `spec/product.md` a Gherkin
|
||||
- Crear scenarios para cada criterio de aceptación
|
||||
- Asegurar que cada scenario tenga link a `@F-XXX`
|
||||
|
||||
3. **Producir artefacto**
|
||||
- Archivo: `work/artifacts/<feature_id>/architect.md`
|
||||
- Contenido: resumen de cambios en SDD/BDD
|
||||
|
||||
### Stage: build (implementer)
|
||||
|
||||
1. **Implementar código** que cumpla los scenarios BDD
|
||||
2. **Escribir tests** que ejecuten los scenarios
|
||||
3. **Actualizar SDD** si hay cambios en componentes
|
||||
|
||||
### Stage: review_gate (reviewer)
|
||||
|
||||
1. **Verificar** que el código implementa lo documentado en SDD
|
||||
2. **Verificar** que tests cubren los scenarios BDD
|
||||
3. **Producir** `work/artifacts/<feature_id>/reviewer.json`
|
||||
|
||||
### Stage: qa_gate (qa)
|
||||
|
||||
1. **Ejecutar** tests BDD (feature files)
|
||||
2. **Verificar** trazabilidad: todos los `@F-XXX` tienen tests
|
||||
3. **Producir** `work/artifacts/<feature_id>/qa.json`
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Herramientas recomendadas
|
||||
|
||||
| Propósito | Herramienta | Notas |
|
||||
|-----------|-------------|-------|
|
||||
| BDD test runner | Behave (Python) / Cucumber (JS/Java) | Ejecuta .feature files |
|
||||
| SDD docs | Markdown + Mermaid diagrams | Portable y versionable |
|
||||
| ADRs |adr-tools o manual | Mantener en `decisions/` |
|
||||
|
||||
### Ejemplo: Python Behave
|
||||
|
||||
```bash
|
||||
# Estructura
|
||||
spec/bdd/features/
|
||||
└── login.feature
|
||||
|
||||
features/
|
||||
└── steps/
|
||||
└── login_steps.py
|
||||
|
||||
# Ejecutar
|
||||
behave features/
|
||||
```
|
||||
|
||||
### Ejemplo: Node.js Cucumber
|
||||
|
||||
```bash
|
||||
# Estructura
|
||||
spec/bdd/features/
|
||||
└── login.feature
|
||||
|
||||
features/
|
||||
└── step_definitions/
|
||||
└── login_steps.js
|
||||
|
||||
# Ejecutar
|
||||
npx cucumber-js features/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Checklist de calidad SDD/BDD
|
||||
|
||||
### SDD Quality
|
||||
- [ ] Cada componente tiene responsabilidad clara
|
||||
- [ ] Interfaces están documentadas
|
||||
- [ ] ADRs para decisiones importantes
|
||||
- [ ] Diagramas Mermaid para arquitectura
|
||||
|
||||
### BDD Quality
|
||||
- [ ] Cada feature tiene al menos un scenario
|
||||
- [ ] Todos los scenarios usan Given/When/Then
|
||||
- [ ] Tags `@F-XXX` para trazabilidad con backlog
|
||||
- [ ] Scenarios son atómicos (no dependen de estado previo)
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Reglas anti-trampa
|
||||
|
||||
1. **SDD no es decoration**: debe reflejar la realidad del código
|
||||
2. **BDD no es documentación de tests**: es especificación executable
|
||||
3. **Discrepancia = bug**: si SDD dice A pero código hace B, el código está mal
|
||||
4. **Sin scenario = sin acceptance**: feature sin BDD scenario no puede cerrarse
|
||||
|
||||
---
|
||||
|
||||
## 📝 Formato de artefacto architect.md
|
||||
|
||||
```markdown
|
||||
# Architect Artefact — Feature: F-XXX
|
||||
|
||||
## SDD Changes
|
||||
- Componentes afectados: [...]
|
||||
- ADRs creados/actualizados: [...]
|
||||
|
||||
## BDD Coverage
|
||||
- Features/Scenarios nuevos: [...]
|
||||
- Coverage: X/Y scenarios cubiertos por tests
|
||||
|
||||
## Decisiones técnicas
|
||||
- Decisión 1: razón
|
||||
- Decisión 2: razón
|
||||
|
||||
## Riesgos identificados
|
||||
- Riesgo 1: mitigación
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Referencias
|
||||
|
||||
- [Gherkin Reference](https://cucumber.io/docs/gherkin/)
|
||||
- [MADR (Markdown Any Decision Records)](https://adr.github.io/madr/)
|
||||
- [BDD with Behave](https://behave.readthedocs.io/)
|
||||
67
spec/sdd/README.md
Normal file
67
spec/sdd/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# SDD — System Design Document
|
||||
|
||||
## Índice
|
||||
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Components](#components)
|
||||
- [Decisions](#decisions)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Frontend
|
||||
F[Client App]
|
||||
end
|
||||
|
||||
subgraph Backend
|
||||
A[API Gateway]
|
||||
S[Services]
|
||||
D[(Database)]
|
||||
end
|
||||
|
||||
F --> A
|
||||
A --> S
|
||||
S --> D
|
||||
```
|
||||
|
||||
### Contexto
|
||||
_Describir el propósito del sistema y su alcance._
|
||||
|
||||
### Restricciones
|
||||
- _Lista de restricciones técnicas o de negocio_
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### Component Template
|
||||
|
||||
Ver `spec/sdd/components/.template.md` para el formato.
|
||||
|
||||
---
|
||||
|
||||
## Decisions
|
||||
|
||||
Ver `spec/sdd/decisions/` para ADRs.
|
||||
|
||||
---
|
||||
|
||||
## Diagrama de secuencia (ejemplo)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U as User
|
||||
participant API
|
||||
participant SVC as Service
|
||||
participant DB as Database
|
||||
|
||||
U->>API: Request
|
||||
API->>SVC: Process
|
||||
SVC->>DB: Query
|
||||
DB-->>SVC: Result
|
||||
SVC-->>API: Response
|
||||
API-->>U: Data
|
||||
```
|
||||
47
spec/sdd/architecture.md
Normal file
47
spec/sdd/architecture.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Architecture Overview — Legacy PHP Product Module
|
||||
|
||||
## Context
|
||||
This repo holds one legacy PHP module copied from production.
|
||||
The module helps product staff create products and generate SEO text.
|
||||
The module also runs one batch worker that updates OpenCart product descriptions.
|
||||
|
||||
Current raw source path was `project/new`.
|
||||
Target stable path is `project/web/index/new`.
|
||||
SQL baseline path is `project/sql/db-25052026.sql` and now contains sanitized synthetic development data.
|
||||
|
||||
## Main flows
|
||||
1. User opens product form.
|
||||
2. Form reads OpenCart data from MariaDB.
|
||||
3. User can open AI helper page for one product text.
|
||||
4. Bulk page writes product ids into `oc_product_queue`.
|
||||
5. CLI worker reads queue, calls OpenAI, updates `oc_product_description`.
|
||||
|
||||
## Constraints
|
||||
- Keep legacy behavior unchanged in layout and config features.
|
||||
- Preserve file contents during move unless config externalization requires value lookup changes.
|
||||
- Keep evidence in repo for each design change.
|
||||
- Do not redesign auth or deploy in these features.
|
||||
|
||||
## System view
|
||||
```mermaid
|
||||
graph TD
|
||||
U[Backoffice user] --> F[Legacy PHP web module]
|
||||
F --> DB[(MariaDB / OpenCart)]
|
||||
F --> Q[oc_product_queue]
|
||||
W[worker_bulk.php CLI worker] --> Q
|
||||
W --> AI[OpenAI API]
|
||||
W --> DB
|
||||
```
|
||||
|
||||
## Files and responsibilities
|
||||
- `bootstrap.php`: shared local config loader and DB helper
|
||||
- `config/local.example.php`: versioned config shape
|
||||
- `config/local.php`: ignored local values file
|
||||
- `index.php`: manual product create form
|
||||
- `describe.php`: one-shot AI description helper
|
||||
- `productos_bulk_update.php`: queue intake and worker log viewer
|
||||
- `productos_modificados.php`: review processed items
|
||||
- `worker_bulk.php`: batch generator and DB updater
|
||||
- `inc/*`: shared layout, prompts, AJAX helpers
|
||||
- `db/conn.php`: shared DB connection for web pages
|
||||
- `logs/*`: runtime debug output from worker and AI calls
|
||||
0
spec/sdd/components/.gitkeep
Normal file
0
spec/sdd/components/.gitkeep
Normal file
8
spec/sdd/components/README.md
Normal file
8
spec/sdd/components/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# SDD components
|
||||
|
||||
Put one markdown file per technical component.
|
||||
|
||||
Example:
|
||||
- `api-gateway.md`
|
||||
- `order-service.md`
|
||||
- `cart-repository.md`
|
||||
33
spec/sdd/components/bulk-seo-worker.md
Normal file
33
spec/sdd/components/bulk-seo-worker.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Component: Bulk SEO worker
|
||||
|
||||
## Responsibility
|
||||
Read product ids from queue.
|
||||
Call OpenAI with EN and ES prompts.
|
||||
Clean output.
|
||||
Update OpenCart product description fields.
|
||||
Write processing logs.
|
||||
|
||||
## Interfaces
|
||||
- Input:
|
||||
- CLI run of `worker_bulk.php`
|
||||
- rows from `oc_product_queue`
|
||||
- prompt files `inc/prompt_en.md` and `inc/prompt_es.md`
|
||||
- Output:
|
||||
- updates in `oc_product_description`
|
||||
- status fields in `oc_product_queue`
|
||||
- log files under `logs/`
|
||||
|
||||
## Dependencies
|
||||
- MariaDB/MySQL
|
||||
- OpenAI Chat Completions API
|
||||
- local prompt markdown files
|
||||
|
||||
## Limits
|
||||
- No secret management yet.
|
||||
- No retry queue store outside DB.
|
||||
- No metrics or structured logs.
|
||||
|
||||
## Success criteria
|
||||
- [ ] Worker path is documented
|
||||
- [ ] Queue and DB side effects are known
|
||||
- [ ] Log location is explicit in design docs
|
||||
25
spec/sdd/components/development-data-baseline.md
Normal file
25
spec/sdd/components/development-data-baseline.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Component: Development data baseline
|
||||
|
||||
## Responsibility
|
||||
Provide one safe local SQL baseline so maintainers can seed a development database for the legacy PHP module.
|
||||
|
||||
## Interfaces
|
||||
- Input:
|
||||
- SQL import command run by maintainer
|
||||
- Output:
|
||||
- local MariaDB database with the schema and synthetic seed data needed by the module
|
||||
|
||||
## Dependencies
|
||||
- `project/sql/db-25052026.sql`
|
||||
- `project/sql/README.md`
|
||||
- local MariaDB/MySQL server
|
||||
|
||||
## Limits
|
||||
- Baseline is intentionally smaller than the former raw snapshot.
|
||||
- Baseline covers current module needs, not the full production dataset.
|
||||
- Private raw snapshots must stay outside git.
|
||||
|
||||
## Success criteria
|
||||
- [ ] Dump path is stable and explicit
|
||||
- [ ] Tracked dump contains only safe synthetic or non-sensitive data
|
||||
- [ ] Docs explain private local dump handling
|
||||
30
spec/sdd/components/legacy-config-loader.md
Normal file
30
spec/sdd/components/legacy-config-loader.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Component: Legacy config loader
|
||||
|
||||
## Responsibility
|
||||
Load local configuration for the legacy PHP module.
|
||||
Expose helper access for DB, OpenAI, URLs, endpoints, and path values.
|
||||
Provide one DB connection factory used by web pages and worker.
|
||||
|
||||
## Interfaces
|
||||
- Input:
|
||||
- `config/local.php` if present
|
||||
- fallback `config/local.example.php` for shape and safe defaults
|
||||
- Output:
|
||||
- config access helpers
|
||||
- mysqli connection helper
|
||||
- normalized path values for logs and routes
|
||||
|
||||
## Dependencies
|
||||
- PHP array config files
|
||||
- `mysqli`
|
||||
- module root path
|
||||
|
||||
## Limits
|
||||
- Does not manage secret rotation.
|
||||
- Does not validate remote credentials.
|
||||
- Does not redesign auth or downstream business logic.
|
||||
|
||||
## Success criteria
|
||||
- [ ] No tracked PHP file contains hard-coded DB or OpenAI secrets
|
||||
- [ ] Entry points use shared config helper
|
||||
- [ ] Local setup path is documented
|
||||
32
spec/sdd/components/legacy-web-module.md
Normal file
32
spec/sdd/components/legacy-web-module.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Component: Legacy web module
|
||||
|
||||
## Responsibility
|
||||
Serve old PHP pages for product create and product SEO work.
|
||||
Render HTML.
|
||||
Read OpenCart data.
|
||||
Write queue rows for batch processing.
|
||||
|
||||
## Interfaces
|
||||
- Input:
|
||||
- browser GET and POST requests
|
||||
- session state from external login flow
|
||||
- Output:
|
||||
- HTML pages
|
||||
- inserts into `oc_product_queue`
|
||||
- writes brand rows and URL alias rows
|
||||
|
||||
## Dependencies
|
||||
- `db/conn.php`
|
||||
- `inc/header.php`, `inc/footer.php`
|
||||
- OpenCart tables
|
||||
- external `success.php` and `login.php` outside repo
|
||||
|
||||
## Limits
|
||||
- Does not own authentication.
|
||||
- Does not own final product creation endpoint.
|
||||
- Uses hard-coded config today.
|
||||
|
||||
## Success criteria
|
||||
- [ ] Module files live under stable repo path
|
||||
- [ ] Relative module structure stays intact
|
||||
- [ ] Pages can still be reviewed as one legacy unit
|
||||
0
spec/sdd/decisions/.gitkeep
Normal file
0
spec/sdd/decisions/.gitkeep
Normal file
33
spec/sdd/decisions/001-store-legacy-app-under-project-web.md
Normal file
33
spec/sdd/decisions/001-store-legacy-app-under-project-web.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# ADR-001: Store legacy app under project web path
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
Legacy PHP code was copied into `project/new`.
|
||||
That path does not explain app role.
|
||||
SQL dump also sits beside code in `project/` root.
|
||||
We need stable layout before deeper refactor.
|
||||
|
||||
## Decision
|
||||
Store legacy web code under `project/web/index/new`.
|
||||
Store SQL dump under `project/sql/db-25052026.sql`.
|
||||
Keep internal legacy file tree unchanged inside module.
|
||||
Do not refactor code in same step.
|
||||
|
||||
## Consequences
|
||||
- Good:
|
||||
- repo layout shows what is web code and what is data
|
||||
- ARNES design docs can point to stable paths
|
||||
- future config and secret cleanup gets easier
|
||||
- Bad:
|
||||
- move may require path-aware follow-up in later features
|
||||
- repo still contains legacy secrets until later cleanup
|
||||
|
||||
## Alternatives considered
|
||||
1. Keep code in `project/new` - rejected because path is temporary and vague.
|
||||
2. Move code to `project/app` - rejected because this is web module, not service code.
|
||||
3. Refactor layout and code now - rejected because scope would grow too much.
|
||||
|
||||
## Date
|
||||
2026-05-25
|
||||
@@ -0,0 +1,33 @@
|
||||
# ADR-002: Use local config loader for legacy module
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
Security gate for F-001 failed.
|
||||
Legacy PHP files still hold DB credentials, OpenAI keys, and production-coupled URLs.
|
||||
The module needs one small config mechanism without large refactor.
|
||||
|
||||
## Decision
|
||||
Add `bootstrap.php` to the legacy module root.
|
||||
Load config from `config/local.php` with fallback to `config/local.example.php`.
|
||||
Expose shared helper functions for config lookup and DB connection.
|
||||
Update web pages and worker to read DB, OpenAI, route, and URL values through this helper.
|
||||
Ignore `config/local.php` in git.
|
||||
|
||||
## Consequences
|
||||
- Good:
|
||||
- secrets leave tracked PHP source files
|
||||
- one config shape is reused by web pages and worker
|
||||
- local setup becomes explicit
|
||||
- Bad:
|
||||
- module still depends on local file management
|
||||
- fallback example config can still fail at runtime until maintainer fills real values
|
||||
|
||||
## Alternatives considered
|
||||
1. Use environment variables only - rejected because this legacy module already expects file-based setup.
|
||||
2. Keep secrets in PHP constants - rejected because tracked source would still hold sensitive values.
|
||||
3. Full framework migration - rejected because scope is too large for this fix.
|
||||
|
||||
## Date
|
||||
2026-05-25
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-003: Replace raw SQL snapshot with sanitized dev baseline
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The tracked SQL file under `project/sql/db-25052026.sql` looked like a production snapshot.
|
||||
It exposed production-like and sensitive data in the working tree.
|
||||
The legacy PHP module still needs a database baseline for local work.
|
||||
|
||||
## Decision
|
||||
Keep the same tracked SQL path but replace its content with a sanitized development baseline.
|
||||
The new baseline contains only the schema and synthetic seed data needed by the legacy PHP module.
|
||||
Document how to keep any private raw dump outside git.
|
||||
|
||||
## Consequences
|
||||
- Good:
|
||||
- active repo tree stops shipping raw sensitive SQL data
|
||||
- local setup remains possible with a smaller safe dataset
|
||||
- module development gets a focused baseline for current pages and worker
|
||||
- Bad:
|
||||
- baseline no longer mirrors the full production dataset
|
||||
- some future work may need extra synthetic fixtures
|
||||
|
||||
## Alternatives considered
|
||||
1. Keep raw dump and add warning only - rejected because data risk remains in tracked files.
|
||||
2. Remove all SQL baseline files - rejected because local development would become harder.
|
||||
3. Rewrite full git history now - rejected because scope is too large for this feature.
|
||||
|
||||
## Date
|
||||
2026-05-25
|
||||
7
spec/sdd/decisions/README.md
Normal file
7
spec/sdd/decisions/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# SDD decisions
|
||||
|
||||
Put ADRs (Architecture Decision Records) here.
|
||||
|
||||
Example:
|
||||
- `001-use-flask.md`
|
||||
- `002-use-mariadb.md`
|
||||
48
spec/tech.md
48
spec/tech.md
@@ -1,19 +1,43 @@
|
||||
# Technical Spec
|
||||
|
||||
## Stack
|
||||
- Lenguaje:
|
||||
- Framework:
|
||||
- Runtime:
|
||||
- Language: PHP, JavaScript, CSS
|
||||
- Framework: legacy custom PHP + OpenCart database schema
|
||||
- Runtime: Apache/Nginx + PHP, MariaDB/MySQL, CLI worker for batch jobs
|
||||
|
||||
## Restricciones
|
||||
- Seguridad:
|
||||
- Rendimiento:
|
||||
- Compatibilidad:
|
||||
## Restrictions
|
||||
- Security:
|
||||
- do not expose secrets in new docs
|
||||
- keep real dump as local dev input only
|
||||
- Performance:
|
||||
- file move must not change app code behavior
|
||||
- Compatibility:
|
||||
- preserve relative file structure inside legacy module
|
||||
- preserve SQL dump file content
|
||||
|
||||
## Dependencias
|
||||
Lista y justificación de dependencias externas.
|
||||
## Dependencies
|
||||
- MariaDB/MySQL dump from `project/sql/db-25052026.sql`
|
||||
- OpenCart tables like `oc_product`, `oc_product_description`, `oc_product_queue`
|
||||
- OpenAI API used by legacy scripts
|
||||
- External login and success endpoints exist outside this repo
|
||||
|
||||
## Observabilidad
|
||||
## Observability
|
||||
- Logging:
|
||||
- Métricas:
|
||||
- Alertas:
|
||||
- current legacy logs live under module `logs/`
|
||||
- Metrics:
|
||||
- none in repo now
|
||||
- Alerts:
|
||||
- none in repo now
|
||||
|
||||
## F-002 technical notes
|
||||
- Add `bootstrap.php` in legacy module root.
|
||||
- Add config files under `project/web/index/new/config/`.
|
||||
- Versioned file stores example values only.
|
||||
- Ignored local file stores real local secrets and URLs.
|
||||
- All PHP entry points must read DB, OpenAI, and route values through config helper.
|
||||
|
||||
## F-003 technical notes
|
||||
- Keep one tracked SQL baseline for safe local development.
|
||||
- Baseline should contain synthetic or non-sensitive seed data only.
|
||||
- Baseline should cover the tables needed by the legacy module pages and worker.
|
||||
- Private raw dumps must stay outside git or in ignored local paths only.
|
||||
|
||||
28
starter-pack/README.md
Normal file
28
starter-pack/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Starter Pack (rápido)
|
||||
|
||||
Este pack sirve para arrancar ARNES en 2 escenarios:
|
||||
|
||||
## A) Proyecto nuevo (greenfield)
|
||||
1. Crea repo vacío.
|
||||
2. Instala el core ARNES desde otro repo fuente (`scripts/install_into_repo.sh`).
|
||||
3. Ajusta `backlog/features.json` (`project`, `description`).
|
||||
4. Copia `starter-pack/backlog.features.bootstrap.json` como primera feature (`type=chore`).
|
||||
5. Ejecuta:
|
||||
- `./scripts/verify.sh`
|
||||
- `python3 scripts/agent_status.py show`
|
||||
|
||||
## B) Proyecto ya empezado (brownfield)
|
||||
1. Instala **solo** el core ARNES dentro del repo existente.
|
||||
2. Mantén tu código actual intacto.
|
||||
3. Añade checks del dominio en `scripts/verify.local.sh`.
|
||||
4. Define features reales del proyecto en `backlog/features.json`.
|
||||
5. Ejecuta:
|
||||
- `./scripts/verify.sh`
|
||||
- `python3 scripts/agent_status.py show`
|
||||
|
||||
## Reglas mínimas
|
||||
- 1 sola feature en `in_progress`.
|
||||
- Tipos válidos: `feature`, `fix`, `bug`, `chore`.
|
||||
- `done` requiere gates: `review/security/qa`.
|
||||
- `done` requiere commit+push final del ticket.
|
||||
- Evidencia en `work/artifacts/<feature_id>/`.
|
||||
31
starter-pack/backlog.features.bootstrap.json
Normal file
31
starter-pack/backlog.features.bootstrap.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"id": "F-001",
|
||||
"type": "chore",
|
||||
"title": "Bootstrap ARNES on project",
|
||||
"problem": "Need base workflow and control",
|
||||
"goal": "Make ARNES ready on this repo",
|
||||
"scope_in": [
|
||||
"Harness setup",
|
||||
"Runtime status",
|
||||
"First verify cycle"
|
||||
],
|
||||
"scope_out": [
|
||||
"Business feature work",
|
||||
"Product redesign"
|
||||
],
|
||||
"priority": "med",
|
||||
"risk": "low",
|
||||
"description": "Problem: Need base workflow and control. Goal: Make ARNES ready on this repo. Scope IN: Harness setup, Runtime status, First verify cycle. Scope OUT: Business feature work, Product redesign. Type: chore. Priority: med. Risk: low.",
|
||||
"acceptance": [
|
||||
"verify.sh is green",
|
||||
"runtime status works",
|
||||
"first feature closes with gates"
|
||||
],
|
||||
"status": "pending",
|
||||
"created_at": "YYYY-MM-DD",
|
||||
"gates": {
|
||||
"review": false,
|
||||
"security": false,
|
||||
"qa": false
|
||||
}
|
||||
}
|
||||
112
tests/test_arnes_core.py
Normal file
112
tests/test_arnes_core.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INSTALL = ROOT / 'scripts' / 'install_into_repo.sh'
|
||||
|
||||
|
||||
class ArnesCoreTests(unittest.TestCase):
|
||||
def run_cmd(self, args, cwd=None, input_text=None, check=True):
|
||||
result = subprocess.run(
|
||||
args,
|
||||
cwd=cwd or ROOT,
|
||||
input=input_text,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
raise AssertionError(
|
||||
f"Command failed: {args}\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
||||
)
|
||||
return result
|
||||
|
||||
def test_install_refuses_source_repo(self):
|
||||
result = self.run_cmd([str(INSTALL), str(ROOT)], check=False)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('Refusing to install ARNES into its own source repository', result.stdout)
|
||||
|
||||
def test_install_start_verify_and_publish_in_external_repo(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
target = tmp_path / 'project-repo'
|
||||
remote = tmp_path / 'remote.git'
|
||||
|
||||
self.run_cmd([str(INSTALL), str(target)])
|
||||
self.assertTrue((target / 'scripts' / 'start.sh').is_file())
|
||||
self.assertFalse((target / 'tests').exists(), 'source self-tests must not be installed into project repo')
|
||||
|
||||
self.run_cmd(['git', 'config', 'user.name', 'ARNES Bot'], cwd=target)
|
||||
self.run_cmd(['git', 'config', 'user.email', 'arnes@example.com'], cwd=target)
|
||||
self.run_cmd(['git', 'init', '--bare', str(remote)])
|
||||
self.run_cmd(['git', 'remote', 'add', 'origin', str(remote)], cwd=target)
|
||||
|
||||
wizard_input = '\n'.join([
|
||||
'demo-project',
|
||||
'Demo project',
|
||||
'project',
|
||||
'1',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
]) + '\n'
|
||||
self.run_cmd(['./scripts/start.sh'], cwd=target, input_text=wizard_input)
|
||||
|
||||
self.assertTrue((target / 'project' / 'tests').is_dir())
|
||||
cfg = json.loads((target / 'harness' / 'project.config.json').read_text(encoding='utf-8'))
|
||||
self.assertEqual(cfg['app_dir'], 'project')
|
||||
self.assertEqual(cfg['commands']['test'], 'python3 -m unittest discover -s project/tests -v')
|
||||
|
||||
verify_result = self.run_cmd(['./scripts/verify.sh'], cwd=target)
|
||||
self.assertIn('Harness verificado', verify_result.stdout)
|
||||
|
||||
self.run_cmd(['python3', 'scripts/new_ticket.py'], cwd=target, input_text='\n'.join([
|
||||
'fix',
|
||||
'Repair docs',
|
||||
'Need docs repair',
|
||||
'Make docs clear',
|
||||
'Docs flow',
|
||||
'No redesign',
|
||||
'low',
|
||||
'med',
|
||||
'Docs clear',
|
||||
'Verify green',
|
||||
'',
|
||||
]) + '\n')
|
||||
|
||||
with (target / 'project' / 'README.md').open('a', encoding='utf-8') as fh:
|
||||
fh.write('\nchange\n')
|
||||
|
||||
publish_result = self.run_cmd(
|
||||
['python3', 'scripts/publish_ticket.py', '--feature-id', 'F-001'],
|
||||
cwd=target,
|
||||
)
|
||||
self.assertIn('done ->', publish_result.stdout)
|
||||
|
||||
publish_path = target / 'work' / 'artifacts' / 'F-001' / 'publish.json'
|
||||
payload = json.loads(publish_path.read_text(encoding='utf-8'))
|
||||
self.assertEqual(payload['verdict'], 'PUBLISHED')
|
||||
self.assertTrue(payload['pushed'])
|
||||
self.assertEqual(payload['remote'], 'origin')
|
||||
|
||||
status_result = self.run_cmd(['git', 'status', '--short'], cwd=target)
|
||||
self.assertEqual(status_result.stdout.strip(), '')
|
||||
|
||||
remote_refs = self.run_cmd(['git', 'ls-remote', 'origin'], cwd=target)
|
||||
self.assertIn('refs/heads/master', remote_refs.stdout)
|
||||
|
||||
def test_agent_status_rejects_invalid_agent(self):
|
||||
result = self.run_cmd(
|
||||
['python3', 'scripts/agent_status.py', 'set', '--agent', 'nope'],
|
||||
check=False,
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
combined = (result.stdout + result.stderr)
|
||||
self.assertIn('Invalid agent: nope', combined)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
31
work/artifacts/F-001/architect.md
Normal file
31
work/artifacts/F-001/architect.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Architect Artefact — Feature: F-001
|
||||
|
||||
## SDD Changes
|
||||
- Added `spec/sdd/architecture.md`
|
||||
- Added component docs:
|
||||
- `spec/sdd/components/legacy-web-module.md`
|
||||
- `spec/sdd/components/bulk-seo-worker.md`
|
||||
- `spec/sdd/components/development-data-baseline.md`
|
||||
- Added ADR:
|
||||
- `spec/sdd/decisions/001-store-legacy-app-under-project-web.md`
|
||||
|
||||
## BDD Coverage
|
||||
- Added `spec/bdd/features/layout/legacy-app-layout.feature`
|
||||
- Coverage target:
|
||||
- stable web module path
|
||||
- stable SQL dump path
|
||||
- design trace exists
|
||||
|
||||
## Technical decisions
|
||||
- Use `project/web/index/new` as stable repo path for copied legacy web code.
|
||||
- Use `project/sql/db-25052026.sql` as stable path for local dev dump.
|
||||
- Keep legacy internal module tree unchanged in this feature.
|
||||
|
||||
## Risks found
|
||||
- Legacy code has hard-coded secrets and production URLs.
|
||||
- External files `login.php` and `success.php` are not in repo.
|
||||
- SQL dump may contain production-like data.
|
||||
|
||||
## Next step
|
||||
- Move files with no content change.
|
||||
- Re-run verify after move.
|
||||
24
work/artifacts/F-001/documenter.md
Normal file
24
work/artifacts/F-001/documenter.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Documenter Artefact — Feature: F-001
|
||||
|
||||
## What changed
|
||||
- Added SDD record for legacy PHP product module.
|
||||
- Added ADR for stable repo layout.
|
||||
- Added BDD trace for layout move.
|
||||
- Moved legacy web code to `project/web/index/new`.
|
||||
- Moved SQL dump to `project/sql/db-25052026.sql`.
|
||||
|
||||
## Files to know
|
||||
- `spec/sdd/architecture.md`
|
||||
- `spec/sdd/components/legacy-web-module.md`
|
||||
- `spec/sdd/components/bulk-seo-worker.md`
|
||||
- `spec/sdd/components/development-data-baseline.md`
|
||||
- `spec/sdd/decisions/001-store-legacy-app-under-project-web.md`
|
||||
- `spec/bdd/features/layout/legacy-app-layout.feature`
|
||||
- `project/web/index/new/`
|
||||
- `project/sql/db-25052026.sql`
|
||||
|
||||
## Notes for next feature
|
||||
- Remove secrets from PHP files.
|
||||
- Externalize DB and API config.
|
||||
- Replace production-coupled URLs and missing external dependencies.
|
||||
- Move runtime logs out of web tree.
|
||||
38
work/artifacts/F-001/implementer.md
Normal file
38
work/artifacts/F-001/implementer.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Implementer Artefact — Feature: F-001
|
||||
|
||||
## Summary
|
||||
Moved legacy PHP module into stable ARNES project path.
|
||||
Moved SQL dump into stable project data path.
|
||||
Kept legacy module internal tree unchanged.
|
||||
Updated local project path docs.
|
||||
|
||||
## Changes
|
||||
- moved `project/new` -> `project/web/index/new`
|
||||
- moved `project/db-25052026.sql` -> `project/sql/db-25052026.sql`
|
||||
- updated `project/README.md`
|
||||
- updated `spec/tech.md`
|
||||
- updated `work/current.md`
|
||||
|
||||
## Evidence
|
||||
- before file count in module: `124`
|
||||
- after file count in module: `124`
|
||||
- before size of module: `640K`
|
||||
- after size of module: `640K`
|
||||
- SQL dump present after move: `project/sql/db-25052026.sql` (`229M`)
|
||||
- old path removed: `project/new`
|
||||
- verify result: `./scripts/verify.sh` OK
|
||||
|
||||
## Checks run
|
||||
- `find project/new -type f | wc -l`
|
||||
- `du -sh project/new project/db-25052026.sql`
|
||||
- `mv project/new project/web/index/new`
|
||||
- `mv project/db-25052026.sql project/sql/db-25052026.sql`
|
||||
- `find project -maxdepth 4 | sort`
|
||||
- `find project/web/index/new -type f | wc -l`
|
||||
- `du -sh project/web/index/new project/sql/db-25052026.sql`
|
||||
- `./scripts/verify.sh`
|
||||
|
||||
## Notes
|
||||
- No functional refactor done.
|
||||
- No secret cleanup done.
|
||||
- External dependencies `login.php` and `success.php` still live outside repo.
|
||||
22
work/artifacts/F-001/qa.json
Normal file
22
work/artifacts/F-001/qa.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"feature_id": "F-001",
|
||||
"agent": "qa",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Acceptance criteria for layout move are satisfied. Design trace exists, target paths exist, old path is removed, and core harness verification is green.",
|
||||
"traceability": [
|
||||
"AC: SDD docs exist and explain current legacy app structure -> spec/sdd/architecture.md and component docs created",
|
||||
"AC: ADR records why code moves under project/web and SQL under project/sql -> spec/sdd/decisions/001-store-legacy-app-under-project-web.md",
|
||||
"AC: Legacy code is moved with same contents and no file loss -> implementer evidence shows same file count and size before/after",
|
||||
"AC: SQL dump is kept as local development baseline in project/sql -> project/sql/db-25052026.sql exists and is referenced in docs",
|
||||
"AC: verify.sh is green -> ./scripts/verify.sh passed after move"
|
||||
],
|
||||
"evidence": [
|
||||
"Reviewed spec/bdd/features/layout/legacy-app-layout.feature",
|
||||
"Reviewed work/artifacts/F-001/implementer.md",
|
||||
"Checked project/web/index/new exists",
|
||||
"Checked project/sql/db-25052026.sql exists",
|
||||
"Checked project/new is removed",
|
||||
"Checked ./scripts/verify.sh output is OK"
|
||||
],
|
||||
"timestamp": "2026-05-25T05:45:00Z"
|
||||
}
|
||||
14
work/artifacts/F-001/reviewer.json
Normal file
14
work/artifacts/F-001/reviewer.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"feature_id": "F-001",
|
||||
"agent": "reviewer",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Layout move is correct. SDD and BDD trace exist. Legacy module and SQL dump now live in explicit stable paths. No file loss was found in move evidence.",
|
||||
"evidence": [
|
||||
"Reviewed work/artifacts/F-001/architect.md",
|
||||
"Reviewed work/artifacts/F-001/implementer.md",
|
||||
"Checked project tree under project/web/index/new and project/sql/db-25052026.sql",
|
||||
"Confirmed old path project/new is removed",
|
||||
"Confirmed ./scripts/verify.sh is green"
|
||||
],
|
||||
"timestamp": "2026-05-25T05:45:00Z"
|
||||
}
|
||||
52
work/artifacts/F-001/security.json
Normal file
52
work/artifacts/F-001/security.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"feature_id": "F-001",
|
||||
"agent": "security",
|
||||
"verdict": "CHANGES_REQUESTED",
|
||||
"summary": "Legacy code still contains hard-coded API credentials, database credentials, and production-coupled endpoints inside versioned files. Feature cannot pass security gate until secrets are removed or externalized.",
|
||||
"checks": [
|
||||
"secret scan",
|
||||
"input and config review",
|
||||
"repo path review"
|
||||
],
|
||||
"findings": [
|
||||
{
|
||||
"severity": "high",
|
||||
"title": "Hard-coded API credential in legacy PHP files",
|
||||
"status": "open",
|
||||
"paths": [
|
||||
"project/web/index/new/describe.php",
|
||||
"project/web/index/new/worker_bulk.php",
|
||||
"project/web/index/new/productos_bulk_update.php"
|
||||
]
|
||||
},
|
||||
{
|
||||
"severity": "high",
|
||||
"title": "Hard-coded database credentials in versioned PHP files",
|
||||
"status": "open",
|
||||
"paths": [
|
||||
"project/web/index/new/worker_bulk.php",
|
||||
"project/web/index/new/productos_modificados.php",
|
||||
"project/web/index/new/productos_bulk_update.php",
|
||||
"project/web/index/new/db/conn.php"
|
||||
]
|
||||
},
|
||||
{
|
||||
"severity": "medium",
|
||||
"title": "Code is coupled to production URLs and external auth/success endpoints",
|
||||
"status": "open",
|
||||
"paths": [
|
||||
"project/web/index/new/index.php",
|
||||
"project/web/index/new/inc/header.php",
|
||||
"project/web/index/new/productos_modificados.php",
|
||||
"project/web/index/new/productos_bulk_update.php"
|
||||
]
|
||||
}
|
||||
],
|
||||
"evidence": [
|
||||
"Ran secret scan on project/web/index/new excluding logs",
|
||||
"Found hard-coded API and DB credentials in PHP source files",
|
||||
"Found production URL coupling and external endpoint references",
|
||||
"Reviewed ADR risk note that secrets remain in repo"
|
||||
],
|
||||
"timestamp": "2026-05-25T05:45:00Z"
|
||||
}
|
||||
28
work/artifacts/F-002/architect.md
Normal file
28
work/artifacts/F-002/architect.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Architect Artefact — Feature: F-002
|
||||
|
||||
## SDD Changes
|
||||
- Added `spec/sdd/components/legacy-config-loader.md`
|
||||
- Added `spec/sdd/decisions/002-use-local-config-loader-for-legacy-module.md`
|
||||
- Extended product, tech, and acceptance specs for F-002
|
||||
|
||||
## BDD Coverage
|
||||
- Added `spec/bdd/features/config/legacy-config.feature`
|
||||
- Coverage target:
|
||||
- no tracked secrets in PHP files
|
||||
- one shared config helper
|
||||
- documented local config setup
|
||||
|
||||
## Technical decisions
|
||||
- Use module-root `bootstrap.php` as one config entry point.
|
||||
- Use ignored `config/local.php` for real local values.
|
||||
- Use versioned `config/local.example.php` for safe shape and defaults.
|
||||
- Share one mysqli helper across web pages and worker.
|
||||
|
||||
## Risks found
|
||||
- Example config will not make app fully runnable until maintainer fills local values.
|
||||
- Legacy logs remain under web tree for now.
|
||||
|
||||
## Next step
|
||||
- Implement bootstrap and config files.
|
||||
- Replace inline secrets and URLs in tracked PHP files.
|
||||
- Run verify and secret scan.
|
||||
27
work/artifacts/F-002/documenter.md
Normal file
27
work/artifacts/F-002/documenter.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Documenter Artefact — Feature: F-002
|
||||
|
||||
## What changed
|
||||
- Added one shared config loader at `project/web/index/new/bootstrap.php`.
|
||||
- Added versioned config template at `project/web/index/new/config/local.example.php`.
|
||||
- Added local setup notes at `project/web/index/new/config/README.md`.
|
||||
- Ignored real local config file `project/web/index/new/config/local.php`.
|
||||
- Updated legacy PHP entry points to use shared config lookups.
|
||||
|
||||
## Important files
|
||||
- `project/web/index/new/bootstrap.php`
|
||||
- `project/web/index/new/config/local.example.php`
|
||||
- `project/web/index/new/config/README.md`
|
||||
- `project/web/index/new/db/conn.php`
|
||||
- `project/web/index/new/inc/header.php`
|
||||
- `project/web/index/new/index.php`
|
||||
- `project/web/index/new/describe.php`
|
||||
- `project/web/index/new/productos_bulk_update.php`
|
||||
- `project/web/index/new/productos_modificados.php`
|
||||
- `project/web/index/new/worker_bulk.php`
|
||||
|
||||
## Local setup note
|
||||
Copy or edit `project/web/index/new/config/local.php` with real local values before running the module.
|
||||
|
||||
## Follow-up
|
||||
- Review the SQL dump for sensitive data and retention policy.
|
||||
- Consider moving runtime logs out of the web tree.
|
||||
35
work/artifacts/F-002/implementer.md
Normal file
35
work/artifacts/F-002/implementer.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Implementer Artefact — Feature: F-002
|
||||
|
||||
## Summary
|
||||
Added one shared config loader for the legacy PHP module.
|
||||
Moved DB, OpenAI, and route values out of tracked PHP source files.
|
||||
Added versioned config template and ignored local config file path.
|
||||
Updated docs and specs for local setup.
|
||||
|
||||
## Code changes
|
||||
- added `project/web/index/new/bootstrap.php`
|
||||
- added `project/web/index/new/config/local.example.php`
|
||||
- added `project/web/index/new/config/README.md`
|
||||
- added `project/web/index/new/README.md`
|
||||
- updated `.gitignore` to ignore `project/web/index/new/config/local.php`
|
||||
- updated PHP entry points to use `legacy_config()` and `legacy_new_mysqli()`
|
||||
- removed inline DB and OpenAI secrets from tracked PHP files
|
||||
- replaced inline production URLs in tracked PHP files with config keys
|
||||
|
||||
## Evidence
|
||||
- `./scripts/verify.sh` -> OK
|
||||
- secret scan on tracked PHP files -> no hard-coded DB or OpenAI secrets found
|
||||
- route scan on tracked PHP files -> no hard-coded production URLs found
|
||||
- ignore check -> `project/web/index/new/config/local.php` is ignored by git
|
||||
- local config docs exist -> `project/web/index/new/config/README.md`
|
||||
- config template exists -> `project/web/index/new/config/local.example.php`
|
||||
|
||||
## Checks run
|
||||
- `./scripts/verify.sh`
|
||||
- `rg -n "(sk-proj-|admin_natural|oo6478022A)" project/web/index/new --glob '!logs/*' --glob '!config/local.php' --glob '!config/local.example.php' --glob '*.php'`
|
||||
- `rg -n "https://mercadodevida\.es|https://www\.mercadodevida\.es" project/web/index/new --glob '!logs/*' --glob '!config/local.php' --glob '!config/local.example.php' --glob '*.php'`
|
||||
- `git check-ignore -v project/web/index/new/config/local.php`
|
||||
|
||||
## Notes
|
||||
- Real local values must be filled in `config/local.php`.
|
||||
- Existing SQL dump remains in repo and should be handled by separate data-security work.
|
||||
14
work/artifacts/F-002/leader-close.json
Normal file
14
work/artifacts/F-002/leader-close.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"feature_id": "F-002",
|
||||
"agent": "leader",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "All required non-leader gates are approved for F-002. Feature is accepted for final publish with shared config loader, tracked secret removal, and green harness verification.",
|
||||
"evidence": [
|
||||
"Reviewed work/artifacts/F-002/reviewer.json -> APPROVED",
|
||||
"Reviewed work/artifacts/F-002/security.json -> APPROVED",
|
||||
"Reviewed work/artifacts/F-002/qa.json -> APPROVED",
|
||||
"Reviewed work/artifacts/F-002/documenter.md",
|
||||
"Ran ./scripts/verify.sh -> OK"
|
||||
],
|
||||
"timestamp": "2026-05-25T06:00:00Z"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user