Skip to content

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.

  • DB (tabla Module en MariaDB) es canónica para metadata estructurada: slug, name, kind, description, status, milestone, priority, config (shape específica por kind), tasks, docPath, autoGenerate.
  • Disco (docs/modules/<slug>/README.md) es un artefacto sincronizado. Cuando autoGenerate=true, el archivo se reescribe por completo desde la metadata; cuando autoGenerate=false, el archivo es libre y solo se importa metadata cuando hay frontmatter YAML al inicio.

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 != lastSyncedHash
uiEdit <= | synced | file-ahead
uiEdit > | ui-ahead | conflict

Casos de borde:

  • Archivo no existe + nunca sincronizado → ui-ahead (esperando primer push)
  • Archivo no existe + tuvimos sync previo → file-ahead (alguien borró el .md)

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:

  1. Resuelve el path al slug (acepta docs/modules/<slug>/README.md, docs/modules/<slug>.md, o cualquier sub-archivo bajo <slug>/).
  2. Lee el archivo, calcula el hash SHA-256, y actualiza lastFileEditAt + syncStatus según detectSyncStatus.
  3. Emite modules:sync-changed por 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.

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 UIpushFromDb({force: true}) reescribe el archivo.
  • Aceptar archivoimportFromDisk({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.

Cuando autoGenerate=true, renderReadme(record) produce:

---
name: <name>
kind: <kind>
status: <status>
milestone: <milestone?>
description: "<description>"
autoGenerate: true
config: {<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 agente
desde 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ó.