Forces a re-fetch of the pan/zoom fix from the previous commit — learned this lesson the hard way earlier in this same feature. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rosary Presenter
A multi-user web app for leading the Rosary, novenas, and the Divine Mercy Chaplet — built for live presentation at prayer services. Live at loveandrosary.com.
What It Does
- Slide-based presentation — navigate prayer-by-prayer with leader/congregation text split on screen
- Rosary bead ring — SVG visualization tracks which bead is active in real time
- Session types — General Rosary, Memorial Rosary, Deceased Novena, Divine Mercy Chaplet
- Novena groups — link 9 daily sessions into one group with a public day-picker page
- Audio uploads — attach MP3/audio per session (up to 50 MB)
- Multi-user — role hierarchy:
superadmin→admin→superuser→user - Public profiles — each user gets a
/usernamepage with their public sessions - Donate strip — optional PayPal / Venmo / Buy Me a Coffee link on public pages
Stack
- PHP 8 + PDO (no framework, no Composer dependencies)
- MySQL 8 / MariaDB
- Vanilla JS (no build step)
- Apache/Nginx with
.htaccessrewrite rules
Setup
1. Configure database
cp config/db.example.php config/db.php
# Edit config/db.php — fill in DB_HOST, DB_NAME, DB_USER, DB_PASS
# Set BASE_URL if deploying to a subdirectory (e.g. '/rosary')
2. Create the database schema
Visit install.php in your browser once — it creates all tables (matching schema.sql, kept as the canonical structure reference) and seeds site_settings defaults, the superadmin account, and the standard prayer library. Delete install.php immediately after.
Default superadmin credentials: supadmin / supadmin — change these immediately.
schema.sql documents the current database structure; there is no separate migration-script chain to run.
3. Configure the web server
Apache — .htaccess is included. Enable mod_rewrite and set AllowOverride All.
Nginx — add to your server block:
location / {
try_files $uri $uri/ @php;
}
location @php {
rewrite ^/([^/]+)/([^/]+)$ /present.php?username=$1&slug=$2 last;
rewrite ^/([^/]+)$ /profile.php?username=$1 last;
}
4. Uploads directory
chmod 755 uploads/
5. SMTP (optional)
Configure outbound email in Admin → Settings for registration confirmation and password reset emails. If left blank, the app will auto-confirm new users instead.
6. Bot protection (optional)
register.php always runs a built-in honeypot + timing trap against scripted signups — no setup needed. On top of that, you can enable Google reCAPTCHA v3: register your domain at google.com/recaptcha (choose reCAPTCHA v3), then enter the Site Key and Secret Key in Admin → Settings → Bot Protection.
7. Scheduled cleanup of unconfirmed accounts (optional)
cron/cleanup_unconfirmed.php permanently deletes accounts that are still unconfirmed 3 days after registering — useful for clearing out bot signups that get past the defenses above. It's CLI-only (refuses to run over HTTP) and is not wired up automatically; schedule it yourself as a cron job. In Hostinger's hPanel: Advanced → Cron Jobs → run daily:
php /home/<your-account>/domains/loveandrosary.com/public_html/cron/cleanup_unconfirmed.php
(Adjust the path to match your actual hosting account.) Confirmed accounts are never touched — only rows with email_confirmed = 0.
Upgrading an Existing Install
schema.sql reflects the current database structure. For a production database that predates the failed_login_attempts / locked_until login-lockout columns, run this once against it manually — it's not applied automatically since there's no migration runner against a live database:
ALTER TABLE users
ADD COLUMN failed_login_attempts INT NOT NULL DEFAULT 0,
ADD COLUMN locked_until DATETIME NULL;
Also needed for the photo reposition/zoom tool (photo_focal_x/photo_focal_y/photo_zoom on both sessions and novena_groups — defaults reproduce today's centered, no-zoom crop, so this is safe to run any time):
ALTER TABLE sessions
ADD COLUMN photo_focal_x FLOAT NOT NULL DEFAULT 50,
ADD COLUMN photo_focal_y FLOAT NOT NULL DEFAULT 50,
ADD COLUMN photo_zoom FLOAT NOT NULL DEFAULT 1;
ALTER TABLE novena_groups
ADD COLUMN photo_focal_x FLOAT NOT NULL DEFAULT 50,
ADD COLUMN photo_focal_y FLOAT NOT NULL DEFAULT 50,
ADD COLUMN photo_zoom FLOAT NOT NULL DEFAULT 1;
Deployment Checklist
config/db.phpfilled in with production credentialsinstall.phpdeleted after first runuploads/is writable by the web serverBASE_URLmatches your subdirectory path (leave empty for domain root)- Superadmin password and email changed
- SMTP configured in Admin → Settings
Project Structure
Rosary/
├── admin/ # Admin dashboard (auth-gated)
│ ├── index.php # Dashboard home
│ ├── setup.php # Create/edit a session
│ ├── novena_group.php
│ ├── users.php
│ ├── settings.php # Site-wide settings (superadmin only)
│ └── audio.php
├── api/ # JSON endpoints (upload, save, delete)
├── assets/
│ ├── css/ # present.css, public.css, setup.css
│ └── js/ # presenter.js, rosary.js, setup.js
├── config/
│ ├── db.example.php # Copy → db.php and fill in credentials
│ └── db.php # (gitignored — contains real credentials)
├── cron/
│ └── cleanup_unconfirmed.php # CLI-only; schedule via host cron
├── data/
│ └── prayers.php # All prayer text + build_decade_slides()
├── includes/
│ ├── auth.php # require_auth(), current_user(), has_role(), login lockout
│ ├── csrf.php # csrf_token(), csrf_field(), csrf_verify()
│ ├── recaptcha.php # recaptcha_enabled(), verify_recaptcha()
│ ├── build_slides.php
│ ├── donate.php
│ └── mailer.php
├── uploads/ # User-uploaded audio (gitignored)
├── index.php # Public home — card grid of sessions
├── present.php # Presentation player (public)
├── novena_public.php # Novena day-picker (public)
├── schema.sql # Canonical database schema (structure only)
├── install.php # Run once, then delete
└── .htaccess # URL rewriting
URL Routing
| URL | Resolves to |
|---|---|
/username/slug |
present.php?username=X&slug=Y |
/username |
profile.php?username=X |
/username/novena-slug |
Redirects to novena_public.php?group_id=X |
License
Private project — all rights reserved.