Logbook
Daily reflections, progress, and lessons learned
Social Publishing Fixed, SEO Overhaul — 40-File i18n Sprint
Two major threads shipped today. Social publishing fixes: Post scheduling was broken in three separate ways. First: scheduled times were being stored as local time instead of UTC — a 6-hour offset for the configured timezone. Second: the timezone was being read from the wrong settings key (dot notation versus underscore format). Third: token decryption was failing for every social account because an extra base64_decode() was applied to data that CodeIgniter's encrypter had already encoded — this caused an "authentication failed" error on every decrypt attempt. Fixed all three. Posts now schedule correctly, display in local time in the UI (database stores UTC, UI converts to app timezone), and publish to Facebook through the scheduled cron command. Added a timezone selector to Admin → General Settings so the app timezone is database-configurable, not hardcoded. Posts management page built with status filters, stats cards, and timezone-aware time display. 40-file SEO and i18n sprint: Complete overhaul of the public-facing frontend for technical SEO correctness, AIO readiness, and full 12-language internationalisation. Duplicate H1 fixed (navbar logo demoted from <h1> to styled <span>). noindex added to thin pages (search results, FAQ keyword mode). Canonical URL strips the ?lang= parameter so hreflang carries the language signal, not the canonical. BreadcrumbList JSON-LD on all content pages. FAQPage JSON-LD with HTML stripping and 20-item cap, eligible for Google FAQ rich results. Article JSON-LD with dateModified and mainEntityOfPage for rich article results. WebSite JSON-LD with SearchAction on homepage for Google Sitelinks Search Box. hreflang for all 12 language variants plus x-default on every page. og:locale mapped to correct locale format per active language. <html lang dir> dynamic per active CMS language. Language helper autoloaded in BaseController. KB article search covers all 11 translated title and content columns. Language switcher correctly appends lang parameter without duplicating. Migration adds 22 translation columns to KB articles (11 languages × 2 fields). Input sanitisation on search and FAQ query parameters with strip_tags and truncation before use in meta tags. Three new test files: 40 assertions covering SEO data contract, FAQ branching, and header variable logic.
The token decryption double-decode bug: CodeIgniter's encrypter returns base64-encoded output. Wrapping that in a second base64_decode produces garbage and throws "authentication failed" with no indication of the real cause. Always read the framework's encrypt/decrypt contract before assuming symmetry. The timezone UTC storage rule: store UTC in the database, convert at the display layer only. Storing local time creates ambiguity that is impossible to resolve correctly if the timezone setting ever changes.
OAuth Refactor — HMAC State, Instagram Fixed, 467 Tests Green
The existing OAuth implementation used database-backed state storage. Every cross-site redirect from Facebook or Instagram was losing the session because the browser's SameSite cookie policy drops the session cookie on the redirect back from the OAuth provider. After multiple attempts to fix session persistence (session save handlers, cookie flags, session restoration tricks), the conclusion was clear: database-backed state is the wrong architecture for this problem. Refactored to HMAC self-validating state tokens. State format encodes the platform and app identifier with an HMAC signature. No database lookup needed on callback — just re-compute the HMAC and compare. The callback routes are now public with HMAC validation instead of admin-protected routes requiring a live session. Session is restored after HMAC validation from the admin ID stored at connect time. Instagram Business token exchange was also corrected. It was hitting the Instagram Basic Display endpoint, which is wrong — Instagram Business accounts are linked to Facebook Pages and use Facebook's Graph API for token exchange. Changed the endpoint and it works correctly. API health check fixed: tokens are cleaned of whitespace before use in Authorization headers to prevent RFC 7230 header validation errors. WhatsApp webhook GET verification wired (Meta sends a GET challenge before starting POST delivery). Webhook signature filter skips validation for GET requests. All 467 tests passing, 2086 assertions, no errors. The test suite now covers HMAC state validation, token exchange for both platforms, account saving with encryption, session handling, and webhook verification.
HMAC-based self-validating state tokens are the correct pattern for OAuth state across cross-site redirects. They require no server-side storage, survive session loss, and validate entirely on the callback. Database-backed state should be the last resort, not the first.
CI Quality Gates, Ads ROI Integration, Social Post Creation, Meta Platform Setup
Four sessions today. Quality pipeline: GitHub Actions workflow now runs PHPStan, PHPUnit, Composer Audit, and Semgrep on every push. PHPStan bootstrap required a CI4 helper loading setup to make static analysis understand the framework magic. Repaired a feature-access path referencing a model that did not match the real schema. PHPUnit now runs without migration warnings. The CI gate is real. Multi-platform ads ROI: Meta Marketing API and Google Ads API both integrated for automated campaign performance sync. Hourly cron commands pull impressions, clicks, spend, conversions, and leads from both platforms into a unified table. Calculated metrics (CPC, CPM, CTR, ROAS, cost per lead) stored alongside raw numbers. Real-time Lead Ads webhook handler auto-creates leads with campaign attribution on form submission. Google Ads OAuth 2.0 flow with refresh token support. Cross-platform dashboard shows campaigns side by side. Social post creation and inbox: Full two-panel post creation UI with AI drafting (three variants: Short, Long, Hook+CTA), AI image generation, live character counter, auto-schedule suggestions. Inbox got manual reply flows for comments (modal with AI draft prefill) and DMs (messenger-style chat bubbles). The credential storage bug was fixed: API settings fields not containing "secret" or "token" in their name were being stored as plaintext but always decrypted on read, returning false and blanking OAuth credentials. Fixed by encrypting every API setting unconditionally on write with a raw-value fallback on decrypt. Meta platform configuration: Completed the full Meta Business App setup — marketing API wizard, system user access token generation, Instagram business account linked. Policy pages seeded for Meta App Review compliance including data deletion policy with GDPR and CCPA workflows. Ad account and business manager configured in API Settings. API status dashboard updated to reflect connection state.
The credential encryption bug (encrypt every API setting, not just fields containing certain keywords) is the kind of assumption that passes code review because it looks intentional. The only way to catch it is testing the full OAuth flow end to end, not just unit-testing individual functions.
CMS AI Generation, CSRF Fix, Social Lead Attribution
Two major threads today. First: CMS AI content generation. Article generation, SEO meta generation, excerpt generation (20–40 words, 1–2 sentences), and re-translation. The CSRF bug was the first thing to fix — the AI modal and translations panel were reading CSRF tokens from non-existent meta tags, so every AI AJAX call was silently failing with a 403. Fixed by using the global csrfName/csrfHash values injected in the header. Added proper timeout handling per provider (120 seconds for blog generation), set PHP time limit to match. Error handling now surfaces actual server messages instead of generic "Request failed" which was making debugging nearly impossible. Content truncation bug fixed in the base skill: a regex using $ in multiline mode stopped non-greedy matching at the first line break, so multi-paragraph AI responses were being cut at line one. Changed to z (true string end). Full HTML content now captured correctly. Second thread: social lead attribution. Added social_platform and social_account_id directly to the leads table instead of JSON custom fields. Lead list filtering by source, platform, and account. Lead detail page shows Social & ROI Attribution block. ROI service now attributes leads and revenue correctly across all connected platforms. CMS translation re-translate flow hardened: skips empty fields, shows spinner, tracks success per field, proper aggregate toast at completion.
CSRF tokens read from meta tags that are never written to the page is a silent failure mode. If AJAX calls are mysteriously 403-ing with no error shown, check the CSRF source before anything else.
Admin UI — RTL Support, Dark Mode, Language Switcher, Automation Fixes
Admin header redesigned: dark/light theme toggle, RTL/LTR direction toggle, and a six-language dropdown. RTL mode swaps the AdminLTE stylesheet and applies a custom override file for sidebar positioning, navbar direction, content margins, breadcrumb, and small-box icon layout. User preferences (theme, direction, language) saved to the database and applied server-side before paint — zero flash on reload. Small sidebar text size improved for readability. Automation engine audit fixes: added five missing operators to the test UI so it matches the full 12-operator set the engine supports, expanded field suggestions to cover all seven event types, fixed rule_id injection so action executions record the correct rule in metadata (was always null). New governance seeder inserts default policy rows for max attempts, retry backoff, escalation threshold, and dead letter configuration. AI cost settings: routing matrix save was blanking budget settings because both forms posted to the same endpoint with non-overlapping fields. Split into two endpoints. Budget display bug fixed where a field name mismatch caused the broker weights to always show 1.0.
Server-side preference rendering (theme, RTL, language) before paint is the correct approach for admin UI personalisation. Client-side preference application causes layout flash on every page load.