Skip to content

Projects — biblioteca Constructor

Ghid pentru agenți (și autori) despre proiecte în Constructor: ce sunt, cum arată biblioteca, și cum se creează / modifică / șterg.
Nu inventa API — folosește doar metodele din DocStore / ProjectLibraryService și tipurile din @gstack/gds-core.

NivelTipRol
LibraryLibraryFileÎntregul workspace multi-proiect: listă + proiect activ + revision. Persistat în localStorage și dual-write în PostgreSQL via GET/PUT /api/library.
ProjectGdsProject (în WorkspaceSnapshot)Un mockup: meta la nivel de proiect + pages[] (web) + screens[] (mobil).
Page / screenGdsPage / GdsScreenO pagină (sau ecran mobil) în proiect: titlu, sidenav opțional (doar pe page), blocks[].

Nu confunda:

  • ștergerea unei pagini ≠ ștergerea unui proiect;
  • activePageId / activeScreenId sunt cursor UI în snapshot, nu ID-ul proiectului;
  • biblioteca (LibraryFile) ≠ un singur GdsProject.
interface LibraryFile {
version: 1;
activeId: string; // id proiect deschis în editor
order: string[]; // ordine afișare (cel mai nou primul)
items: Record<string, WorkspaceSnapshot>;
revision?: number; // monotonic; stale autosave guard (cross-tab)
}
interface WorkspaceSnapshot {
project: GdsProject;
activePageId: string;
activeScreenId: string;
editSurface: 'page' | 'screen';
}

Cheie locală: gds.constructor.library.v1.
Sincronizare cross-tab: BroadcastChannel + evenimentul storage (același browser).
Durabilitate: ProjectLibraryService face dual-write — după fiecare persist() local, PUT /api/library (debounced) către Postgres; la boot face GET /api/library și merge după revision (nu șterge date locale dacă API/DB e jos).
Salvare explicită: butonul Salvează din tabstrip (app-tabs) → DocStore.saveProjectToDatabase() → PUT /api/projects/:id (JSON-ul proiectului activ din localStorage).

Terminal window
# Pornește containerul (port host default 5432; dacă e ocupat: POSTGRES_PORT=5433)
npm run db:up
# Variabile în `.env` (vezi `.env.example`): POSTGRES_* sau DATABASE_URL
# API (:4202) citește aceleași variabile — restart `npm start` după db:up

Schema: .database/init.sql (constructor_library = blob LibraryFile, constructor_projects = snapshot per proiect).
Health: GET /api/health → { ok: true, db: "up"|"down"|"disabled" }.
Per proiect: GET/PUT /api/projects/:id → { id, snapshot } / { id, snapshot, revision }.

Semnale utile pe ProjectLibraryService: activeId, summaries, entries, peerRevision, revision(), pushActiveProjectToRemote().

Contract canonic: projects/gds-core/src/lib/blocks.ts → GdsProject.

interface GdsProject {
version: 2;
meta: {
title: string; // numele proiectului (card / rename)
product: string; // produs (sincronizat și din sidenav.product.title)
accent: AccentName; // indigo|emerald|amber|rose|blue|teal|…
font?: FontName; // default ibm-plex
borderColor?: string; // opțional; omit → token default
lang: Lang; // 'en' | 'ro'
seed: number;
gridCell?: number;
boardWidthPx?: number;
boardHeightPx?: number;
deletedPageIds?: string[]; // tombstone la merge — nu reînvia pagini șterse
};
pages: GdsPage[];
screens?: GdsScreen[]; // după normalize: întotdeauna ≥1
}

Accent / font / border aplicate global pe toate proiectele: DocStore.setGlobalAccent / setGlobalFont / setGlobalBorderColor → ProjectLibraryService.applyThemeToAll.

  • Lista / carduri: app-projects-page (projects-page.component.ts) — create, open, rename, delete, export HTML.
  • Switch activ: DocStore.openProject(id) (flush autosave → library.open → applySnapshot).

API real (preferat):

docStore.createProject();

Flux: flushAutosave → library.create(lang) → applySnapshot.

Ce face ProjectLibraryService.create:

  1. Generează id proj-<base36>-….
  2. Seed din initialProject() — gol: o pagină Home, un screen Screen 1, fără sidenav / blocuri.
  3. Meta default: title: 'New mockup', product: 'Product', accent/font default, lang: 'en' (create forțează EN în intern).
  4. Al 2-lea+ proiect: titlul devine Mockup 2, Mockup 3, …
  5. Setează activeId pe noul proiect, îl pune primul în order, persist() (bump revision + localStorage + PUT debounced).

Gol vs seed GLog:

  • Create din UI/library = gol (initialProject), nu apelează buildGlogProject.
  • GLog canonic: buildGlogProject(lang) în glog.project.ts (ex. tools/seed-glog.ts). Dacă meta.title / meta.product e glog, normalizeProject rulează repairGlogProject (refresh demo blocks, drop pagină Settings veche).
  • Nu inventa un „create seeded” pe DocStore — nu există; seed-ul GLog e tool / repair, nu butonul „New project”.

Înainte (un proiect activ):

{
"version": 1,
"activeId": "proj-abc",
"order": ["proj-abc"],
"items": {
"proj-abc": {
"project": {
"version": 2,
"meta": {
"title": "New mockup",
"product": "Product",
"accent": "indigo",
"lang": "en",
"seed": 42
},
"pages": [{ "id": "p1", "title": "Home", "sidenav": null, "blocks": [] }],
"screens": [{ "id": "s1", "title": "Screen 1", "blocks": [] }]
},
"activePageId": "p1",
"activeScreenId": "s1",
"editSurface": "page"
}
},
"revision": 3
}

După createProject():

{
"version": 1,
"activeId": "proj-xyz",
"order": ["proj-xyz", "proj-abc"],
"items": {
"proj-xyz": {
"project": {
"version": 2,
"meta": {
"title": "Mockup 2",
"product": "Product",
"accent": "indigo",
"lang": "en",
"seed": 123456789
},
"pages": [{ "id": "p…", "title": "Home", "sidenav": null, "blocks": [] }],
"screens": [{ "id": "s…", "title": "Screen 1", "blocks": [] }]
},
"activePageId": "p…",
"activeScreenId": "s…",
"editSurface": "page"
},
"proj-abc": { "…": "neschimbat" }
},
"revision": 4
}
AcțiuneAPI
Switch activdocStore.openProject(id) → boolean (false dacă id necunoscut)
RenamedocStore.renameProject(id, title) — trim obligatoriu; pe activ apelează setTitle (cu history); pe inactiv library.rename
Meta titlu (activ)docStore.setTitle(title)
Accent / font / border (doar activ)setAccent / setFont / setBorderColor
Accent / font / border (toată library)setGlobalAccent / setGlobalFont / setGlobalBorderColor
Board sizesetBoardSize(w, h)
Salvare canvas activautosave → library.saveActive(snapshot, expectedRev?); false dacă expectedRev ≠ revision() (stale)

saveActive nu șterge pagini sibling din library dacă DocStore vine cu mai puține pagini decât snapshot-ul existent (protecție wipe).

docStore.renameProject('proj-xyz', 'Portal cetățean');
docStore.openProject('proj-abc'); // salvează activul curent, apoi încarcă proj-abc

După rename pe proj-xyz: items['proj-xyz'].project.meta.title === 'Portal cetățean' (și cardul din summaries / entries).

docStore.deleteProject(id); // boolean

Reguli reale (library.delete):

  1. Refuză dacă rămâne un singur proiect (Object.keys(items).length <= 1) → { ok: false }.
  2. Refuză dacă id lipsește din items.
  3. Scoate din items + order; bump revision; write localStorage; BroadcastChannel pe peeri.
  4. Dacă proiectul șters era activ: activeId = order[0], returnează snapshot-ul noului activ → DocStore applySnapshot.
  5. Dacă șters ≠ activ: editorul rămâne pe proiectul curent (switched: false).

UI: confirm dialog; butonul e no-op când entries.length <= 1.

Înainte: order: ["proj-xyz", "proj-abc"], activeId: "proj-xyz".

După deleteProject('proj-xyz'):

{
"version": 1,
"activeId": "proj-abc",
"order": ["proj-abc"],
"items": { "proj-abc": { "…": "…" } },
"revision": 5
}

După încercare de a șterge ultimul proiect: library neschimbată, deleteProject → false.

  • revision crește la fiecare persist / delete / theme-all write.
  • Autosave cu expectedRev diferit de library → skip (nu scrie peste peer).
  • Cross-tab: BroadcastChannel + storage → reloadFromLocalStorage + mergeProjectPages (reatașează pagini locale lipsă dacă peer le-a dropat fără deletedPageIds).
  • Dedup pe titlu (case-insensitive) la normalize — evită duplicate GLog / seed.
  • Nu „câștiga” o cursă inventând un revision mai mare manual în JSON — lasă library service să avanseze.
  1. Nu șterge / golește items la {} din agent — lasă cel puțin un proiect.
  2. Nu inventa race pe revision (scrieri directe în localStorage cu rev inventat).
  3. Nu confunda proiect cu pagină: addPage / remove page ≠ createProject / deleteProject.
  4. Nu presupune că create(lang) păstrează limba cerută — implementarea internă seed-uiește EN.
  5. Nu aștepta ca chat AI să muteze library fără wiring — vezi mai jos.
  6. Nu inventa props pe componente GDS ca să „creezi un proiect”.
  7. Dual-write Postgres e opțional: fără DB, localStorage rămâne sursa de adevăr a sesiunii; cu npm run db:up, GET/PUT /api/library sincronizează același LibraryFile, iar Salvează din tabstrip face PUT /api/projects/:id.

AiAgentService trimite la /api/ai/chat:

{
messages,
project: this.store.project(), // GdsProject activ (meta + pages + screens)
selection: {
selectedId, activePageId, activeScreenId, editSurface, activeProjectId, lang,
},
library: {
activeId,
projects: [{ id, title }, …], // din projectSummaries — nu tot LibraryFile
},
}

Acțiuni library expuse în chat (via ai-agent-actions / Kimi tools): create_project, open_project, rename_project.
Nu există delete_project în tool surface — ștergerea rămâne UI-only (DocStore.deleteProject).

Screens: paritate cu pages (select_screen / add_screen / rename_screen / remove_screen / duplicate_screen); add_block poate ținti screenId (mutually exclusive cu pageId).

Lang: set_lang → DocStore.setLang('en'|'ro').

Documentează totuși API-urile reale de mai sus: sursa de adevăr rămâne DocStore + ProjectLibraryService.

FișierRol
projects/constructor/src/app/state/project-library.service.tsLibrary: create, open, rename, delete, saveActive, pushActiveProjectToRemote, theme-all, revision
projects/constructor/src/app/state/doc-store.tsEditor: createProject, openProject, renameProject, deleteProject, saveProjectToDatabase, meta
projects/constructor/src/app/state/document.factory.tsinitialProject, normalizeProject
projects/constructor/src/app/state/glog.project.tsbuildGlogProject, repairGlogProject
projects/constructor/src/app/builder/projects-page.component.tsUI library
projects/constructor/src/app/builder/tabs.component.tsTabstrip pagini + buton Salvează → DB
projects/gds-core/src/lib/blocks.tsGdsProject
tools/api-server.mjs/api/library, /api/projects/:id, /api/health, /api/ai/*
tools/library-db.mjsLayer Postgres (getLibrary / putLibrary / getProject / putProject)