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 22, 2026 Project: #Portfolio
productive

Version System, Admin Settings Expansion, Logbook Live Search

Two sessions today. The morning one was focused on the admin layer, the evening one on the frontend logbook. Admin Settings Expansion Added a proper version system — APP_VERSION constant in Config, displayed in the admin footer alongside the site name pulled from the database instead of hardcoded text. Small thing but it means I can see at a glance whether the live server is running the latest deploy. Three new admin settings pages wired up. Email settings now manages SMTP configuration (host, port, encryption, credentials, from address, notification recipient) through a dedicated /admin/email page — password field shows a set/not-set indicator without exposing the value. Biznetrix integration settings moved to /admin/biznetrix/settings with the API URL visible and the API key masked. Google Translate key gets its own field in the main settings with the same set/not-set pattern. All three write to .env via a shared writeEnvKey() helper extracted into BaseController — previously each controller had its own copy of that logic. DRY. Admin layout housekeeping: seven dashboard-style pages and six social media sub-pages had redundant inner content-header and section.content wrappers duplicating what the master layout already provides. Stripped them all. Action buttons (Add Provider, New Category, etc.) moved to float-right inside card headers, consistent with every other CRUD page on the platform. Logbook Live Search and Pagination The logbook frontend now supports clean query params (?q= and ?page=), paginates at five entries per page, and updates results via debounced AJAX while typing — searches across date and content. Centered header, simplified search UI, better input padding. Fixed a pagination JS bug where clicking a page link was injecting the full page HTML (including the footer) into the results list, causing duplicated footer rendering. The interceptor now only captures pagination links, not every anchor on the page. Auto-translate on update is now "fill missing only" for Blog and Works — when Google Translate is configured, update saves fill any translation column that is still empty, but never overwrite an existing manual translation. Right behaviour: manual edits should not get silently overwritten by the API.

Lesson

The "set/not-set indicator without exposing the value" pattern for sensitive fields (SMTP password, API keys) is the correct admin UX. Never render credential values back into HTML, even masked — a view-source reveals them. Store, compare, indicate.

Read More →
Prev 1 2 3 4 Next