# Admin Workflow and Hardening Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Complete the plugin's usable admin workflow for configuring sync pairs, testing connections, importing/exporting package files, viewing operation results/logs, and running a final smoke checklist with hardened security and user-facing errors. **Architecture:** Phase 6 keeps the current import-first sync engine and adds admin workflows around it. Settings stay option-backed, state-changing admin actions use dedicated controllers with capability and nonce checks, and templates receive pre-sanitized view data while still escaping every output at render time. **Tech Stack:** PHP 7.4, WordPress admin APIs, WordPress HTTP/REST APIs, PHPUnit, PHPStan, PHPCS/WPCS, existing `SettingsRepository`, `RestTransportClient`, `JsonFileTransport`, `SyncEngine`, and `OptionLogger`. --- ## File Structure - `src/Settings/Settings.php`: expand typed settings to include sync pair credentials, URL mappings, selected content types, default direction, and retention/debug controls. - `src/Admin/AdminNotice.php`: value object for redirect-driven admin notices with `type`, `message`, and optional result context. - `src/Admin/AdminNoticeRepository.php`: converts `$_GET` query flags into safe notices for templates. - `src/Admin/SettingsController.php`: handles settings saves through `admin_post_wpcs_save_settings`. - `src/Admin/ConnectionTestController.php`: handles connection diagnostics through `admin_post_wpcs_test_connection`. - `src/Admin/FileExportController.php`: exports a valid empty package scaffold for a configured pair until full extraction is implemented. - `src/Admin/LogController.php`: handles log clearing through `admin_post_wpcs_clear_logs`. - `src/Admin/AdminPage.php`: registers the new controllers and passes settings/notices/logs to the dashboard template. - `src/Logging/OptionLogger.php`: add safe read/clear helpers for operation history. - `src/Transport/RestTransportClient.php`: treat REST `accepted: false` JSON responses as remote rejections. - `templates/admin/dashboard.php`: replace the placeholder dashboard with forms for settings, diagnostics, file import/export, operation history, and smoke guidance. - `tests/Unit/Admin/*Test.php`: unit coverage for notices, settings save, connection diagnostics, export, log clearing, and dashboard rendering. - `tests/Unit/SettingsTest.php`: settings expansion and sanitization coverage. - `tests/Unit/Transport/RestTransportClientTest.php`: REST rejected-body coverage. - `docs/smoke/phase-6-admin-hardening.md`: manual smoke checklist and known local environment notes. --- ## Task 1: Expand Settings for Admin Workflow **Files:** - Modify: `src/Settings/Settings.php` - Modify: `tests/Unit/SettingsTest.php` - [ ] **Step 1: Write failing settings tests** Add tests to `tests/Unit/SettingsTest.php`: ```php public function test_it_sanitizes_full_admin_workflow_settings(): void { $settings = Settings::fromArray( array( 'sync_pairs' => array( array( 'name' => 'Production to Staging', 'source_url' => 'https://example.test/', 'destination_url' => 'https://staging.example.test/', 'username' => '', 'application_password' => 'secret app password', 'default_direction' => 'push', 'content_types' => array( 'posts', 'terms', 'media', 'bad_type' ), 'url_mappings' => array( array( 'source' => 'https://example.test', 'destination' => 'https://staging.example.test', ), ), ), ), 'log_retention' => '50', 'debug_logging' => '1', ) ); $pairs = $settings->syncPairs(); self::assertSame( 'Production to Staging', $pairs[0]['name'] ); self::assertSame( 'https://example.test/', $pairs[0]['source_url'] ); self::assertSame( 'https://staging.example.test/', $pairs[0]['destination_url'] ); self::assertSame( 'codex', $pairs[0]['username'] ); self::assertSame( 'secret app password', $pairs[0]['application_password'] ); self::assertSame( 'push', $pairs[0]['default_direction'] ); self::assertSame( array( 'posts', 'terms', 'media' ), $pairs[0]['content_types'] ); self::assertSame( array( array( 'source' => 'https://example.test', 'destination' => 'https://staging.example.test', ), ), $pairs[0]['url_mappings'] ); self::assertSame( 50, $settings->logRetention() ); self::assertTrue( $settings->debugLoggingEnabled() ); } ``` - [ ] **Step 2: Run tests to verify failure** Run: `composer test -- --filter SettingsTest` Expected: FAIL because `logRetention()`, `debugLoggingEnabled()`, and expanded sync pair keys do not exist. - [ ] **Step 3: Implement settings expansion** Update `Settings` to support: ```php private const DIRECTIONS = array( 'push', 'pull' ); private const CONTENT_TYPES = array( 'posts', 'terms', 'media', 'custom_post_types' ); private const MIN_LOGS = 10; private const MAX_LOGS = 1000; public function logRetention(): int; public function debugLoggingEnabled(): bool; ``` Update `sanitizeSyncPairs()` so each pair returns: ```php array( 'name' => $name, 'source_url' => $source_url, 'destination_url' => $destination_url, 'username' => $username, 'application_password' => $application_password, 'default_direction' => $direction, 'content_types' => $content_types, 'url_mappings' => $url_mappings, ) ``` Rules: - Preserve legacy `name`, `source_url`, and `destination_url` behavior. - Sanitize `username` with `sanitize_text_field()`. - Sanitize `application_password` with `sanitize_text_field()` and never log it. - Keep only allowed content types. - Keep only URL mappings where both source and destination are non-empty URLs. - Clamp log retention between `10` and `1000`. - Default direction to `push`. - [ ] **Step 4: Run focused tests** Run: `composer test -- --filter SettingsTest` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/Settings/Settings.php tests/Unit/SettingsTest.php git commit -m "feat: expand admin sync settings" ``` --- ## Task 2: Admin Notices **Files:** - Create: `src/Admin/AdminNotice.php` - Create: `src/Admin/AdminNoticeRepository.php` - Create: `tests/Unit/Admin/AdminNoticeRepositoryTest.php` - [ ] **Step 1: Write failing notice tests** Create `tests/Unit/Admin/AdminNoticeRepositoryTest.php`: ```php current(); self::assertSame( 'success', $notices[0]->type() ); self::assertSame( 'The package JSON file was imported successfully.', $notices[0]->message() ); } public function test_it_sanitizes_error_notices(): void { $_GET['wpcs_import_error'] = ''; $notices = ( new AdminNoticeRepository() )->current(); self::assertSame( 'error', $notices[0]->type() ); self::assertSame( 'Bad package', $notices[0]->message() ); } } ``` - [ ] **Step 2: Run tests to verify failure** Run: `composer test -- --filter AdminNoticeRepositoryTest` Expected: FAIL with missing classes. - [ ] **Step 3: Implement notices** Create `AdminNotice` with: ```php public function __construct( string $type, string $message ); public function type(): string; public function message(): string; ``` Allowed types: `success`, `warning`, `error`, `info`; fallback to `info`. Create `AdminNoticeRepository::current(): array` returning notices for: - `wpcs_imported=1`: success, imported successfully. - `wpcs_import_error`: sanitized error message. - `wpcs_settings_saved=1`: success, settings saved. - `wpcs_connection_ok=1`: success, REST connection succeeded. - `wpcs_connection_error`: sanitized error message. - `wpcs_logs_cleared=1`: success, logs cleared. - `wpcs_export_error`: sanitized error message. - [ ] **Step 4: Run focused tests** Run: `composer test -- --filter AdminNoticeRepositoryTest` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/Admin/AdminNotice.php src/Admin/AdminNoticeRepository.php tests/Unit/Admin/AdminNoticeRepositoryTest.php git commit -m "feat: add admin notices" ``` --- ## Task 3: Settings Save Controller **Files:** - Create: `src/Admin/SettingsController.php` - Modify: `src/Plugin.php` - Create: `tests/Unit/Admin/SettingsControllerTest.php` - Modify: `tests/Unit/PluginTest.php` - [ ] **Step 1: Write failing controller tests** Create `tests/Unit/Admin/SettingsControllerTest.php`: ```php 'debug', 'conflict_strategy' => 'manual_review', ); ( new SettingsController( new SettingsRepository() ) )->handleSave(); self::assertSame( 'debug', $GLOBALS['wpcs_test_options'][ SettingsRepository::OPTION_NAME ]['logging_level'] ); self::assertStringContainsString( 'wpcs_settings_saved=1', $GLOBALS['wpcs_redirect_location'] ); } } ``` - [ ] **Step 2: Run tests to verify failure** Run: `composer test -- --filter SettingsControllerTest` Expected: FAIL with missing `SettingsController`. - [ ] **Step 3: Implement controller** Create `SettingsController`: ```php final class SettingsController { private SettingsRepository $settings_repository; public function __construct( SettingsRepository $settings_repository ) {} public function register(): void; public function handleSave(): void; } ``` Behavior: - `register()` hooks `admin_post_wpcs_save_settings`. - `handleSave()` requires `manage_options`. - Verify nonce `wpcs_save_settings` / `wpcs_settings_nonce`. - Read `$_POST['wpcs_settings']`, unslash with `wp_unslash()`, require array. - Persist through `SettingsRepository::save( Settings::fromArray( $data ) )`. - Redirect to dashboard with `wpcs_settings_saved=1`. Update `Plugin::create()` to register `SettingsController`. Update `Plugin::register()` to call `SettingsController::register()`. Update `PluginTest` to assert the service exists and the hook is registered. - [ ] **Step 4: Run focused tests** Run: `composer test -- --filter "SettingsControllerTest|PluginTest"` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/Admin/SettingsController.php src/Plugin.php tests/Unit/Admin/SettingsControllerTest.php tests/Unit/PluginTest.php git commit -m "feat: save admin sync settings" ``` --- ## Task 4: Dashboard Settings Form **Files:** - Modify: `templates/admin/dashboard.php` - Modify: `src/Admin/AdminPage.php` - Modify: `tests/Unit/Admin/DashboardTemplateTest.php` - [ ] **Step 1: Write failing dashboard tests** Add tests: ```php public function test_it_renders_settings_form_with_nonce_and_escaped_pair_values(): void { $settings = Settings::fromArray( array( 'sync_pairs' => array( array( 'name' => 'Staging', 'source_url' => 'https://example.test', 'destination_url' => 'https://staging.example.test', 'username' => 'codex', ), ), ) ); $output = $this->renderDashboard( $settings ); self::assertStringContainsString( 'action="https://example.test/wp-admin/admin-post.php"', $output ); self::assertStringContainsString( 'name="action" value="wpcs_save_settings"', $output ); self::assertStringContainsString( 'Staging', $output ); self::assertStringNotContainsString( 'Staging', $output ); self::assertStringContainsString( 'name="wpcs_settings[sync_pairs][0][application_password]"', $output ); } ``` Update `renderDashboard()` to accept optional `Settings $settings`. - [ ] **Step 2: Run test to verify failure** Run: `composer test -- --filter DashboardTemplateTest` Expected: FAIL because the dashboard does not render the settings form. - [ ] **Step 3: Implement dashboard form** Update `dashboard.php`: - Replace the foundation notice with a "Configuration" section. - Render a form posting to `admin-post.php`. - Include `action=wpcs_save_settings`. - Include nonce `wp_nonce_field( 'wpcs_save_settings', 'wpcs_settings_nonce' )`. - Render existing sync pairs; if none exist, render one blank pair row. - Fields per pair: name, source URL, destination URL, username, application password, default direction, content types, URL mappings. - Never render saved application password values back into the password field; render placeholder text only. - Escape every attribute with `esc_attr()` and every text node with `esc_html()`. - [ ] **Step 4: Run focused tests** Run: `composer test -- --filter DashboardTemplateTest` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add templates/admin/dashboard.php src/Admin/AdminPage.php tests/Unit/Admin/DashboardTemplateTest.php git commit -m "feat: render admin settings workflow" ``` --- ## Task 5: Connection Diagnostics **Files:** - Create: `src/Admin/ConnectionTestController.php` - Modify: `src/Plugin.php` - Create: `tests/Unit/Admin/ConnectionTestControllerTest.php` - Modify: `tests/Unit/Admin/DashboardTemplateTest.php` - Modify: `templates/admin/dashboard.php` - [ ] **Step 1: Write failing diagnostics tests** Create `ConnectionTestControllerTest` that injects `RestTransportClient`, posts `pair_index=0`, and verifies: - capability and nonce are required. - success redirects with `wpcs_connection_ok=1`. - failures redirect with `wpcs_connection_error`. - application password is never placed in redirect query args. Use settings fixture: ```php update_option( SettingsRepository::OPTION_NAME, array( 'sync_pairs' => array( array( 'name' => 'Staging', 'destination_url' => 'https://destination.test', 'username' => 'codex', 'application_password' => 'app-pass', ), ), ), false ); ``` - [ ] **Step 2: Run tests to verify failure** Run: `composer test -- --filter ConnectionTestControllerTest` Expected: FAIL with missing controller. - [ ] **Step 3: Implement diagnostics controller** Create `ConnectionTestController`: - Hook `admin_post_wpcs_test_connection`. - Require `manage_options`. - Verify nonce `wpcs_test_connection` / `wpcs_connection_nonce`. - Read `pair_index` as integer. - Load selected pair from settings. - Call `RestTransportClient::testConnection( destination_url, username, application_password )`. - On success redirect with `wpcs_connection_ok=1`. - On `RestTransportException` redirect with sanitized `wpcs_connection_error`. - Log success/failure without credentials. Update `Plugin` to register the controller. Update dashboard to add a "Test REST Connection" button per pair with nonce. - [ ] **Step 4: Run focused tests** Run: `composer test -- --filter "ConnectionTestControllerTest|DashboardTemplateTest|PluginTest"` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/Admin/ConnectionTestController.php src/Plugin.php templates/admin/dashboard.php tests/Unit/Admin/ConnectionTestControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php tests/Unit/PluginTest.php git commit -m "feat: add connection diagnostics" ``` --- ## Task 6: REST Transport Rejected Body Handling **Files:** - Modify: `src/Transport/RestTransportClient.php` - Modify: `tests/Unit/Transport/RestTransportClientTest.php` - [ ] **Step 1: Write failing REST client test** Add: ```php public function test_it_throws_when_receive_endpoint_returns_accepted_false(): void { $GLOBALS['wpcs_http_response'] = array( 'response' => array( 'code' => 200 ), 'body' => '{"accepted":false,"import":{"errors":["Posts failed."]}}', ); $client = new RestTransportClient(); $this->expectException( RestTransportException::class ); $this->expectExceptionMessage( 'Posts failed.' ); $client->sendPackage( 'https://destination.test', 'codex', 'app-pass', $this->package() ); } ``` - [ ] **Step 2: Run test to verify failure** Run: `composer test -- --filter RestTransportClientTest` Expected: FAIL because HTTP 200 currently counts as success. - [ ] **Step 3: Implement body inspection** Update `assertSuccessfulResponse()`: - If status code is expected, decode body. - If decoded body has `accepted === false`, throw `RestTransportException::remoteRejected()`. - Prefer first string in `import.errors`, then first string in `errors`, then `message`, then fallback. - [ ] **Step 4: Run focused tests** Run: `composer test -- --filter RestTransportClientTest` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/Transport/RestTransportClient.php tests/Unit/Transport/RestTransportClientTest.php git commit -m "fix: reject failed rest imports" ``` --- ## Task 7: Operation Logs and Clear Action **Files:** - Modify: `src/Logging/OptionLogger.php` - Create: `src/Admin/LogController.php` - Modify: `src/Plugin.php` - Modify: `templates/admin/dashboard.php` - Modify: `tests/Unit/OptionLoggerTest.php` - Create: `tests/Unit/Admin/LogControllerTest.php` - Modify: `tests/Unit/Admin/DashboardTemplateTest.php` - [ ] **Step 1: Write failing logger/controller tests** Add `OptionLoggerTest` coverage for: ```php $logger = new OptionLogger( 10 ); $logger->info( 'Imported content package.' ); self::assertCount( 1, $logger->entries() ); $logger->clear(); self::assertSame( array(), $logger->entries() ); ``` Create `LogControllerTest` verifying: - capability and nonce are required. - `handleClear()` clears `OptionLogger::OPTION_NAME`. - redirect contains `wpcs_logs_cleared=1`. - [ ] **Step 2: Run tests to verify failure** Run: `composer test -- --filter "OptionLoggerTest|LogControllerTest"` Expected: FAIL with missing methods/controller. - [ ] **Step 3: Implement logs** Update `OptionLogger`: ```php public function entries(): array; public function clear(): void; ``` Create `LogController`: - Hook `admin_post_wpcs_clear_logs`. - Require `manage_options`. - Verify nonce `wpcs_clear_logs` / `wpcs_logs_nonce`. - Clear logs. - Redirect with `wpcs_logs_cleared=1`. Update dashboard: - Render a recent operation history table. - Show timestamp, level, message, and redacted context summary. - Add "Clear Logs" form with nonce. - [ ] **Step 4: Run focused tests** Run: `composer test -- --filter "OptionLoggerTest|LogControllerTest|DashboardTemplateTest|PluginTest"` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/Logging/OptionLogger.php src/Admin/LogController.php src/Plugin.php templates/admin/dashboard.php tests/Unit/OptionLoggerTest.php tests/Unit/Admin/LogControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php tests/Unit/PluginTest.php git commit -m "feat: add operation log controls" ``` --- ## Task 8: File Export Scaffold **Files:** - Create: `src/Admin/FileExportController.php` - Modify: `src/Plugin.php` - Modify: `templates/admin/dashboard.php` - Create: `tests/Unit/Admin/FileExportControllerTest.php` - Modify: `tests/Unit/Admin/DashboardTemplateTest.php` - [ ] **Step 1: Write failing export tests** Create `FileExportControllerTest` verifying: - capability and nonce are required. - a configured pair exports a valid JSON package with all four record buckets. - response headers include `Content-Type: application/json`. - no content mutation occurs during export. - [ ] **Step 2: Run tests to verify failure** Run: `composer test -- --filter FileExportControllerTest` Expected: FAIL with missing controller. - [ ] **Step 3: Implement export scaffold** Create `FileExportController`: - Hook `admin_post_wpcs_export_package`. - Require `manage_options`. - Verify nonce `wpcs_export_package` / `wpcs_export_nonce`. - Read `pair_index`. - Build an empty `ContentPackage` using the selected pair: - `source.site_url` from pair source URL. - `destination.site_url` from pair destination URL. - manifest counts all zero. - records buckets all empty. - checksum from `PackageChecksum::records()`. - Export with `JsonFileTransport::export()`. - Send JSON download headers. Update dashboard with an "Export Empty Package" form and helper copy stating full extraction will be added in a later slice. - [ ] **Step 4: Run focused tests** Run: `composer test -- --filter "FileExportControllerTest|DashboardTemplateTest|PluginTest"` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/Admin/FileExportController.php src/Plugin.php templates/admin/dashboard.php tests/Unit/Admin/FileExportControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php tests/Unit/PluginTest.php git commit -m "feat: add package export scaffold" ``` --- ## Task 9: Import Result UI Hardening **Files:** - Modify: `src/Admin/FileImportController.php` - Modify: `templates/admin/dashboard.php` - Modify: `tests/Unit/Admin/FileImportControllerTest.php` - Modify: `tests/Unit/Admin/DashboardTemplateTest.php` - [ ] **Step 1: Write failing result tests** Add file import test asserting success redirects include counts: ```php self::assertStringContainsString( 'wpcs_created=0', $GLOBALS['wpcs_redirect_location'] ); self::assertStringContainsString( 'wpcs_updated=0', $GLOBALS['wpcs_redirect_location'] ); self::assertStringContainsString( 'wpcs_skipped=0', $GLOBALS['wpcs_redirect_location'] ); self::assertStringContainsString( 'wpcs_conflicts=0', $GLOBALS['wpcs_redirect_location'] ); ``` Add dashboard test asserting the success notice includes created/updated/skipped/conflict counts from sanitized query args. - [ ] **Step 2: Run tests to verify failure** Run: `composer test -- --filter "FileImportControllerTest|DashboardTemplateTest"` Expected: FAIL because result counts are not in redirects or UI. - [ ] **Step 3: Implement result count redirects** Update `FileImportController` success redirect: ```php array( 'wpcs_imported' => '1', 'wpcs_created' => (string) $result->created(), 'wpcs_updated' => (string) $result->updated(), 'wpcs_skipped' => (string) $result->skipped(), 'wpcs_conflicts' => (string) $result->conflicts(), ) ``` Update dashboard to display counts with `absint()` and escaped labels. - [ ] **Step 4: Run focused tests** Run: `composer test -- --filter "FileImportControllerTest|DashboardTemplateTest"` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add src/Admin/FileImportController.php templates/admin/dashboard.php tests/Unit/Admin/FileImportControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php git commit -m "feat: show import result summaries" ``` --- ## Task 10: Smoke Checklist Documentation **Files:** - Create: `docs/smoke/phase-6-admin-hardening.md` - [ ] **Step 1: Create smoke checklist** Create `docs/smoke/phase-6-admin-hardening.md` with: ```markdown # Phase 6 Admin Hardening Smoke Checklist ## Environment - WordPress site URL: - Plugin branch/commit: - PHP version: - WordPress version: ## Checks - [ ] Plugin activates without fatal errors. - [ ] Tools -> Content Sync loads for an administrator. - [ ] Non-administrators cannot access the dashboard. - [ ] Settings save rejects missing/invalid nonce. - [ ] Settings save persists sync pair name, URLs, username, content types, direction, and URL mappings. - [ ] Saved application password is not rendered back into the password field. - [ ] REST status endpoint rejects unauthenticated HTTP requests. - [ ] REST status endpoint accepts authenticated requests when the server passes `Authorization` to PHP. - [ ] Connection test succeeds with a valid application password. - [ ] Connection test shows an actionable error for invalid credentials. - [ ] Invalid package file import redirects with an error notice. - [ ] Valid empty package import redirects with success and result counts. - [ ] REST package POST accepts a valid package and includes import counts. - [ ] REST package POST rejects invalid package data. - [ ] Operation log table shows recent events with redacted credential-like fields. - [ ] Clear logs requires nonce and removes log entries. - [ ] Export scaffold downloads valid JSON. ## Local Notes - On Herd/nginx, direct HTTP Basic auth may require forwarding the `Authorization` header. If HTTP application-password smoke returns 401 while internal REST dispatch passes, verify server auth header configuration before treating it as a plugin failure. ``` - [ ] **Step 2: Commit docs** ```bash git add docs/smoke/phase-6-admin-hardening.md git commit -m "docs: add admin hardening smoke checklist" ``` --- ## Task 11: Full Phase 6 Verification **Files:** - Verify all files changed in Tasks 1-10. - [ ] **Step 1: Run Composer validation** Run: `composer validate --strict` Expected: PASS with `./composer.json is valid`. - [ ] **Step 2: Run PHPCS** Run: `composer lint` Expected: PASS with no PHPCS errors or warnings. - [ ] **Step 3: Run PHPStan** Run: `vendor\bin\phpstan analyse --memory-limit=1G` Expected: PASS with `[OK] No errors`. - [ ] **Step 4: Run PHPUnit** Run: `composer test` Expected: PASS with all unit tests passing. - [ ] **Step 5: Copy runtime files to Herd test plugin** Run: ```powershell Copy-Item -Path src,templates,wp-content-sync.php,composer.json -Destination 'C:\Users\ksolo\Herd\basic-wp\wp-content\plugins\WP-Content-Sync' -Recurse -Force ``` Expected: command exits 0. - [ ] **Step 6: Run manual smoke checklist** Use `docs/smoke/phase-6-admin-hardening.md`. Required checks before completion: - Plugin active. - Admin dashboard loads after login. - Settings save works and rejects invalid nonce. - Connection diagnostics show success/failure notices. - Invalid file import redirects with error. - Valid empty file import redirects with success counts. - REST unauthenticated status returns 401. - REST valid and invalid package behavior is verified by HTTP application password if server passes Basic auth, otherwise by internal REST dispatch plus documented server caveat. - Logs render and clear. - Export scaffold downloads valid JSON. - [ ] **Step 7: Commit verification notes if changed** If smoke notes are updated with environment results: ```bash git add docs/smoke/phase-6-admin-hardening.md git commit -m "docs: record phase 6 smoke results" ``` --- ## Spec Coverage - Sync pair configuration is covered by Tasks 1, 3, and 4. - Credentials and authentication setup are covered by Tasks 1, 4, and 5, with password fields not rendered back to the browser. - URL mapping configuration is covered by Task 1 and Task 4. - Content type selection and default sync direction are covered by Task 1 and Task 4. - Connection diagnostics are covered by Task 5. - Import screens and user-facing import errors are covered by Tasks 2, 4, and 9. - Export screen is covered by Task 8 as a valid package scaffold until full extraction is implemented. - Operation history and debug/log controls are covered by Task 7. - REST failure hardening is covered by Task 6. - Nonces/capabilities for state-changing admin actions are covered by Tasks 3, 5, 7, 8, and existing file import tests. - Final smoke/integration checklist is covered by Tasks 10 and 11. ## Deferred Work - Full content extraction for non-empty package exports remains a later slice because Phase 5 built import orchestration and handlers, not source extraction orchestration. - Background queues, progress polling, and cancelable long-running operations remain a later scalability slice. - HTTP Basic authentication pass-through is a server configuration concern; plugin REST behavior remains covered by controller tests and internal REST dispatch smoke. ## Placeholder Scan - No unresolved placeholder markers are intentionally included. - Each task names exact files, expected failing tests, implementation behavior, verification commands, and commit messages. - Deferred work is explicitly scoped with rationale and is not required for Phase 6 exit criteria in this implementation slice.