Logbook

Daily reflections, progress, and lessons learned

May 31, 2026 Project: #Biznetrix
productive

File Manager, DB Browser, Server CLI Hardening, Qdrant on Hetzner

A big infrastructure day. CKFinder removed entirely — 695 vendored files deleted. Replaced with a purpose-built file manager: three JSON endpoints (list, upload, delete), thumbnail grid for images, icon list for files, drag-and-drop upload with progress bar, per-file delete with confirmation, search and filter, window.FileManager.open(callback) API for any Summernote field. Full control, no vendor lock-in, no authentication edge cases. DB browser module built: full phpMyAdmin alternative embedded in the admin panel for super-admin only. Table list, paginated data browse, structure view, row insert/edit/delete, SQL runner with Ctrl+Enter support, CSV and SQL INSERT export. Security: table names validated against allowlist and real DB tables, destructive SQL patterns audit-logged with warning banner in the UI, SQL runner rate-limited, export capped at 100,000 rows. Server CLI: super admin now has full server tree access (root, var/log, usr/bin). PHP binary resolution hardened to try the versioned binary first, then fall back gracefully — fixes "command not found" errors on Hetzner where the web process PATH does not include the right PHP. 2FA removed from server CLI and maintenance actions (was blocking operations for accounts without 2FA configured). Production: Qdrant vector database installed on the Hetzner server. Binary, config, systemd service, firewall rules. PHP IMAP extension installed via PECL for the email module (PHP 8.5 removed IMAP from core). Hetzner blocks outbound SMTP by default — support ticket submitted to unblock. Deploy workflow updated to group ownership model with setgid bit, so deployments no longer need elevated chown on writable directories.

Lesson

Replacing a complex third-party dependency with a focused in-house alternative is usually the right call when: (1) you keep hitting edge cases the vendor did not anticipate, (2) the dependency is larger than the problem it solves, (3) you understand the domain well enough to build it right. CKFinder was all three.

Read More →
May 30, 2026 Project: #Biznetrix
productive

Frontend Pricing, Contact Form, Service Images, Full SEO Stack, Security Headers

A wide session. Frontend pricing plans as a separate module from SaaS billing plans — service pages can have their own pricing display. Contact form with rate limiting (5 per 5 minutes per IP) and flash validation display. Service featured image upload with server-side MIME validation, preview with AJAX remove, old file cleanup on replace or delete. The public-facing header template was completely rewritten: dynamic title, Open Graph (type, title, description, image, URL, site name, article timestamps), Twitter Card, JSON-LD (WebSite on homepage, Article on posts, Organization elsewhere), canonical URL, robots meta, Google Analytics and GTM (only when IDs are configured). Also a full SEO settings card in Admin → General: separator, default OG image, canonical base, robots default, Twitter handle, analytics IDs, Search Console verification. Security headers filter built from scratch — the CI4 built-in alias did not exist in this build. X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy, HSTS. Session timeout reduced from 2 hours to 30 minutes. Exception messages no longer leak to user-facing flash messages.

Lesson

Never echo exception messages to user-facing flash. Log the full detail, show a generic message. Exception message disclosure is listed in OWASP Top 10 for a reason.

Read More →
May 26, 2026 Project: #Biznetrix
determined

Billing Audit — Seven Bugs Fixed Before First Real Invoice

Pre-deployment billing audit. Found and fixed seven bugs across the billing and tenancy layer. Three were critical — they would have silently corrupted data at runtime. The tenant package assignment was trying to write to a column that does not exist. The billing system was querying the wrong role when looking up tenant admin email addresses, meaning billing notifications would never be sent. The invoice payment recording was using wrong field names that do not exist in the invoices schema. Three more were schema name mismatches (wrong table aliases in queries, wrong status enum values, wrong field names in renewal invoice SQL). One was a validation rule pointing at the wrong table name. All seven fixed before any real invoices ran. The audit took a full session but it is the kind of work that saves client relationships. Silent data corruption in a billing system is not a bug you want to discover from a client complaint.

Lesson

Audit your billing layer before go-live, not after. Schema drift between models and actual table structure is silent in PHP — the code executes without error while writing to columns that do not exist.

Read More →
May 26, 2026 Project: #Portfolio
accomplished

Platform-Aware Content Plan, Image Crop, AI Image Generation, Security Hardening

Two patches today, both substantial. Platform-Aware Content Plan The 30-day content plan from yesterday got a major upgrade. GenerateContentPlanSkill now receives the selected account platforms and generates separate, platform-optimised captions per day. Each platform has its own style guide baked into the AI prompt: Facebook gets conversational copy at 100–250 words with max two emojis and a question at the end; LinkedIn gets professional thought leadership at 150–400 words with zero or one emoji and a bold opening insight; Instagram gets a hook in the first line, 50–150 words, 3–6 emojis, 8–15 hashtags. The preview shows a Bootstrap tab UI — Facebook, LinkedIn, Instagram tabs with editable text areas. storePlan() saves the correct platform caption to each account's post record. Token limit was raised from 4,000 to 8,000 and timeout from 60 to 120 seconds — 30 days of full captions across three platforms simply cannot fit in 4,000 tokens. The GeminiProvider was also ignoring the caller's timeout option and always using 60 seconds hardcoded; fixed to read $options['timeout']. AIManager was dropping the timeout key through array_filter — fixed to forward it correctly. Fixed BaseSkill::generate() always returning success=true regardless of the AI response. API failures (401, quota exceeded, network error) were silently producing empty content and the controller showed "No content returned" with no indication of the real problem. Now propagates success=false with the actual error message. Split "No content returned" into two distinct messages: real AI errors surface directly, parse failures show the first 300 characters of raw AI output so format mismatches are visible immediately. Image Crop Tool and AI Image Generation Cropper.js 1.6.2 integrated on create/edit post. Images are cropped client-side before upload using preset aspect ratios: 4:5 Portrait for Instagram, 1:1 Square, 1.91:1 Landscape, 1200×630 for Facebook and LinkedIn, and Free. Cropped canvas exported at 1080 px wide as JPEG before sending to the server — consistent dimensions regardless of original file size. AI image generation added as a collapsible card on create/edit post. Prompt input calls POST /social-media/generate-image which routes to Gemini's image generation model, saves the base64 result to uploads/social/, and returns the URL alongside a refreshed CSRF token. Preview shown immediately; "Add to Post" attaches the image to the upload grid. The content plan carries image concepts through the plan preview into the edit post page — if a plan day had an image concept, the edit form pre-fills the AI image prompt and auto-expands the generation panel. Platform image hints now shown dynamically as account checkboxes are toggled: Instagram shows the aspect ratio warning (0.8–1.91), Facebook recommends 1200×630, LinkedIn recommends 1200×627. Security Hardening and DB Cleanup CSRF exception in the filter narrowed from the api/* wildcard down to the four specific POST routes that are legitimately session-free: contact form, Biznetrix ack endpoint, RAG query, and RAG documents. Every other API route is now CSRF-protected by default. The wildcard was always a temporary measure and this closes it. EmbeddingService now takes a constructor-injected CURLRequest instance shared for the lifetime of the service, instead of creating a new HTTP connection inside every generate() call. In batch embedding workflows this was allocating a new connection per document — fixed. Same pattern applied to TranslationService, which was using raw curl_init/curl_exec/curl_close; replaced with the CI4 CURLRequest for consistency and test-injectability. Database cleanup migration: sets cover_image = NULL on any posts row still carrying the old placeholder avatar path. The file_exists() guard was already handling these correctly at render time, but having the placeholder value in the database was misleading when reading records directly. PHPStan at 0 errors on 403 files. PHPUnit 171/171 passing.

Lesson

Three things stood out today. The BaseSkill always-returning-success bug: any function that communicates failure should never lie. Silent success on AI failure is worse than a thrown exception because it produces wrong content with no signal that anything went wrong. Constructor-injected HTTP clients vs per-call creation: the per-call pattern in EmbeddingService was invisible in single-document flows but became a problem in batch processing. Inject shared resources at construction time. And the CSRF wildcard: api/* is never the right long-term answer, even if it is the right quick fix. Always narrow it to the specific routes that actually need it.

Read More →
May 25, 2026 Project: #Portfolio
productive

Instagram OAuth Fixed, Media Uploads, 30-Day AI Content Plan

One major patch today covering three features. Instagram "Cannot Parse Access Token" — Dual-Flow Fix Instagram connection was failing with "Cannot parse access token" and I finally understood why. The flow was taking a Facebook user token (obtained via Facebook Login with Instagram Business permissions) and sending it to graph.instagram.com. That endpoint rejects Facebook tokens — it only accepts tokens from Instagram's own Basic Display API, which is a different product entirely. Instagram Business accounts are linked to Facebook Pages and use the Facebook Graph API, not the Instagram endpoint. Fixed with a dual-flow fallback: tries graph.instagram.com/me first (correct path for Instagram Login), then falls back to graph.facebook.com/me/accounts (Facebook Business API path) when the first attempt returns no account ID. The fallback is what actually works for Business accounts. Instagram now connects successfully. Media Uploads on Create and Edit Post Social post create and edit both now have a drag-drop upload zone with URL input and removable thumbnail preview grid. Files save to public/uploads/social/, URLs are accepted directly. On edit, existing media shows as removable thumbnails; keep_media[] inputs preserve existing URLs through the save so you do not lose previous media by only uploading one new file. 30-Day AI Content Plan New page at /admin/social-media/plan — generates a day-by-day content calendar using a GenerateContentPlanSkill. Supports 7, 14, and 30-day plans with brand, audience, goal, and tone inputs. The preview page shows all days as editable text areas so you can adjust any caption before committing. Toggle individual days on or off before bulk-saving. Approved days save as draft posts in the queue, ready for scheduling.

Lesson

The Instagram "cannot parse access token" error is one of the most misleading errors in the Meta ecosystem. The token is valid — it is just being sent to the wrong endpoint. Instagram Business and Instagram Basic Display are separate products with separate token flows, even though they appear connected in the Facebook developer console. Always check which endpoint expects which token type.

Read More →
Prev 1 2 3 4 Next