Commit Graph
23 Commits
Author SHA1 Message Date
UbuntuandClaude Opus 5 06088aee8b Replace Vincent's profile photo with the new studio shot
Both profile images are regenerated from the same new source photo,
cropped square and stripped of EXIF (the original iPhone file carried
GPS coordinates).

The two crops differ on purpose, because the display sizes do:
- massage/vincent/vincent.jpg (400px, shown as a 150px circle) gets a
  wide head-and-shoulders crop showing the studio behind him
- worker_Vincent_Pusch.jpg (300px, shown as a 75px circle in the
  booking worker picker) stays tight, so the face is still
  recognisable at that size

Filenames are unchanged, so workers.worker_profile_img needs no
migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wwCBWRnEZuorhv34FVUkY
2026-08-16 10:27:59 +00:00
UbuntuandClaude Opus 5 baa597a7c5 Guard against null permission_ids in getModulesByPermissionSlug()
modules.permission_ids is NULL for module 8 ("Szerviz kategoriak") on every
environment, and PHP 8.1 deprecates passing null to explode(). The notice was
only visible on prod because its index.php still has the stock
error_reporting(-1), whereas dev was previously changed to
E_ALL & ~E_DEPRECATED & ~E_WARNING & ~E_NOTICE.

- Skip rows whose permission_ids is null or empty. Behaviour-preserving:
  explode(',', null) returned array(''), which matched no permission id, so
  such modules were already excluded. Verified by simulating old vs new
  against the real module rows across six permission-id values including 0,
  '1' and 'admin' - identical results.
- Return early when the slug matches no permission, rather than dereferencing
  null on the next line.

Does not address the wider issue that prod runs ENVIRONMENT='development'
with display_errors=1, so any notice is rendered to real visitors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 10:49:07 +00:00
UbuntuandClaude Opus 5 35115404be Validate the guest e-mail before the booking is saved
An address like "asdf" used to be accepted: the field was type="text" with
only `required`, and there was no server-side check. createBooking() runs
long before sendEmail(), so the failure was silent rather than loud -
reproduced end to end on test:

  - the booking row WAS created, with a manage_token
  - PHPMailer's addAddress() threw, so nothing was ever sent
  - the Location header was already queued, so the guest was redirected to
    the normal "booking finished" page and saw success
  - Evelin is a CC on that same message, so the salon was not told either
  - the guest had no manage link, so they could not cancel

Fixes
- booking_process() rejects an empty or malformed address BEFORE any write,
  returning invalid_email / HTTP 400. Message added in all three languages,
  worded to say why it matters (the confirmation and the manage link go
  there). filter_var is equal-or-stricter than PHPMailer's own validator -
  checked against it on ten cases - so anything accepted here cannot throw
  later.
- The three public booking forms use type="email", so most typos never
  reach the server.
- Removed three debug echoes from User_model::sendEmail() that leaked $lang
  and Hungarian strings ("Üzenet elküldve", "Üzenetküldési hiba. Mailer
  Error: ...") into the guest-facing response.

Scope
- Public flow only. 508 existing bookings have an empty guest_email because
  admin-created block bookings legitimately have none; those go through
  Admin::booking_process(), which is untouched, and its form stays
  type="text".
- Not covered: a valid address whose SMTP delivery fails still leaves the
  booking created and the guest seeing success, logged only via
  log_message(). Different failure mode, needs a separate decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:55:33 +00:00
UbuntuandClaude Opus 5 7f51a795a5 Cache-bust local css/js so a deploy is picked up immediately
Apache serves this site's static files with only Last-Modified/ETag and no
Cache-Control or Expires, so browsers apply heuristic freshness and can hold
a stale stylesheet for hours. Deploys here are a file copy, so nothing else
signals a change. During development this masked CSS edits three separate
times; on the live site a returning customer would keep the old stylesheet
after a deploy with no way to know.

Adds asset_ver() (autoloaded), which returns the asset URL with the file's
mtime appended, so the URL itself changes whenever the file does. Falls back
to the plain URL when the file is missing, so a bad path degrades to the
previous behaviour rather than warning.

All 54 local css/js links now route through it, across the four *-head.php
and four *-skeleton-bottom.php includes.

Notes
- Remote assets are deliberately untouched: Google Fonts, momentjs,
  cookieyes and the googleapis jQuery keep their own URLs. Only files inside
  the webroot are versioned.
- vertical-head.php has one dynamic path (assets/css/<?= $vertical['css'] ?>)
  which is special-cased; a generic rewrite would swallow the nested <?php.
- Images are NOT versioned. Replacing an image under the same filename can
  still serve stale.
- head.php is shared by the public home page and the whole admin portal;
  /login verified to still render with versioned CSS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:00:40 +00:00
UbuntuandClaude Opus 5 de38aae8d5 Apply client design feedback on the parking copy, icons and barber layout
Four items from the salon, all three languages where applicable.

Massage section icon
- The icon above Om oss (and above Behandlinger, Priser and Åpningstider -
  it is the same image in all four) was beauty's makeup icon, copied in as
  a placeholder before massage had any assets of its own. Replaced with the
  herbal-compress icon tinted to the massage sage, mirroring how beauty's
  is tinted to its rose. beauty/ollo_rose.png is untouched; the copy under
  assets/img/massage/ is removed, and the one dead template rule that still
  pointed at it (.box-heading:before, which renders on no page) repointed.

Parking copy, barber + beauty + massage, no/en/hu
- Dropped the "park free of charge during the treatments" clause from the
  owner bio and replaced it with the new two-space wording.
- Replaced the note under Åpningstider. The Norwegian original was
  misspelled differently in each vertical ("kundeprarking" on barber,
  "kunderparking" on beauty); both are gone.
- massage picks both up automatically: its owner block is extracted from
  beauty-form-<lang>.php at generation time, and the hours note comes from
  the generator.

Barber treatments grid: level the six icons
- .service-text reserves padding-bottom for the 80px floated icon but has
  no height, so every block sizes to its own text. Measured on the live
  page the left column ran 155/155px against the right column's 185/206px,
  because its titles wrap to two lines - so the icons drifted further apart
  with each row (0, 30, 81px).
- Trimmed the reserved padding and gave every block the same floor, so all
  six are identical and the rows line up. 195px clears the tallest block
  and leaves room for a three-line title at the narrow end of the desktop
  range. Desktop only: below 981px the columns stack full width, where a
  floor would only add dead space.
- beauty drifts 20px and massage 0px, so both are left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:00:25 +00:00
UbuntuandClaude Opus 5 d8fe7724a2 Remove the massør vacancy now that the position is filled
Vincent Pusch has been hired, so the massage job posting is withdrawn:
deletes ledigestilling/massor.html and its card from the vacancy index.

The remaining Barber and Negletekniker postings are untouched, and the
"Ledige stillinger" footer links point at ledigestilling/ rather than the
individual pages, so nothing else needed changing. Verified no dangling
reference to massor.html remains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:36:51 +00:00
UbuntuandClaude Opus 5 5c30467fbd Add massage as a third vertical, driven by a config registry
Introduces /massage alongside barber and beauty: landing page, booking
flow, admin support, home tile and SEO entries, in all three languages.

Architecture
- application/config/verticals.php + vertical_helper.php: one registry
  entry per vertical (branding, assets, views, behaviour flags). A fourth
  vertical is a config entry plus content files.
- Strangler: barber and beauty keep pointing at their existing view files,
  so their rendered HTML is unchanged. Only massage uses the new generic
  pages/vertical-*.php and includes/vertical-*.php views, which collapse
  the four duplicated per-language nav/footer branches into one.
- Pages::vertical() + one route; booking(), booking_finished(),
  _booking_error() and manage_booking() are now registry-driven.

Worker/vertical coupling
- getActiveWorkers() derives the vertical from services.service_category_id
  instead of the workers.is_barber / is_beauty flags, which were a
  hand-maintained cache of exactly that fact. Verified against production
  data: the derived set reproduced the stored flags for every worker, in
  both verticals. No schema change was needed for massage.
- The legacy flags are now written through from the category so a rollback
  cannot strand a new worker, and the admin worker UI shows the derived
  verticals read-only instead of two dropdowns that controlled nothing.

Bug fixes found along the way (all pre-existing)
- booking_process() had no server-side category guard; cross-vertical
  mixing was prevented only by client-side JS.
- add-service-form / add-worker-form emitted `selected` on every category
  option, so the newest category silently became the default.
- update-service-form offered only barber/beauty, so editing a service of
  any other type silently rewrote it.
- getWorkerScheduleByDay ignored schedule overrides while getAvailableTimes
  honoured them, so slots could be shown and then rejected. Added an
  override-aware getWorkerScheduleForDate() and used it in both guards.
- Booking lists dereferenced a null service if one had been hard-deleted.
- main.css: .tiles was tuned for exactly two tiles, including an
  absolutely-positioned .style1 at the 1280px breakpoint.

Massage-specific behaviour, opt-in per vertical
- strip_category_prefix: grouped service lists show "50 min" under the
  treatment heading rather than repeating the full name. The full name is
  carried in data-service-name so the totals panel stays unambiguous, and
  services.service_name is untouched for emails and admin.
- single_service_booking: one treatment per booking, enforced in the UI and
  in booking_process(). Re-clicking the selection releases it.
- Displayed treatment time (50/80/110 min) is in the service name; the
  booked slot (60/90/120 min) is service_time and covers changing and
  payment. service_time is never shown to the guest.

DB migrations for dev/prod are in documents/ - additive only, no ALTER.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:34:47 +00:00
UbuntuandClaude Opus 4.8 0fd7f7a7a5 Fix "Conflict. Timeslot is taken" raw-JSON error on booking
Customers intermittently hit a full-screen raw JSON error when booking:
{"error":"Conflict. Timeslot is taken or does not fit the service."}

booking_process() re-validates the chosen slot at submit time and returned
409/403 raw JSON. Because the public booking form is a full-page POST, that
JSON filled the whole screen.

The trigger is a double submit. After inserting the booking, booking_process()
synchronously runs two Google Calendar createEvent calls, a lunch sync, an ntfy
push and an SMTP confirmation e-mail before redirecting - several seconds - and
the submit button was never disabled. On mobile the guest taps "Send" again; the
second request arrives after the first has committed, so the slot reads as taken.

Evidence: 168 duplicate booking pairs exist in prod (same guest, slot and worker,
consecutive booking ids, including runs of four). All are from 2025, none from
2026 - the 409 guard added around May 2025 converted those silent duplicates
into today's visible error.

Prevent the double submit:
- disable the submit button and relabel it on first submit, ignore later ones
- add a hidden sendBooking field, since disabling a submit button can drop its
  name/value from the POST and booking_process() bails to the homepage without it

Handle it gracefully when it still happens:
- new _booking_error() renders a localised page in the right skin instead of raw
  JSON, replacing all six JSON responses in booking_process()
- new booking-error views for barber/beauty in no/en/hu, each with a message per
  error case and a link back to booking
- new Service_model::getBookingBySlotAndGuest(); if the guest's own booking for
  that exact slot already exists the submit is a duplicate rather than a real
  conflict, so finish normally instead of erroring. Guarded on a non-empty
  e-mail, as admin block bookings are stored with an empty guest_email.

No schema change. Verified on test, dev and prod: friendly page in all three
languages and both skins, double submit redirects to booking-finished without
creating a duplicate row, and no raw JSON in any response.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJso3iGT7TkW5tm4RSBohs
2026-07-21 16:21:53 +00:00
UbuntuandClaude Fable 5 06966a898a Add job postings section with vacancies listing and footer links
- New /ledigestilling/ index listing all open positions (split
  barber/beauty card styling)
- Three ads: negletekniker + massør (beauty design), barber (gold
  barber design with recolored CSS + assets)
- Rename ledigestilling.html -> negletekniker.html with 301 redirect
- "Ledige stillinger" footer link on barber/beauty pages (NO/EN/HU)
- Back-to-list link on each ad page

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:32:17 +00:00
UbuntuandClaude Opus 4.7 29467211c5 Redirect direct GETs of booking-process to home
Hitting /booking-process via GET (or any POST without sendBooking) fell
through past the form-submit guard and tripped three undefined-variable
warnings on the final sendEmail() call. Bail out to the site root
early, matching the pattern in manage_booking_process.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 08:06:43 +00:00
UbuntuandClaude Opus 4.7 f86952dc95 Filter manage-booking workers by service category
Guests modifying a booking saw every worker for the subpage, so an
eyelash booking exposed nail-only workers as switchable. Now the worker
list is scoped to the booking's service category, other-category
services are disabled in step 1, and the process handler rejects any
worker/service category mismatch to defend against crafted POSTs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 07:43:15 +00:00
Ubuntu 4a93b5efa0 Fix undefined array key in header on short URLs
The language switcher in barber/beauty headers reads
$currentPageUrlArray[2] unguarded after exploding REQUEST_URI on '/'.
On routes with no language prefix (e.g. POST /manage-booking-cancel
rendering the cancelled page inline), the array has only 2 elements,
triggering "Undefined array key 2" warnings and producing malformed
language links like https://studiobeve.no/en//.

Normalize the array with += [2 => '', 3 => ''] right after the explode
so indices 2 and 3 always exist.
2026-05-11 19:28:33 +00:00
UbuntuandClaude Opus 4.7 92a84aadb1 Remove dead strftime() call in worker calendar form
The strftime() result was immediately overwritten by a hardcoded
Hungarian month-name array. Drop the dead line so PHP 8.1+ stops
emitting an E_DEPRECATED warning when prod loads the worker calendar.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 11:41:47 +00:00
UbuntuandClaude Opus 4.7 e8aa9084c1 Include manage link + 24h notice in admin notification emails
Admin create/modify booking notifications now match the public
confirmation/modify emails. Admin-created bookings get a manage_token
generated; admin-modified bookings reuse the existing token (or
backfill one if missing).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 11:29:48 +00:00
Ubuntu e24e68f603 Fix manage-booking time slots; align modify email with original
- getAvailableTimes() takes optional exclude_booking_id so a guest's
  own booking isn't counted as a conflict when editing — original time
  now reappears when extending services
- Manage-booking AJAX passes manage_token; server resolves to booking_id
- manage_booking_process uses the new param instead of the date-swap
  workaround (removes a small race-condition risk)
- Modify-booking emails (no/en/hu) now include Name/Email/Phone rows
  and the 24h cancellation policy, matching the original booking email
2026-05-10 13:18:07 +00:00
UbuntuandClaude Opus 4.7 1e32a146a7 Fix git remote URL scheme in CLAUDE.md (http → https)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 12:42:26 +00:00
UbuntuandClaude Opus 4.7 d7bdcac978 Add search filter to services admin list; refresh load_services.json
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 12:41:04 +00:00
UbuntuandClaude Opus 4.7 52e00216f7 Fix admin update forms — restore proper value="" quoting
The security hardening commit accidentally rendered every input value as
value=\"...\" (literal backslash-quote in HTML), which mangled all
submitted fields including the hidden worker_id/service_id, causing
UPDATE to match zero rows and silently no-op.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 12:40:50 +00:00
UbuntuandClaude Opus 4.6 6abb90329d Add visual booking calendar (weekly Teams-style view)
- New weekly calendar at /bookings/calendar with time blocks per booking
- Color-coded by worker, overlapping bookings shown side-by-side
- Click booking to see details, edit or delete
- Worker filter dropdown, week navigation (prev/next/today)
- AJAX week loading for smooth navigation
- Link between list view and calendar view

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 14:03:41 +00:00
UbuntuandClaude Opus 4.6 a100acddec Add Kateryna gallery images (2/2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 13:03:05 +00:00
UbuntuandClaude Opus 4.6 e95b32d017 Add Kateryna worker profile and gallery images (1/2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 13:03:00 +00:00
UbuntuandClaude Opus 4.6 0e172ceea8 Sync outstanding changes: beauty forms, config updates, gitignore
- Update beauty booking forms (en, hu, no)
- Update manage-booking view, worker calendar view
- Update config.php, google_calendar.php, GoogleCalendar library
- Add CLAUDE.md project instructions
- Add service-account-key.json to .gitignore
- Update ledigestilling page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 13:02:18 +00:00
UbuntuandClaude Opus 4.6 420bcb37fd Security hardening: fix SQLi, XSS, file upload, and migrate DB to RDS
- Fix all SQL injection vulnerabilities across Service_model, User_model,
  Module_model, Log_model, and Admin controller using parameterized queries
- Add htmlspecialchars() to all user-controlled output in admin views
  (bookings, services, workers, service categories, login form)
- Fix XSS in AJAX worker response and manage-booking-cancelled view
- Add file extension whitelist (jpg, jpeg, png, gif, webp) to all uploads
- Remove webshell (pentest2.php) from assets/img/profiles/
- Stop logging plaintext passwords on failed login attempts
- Migrate database.php hostname from localhost to AWS RDS endpoint
- Fix dropdown styling (white-on-white) in worker calendar view

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-07 13:00:32 +00:00