Update
This commit is contained in:
@@ -12,7 +12,7 @@ class AdminsController < ApplicationController
|
|||||||
else
|
else
|
||||||
session[:admin_unlocked] = false
|
session[:admin_unlocked] = false
|
||||||
@unlocked = false
|
@unlocked = false
|
||||||
flash.now[:alert] = "That passphrase did not unlock anything. Check the advert again."
|
flash.now[:alert] = "That passphrase did not unlock anything. Check the stock page again."
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ class ListingsController < ApplicationController
|
|||||||
stock_number: "FC-718-421",
|
stock_number: "FC-718-421",
|
||||||
exterior: "Graphite Blue Metallic",
|
exterior: "Graphite Blue Metallic",
|
||||||
interior: "Black leather with chalk stitching",
|
interior: "Black leather with chalk stitching",
|
||||||
seller_name: "Amelia Bennett",
|
dealer_name: "Forecourt Specialist Cars",
|
||||||
seller_role: "Verified private seller",
|
dealer_tagline: "Indoor showroom · Appointment-led viewings",
|
||||||
seller_since: "On Forecourt since 2019",
|
dealer_since: "Established 2012",
|
||||||
response_time: "Usually replies within 14 minutes",
|
dealer_contact: "01483 905210",
|
||||||
|
dealer_hours: "Mon-Sat 09:00-17:30",
|
||||||
|
dealer_address: "Woodbridge Meadows, Guildford",
|
||||||
image_caption: "Photo set 03 // ingest note JHDU // something seems shifted.",
|
image_caption: "Photo set 03 // ingest note JHDU // something seems shifted.",
|
||||||
hero_image: "https://images.unsplash.com/photo-1494976388531-d1058494cdd8?auto=format&fit=crop&w=1600&q=80",
|
hero_image: "https://images.unsplash.com/photo-1494976388531-d1058494cdd8?auto=format&fit=crop&w=1600&q=80",
|
||||||
gallery: [
|
gallery: [
|
||||||
@@ -27,11 +29,11 @@ class ListingsController < ApplicationController
|
|||||||
highlights: [
|
highlights: [
|
||||||
"Original paint-depth readings documented in the gallery",
|
"Original paint-depth readings documented in the gallery",
|
||||||
"Fresh Michelin PS4S tyres with under 1,500 miles",
|
"Fresh Michelin PS4S tyres with under 1,500 miles",
|
||||||
"Annual brake fluid and gearbox service invoices retained from the seller's specialist"
|
"Annual brake fluid and gearbox service invoices retained in the vehicle file"
|
||||||
],
|
],
|
||||||
overview: [
|
overview: [
|
||||||
"This Cayman S was specced exactly the way enthusiasts usually hope to find one: manual gearbox, Sport Chrono, PASM, and no unnecessary aero add-ons. The seller has owned it for four years, used it as a weekend car, and kept every service invoice in order.",
|
"This Cayman S was specced exactly the way enthusiasts usually hope to find one: manual gearbox, Sport Chrono, PASM, and no unnecessary aero add-ons. Forecourt acquired it from a long-term local owner and has kept the supporting service file in order.",
|
||||||
"Cosmetically it presents like a well-kept private sale rather than a detail-heavy showroom car. There are two tiny stone marks on the nose, the front splitter shows light road wear, and the cabin leather has stayed impressively matte.",
|
"Cosmetically it presents like an honestly prepared specialist-stock car rather than an over-restored showroom piece. There are two tiny stone marks on the nose, the front splitter shows light road wear, and the cabin leather has stayed impressively matte.",
|
||||||
"Forecourt's intake notes show an HPI-clear history, no insurance loss markers, two keys, the original order paperwork, and recent borescope images from the last annual inspection."
|
"Forecourt's intake notes show an HPI-clear history, no insurance loss markers, two keys, the original order paperwork, and recent borescope images from the last annual inspection."
|
||||||
],
|
],
|
||||||
specs: [
|
specs: [
|
||||||
@@ -45,13 +47,13 @@ class ListingsController < ApplicationController
|
|||||||
[ "Keepers", "2 keepers" ],
|
[ "Keepers", "2 keepers" ],
|
||||||
[ "Vehicle history", "HPI clear, no outstanding finance" ]
|
[ "Vehicle history", "HPI clear, no outstanding finance" ]
|
||||||
],
|
],
|
||||||
document_packet: [
|
factory_options: [
|
||||||
{ source: "Original order form", detail: "Factory spec confirmed against supplied paperwork", file_type: "PDF" },
|
{ code: "AAG", option: "Aluminium look fuel cap", verified_from: "Order form" },
|
||||||
{ source: "Annual alignment sheet", detail: "Alignment printout tucked behind service invoices", file_type: "Scan" },
|
{ code: "P04", option: "Automatically dimming mirrors", verified_from: "Photo match" },
|
||||||
{ source: "Battery conditioner leaflet", detail: "Charger notes only, no service relevance", file_type: "JPG" },
|
{ code: "XLS", option: "Xenon headlamps with PDLS", verified_from: "Order form" },
|
||||||
{ source: "Paint protection card", detail: "XPEL warranty card added to the folder", file_type: "Scan" },
|
{ code: "QR5", option: "Sport Chrono display surround", verified_from: "Prep sheet" },
|
||||||
{ source: "Key handover note", detail: "Leather sleeve re-dyed to match the cabin", file_type: "Scan" },
|
{ code: "LTH", option: "Leather steering column casing", verified_from: "Order form" },
|
||||||
{ source: "Roadside kit insert", detail: "Emergency cartridge dated and photographed", file_type: "Scan" }
|
{ code: "EPK", option: "Extended interior package", verified_from: "Order form" }
|
||||||
],
|
],
|
||||||
condition_notes: [
|
condition_notes: [
|
||||||
"Cold-start video archived from 24 April",
|
"Cold-start video archived from 24 April",
|
||||||
@@ -59,18 +61,18 @@ class ListingsController < ApplicationController
|
|||||||
"Underbody photos show no corrosion bloom",
|
"Underbody photos show no corrosion bloom",
|
||||||
"Driver's bolster has only light creasing"
|
"Driver's bolster has only light creasing"
|
||||||
],
|
],
|
||||||
seller_notes: [
|
dealer_notes: [
|
||||||
"Always warmed through before spirited driving",
|
"Two keys, tracker cards and handbook pack present",
|
||||||
"No track days, no winter salt exposure",
|
"Fresh MOT issued ahead of photography",
|
||||||
"Super unleaded only, documented with fuel log",
|
"Geometry check filed with the vehicle history pack",
|
||||||
"Includes factory battery conditioner and indoor cover"
|
"Viewings by appointment in the indoor showroom"
|
||||||
],
|
],
|
||||||
prep_tickets: [
|
inspection_log: [
|
||||||
{ task: "Bay card print", stamp: "2026-04-24 08:31 BST", code: "PDI-24-09", status: "Open" },
|
{ note: "Front splitter edge photographed", stamp: "2026-04-24 08:31 BST", code: "OBS-17-09", status: "Advisory" },
|
||||||
{ task: "Handover pack filed", stamp: "2026-04-24 08:27 BST", code: "PDI-24-11", status: "Closed" },
|
{ note: "Battery conditioner case checked", stamp: "2026-04-24 08:27 BST", code: "INS-88-52", status: "Filed" },
|
||||||
{ task: "Road test sign-off", stamp: "2026-04-24 08:21 BST", code: "PDI-24-18", status: "Closed" },
|
{ note: "Road test complete", stamp: "2026-04-24 08:21 BST", code: "INS-88-73", status: "Filed" },
|
||||||
{ task: "Alarm fob check", stamp: "2026-04-24 08:16 BST", code: "PDI-24-01", status: "Closed" },
|
{ note: "Alarm fob battery confirmed", stamp: "2026-04-24 08:16 BST", code: "INS-88-21", status: "Filed" },
|
||||||
{ task: "Paint-depth sheet scanned", stamp: "2026-04-24 08:12 BST", code: "PDI-24-16", status: "Closed" }
|
{ note: "Paint-depth sheet attached", stamp: "2026-04-24 08:12 BST", code: "INS-88-71", status: "Filed" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
const REVIEW_PATH = "/internal/pdi-bundle-7c4f"
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["hintPanel", "progress"]
|
static targets = ["hintPanel", "progress", "reviewPath"]
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.clickCount = 0
|
this.clickCount = 0
|
||||||
|
|
||||||
console.info("[Forecourt QA] Local review tools remain mounted at /admin.")
|
if (this.hasReviewPathTarget) {
|
||||||
console.debug("[Forecourt QA] One payload on this page is encoded for transport, not encrypted.")
|
this.reviewPathTarget.textContent = REVIEW_PATH
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tapLogo() {
|
tapLogo() {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-amber-400">Forecourt</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-amber-400">Forecourt</p>
|
||||||
<h1 class="mt-1 text-2xl font-semibold text-white">Local admin console</h1>
|
<h1 class="mt-1 text-2xl font-semibold text-white">Local admin console</h1>
|
||||||
</div>
|
</div>
|
||||||
<%= link_to "Back to advert", root_path, class: "inline-flex items-center rounded-full border border-white/15 px-4 py-2 text-sm font-medium text-zinc-200 transition hover:border-white/30 hover:text-white" %>
|
<%= link_to "Back to vehicle", root_path, class: "inline-flex items-center rounded-full border border-white/15 px-4 py-2 text-sm font-medium text-zinc-200 transition hover:border-white/30 hover:text-white" %>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -35,19 +35,19 @@
|
|||||||
|
|
||||||
<div class="mt-8 grid gap-4 md:grid-cols-2">
|
<div class="mt-8 grid gap-4 md:grid-cols-2">
|
||||||
<button class="rounded-2xl border border-rose-400/35 bg-rose-500/10 px-5 py-4 text-left transition hover:bg-rose-500/15">
|
<button class="rounded-2xl border border-rose-400/35 bg-rose-500/10 px-5 py-4 text-left transition hover:bg-rose-500/15">
|
||||||
<p class="text-sm font-semibold text-rose-100">Delete adverts</p>
|
<p class="text-sm font-semibold text-rose-100">Delete stock page</p>
|
||||||
<p class="mt-1 text-sm text-rose-200/80">Soft-delete every draft older than 30 days.</p>
|
<p class="mt-1 text-sm text-rose-200/80">Soft-delete every draft older than 30 days.</p>
|
||||||
</button>
|
</button>
|
||||||
<button class="rounded-2xl border border-amber-400/35 bg-amber-500/10 px-5 py-4 text-left transition hover:bg-amber-500/15">
|
<button class="rounded-2xl border border-amber-400/35 bg-amber-500/10 px-5 py-4 text-left transition hover:bg-amber-500/15">
|
||||||
<p class="text-sm font-semibold text-amber-100">Ban Seller</p>
|
<p class="text-sm font-semibold text-amber-100">Mark reserved</p>
|
||||||
<p class="mt-1 text-sm text-amber-200/80">Suspend an account and freeze outbound messages.</p>
|
<p class="mt-1 text-sm text-amber-200/80">Hold a vehicle against inbound showroom enquiries.</p>
|
||||||
</button>
|
</button>
|
||||||
<button class="rounded-2xl border border-sky-400/35 bg-sky-500/10 px-5 py-4 text-left transition hover:bg-sky-500/15">
|
<button class="rounded-2xl border border-sky-400/35 bg-sky-500/10 px-5 py-4 text-left transition hover:bg-sky-500/15">
|
||||||
<p class="text-sm font-semibold text-sky-100">Rewrite price cache</p>
|
<p class="text-sm font-semibold text-sky-100">Rewrite price cache</p>
|
||||||
<p class="mt-1 text-sm text-sky-200/80">Force a fresh estimate against stale market comparables.</p>
|
<p class="mt-1 text-sm text-sky-200/80">Force a fresh estimate against stale market comparables.</p>
|
||||||
</button>
|
</button>
|
||||||
<button class="rounded-2xl border border-fuchsia-400/35 bg-fuchsia-500/10 px-5 py-4 text-left transition hover:bg-fuchsia-500/15">
|
<button class="rounded-2xl border border-fuchsia-400/35 bg-fuchsia-500/10 px-5 py-4 text-left transition hover:bg-fuchsia-500/15">
|
||||||
<p class="text-sm font-semibold text-fuchsia-100">Feature advert</p>
|
<p class="text-sm font-semibold text-fuchsia-100">Feature vehicle</p>
|
||||||
<p class="mt-1 text-sm text-fuchsia-200/80">Pin a vehicle to the top rail for 24 hours.</p>
|
<p class="mt-1 text-sm text-fuchsia-200/80">Pin a vehicle to the top rail for 24 hours.</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,15 +62,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-5 grid gap-4 md:grid-cols-2">
|
<div class="mt-5 grid gap-4 md:grid-cols-2">
|
||||||
<label class="flex items-center justify-between rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-zinc-300">
|
<label class="flex items-center justify-between rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-zinc-300">
|
||||||
Spotlight seller badges
|
Spotlight stock badges
|
||||||
<input type="checkbox" checked disabled class="h-4 w-4 rounded border-zinc-700 bg-zinc-900 text-emerald-400">
|
<input type="checkbox" checked disabled class="h-4 w-4 rounded border-zinc-700 bg-zinc-900 text-emerald-400">
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center justify-between rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-zinc-300">
|
<label class="flex items-center justify-between rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-zinc-300">
|
||||||
Quiet suspicious offers
|
Quiet duplicate enquiries
|
||||||
<input type="checkbox" checked disabled class="h-4 w-4 rounded border-zinc-700 bg-zinc-900 text-emerald-400">
|
<input type="checkbox" checked disabled class="h-4 w-4 rounded border-zinc-700 bg-zinc-900 text-emerald-400">
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center justify-between rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-zinc-300">
|
<label class="flex items-center justify-between rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-zinc-300">
|
||||||
Auto-hide low-effort adverts
|
Auto-hide thin stock pages
|
||||||
<input type="checkbox" disabled class="h-4 w-4 rounded border-zinc-700 bg-zinc-900 text-emerald-400">
|
<input type="checkbox" disabled class="h-4 w-4 rounded border-zinc-700 bg-zinc-900 text-emerald-400">
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center justify-between rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-zinc-300">
|
<label class="flex items-center justify-between rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-zinc-300">
|
||||||
@@ -90,8 +90,8 @@
|
|||||||
<p class="mt-1 text-zinc-400">Ran 18 minutes ago by `ops-preview`.</p>
|
<p class="mt-1 text-zinc-400">Ran 18 minutes ago by `ops-preview`.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-white/10 bg-zinc-950/50 p-4">
|
<div class="rounded-2xl border border-white/10 bg-zinc-950/50 p-4">
|
||||||
<p class="font-medium text-white">Manual seller review</p>
|
<p class="font-medium text-white">Manual stock review</p>
|
||||||
<p class="mt-1 text-zinc-400">Awaiting notes from trust-and-safety.</p>
|
<p class="mt-1 text-zinc-400">Awaiting sign-off from the prep team.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-white/10 bg-zinc-950/50 p-4">
|
<div class="rounded-2xl border border-white/10 bg-zinc-950/50 p-4">
|
||||||
<p class="font-medium text-white">Homepage merchandising slot 02</p>
|
<p class="font-medium text-white">Homepage merchandising slot 02</p>
|
||||||
@@ -114,9 +114,12 @@
|
|||||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-400">Restricted access</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-400">Restricted access</p>
|
||||||
<h2 class="mt-2 text-3xl font-semibold text-white">Admin login required</h2>
|
<h2 class="mt-2 text-3xl font-semibold text-white">Admin login required</h2>
|
||||||
<p class="mt-3 text-sm leading-6 text-zinc-300">
|
<p class="mt-3 text-sm leading-6 text-zinc-300">
|
||||||
Enter the four-word access phrase from the advert page in the format
|
Enter the four-word access phrase from the stock page in the format
|
||||||
<span class="rounded bg-white/10 px-2 py-1 font-mono text-xs text-zinc-100">WORD_1-WORD_2-WORD_3-WORD_4</span>.
|
<span class="rounded bg-white/10 px-2 py-1 font-mono text-xs text-zinc-100">WORD_1-WORD_2-WORD_3-WORD_4</span>.
|
||||||
</p>
|
</p>
|
||||||
|
<p class="mt-2 text-sm text-zinc-500">
|
||||||
|
Use the words in page order.
|
||||||
|
</p>
|
||||||
|
|
||||||
<%= form_with url: admin_path, method: :post, class: "mt-8 space-y-5" do |form| %>
|
<%= form_with url: admin_path, method: :post, class: "mt-8 space-y-5" do |form| %>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<% content_for :title, "#{@listing[:title]} | Forecourt" %>
|
<% content_for :title, "#{@listing[:title]} | Forecourt" %>
|
||||||
|
|
||||||
<div data-controller="listing-puzzle" class="min-h-screen bg-zinc-50 text-zinc-900">
|
<div data-controller="listing-puzzle" class="min-h-screen bg-zinc-50 text-zinc-900">
|
||||||
<!-- Forecourt QA: local review tools still live at /admin. -->
|
|
||||||
|
|
||||||
<div data-listing-puzzle-target="hintPanel" class="fixed bottom-5 right-5 z-50 hidden w-[22rem] rounded-3xl border border-zinc-200 bg-white p-5 shadow-2xl shadow-zinc-900/15">
|
<div data-listing-puzzle-target="hintPanel" class="fixed bottom-5 right-5 z-50 hidden w-[22rem] rounded-3xl border border-zinc-200 bg-white p-5 shadow-2xl shadow-zinc-900/15">
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -12,7 +10,8 @@
|
|||||||
<button type="button" data-action="listing-puzzle#dismissHint" class="rounded-full border border-zinc-200 px-2.5 py-1 text-sm text-zinc-500 transition hover:border-zinc-300 hover:text-zinc-700">Close</button>
|
<button type="button" data-action="listing-puzzle#dismissHint" class="rounded-full border border-zinc-200 px-2.5 py-1 text-sm text-zinc-500 transition hover:border-zinc-300 hover:text-zinc-700">Close</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-sm leading-6 text-zinc-600">
|
<p class="mt-3 text-sm leading-6 text-zinc-600">
|
||||||
The advert has clues in captions, checklists, page data, and repeating IDs. Devtools will help, and the local-only review screen still answers at <span class="font-mono text-xs text-zinc-900">/admin</span>.
|
The stock page has clues in captions, checklists, page data, and repeating IDs. Devtools will help, and one local-only review path is still wired into the page controller:
|
||||||
|
<span data-listing-puzzle-target="reviewPath" class="font-mono text-xs text-zinc-900"></span>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-3 text-xs text-zinc-500">Logo taps tracked: <span data-listing-puzzle-target="progress">0/5</span></p>
|
<p class="mt-3 text-xs text-zinc-500">Logo taps tracked: <span data-listing-puzzle-target="progress">0/5</span></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,12 +22,12 @@
|
|||||||
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-zinc-950 text-sm font-semibold text-white">M</span>
|
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-zinc-950 text-sm font-semibold text-white">M</span>
|
||||||
<span>
|
<span>
|
||||||
<span class="block text-sm font-semibold text-zinc-950">Forecourt</span>
|
<span class="block text-sm font-semibold text-zinc-950">Forecourt</span>
|
||||||
<span class="block text-xs text-zinc-500">Curated enthusiast adverts</span>
|
<span class="block text-xs text-zinc-500">Specialist sports and GT stock</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="hidden items-center gap-3 md:flex">
|
<div class="hidden items-center gap-3 md:flex">
|
||||||
<span class="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">Verified seller</span>
|
<span class="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">Dealer approved</span>
|
||||||
<span class="rounded-full border border-zinc-200 px-3 py-1 text-xs font-medium text-zinc-600">Price reduced this week</span>
|
<span class="rounded-full border border-zinc-200 px-3 py-1 text-xs font-medium text-zinc-600">Price reduced this week</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
<section class="border-b border-zinc-200 pb-10">
|
<section class="border-b border-zinc-200 pb-10">
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Advert FC-718-421</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Stock FC-718-421</p>
|
||||||
<h1 class="mt-2 text-4xl font-semibold tracking-tight text-zinc-950"><%= @listing[:title] %></h1>
|
<h1 class="mt-2 text-4xl font-semibold tracking-tight text-zinc-950"><%= @listing[:title] %></h1>
|
||||||
<p class="mt-3 max-w-3xl text-sm leading-6 text-zinc-600"><%= @listing[:subtitle] %></p>
|
<p class="mt-3 max-w-3xl text-sm leading-6 text-zinc-600"><%= @listing[:subtitle] %></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,8 +91,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 space-y-3">
|
<div class="mt-6 space-y-3">
|
||||||
<button class="inline-flex w-full items-center justify-center rounded-full bg-zinc-950 px-5 py-3 text-sm font-semibold text-white transition hover:bg-zinc-800">Reserve for 24 hours</button>
|
<button class="inline-flex w-full items-center justify-center rounded-full bg-zinc-950 px-5 py-3 text-sm font-semibold text-white transition hover:bg-zinc-800">Book a viewing</button>
|
||||||
<button class="inline-flex w-full items-center justify-center rounded-full border border-zinc-200 px-5 py-3 text-sm font-semibold text-zinc-900 transition hover:border-zinc-300 hover:bg-zinc-50">Message seller</button>
|
<button class="inline-flex w-full items-center justify-center rounded-full border border-zinc-200 px-5 py-3 text-sm font-semibold text-zinc-900 transition hover:border-zinc-300 hover:bg-zinc-50">Request video walkaround</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl class="mt-6 grid gap-4 border-t border-zinc-200 pt-6 text-sm text-zinc-600">
|
<dl class="mt-6 grid gap-4 border-t border-zinc-200 pt-6 text-sm text-zinc-600">
|
||||||
@@ -116,12 +115,14 @@
|
|||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<div class="mt-6 border-t border-zinc-200 pt-6">
|
<div class="mt-6 border-t border-zinc-200 pt-6">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Seller</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Dealership</p>
|
||||||
<p class="mt-2 text-lg font-semibold text-zinc-950"><%= @listing[:seller_name] %></p>
|
<p class="mt-2 text-lg font-semibold text-zinc-950"><%= @listing[:dealer_name] %></p>
|
||||||
<p class="mt-1 text-sm text-zinc-600"><%= @listing[:seller_role] %></p>
|
<p class="mt-1 text-sm text-zinc-600"><%= @listing[:dealer_tagline] %></p>
|
||||||
<div class="mt-4 space-y-2 text-sm text-zinc-600">
|
<div class="mt-4 space-y-2 text-sm text-zinc-600">
|
||||||
<p><%= @listing[:seller_since] %></p>
|
<p><%= @listing[:dealer_since] %></p>
|
||||||
<p><%= @listing[:response_time] %></p>
|
<p><%= @listing[:dealer_hours] %></p>
|
||||||
|
<p><%= @listing[:dealer_address] %></p>
|
||||||
|
<p class="font-medium text-zinc-900"><%= @listing[:dealer_contact] %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,20 +164,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Document packet</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Factory options</p>
|
||||||
<p class="mt-3 text-sm leading-6 text-zinc-600">The seller uploaded the original paperwork bundle and a few newer scans. Check the details carefully.</p>
|
<p class="mt-3 text-sm leading-6 text-zinc-600">The stock sheet is longer than the original order form. Check the verified items carefully.</p>
|
||||||
<div class="mt-4 overflow-hidden rounded-3xl border border-zinc-200 bg-white">
|
<div class="mt-4 overflow-hidden rounded-3xl border border-zinc-200 bg-white">
|
||||||
<div class="grid grid-cols-[minmax(0,0.9fr)_minmax(0,1.3fr)_5.5rem] gap-4 border-b border-zinc-200 px-5 py-3 text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">
|
<div class="grid grid-cols-[5.5rem_minmax(0,1.35fr)_8rem] gap-4 border-b border-zinc-200 px-5 py-3 text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">
|
||||||
<p>Source</p>
|
<p>Code</p>
|
||||||
<p>Detail</p>
|
<p>Option</p>
|
||||||
<p class="text-right">File</p>
|
<p class="text-right">Verified</p>
|
||||||
</div>
|
</div>
|
||||||
<% @listing[:document_packet].each do |entry| %>
|
<% @listing[:factory_options].each do |entry| %>
|
||||||
<div class="grid grid-cols-[minmax(0,0.9fr)_minmax(0,1.3fr)_5.5rem] gap-4 border-b border-zinc-100 px-5 py-4 text-sm last:border-b-0">
|
<div class="grid grid-cols-[5.5rem_minmax(0,1.35fr)_8rem] gap-4 border-b border-zinc-100 px-5 py-4 text-sm last:border-b-0">
|
||||||
<p class="font-medium text-zinc-900"><%= entry[:source] %></p>
|
<p class="font-mono text-xs font-semibold tracking-[0.18em] text-zinc-500"><%= entry[:code] %></p>
|
||||||
<p class="text-zinc-700"><%= entry[:detail] %></p>
|
<p class="text-zinc-900"><%= entry[:option] %></p>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<span class="rounded-full border border-zinc-200 bg-zinc-50 px-3 py-1 text-xs font-medium text-zinc-600"><%= entry[:file_type] %></span>
|
<span class="<%= entry[:verified_from] == 'Order form' ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-zinc-200 bg-zinc-50 text-zinc-600' %> rounded-full border px-3 py-1 text-xs font-medium"><%= entry[:verified_from] %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -186,33 +187,33 @@
|
|||||||
|
|
||||||
<section class="grid gap-12 border-b border-zinc-200 py-10 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,0.9fr)]">
|
<section class="grid gap-12 border-b border-zinc-200 py-10 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,0.9fr)]">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Inspection notes</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Condition summary</p>
|
||||||
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||||
<% @listing[:condition_notes].each do |note| %>
|
<% @listing[:condition_notes].each do |note| %>
|
||||||
<div class="rounded-2xl border border-zinc-200 bg-white px-4 py-4 text-sm text-zinc-700"><%= note %></div>
|
<div class="rounded-2xl border border-zinc-200 bg-white px-4 py-4 text-sm text-zinc-700"><%= note %></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-8 text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Seller notes</p>
|
<p class="mt-8 text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Dealer notes</p>
|
||||||
<div class="mt-4 space-y-3 text-sm leading-6 text-zinc-700">
|
<div class="mt-4 space-y-3 text-sm leading-6 text-zinc-700">
|
||||||
<% @listing[:seller_notes].each do |note| %>
|
<% @listing[:dealer_notes].each do |note| %>
|
||||||
<p><%= note %></p>
|
<p><%= note %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Pre-sale prep tickets</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Inspection notes</p>
|
||||||
<p class="mt-3 text-sm leading-6 text-zinc-600">The prep desk export is still sorted by latest activity. Closed tickets all share the same base code, and only one part changes.</p>
|
<p class="mt-3 text-sm leading-6 text-zinc-600">The workshop printout is still shown newest first. Only the notes filed into the final inspection bundle share the same base code, and the service desk phone still uses the old multi-tap keypad labels.</p>
|
||||||
<div class="mt-4 divide-y divide-zinc-200 rounded-3xl border border-zinc-200 bg-white">
|
<div class="mt-4 divide-y divide-zinc-200 rounded-3xl border border-zinc-200 bg-white">
|
||||||
<% @listing[:prep_tickets].each do |entry| %>
|
<% @listing[:inspection_log].each do |entry| %>
|
||||||
<div class="flex items-center justify-between gap-4 px-5 py-4">
|
<div class="flex items-center justify-between gap-4 px-5 py-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-zinc-900"><%= entry[:task] %></p>
|
<p class="text-sm font-medium text-zinc-900"><%= entry[:note] %></p>
|
||||||
<p class="mt-1 text-sm text-zinc-500"><%= entry[:stamp] %></p>
|
<p class="mt-1 text-sm text-zinc-500"><%= entry[:stamp] %></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="<%= entry[:status] == 'Closed' ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-amber-200 bg-amber-50 text-amber-700' %> rounded-full border px-3 py-1 text-xs font-medium"><%= entry[:status] %></span>
|
<span class="<%= entry[:status] == 'Filed' ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-amber-200 bg-amber-50 text-amber-700' %> rounded-full border px-3 py-1 text-xs font-medium"><%= entry[:status] %></span>
|
||||||
<code class="rounded-full bg-zinc-950 px-3 py-1.5 text-xs text-zinc-100"><%= entry[:code] %></code>
|
<code class="rounded-full bg-zinc-950 px-3 py-1.5 text-xs text-zinc-100"><%= entry[:code] %></code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,9 +225,9 @@
|
|||||||
<section class="py-10">
|
<section class="py-10">
|
||||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Forecourt promise</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-zinc-500">Forecourt promise</p>
|
||||||
<p class="mt-2 max-w-3xl text-sm leading-6 text-zinc-600">
|
<p class="mt-2 max-w-3xl text-sm leading-6 text-zinc-600">
|
||||||
Every enthusiast advert includes verified vehicle-history data, recent visual documentation, and just enough intake weirdness to remind you a human touched it somewhere along the way.
|
Every car in stock includes verified vehicle-history data, recent visual documentation, and just enough intake weirdness to remind you a human touched it somewhere along the way.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-zinc-500">
|
<div class="text-sm text-zinc-500">
|
||||||
|
|||||||
+1
-1
@@ -11,5 +11,5 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
root "listings#show"
|
root "listings#show"
|
||||||
get "listings/graphite-cayman-s", to: "listings#show", as: :listing
|
get "listings/graphite-cayman-s", to: "listings#show", as: :listing
|
||||||
resource :admin, only: %i[show create destroy], controller: "admins"
|
resource :admin, path: "internal/pdi-bundle-7c4f", only: %i[show create destroy], controller: "admins"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,35 +8,34 @@ This file is for the puzzle owner only and should not be linked from the UI.
|
|||||||
|
|
||||||
## Route
|
## Route
|
||||||
|
|
||||||
- Hidden route: `/admin`
|
- Hidden route: `/internal/pdi-bundle-7c4f`
|
||||||
- Discovery points on the advert page:
|
- Discovery points on the stock page:
|
||||||
- HTML comment near the top of the page source
|
- Stimulus controller source for the stock page (`listing_puzzle_controller`)
|
||||||
- Browser console message from the Stimulus controller
|
- Optional hidden hint drawer after clicking the Forecourt logo 5 times (the drawer renders the controller's stored path at runtime)
|
||||||
- Optional hidden hint drawer after clicking the Forecourt logo 5 times
|
|
||||||
|
|
||||||
## Stage 1
|
## Stage 1
|
||||||
|
|
||||||
- Location: main image caption on the advert page
|
- Location: main image caption on the stock page
|
||||||
- Visible clue: `Photo set 03 // ingest note JHDU // something seems shifted.`
|
- Visible clue: `Photo set 03 // ingest note JHDU // something seems shifted.`
|
||||||
- Solve method: Caesar shift back by 3
|
- Solve method: Caesar shift back by 3
|
||||||
- Answer: `GEAR`
|
- Answer: `GEAR`
|
||||||
|
|
||||||
## Stage 2
|
## Stage 2
|
||||||
|
|
||||||
- Location: "Document packet" table
|
- Location: "Factory options" table
|
||||||
- Visible hint: `The seller uploaded the original paperwork bundle and a few newer scans. Check the details carefully.`
|
- Visible hint: `The stock sheet is longer than the original order form. Check the verified items carefully.`
|
||||||
- Solve method:
|
- Solve method:
|
||||||
- use only the rows marked `Scan`
|
- use only the rows verified from `Order form`
|
||||||
- then take the first letter of each entry in the `Detail` column
|
- then take the first letter of each option name in top-to-bottom order
|
||||||
- Alignment printout tucked behind service invoices
|
- Aluminium look fuel cap
|
||||||
- XPEL warranty card added to the folder
|
- Xenon headlamps with PDLS
|
||||||
- Leather sleeve re-dyed to match the cabin
|
- Leather steering column casing
|
||||||
- Emergency cartridge dated and photographed
|
- Extended interior package
|
||||||
- Answer: `AXLE`
|
- Answer: `AXLE`
|
||||||
|
|
||||||
## Stage 3
|
## Stage 3
|
||||||
|
|
||||||
- Location: hidden JSON blob in the advert page source
|
- Location: hidden JSON blob in the stock page source
|
||||||
- Element ID: `forecourt-media-manifest`
|
- Element ID: `forecourt-media-manifest`
|
||||||
- Encoded value: `VFVSQk8=`
|
- Encoded value: `VFVSQk8=`
|
||||||
- Solve method: Base64 decode
|
- Solve method: Base64 decode
|
||||||
@@ -44,26 +43,28 @@ This file is for the puzzle owner only and should not be linked from the UI.
|
|||||||
|
|
||||||
## Stage 4
|
## Stage 4
|
||||||
|
|
||||||
- Location: "Pre-sale prep tickets" card
|
- Location: "Inspection notes" card
|
||||||
- Visible hint: `The prep desk export is still sorted by latest activity. Closed tickets all share the same base code, and only one part changes.`
|
- Visible hint: `The workshop printout is still shown newest first. Only the notes filed into the final inspection bundle share the same base code, and the service desk phone still uses the old multi-tap keypad labels.`
|
||||||
- Ticket codes:
|
- Note references:
|
||||||
- `PDI-24-09` (`Open`, ignore this row)
|
- `OBS-17-09` (`Advisory`, ignore this row)
|
||||||
- `PDI-24-11`
|
- `INS-88-52`
|
||||||
- `PDI-24-18`
|
- `INS-88-73`
|
||||||
- `PDI-24-01`
|
- `INS-88-21`
|
||||||
- `PDI-24-16`
|
- `INS-88-71`
|
||||||
- Solve method:
|
- Solve method:
|
||||||
- use only the `Closed` tickets
|
- use only the notes marked `Filed`
|
||||||
- the card is shown newest first, so read the closed tickets from oldest to newest
|
- ignore the one advisory entry with a different base code
|
||||||
- map the changing numeric suffixes with A=1
|
- the card is shown newest first, so read the filed notes from oldest to newest
|
||||||
- 16 = P
|
- decode the final two digits as classic mobile keypad `key + tap count`
|
||||||
- 01 = A
|
- 71 = P (`7` pressed once)
|
||||||
- 18 = R
|
- 21 = A (`2` pressed once)
|
||||||
- 11 = K
|
- 73 = R (`7` pressed three times)
|
||||||
|
- 52 = K (`5` pressed twice)
|
||||||
- Answer: `PARK`
|
- Answer: `PARK`
|
||||||
|
|
||||||
## Admin behavior
|
## Admin behavior
|
||||||
|
|
||||||
- `GET /admin` shows the login prompt
|
- `GET /internal/pdi-bundle-7c4f` shows the login prompt
|
||||||
|
- The login prompt tells solvers to use the words in page order
|
||||||
- Entering `GEAR-AXLE-TURBO-PARK` unlocks the fake admin panel
|
- Entering `GEAR-AXLE-TURBO-PARK` unlocks the fake admin panel
|
||||||
- The panel is session-backed only and can be cleared with the "Clear admin session" button
|
- The panel is session-backed only and can be cleared with the "Clear admin session" button
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class MotorlotFlowTest < ActionDispatch::IntegrationTest
|
class MotorlotFlowTest < ActionDispatch::IntegrationTest
|
||||||
test "advert page renders puzzle listing" do
|
test "stock page renders puzzle listing" do
|
||||||
get root_path
|
get root_path
|
||||||
|
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_includes response.body, "2021 Porsche 718 Cayman S"
|
assert_includes response.body, "2021 Porsche 718 Cayman S"
|
||||||
assert_includes response.body, "JHDU"
|
assert_includes response.body, "JHDU"
|
||||||
assert_includes response.body, "VFVSQk8="
|
assert_includes response.body, "VFVSQk8="
|
||||||
assert_includes response.body, "PDI-24-16"
|
assert_includes response.body, "INS-88-71"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "admin unlock succeeds with the full password" do
|
test "admin unlock succeeds with the full password" do
|
||||||
@@ -19,7 +19,7 @@ class MotorlotFlowTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_includes response.body, "Admin Panel Unlocked"
|
assert_includes response.body, "Admin Panel Unlocked"
|
||||||
assert_includes response.body, "Delete adverts"
|
assert_includes response.body, "Delete stock page"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "admin unlock rejects invalid passwords" do
|
test "admin unlock rejects invalid passwords" do
|
||||||
|
|||||||
Reference in New Issue
Block a user