core

PHP Simple Fast & Secure

View on GitHub

Home › Admin UI › Design history › Admin 2.0 Native Areas Implementation Plan

Admin 2.0 Native Areas Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Reemplazar los cinco paneles administrativos legacy por interfaces Angular 22 nativas que consumen contratos JSON v2 autenticados.

Architecture: AdminFrontendController se divide en adaptadores JSON específicos por área, apoyados en una capa común que transforma ConfigForm, AdminForm y ModuleForm en DTOs de formulario. Angular consume exclusivamente estos contratos mediante AdminApiService; las páginas no cargan Twig, AngularJS ni documentos embebidos.

Tech Stack: PHP 8.3, PSFS Router/Form/DTO, Angular 22 standalone, RxJS, Docker Compose, PHPUnit y Playwright.

Global Constraints


Task 1: Eliminar el puente iframe y definir contratos Angular comunes

Files:

Interfaces:

it('unwraps a successful administrative envelope', () => {
  service.get<{ value: string }>('/admin/api/v2/example').subscribe((result) => {
    expect(result.data.value).toBe('ok');
  });
  request.expectOne('/admin/api/v2/example').flush({ ok: true, message: null, data: { value: 'ok' }, errors: {} });
});

it('keeps structured backend errors', () => {
  service.put('/admin/api/v2/example', {}).subscribe({ error: (error) => {
    expect(error.errors.name).toEqual(['Required']);
  }});
  request.expectOne('/admin/api/v2/example').flush({ ok: false, message: 'Invalid form', data: null, errors: { name: ['Required'] } }, { status: 422, statusText: 'Unprocessable Entity' });
});

Run inside Node container: npm run test -- --include='**/admin-api.service.spec.ts'

Expected: compilation failure naming AdminApiService.

export interface AdminEnvelope<T> {
  ok: boolean;
  message: string | null;
  data: T;
  errors: Record<string, string[]>;
}

get<T>(path: string): Observable<AdminEnvelope<T>> {
  return this.http.get<AdminEnvelope<T>>(`/admin/api/v2/${path}`);
}

For POST, PUT and DELETE, call the same root with JSON payloads and preserve HttpErrorResponse.error when it matches AdminEnvelope<null>.

Delete the component and iframe CSS. Add native route components only; each route starts with a loading, success and structured error state.

Run inside Node container: npm run test and npm run build:admin

Expected: both complete with no iframe selector or bypassSecurityTrustResourceUrl in ui/projects/admin.

Do not commit. Present the new shell screenshot and git diff for approval.

Task 2: Crear la serialización segura y adaptadores de formulario PHP

Files:

Interfaces:

public function testRemovesSecurityTokensAndMasksSecretValues(): void
{
    $schema = (new AdminFormSchemaFactory())->fromForm($form);
    self::assertArrayNotHasKey('__token', $schema['fields']);
    self::assertSame('', $schema['fields']['root.api.secret']['value']);
}

public function testKeepsFieldTypeOptionsAndRequiredFlag(): void
{
    self::assertSame('select', $schema['fields']['profile']['type']);
    self::assertTrue($schema['fields']['profile']['required']);
    self::assertNotEmpty($schema['fields']['profile']['options']);
}

Run: docker exec <php-container> php vendor/bin/phpunit tests/base/admin/AdminFormSchemaFactoryTest.php

Expected: class-not-found failure.

Use the existing form field metadata (getFields() and DTO serialization). Preserve only name, label, type, value, required, options, help and validation rules. Omit CSRF/session tokens and blank values for fields whose name contains secret, password, token or hash.

Run: docker exec <php-container> php vendor/bin/phpunit tests/base/admin/AdminFormSchemaFactoryTest.php

Expected: PASS.

Do not commit. Confirm the emitted schema contains no current secret from config.json.

Task 3: Migrar rutas y documentación como páginas Angular de sólo lectura

Files:

Interfaces:

public function testRoutesEndpointReturnsTheRouterCatalogWithoutHtml(): void
{
    $response = $controller->routes();
    self::assertStringContainsString('"ok":true', $response);
    self::assertStringNotContainsString('<html', $response);
}

public function testRegenerationKeepsTheExistingAuthorizationBoundary(): void
{
    $this->expectException(ApiException::class);
    $controller->regenerate();
}

Run: docker exec <php-container> php vendor/bin/phpunit tests/controller/AdminFrontendRoutesControllerTest.php

Expected: route-controller class missing.

routes() uses Router::getInstance()->getSlugs(). regenerate() keeps the exact hydrateRouting() flow and returns success/failure JSON. Documentation validates Router::domainExists($domain) before invoking the same DocumentorService formatter used by DocumentorController.

RoutesPageComponent renders sortable route rows and an explicit regeneration button. DocumentationPageComponent lists domains, requests a selected document and presents its JSON or a Swagger-compatible link. Both display AdminEnvelope errors.

Run PHP: docker exec <php-container> php vendor/bin/phpunit tests/controller/AdminFrontendRoutesControllerTest.php

Run Node: docker exec <node-container> npm run build:admin

Browser: authenticate with Basic Auth and load /admin-v2/routes and /admin-v2/api/docs; assert no iframe element and successful JSON requests.

Do not commit. Validate route regeneration manually only after reviewing its action and result.

Task 4: Migrar configuración a formulario Angular nativo

Files:

Interfaces:

public function testConfigReadDoesNotExposeConfiguredSecrets(): void
{
    $body = $controller->show();
    self::assertStringNotContainsString((string) Config::getParam('root.api.secret'), $body);
}

public function testInvalidConfigWriteReturnsFieldErrorsAndDoesNotSave(): void
{
    $response = $controller->update();
    self::assertSame(422, ResponseHelper::getStatusCode());
    self::assertStringContainsString('errors', $response);
}

Run: docker exec <php-container> php vendor/bin/phpunit tests/controller/AdminFrontendConfigControllerTest.php

Expected: class-not-found failure.

Build ConfigForm from the same required/optional arrays and dumpConfig() as ConfigController. For update, hydrate only the supplied JSON payload, run isValid(), then call Config::save() only on valid data and return changed field names. Keep assertSuperAdminConfigWriteAccess() equivalent in the adapter.

Render text, password, checkbox, select and textarea fields using Reactive Forms. Do not render masked secrets as existing values. On success, display returned message; on 422, attach each error to its form control.

Run controller tests with injected form doubles. In browser, load /admin-v2/config, submit an invalid payload and assert errors render. Do not submit a successful mutation against the shared config.

Do not commit. Request approval before a manually verified valid config write.

Task 5: Migrar gestión de usuarios a Angular nativo

Files:

Interfaces:

public function testUsersListDoesNotReturnHashesOrPasswords(): void
{
    $body = $controller->index();
    self::assertStringNotContainsString('hash', $body);
    self::assertStringNotContainsString('password', $body);
}

public function testDeleteUsesTheExistingSuperAdminGuard(): void
{
    $this->expectException(ApiException::class);
    $controller->delete('other-admin');
}

Run: docker exec <php-container> php vendor/bin/phpunit tests/controller/AdminFrontendUsersControllerTest.php

Return aliases and profiles only. Reuse AdminForm validation for create/update. Reuse the existing super-admin condition from UserController before every mutation. Preserve status 400/403/422 in structured JSON.

Render table rows, create/edit form and a destructive action requiring an explicit confirmation. Do not expose password hashes; a password field is write-only.

Run PHP and Angular focused tests. In browser, verify list rendering and invalid-create error. Do not create or delete actual local administrators without human approval.

Do not commit. Obtain approval before a persistent create/update/delete browser test.

Task 6: Migrar generador de módulos a Angular nativo

Files:

Interfaces:

public function testModuleSchemaIsJsonAndContainsControllerTypeOptions(): void
{
    $body = $controller->schema();
    self::assertStringContainsString('controllerType', $body);
    self::assertStringNotContainsString('<form', $body);
}

public function testInvalidModuleNameReturns422BeforeGeneratorIsCalled(): void
{
    $response = $controller->create();
    self::assertSame(422, ResponseHelper::getStatusCode());
    self::assertStringContainsString('errors', $response);
}

Run: docker exec <php-container> php vendor/bin/phpunit tests/controller/AdminFrontendModulesControllerTest.php

Build and hydrate ModuleForm. Keep the legacy module normalization (\\ and / handling) and call GeneratorHelper::checkCustomNamespaceApi() before GeneratorService::createStructureModule(). Return generator exceptions as AdminEnvelope failures without HTML.

Reuse DynamicFormComponent; show a pre-submit warning that this operation writes a module. Display returned module name and structured failure text.

Run focused PHP/Angular tests. In browser, submit invalid data only. Create no module until the user supplies the intended test-module specification.

Do not commit. Await user instruction for the test module creation.

Task 7: Eliminar legacy embeds, documentar y ejecutar paridad de navegación

Files:

Interfaces:

for (const path of ['config', 'setup', 'module', 'routes', 'api/docs']) {
  await page.goto(`/admin/${path}?__front=v2`);
  await expect(page.locator('iframe')).toHaveCount(0);
  await expect(page.locator('psfs-admin-root')).toBeVisible();
}

Run: docker compose --profile e2e run --rm ui-e2e npx playwright test e2e/admin-v2-core.spec.mjs

Expected: failure on iframe count or missing JSON endpoints before Tasks 3-6 complete.

Delete AdminLegacyPanelComponent, iframe styles and any bypassSecurityTrustResourceUrl import. Update routes to native page components.

Document endpoint contracts, the no-iframe invariant, Docker commands and the temporary HMR direct-socket limitation. Keep rollback instructions using __front=legacy.

Run PHP: docker exec <php-container> php vendor/bin/phpunit tests/base/AdminFrontendVersionResolverTest.php tests/controller/AdminFrontend*ControllerTest.php

Run Node: docker exec <node-container> npm run build:admin

Run browser E2E: docker compose --profile e2e run --rm ui-e2e npx playwright test e2e/admin-v2-core.spec.mjs

Expected: all pass, no config write, no iframe and no legacy document request from a v2 route.

Do not commit. Present test output, screenshots and the full diff for approval.

Plan self-review


← Back to Design history