Sincronización de módulos — DB ↔ docs/modules/
Synced read-only from
/home/synnet/mirrors/synvirt-product/docs/MODULE_SYNC.md. Edit at the source, not here.
Sincronización de módulos — DB ↔ docs/modules/
Section titled “Sincronización de módulos — DB ↔ docs/modules/”Esta nota describe cómo el catálogo editable de módulos en
SynVirtDev Console → Ajustes → Módulos se mantiene en sincronía con
los archivos docs/modules/<slug>/README.md del repo SynVirt.
Fuente de verdad
Section titled “Fuente de verdad”- DB (tabla
Moduleen MariaDB) es canónica para metadata estructurada:slug,name,kind,description,status,milestone,priority,config(shape específica porkind),tasks,docPath,autoGenerate. - Disco (
docs/modules/<slug>/README.md) es un artefacto sincronizado. CuandoautoGenerate=true, el archivo se reescribe por completo desde la metadata; cuandoautoGenerate=false, el archivo es libre y solo se importa metadata cuando hay frontmatter YAML al inicio.
Reconciliación
Section titled “Reconciliación”Cada módulo guarda tres timestamps + un hash:
| campo | qué refleja |
|---|---|
lastUiEditAt |
última vez que la UI / API mutó la fila |
lastFileEditAt |
última vez que el watcher detectó cambio en disco |
lastSyncedAt |
última vez que UI y disco quedaron de acuerdo |
lastSyncedHash |
hash SHA-256 del archivo en lastSyncedAt |
Función detectSyncStatus(record, disk):
disk.hash == lastSyncedHash | disk.hash != lastSyncedHashuiEdit <= | synced | file-aheaduiEdit > | ui-ahead | conflictCasos de borde:
- Archivo no existe + nunca sincronizado →
ui-ahead(esperando primer push) - Archivo no existe + tuvimos sync previo →
file-ahead(alguien borró el .md)
Watcher
Section titled “Watcher”crates/synvirtdev-console/src/modules/docs/server/watcher.ts (chokidar)
mira docs/** y enruta los cambios bajo docs/modules/** al handler
onDocsModulesChange() del módulo modules. El handler:
- Resuelve el path al
slug(aceptadocs/modules/<slug>/README.md,docs/modules/<slug>.md, o cualquier sub-archivo bajo<slug>/). - Lee el archivo, calcula el hash SHA-256, y actualiza
lastFileEditAt+syncStatussegúndetectSyncStatus. - Emite
modules:sync-changedpor Socket.io para que la UI refresque la card sin polling.
Worktrees ignorados. Los paths que contienen /dev-worktrees/ se
descartan: el watcher solo reacciona a cambios en master/main.
El watcher no auto-importa. Detecta el file-ahead y lo deja al
operador. Importar archivo → DB requiere acción explícita en la UI o
una llamada a POST /api/settings/modules/<slug>/import-from-file.
Resolución de conflictos
Section titled “Resolución de conflictos”Si UI y archivo cambian entre dos lastSyncedAt, el módulo entra en
conflict. La UI muestra un Diff side-by-side (Monaco DiffEditor)
entre lo que el server escribiría desde la DB y lo que está en disco.
El operador elige una estrategia:
- Aceptar UI —
pushFromDb({force: true})reescribe el archivo. - Aceptar archivo —
importFromDisk({force: true})lee frontmatter y blurb, los pisa sobre la fila DB. - Merge manual — el operador edita el resultado en Monaco; el server escribe el contenido literal y luego re-importa la metadata para que DB y archivo queden idénticos.
Generación automática
Section titled “Generación automática”Cuando autoGenerate=true, renderReadme(record) produce:
---name: <name>kind: <kind>status: <status>milestone: <milestone?>description: "<description>"autoGenerate: trueconfig: {<JSON inline>}tasks: [<JSON inline>]---# <name>
> <description>
**Tipo:** `<kind>` · **Status:** <status> · **Hito:** <milestone>
## Componentes<lista derivada del kind>
## Tareas sugeridas- [ ] tarea 1- [ ] tarea 2
## Configuración```json<JSON pretty-print>Documento generado automáticamente desde Ajustes → Módulos. No editar a mano si “Auto-generar” está ON.
El footer sentinela (`Documento generado automáticamente…`) permite a`isAutoGenerated()` detectar archivos manejados por la UI; con eso`pushFromDb` se niega a sobreescribir un README hand-edited cuando`autoGenerate=true` (a menos que pases `force`).
## Endpoints
| Método | Path | Descripción ||--------|----------------------------------------------------------------|--------------------------------------|| GET | `/api/settings/modules` | Listar (filtros: `kind`, `status`, `q`, `includeArchived`) || GET | `/api/settings/modules/:slug` | Detalle || POST | `/api/settings/modules` | Crear || PUT | `/api/settings/modules/:slug` | Actualizar || DELETE | `/api/settings/modules/:slug` | Borrar (`{archive: true}` o `{confirmSlug: <slug>}`) || POST | `/api/settings/modules/:slug/regenerate-doc` | Push DB → archivo || POST | `/api/settings/modules/:slug/import-from-file` | Pull archivo → DB || POST | `/api/settings/modules/:slug/resolve-conflict` | `{strategy: 'ui' \| 'file' \| 'manual', mergedContent?}` || POST | `/api/settings/modules/:slug/conflict-preview` | `{side: 'ui' \| 'file'}` → markdown crudo (para el diff) || POST | `/api/settings/modules/rescan-repo` | Reporta `newDetected`, `orphans`, `unmappedCrates`, `drifts` || POST | `/api/settings/modules/regenerate-index` | Reescribe `docs/modules/README.md` || GET | `/api/settings/modules/cargo-workspace` | Lista crates del workspace Rust || GET | `/api/settings/modules/scripts-discovery` | Lista scripts en `scripts/`, `live/`, `tools/`, `iso-builder/` |
Todos requieren JWT válido (cookie). Cada mutación graba un`AuditLog` con `action=settings.modules.*`.
## Bloqueo por agentes activos
Borrar un módulo (no archivar) falla con HTTP 409 si existe un`AgentLock` activo apuntando a ese slug. Hay que terminar el agentedesde el tab Módulos antes de poder eliminar.
## Migración inicial
`src/modules/modules/server/seed.ts` parsea `docs/modules/README.md`con el parser legacy y siembra la tabla `Module`:
- Heurística: si existe `crates/<slug>` o `crates/synvirt-<slug>` → `rust-crate`; `performance|kernel-tuning|sysctl|tuning` → `os-tuning`; `web-ux`/`web-installer` → `vue-app`; sufijos `iso|tools|installer|live|guest-tools|virt-tools` → `iso-builder`; default `rust-crate`.- Run dry-run primero: `tsx src/modules/modules/server/seed.ts --dry-run`.- El operador ajusta el `kind` desde la UI si la heurística falló.