pguzman d1615a9e01 Fix crop editor's drag/zoom math not matching its own preview
Reported: dragging toward an image edge (e.g. to reveal a cropped-off
head) stopped short of what the reference preview showed was possible —
the two panels visibly disagreed.

Root cause: transform-origin was set to match the pan position (X%,Y%)
instead of staying at the CSS default (50% 50%, the box's own center).
CSS object-position places the *unzoomed* crop; transform:scale then
magnifies around transform-origin. Tying that origin to X/Y meant zoom
dragged its own anchor point toward whichever edge you'd panned to,
instead of always magnifying what's actually centered in the frame —
harmless near the middle (why initial testing looked fine) but
increasingly wrong the closer you drag to an edge, exactly where you'd
need to go to reach a cropped head. A second, smaller error was in how
the reference-rectangle preview converted focal position to natural-image
coordinates (didn't account for the zoom-independent anchor point).

Fixed both the crop editor's own math and includes/photo.php's
photo_crop_style() (used for every final render — cards, previews) to
drop the origin back to the CSS default and use the correct geometry.
clampFocal() also simplifies to a plain [0,100] clamp — under real
object-position semantics that's always a valid, fully-covered crop at
any zoom >= 1, no image-dimension-dependent math needed.

Verified in a standalone test harness: dragging now reaches all the way
to an image's edges, zoom stays synced between the editor's live frame
and its reference-rectangle preview, and the applied result matches the
editor's preview exactly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 10:28:40 -07:00

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: superadminadminsuperuseruser
  • Public profiles — each user gets a /username page 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 .htaccess rewrite 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 / supadminchange 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.php filled in with production credentials
  • install.php deleted after first run
  • uploads/ is writable by the web server
  • BASE_URL matches 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.

S
Description
Rosary Presenter App — multi-user PHP/MySQL presentation tool for praying the Rosary, novenas, and the Divine Mercy Chaplet. Live at loveandrosary.com.
Readme 540 KiB
Languages
PHP 70.9%
JavaScript 18%
CSS 11.1%