Vibeboard setup
1. Database: nothing to import. Create an empty MySQL database, put its name and login in config.php, and open the site: it builds and
   upgrades its own tables from migrations/ (AUTO_MIGRATE). Or run  php migrate.php  (status: php migrate.php status; new migration:
   php migrate.php new add_something). migrations/snapshot.sql and vibeboard.sql are full copies for phpMyAdmin imports: after importing
   one, the app recognises it and does not repeat any step.
2. Edit config.php (DB login; AI_KEY later, when you add generation).
3. Local test: php -S localhost:8000 inside this folder, then open http://localhost:8000
4. Make sure uploads/ is writable and the PHP GD extension is enabled.
Install as an app: needs HTTPS (localhost is fine). Android/Chrome shows an Install button.
On iPhone: Safari, Share, Add to Home Screen.
5. Make yourself a moderator (after signing up):
   UPDATE users SET is_admin=1, email_verified=1 WHERE email='you@example.com';
   Reports then appear under Profile > Reports.
6. Email verification (config.php):
   - Set BASE_URL to your public address, MAIL_FROM to an address on your domain, and the SMTP_* values from your
     mail provider (Gmail app password, Brevo, Mailgun, SendGrid, Postmark all work over port 587 / tls).
   - Leave SMTP_HOST empty while testing: emails are written to logs/mail.log instead of being sent.
   - For inbox delivery in production, set up SPF and DKIM for your sending domain with your provider.
7. Password reset uses the same email settings. Links work for 1 hour; leave SMTP_HOST empty to read them in logs/mail.log.
8. Stay logged in: the login box has 'Keep me logged in on this device' (on by default). The device cookie lasts 400 days (the browser maximum)
   and renews on every visit, so a device used at least once a year stays logged in. Use HTTPS in production.
   Profile > Devices lists signed-in devices; password reset, a ban, or 'Log out everywhere' signs out all of them.
9. 'Paste an image link' downloads on the server: it needs the PHP curl extension, refuses private addresses and does not follow redirects.
10. Push alerts: needs HTTPS and the PHP openssl + curl extensions. VAPID keys are created automatically in logs/vapid.json
    (keep that file private; on nginx add a rule that blocks /logs/). Users turn alerts on in Profile > Settings.
11. Videos: MP4 or WebM up to 25 MB / 60 s. Raise upload_max_filesize and post_max_size in php.ini to at least 30M, and serve with
    Apache or nginx (the php -S test server does not support video seeking on iPhone).
12. Photo search. Without a key it matches shape and colour only. Put an Anthropic API key in VISION_KEY (config.php) and it
    understands the picture: it names what it sees, finds pins by those words (plus look), and auto-tags every new pin so normal
    search and Related pins get smarter too. Run  php backfill_ai.php  once to tag existing pins. Photos are sent to Anthropic for
    analysis and are not stored. Limits: 30 photo searches and 60 auto-tags per user per hour.
13. Stylist (Stylist tab, or 'Ask the stylist' after a search): turns 'outfit for a beach wedding' into themed sets of pins and can save them
    as a board. It needs VISION_KEY; without it, it searches your words directly. 'Generate new ideas' needs AI_KEY.
14. Sharing: Share links open pin.php, which gives WhatsApp, iMessage and search engines a title and picture. Add this line to
    robots.txt once you know your address:  Sitemap: https://YOUR-SITE/sitemap.php
15. legal.php has plain-language Terms, Privacy and Guidelines. They are starting points: have a lawyer review them and fill in
    your real contact address (MAIL_FROM is used) before you launch.
16. Sign-ups: open by default (REGISTRATION in config.php; set 'closed' to stop new accounts). The very first account becomes the moderator
    automatically. While SMTP_HOST is empty, new accounts are confirmed automatically so you can try the app straight away; once you add
    mail settings, new members must confirm their email (set REQUIRE_EMAIL_VERIFY to false to skip that).
17. Search: pins now use a MySQL FULLTEXT index (word-start matching, best matches first). Words shorter than 3 letters, or searches
    the index finds nothing for, fall back to the older substring search automatically.
18. Scaling files: set STORAGE to 's3' plus S3_ENDPOINT, S3_BUCKET, S3_KEY, S3_SECRET and CDN_URL (works with Cloudflare R2, AWS S3, Backblaze
    B2, DigitalOcean Spaces, MinIO). Make the bucket publicly readable (or put a CDN in front) and run  php migrate_to_s3.php  once to copy
    existing files. Uploaded pictures are also shrunk to 1600px wide, and local files are sent with a one-year cache header.
19. Admin page (admin.php, moderators only; link on your Profile): AI calls, tokens and estimated cost per day, the heaviest users, and
    database backups. Set AI_DAILY_TOKENS in config.php to pause AI for the rest of the day when a budget is hit (the app falls back to
    normal search), and AI_PRICE_IN / AI_PRICE_OUT / AI_PRICE_IMAGE to your model's prices to see a cost estimate.
20. Backups: the 'Back up now' button, or add to cron:  0 3 * * * php /full/path/to/backup.php   (keeps BACKUP_KEEP files in backups/,
    blocked from the web; nginx needs a rule blocking /backups/ and /logs/). Copy backups off the server. uploads/ is not in the dump.
21. Something wrong after uploading? Open /check.php on your site. It lists, in plain words, anything missing (PHP version, extensions, database login,
    folder permissions, upload limits). It shows no passwords. Delete it once everything is green. Server errors show a ref you can find in logs/error.log.
