MAIN - #17280
MAIN#17280niteeshkanna-sh wants to merge 237 commits into
Conversation
|
Hi @niteeshkanna-sh! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
Scaffold a new standalone single-page app alongside the existing project: - Vite 8 with @vitejs/plugin-react, TypeScript, React 19 - Tailwind CSS v4 wired through @tailwindcss/vite - Replace the Vite demo page with a minimal starter component - Pin an inline (empty) PostCSS config so Vite does not walk up and load the parent directory's postcss.config.js, which broke the build - README covering setup, scripts, and layout Verified: npm run lint, npm run build, and npm run dev all pass, and the dev server renders the styled page with working component state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
`npm ci` failed at the install step of every CI job:
npm ERR! `npm ci` can only install packages when your package.json
npm ERR! and package-lock.json are in sync.
npm ERR! Missing: yaml@2.9.1 from lock file
The lockfile predates the current dependency tree: tailwindcss 3.4.19
pulls postcss-load-config, which needs yaml@^2, but the lockfile only
carried the top-level yaml@1.10.2.
Regenerated with `npm install --package-lock-only`. The change adds the
missing nested yaml@2.9.1 and drops some stale `peer` metadata flags; no
dependency version or integrity hash changes.
Verified `npm ci` validates under both npm 10 and npm 8.19.4, the version
the lint workflow pins via Node 16.
This unblocks the install step only. The workflows have further problems
that predate this branch, noted on the pull request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Adds a workflow that builds my-app and publishes it to GitHub Pages, plus the CNAME for the custom domain. The workflow installs with `npm ci`, lints, builds, and uploads my-app/dist as the Pages artifact. It triggers on pushes to main that touch my-app/, and on manual dispatch. Because it is scoped to main, merging is what deploys; nothing publishes from a feature branch. `public/CNAME` is copied verbatim into dist/ by Vite, landing at the site root where Pages looks for it. Vite's default base of '/' is correct here: a custom domain serves from the root, and the built asset paths are root-relative to match. Verified by running the workflow's exact steps locally -- npm ci, npm run lint, npm run build -- and confirming dist/ contains CNAME, index.html with root-relative asset URLs, and the hashed JS/CSS bundles. Deploying still needs two manual steps that cannot be done from here: enabling Pages with the GitHub Actions source, and pointing DNS at GitHub. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Scaffolds my-app/ as a standalone Vite + React + TypeScript + Tailwind SPA, syncs the root package-lock.json with package.json so npm ci works again, and adds a GitHub Pages deploy workflow with a CNAME for niteshacars.in. Merged at the repo owner's request. CI is red from pre-existing failures that reproduce on main and are unrelated to these changes; see the pull request description for the four causes.
Every workflow run in this repo's history has failed, on main as well as on every branch. The cause is commit dc079b1, which replaced the monorepo's root package.json with a create-react-app app's. That dropped the `workspaces` field and the alex, eslint, prettier and test:integration scripts the inherited CI depends on, leaving four distinct failures: lint npm run alex -> Missing script: "alex" Build / build react-scripts build -> no public/index.html Integration Tests npm run test:integration -> Missing script E2E tasks/publish.sh -> No workspaces found Remove build-and-test.yml, e2e-base.yml and lint.yml. These are upstream create-react-app CI: they exercise packages/ through lerna, verdaccio and puppeteer on Node 16. Restoring the old root package.json would not fix them either -- it runs `prettier --list-different` and `eslint .` across the whole repo, now including the uploaded public_html/ tree and my-app/, so it trades four failures for a larger set. Retarget node.js.yml at my-app, the app this repo actively maintains, running install, lint and build. Drop the 18.x matrix leg: Vite 8 requires ^20.19.0 || >=22.12.0, so that leg could never pass. This also closes a real gap. No workflow covered my-app on pull requests -- deploy-my-app.yml only runs on main -- so its code was never built or linted before reaching the default branch. packages/, docusaurus/, tasks/ and test/ are left untouched; only CI changes. Verified by running the workflow's exact steps -- npm ci, npm run lint, npm run build -- from my-app on Node 22. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Removes the inherited upstream create-react-app CI (build-and-test.yml, e2e-base.yml, lint.yml), which exercised packages/ through lerna, verdaccio and puppeteer on Node 16 and could not pass after commit dc079b1 replaced the monorepo root package.json with a create-react-app app's. Retargets node.js.yml at my-app, running install, lint and build on Node 20.x and 22.x. The 18.x leg is dropped because Vite 8 requires ^20.19.0 || >=22.12.0. This also gives my-app its first pull request coverage; deploy-my-app.yml only runs on main, so its code was previously never built or linted before reaching the default branch.
This repository forked create-react-app and carried its entire monorepo, none of which was ever developed here. Commit dc079b1 replaced the root package.json with an app's, dropping the workspaces field, so the monorepo had already stopped being buildable; PR #4 then removed the CI that exercised it. What remained was 116k lines of upstream source that nothing builds, tests, or ships. Removed: packages/ 334 files, 7.7M upstream CRA packages docusaurus/ 59 files, 508K upstream documentation site test/ 78 files, 456K upstream integration tests tasks/ 15 files, 88K upstream release and e2e scripts CHANGELOG*.md 6 files upstream release history CONTRIBUTING.md upstream contributor guide SECURITY.md Create React App disclosure policy screencast*.svg upstream README assets Also removed, because each only configured the directories above and now resolves to nothing: lerna.json monorepo config, no packages left netlify.toml builds docusaurus/website .eslintrc.json, .eslintignore scoped to packages/, test/, docusaurus/ .prettierrc, .prettierignore scoped to packages/, test/ .alexrc, .alexignore alex prose linter, upstream prose only .github/CODEOWNERS assigns packages/ and docusaurus/ to upstream maintainers who do not work here .github/FUNDING.yml upstream funding links .github/ISSUE_TEMPLATE/ templates for reporting CRA bugs .github/lock.yml, stale.yml upstream issue-bot config LICENSE is kept: the fork lineage is real and removing a license is not a cleanup decision. CODE_OF_CONDUCT.md and the pull request template are generic and still apply. README.md described a create-react-app app at the repo root via `npm start`. No such app exists -- the root has no src/ or public/ -- so it was boilerplate for something that could not run. Rewritten to describe what is actually here: my-app and public_html, how each is built and deployed, and this history. Nothing in my-app/ or public_html/ referenced any removed path. Verified with the CI steps -- npm ci, npm run lint, npm run build -- from my-app, and dist/ still contains CNAME, index.html and the hashed bundles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Removes ~116,000 lines of upstream create-react-app source that nothing in this repository builds, tests, or ships: packages/, docusaurus/, test/, tasks/, the changelogs, CONTRIBUTING.md, SECURITY.md and the screencast assets, along with the configs that only scoped to those directories -- lerna.json, netlify.toml, the eslint/prettier/alex configs, and the upstream .github meta files. LICENSE is kept; the fork lineage is real. CODE_OF_CONDUCT.md and the pull request template are generic and still apply. README.md described a create-react-app app at the repo root that does not exist, since the root has no src/ or public/. Rewritten to describe my-app and public_html, how each is built and deployed, and this history. Verified with the CI steps from my-app -- npm ci, npm run lint, npm run build -- and by grepping the tree for references to every removed path.
Replaces the Vite starter with an actual site for the business: hero, filterable fleet, how-it-works, and an enquiry form that posts to the live admin API. The old public site's theme is not in this repository. asset-manifest.json references /static/css/main.0700b630.css and /static/js/main.b3748527.js, and neither file was ever committed. What did survive is the brand token block in public_html/admin.niteshacars.in/admin/admin.css, whose comments name the colours outright -- navy #0A0E20, gold #F5A500, Poppins, 14px radius, warm #FAF8F4 ground. Those are carried into @theme in index.css, so the public site and the admin panel now share one palette. The fleet in src/data/cars.ts is placeholder data shaped to the real vehicles and vehicle_rates tables: body type, fuel, transmission, seats, model year, daily/weekly/monthly rates, KM limit, extra-KM rate and deposit. Editing that file is all it takes to put the real cars up. The enquiry form is wired to the real endpoint rather than mocked. api/enquiry-submit.php needs no sign-in and config.sample.php already allowlists https://niteshacars.in for CORS, so submissions land in the admin panel. It sends the honeypot field the endpoint expects, surfaces per-field errors from the API, and shows the returned enquiry number on success. vehicle_id is deliberately not sent. The endpoint validates it against the vehicles table and the placeholder ids do not exist there, so sending one would be rejected; the chosen car goes in `requirements` as free text instead. Once cars.ts carries real ids the vehicle can be linked properly. Live inventory is not fetched because api/vehicles.php calls api_guard('vehicle.view'), which a public page cannot satisfy. Images reuse the two web-sized files already in public_html: background car.webp (8.4K) and brezza1.avif (27K). The two PNGs there are 876K and 3.5M, too heavy to ship without processing. Verified with npm ci, npm run lint and npm run build, then driven in a real browser: six cards render, the SUV filter narrows to two, and clicking a card's enquire button preselects that car in the form. The only console error is the Google Fonts request, which this sandbox's proxy blocks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Replaces the Vite starter with a site for the business: hero, filterable fleet, how-it-works, and an enquiry form posting to the live admin API. The theme is rebuilt from the brand tokens in the admin panel's admin.css -- navy #0A0E20, gold #F5A500, Poppins, 14px radius -- because the old public site's compiled CSS and JS were never committed to this repository. The fleet in src/data/cars.ts is placeholder data shaped to the real vehicles and vehicle_rates tables; editing that file puts the real cars up. Live inventory cannot be fetched because api/vehicles.php requires authentication. Verified with lint and build on Node 20 and 22, and driven in a browser.
public_html/admin.niteshacars.in/admin/config.php was committed with real values: a database name, user and password that are in use. This repository is a fork of facebook/create-react-app and is public, so those credentials are readable by anyone who finds it. Untracks the file and adds it to .gitignore. config.sample.php is the template to copy on a new server, and its values were checked against the live ones -- they differ, so the sample leaks nothing. The working copy is left in place; `git rm --cached` only stops tracking. The copy on the server is untouched, and the panel keeps running. This does NOT remove the file from git history. Every commit that carried it still does, so the credentials remain readable to anyone who clones. Rotating the database password is what actually closes this; that has to happen in the hosting panel. Scanned the rest of public_html for other committed secrets. Only two other files matched a credential-shaped pattern, and neither is one: config.sample.php holds example values, and tools/test-auth.php uses a fixed throwaway password for a test user it creates and deletes in the same run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
config.php was committed with a real database name, user and password. This repository is public, so those values were readable by anyone who found it. Untracks the file and adds it to .gitignore. The working copy and the server's copy are both untouched, so the admin panel keeps running. This does not remove the file from git history; rotating the database password in the hosting panel is what actually closes the exposure.
Replaces the placeholders the site shipped with -- +91 90000 00000 and hello@niteshacars.in -- which were visible to customers on the live domain. Four places: the header's Call us button, and the footer's phone and email, each as both the visible text and the tel:/mailto: href. The tel: href carries no spaces (+916374942976) so phone dialers parse it, while the visible text keeps the readable +91 63749 42976 grouping. Verified in a browser: all four resolve to the new values, and a grep over my-app confirms no placeholder survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Replaces the placeholders the site shipped with -- +91 90000 00000 and hello@niteshacars.in -- which were visible to customers on the live domain. Four places: the header's Call us button, and the footer's phone and email, each as both the visible text and the tel:/mailto: href. The tel: href carries no spaces so phone dialers parse it, while the visible text keeps the readable +91 63749 42976 grouping. Verified in a browser, and by grepping my-app for surviving placeholders.
The site shipped with six invented vehicles -- a Brezza, Swift, City, Innova,
Baleno and Nexon EV -- with invented rates, KM limits and deposits. On a live
domain that is worse than showing nothing: a customer could read a price for a
car that does not exist and call about it.
cars.ts is now an empty array with a commented template for one entry. Adding
the real fleet is filling it back in; nothing else has to change.
The three places that read the array each handle it being empty, so the page
stays coherent rather than rendering a blank grid:
Fleet shows a card inviting an enquiry or a call, and hides the
body-type filters, which would otherwise be a row of controls
that filter nothing
Enquiry hides the car picker, and relabels the free-text field to
"What kind of car do you need?" so the visitor can say what the
dropdown would have asked
Header unchanged; it never read the fleet
Verified in a browser: no placeholder name appears anywhere on the page, zero
cards and zero filters render, the empty-state card shows, the car picker is
gone, the enquiry form still works, and there are no page errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Replaces the single scrolling page with six routes matching the old site's menu: Home, About Us, Our Cars, Tariff, Blog, Contact, plus a 404. / Home hero, how it works, a closing call to action /about About Us what hiring involves; deliberately generic copy /cars Our Cars the fleet listing /tariff Tariff a rate table built from the same fleet data /blog Blog post list, currently empty /contact Contact phone and email cards, and the enquiry form * 404 a real page rather than a blank screen Uses react-router-dom. The nav marks the current page with a gold dot above it, matching the menu on the old site, and collapses to a button-toggled list below the lg breakpoint so the six items do not crowd a phone. Two components were written for one page and had to be rewired. Fleet took an onEnquire callback to scroll down to the form; it now links to /contact?car=<name>, and Enquiry reads that query parameter, so choosing a car survives the navigation between pages. Neither takes props any more. Tariff and Blog handle being empty the same way the fleet listing does: a card that invites a call or an enquiry, rather than an empty table or an empty list. About and Blog carry TODO comments -- their copy states only what the booking flow actually does, because inventing a founding year, a fleet size or posts would put untrue claims on a live site. scripts/spa-fallback.mjs copies dist/index.html to dist/404.html after every build. GitHub Pages serves static files and knows nothing about client-side routes, so without it a direct visit to /about, or a refresh on /cars, returns Pages' own 404 and the app never boots. Pages does serve 404.html for anything it cannot match, so an exact copy loads the app and the router resolves the URL. Copied rather than symlinked because the Pages artifact upload does not follow symlinks, and written in Node so the build still works on Windows. ScrollToTop resets scroll on navigation, since a client-side route change otherwise keeps the previous scroll position and opens a page halfway down. Hash links are left alone so #anchors still work. Verified in a browser: all seven routes render the right heading, clicking a nav item changes the URL, /contact?car= is handled with the fleet empty, the enquiry form is present, the mobile menu opens with all seven links, and no page errors are raised. 404.html is byte-identical to index.html. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
) Two changes shipped together. The six invented vehicles are gone. cars.ts is now an empty array with a commented template; everything reading it handles being empty, so no page renders a hole. Adding the real fleet is filling the array back in. The single scrolling page becomes six routes matching the old site's menu -- Home, About Us, Our Cars, Tariff, Blog, Contact -- plus a 404 page. The nav marks the current page with a gold dot and collapses to a toggle on phones. Fleet and Enquiry were written for one page; a car chosen on the fleet page now travels as /contact?car=<name> so the choice survives the navigation. scripts/spa-fallback.mjs copies dist/index.html to dist/404.html after every build. GitHub Pages knows nothing about client-side routes, so without it a direct visit to /about or a refresh on /cars returns Pages' own 404 and the app never boots. About and Blog carry TODO comments: their copy states only what the booking flow actually does, rather than inventing a founding year or posts.
Two pieces: a public read-only endpoint so the panel becomes the single place the fleet is managed, and the SEO work needed for a client-rendered site on a static host. ## The endpoint api/public-vehicles.php returns vehicles whose status is Available, with rates read from the dated rate card the same way the admin reads them, so a price scheduled for next month does not leak out early. Adding a car in the panel puts it on the site; setting one to Maintenance takes it off. It deliberately omits columns the admin's own endpoint returns: reg_number, a plate is not the public's business; current_km, which reveals how hard a car has been worked; created_by and created_at, internal; colour, a swatch for the panel's UI. A vehicle with no rate card is skipped rather than listed with a blank price, since that would invite an enquiry nobody can answer. CORS uses the existing public_site_origin allowlist, echoed rather than wildcarded, matching enquiry-submit.php. GET only. No session, no writes. The site falls back to src/data/cars.ts when the request fails -- offline, a CORS rejection, or the endpoint not yet uploaded. That file is empty, so the page degrades to its "ask us what's available" state instead of an error. ## SEO The app renders in the browser, so every route was served the same index.html carrying the home page's title. Google runs JavaScript and would eventually see the right tags, but WhatsApp, Facebook and X do not run it at all -- they read the HTML as served and stop. Sharing niteshacars.in/tariff previewed as the home page. scripts/prerender-seo.mjs now writes a directory per route with that route's title, description, canonical and og: tags baked in. Pages serves /tariff/ from /tariff/index.html. dist/404.html keeps the root page's tags, since it is what Pages serves for unmatched paths. src/data/seo.json is the single source for those strings and is also read by src/lib/useSeo.ts, which updates the same tags on client-side navigation -- the app never re-requests HTML after boot, so without it the title would stay on whichever page loaded first. Also added: sitemap.xml generated from the same file, robots.txt pointing at it, and AutoRental structured data in index.html carrying the business name, phone and email, which is what local search results are built from. This replaces scripts/spa-fallback.mjs; the 404 copy now happens here. Verified: php -l passes on the endpoint; all six routes build with their own title, canonical and og:url; 404.html carries the root tags; the sitemap lists every route. In a browser with the API unreachable, the listing falls back to the empty state rather than hanging, the title changes on navigation, and no page errors are raised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
api/public-vehicles.php returns Available vehicles with rates from the dated rate card, so adding a car in the admin panel puts it on the site. It omits registration numbers, current KM and internal columns, and skips vehicles with no rate card rather than listing a blank price. CORS reuses the existing public_site_origin allowlist. The site falls back to an empty cars.ts when the request fails, so it degrades rather than erroring. For SEO, every route was served the same index.html carrying the home page's title. Google runs JavaScript, but WhatsApp, Facebook and X do not -- sharing /tariff previewed as the home page. The build now writes a directory per route with its own title, description, canonical and og: tags, plus sitemap.xml, robots.txt and AutoRental structured data. The endpoint still has to be uploaded to the server; the repo is not deployed to Hostinger.
The metadata described the service generically, so it competed with every self-drive rental in India and matched none of the searches that actually convert. Someone looking for a car does not search "self-drive car hire" -- they search "car rental Nagercoil". Titles and descriptions now name the district and its towns, kept inside the lengths search results truncate at: every title is 42-53 characters against a ~60 limit, and every description 107-150 against ~155, so none is cut mid sentence. The structured data declares the service area rather than only the business: areaServed is Kanyakumari district within Tamil Nadu, and the address carries Nagercoil and Tamil Nadu. No street address or coordinates are invented -- those have to be real to be worth anything, and a wrong one is worse than none. Local ranking also reads the visible page, not just the head. AreasServed lists the twelve main towns as text on the home and contact pages, so a search for "car rental Marthandam" lands on a page that says Marthandam. The hero now names the district and four largest towns in its opening paragraph. Verified in a browser: all six towns checked appear on the home page, the title is the district one, and the JSON-LD parses with areaServed "Kanyakumari district", locality Nagercoil, region Tamil Nadu. No page errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Search Console shows the site at position 3.2 for its main query, with clicks down 47% over 28 days. The indexed listing explains part of why: it advertises "NiteSha Cars & Bikes ... premium cars, wedding rentals, and tourist vehicle services", and the rebuilt site mentioned none of that. Replacing a page that ranks with one covering less of what the business does is how a ranking is lost, and the click drop may already be that. Three services now have their own pages, because they are separate searches and a page can only rank for what it is about. Somebody hiring a scooter is not the person booking a wedding car. /bikes two-wheelers, hourly to weekly /wedding-cars decorated cars, reserved dates, vehicles for the family /tourist-vehicles cars and vans with a driver, sightseeing and temple tours The brand was wrong throughout: the site said "Nitesha Cars", the indexed listing and the business are "NiteSha Cars & Bikes". Inconsistent naming costs local ranking directly, so it now comes from seo.json wherever it appears. Titles now lead with Nagercoil rather than Kanyakumari. The district name is what people add after the town, not before it, and the competitor ranking above us leads with Nagercoil too. All nine titles are 40-51 characters and all nine descriptions 107-152, inside where results truncate. The home h1 was "Take the wheel. We'll handle the rest." -- the strongest on-page heading, carrying no keyword at all. It now reads "Self drive car & bike rental in Nagercoil and across Kanyakumari district", with the slogan kept as a tagline beneath it. Nine nav items do not fit a desktop row, so the four services sit behind one Services trigger, which keeps the six-item menu shape the old site had. Each service keeps its own route. The mobile menu lists all nine flat. Menus now close from the click that navigates rather than an effect watching the path, which oxlint flagged as cascading an extra render. Verified in a browser: all nine routes render their own h1, the brand appears in the header, the dropdown holds four items and closes after navigating, and the home page links to each service two or three times over -- nav, card grid and footer. No page errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Motion throughout: the hero staggers in, sections and cards fade up as they are scrolled to, each route change fades the new page in, menus drop open, and cards lift on hover. Built on CSS keyframes and one IntersectionObserver rather than a motion library. Framer Motion is around 34 kB gzipped; this is 0.43 kB, measured against the previous build. That matters here specifically: the site competes on local search, Core Web Vitals feed that, and most visitors arrive on a mid-range phone over mobile data. Only opacity and transform are animated. Both are composited, so no frame triggers layout or paint -- animating height, top or margin is what makes a site judder on the hardware most people actually have. translate3d and scale3d keep the work on the GPU. The reveal uses an IntersectionObserver, not a scroll listener: intersections are reported off the main thread, where a scroll handler would run on every frame of every scroll. Each element unobserves after firing, so content animates once rather than re-animating whenever it passes the viewport again. Route transitions need main to be keyed on the pathname. Without the key React reuses the DOM node, no mount happens, and the animation never replays. prefers-reduced-motion is handled in both directions. The whole motion block is inside a no-preference query, and a reduce query resets [data-reveal] to full opacity -- without that second rule the reveal would leave every section permanently invisible for anyone who has asked their system for less motion, which is worse than having no animation at all. AreasServed is rewritten rather than patched: wrapping its list items left the JSX unbalanced, and the structure reads better with the chip as a span inside the revealed li. Verified in a browser: a below-fold section measures opacity 0 before scroll and 1 after, the hero is already at full opacity on load, an animationstart event for fade-up fires on main at every route change, and under reduced motion zero elements are left invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Kanyakumari district sends a lot of people to the Gulf, Singapore and Malaysia, and they come back for weddings, the December holidays and family occasions. Their problems are not a local customer's: they book months ahead from another country, land at an airport in a different state, and usually need a vehicle for weeks rather than days. None of that was addressed anywhere on the site. /nri covers the things they actually ask about -- arranging a vehicle before flying, being met at Trivandrum, which licence works, and dropping the car back on the way out. Trivandrum is named specifically because it is the nearest international airport to Nagercoil and where most Gulf and Singapore flights land, and because "car rental Trivandrum airport Nagercoil" is a search somebody makes. WhatsApp is the primary call to action on this page, per the owner. It costs nothing from abroad and survives the time difference, where a phone call to India from the Gulf does neither. The link is wa.me with the country code and no punctuation, which is what the format requires -- a space or a leading plus fails silently. It carries a prefilled message with blanks for arrival and return dates, so the first message already has the information we need. The two offers the owner chose, a long-stay discount and airport pickup, are described without figures. No percentage, no rate, no "free". They have not set the terms, and a number on a live page is a promise a customer can hold them to; inventing one would commit their money. The copy says the daily rate comes down for longer hires and to ask for the rate, which is true and still converts. A test asserts no percentage or rupee figure appears on the page. The licence guidance is the part most worth getting right, since being turned away at the counter after a night flight is the failure people fear: an Indian licence works if unexpired, a foreign one needs an International Driving Permit alongside, passport and visa or OCI as photo ID, and send a photo ahead so it is confirmed before travelling rather than on arrival. The services grid now holds five cards and moves to a 3-then-5 column layout so none is orphaned on its own row. Verified in a browser: the page renders with its own title, the WhatsApp link resolves to wa.me/916374942976 with the prefilled text and opens in a new tab with noopener, Trivandrum, the IDP rule, long-stay and weddings are all mentioned, no invented figure appears anywhere, and the home page links to it twice. No page errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
"Monthly car rental Nagercoil" is a different search from "self drive car rental Nagercoil", and the competitor ranking above us has a page for it while we had nothing. A page can only rank for what it is about. Their version is a section: a paragraph, four bullets on what affects the rate, and a button. This goes further, because the way to outrank a thin page is to answer what it leaves out. Six questions somebody actually has before handing over a month of hire -- how the KM allowance works across a month rather than a day, who services the car while they have it, what happens on a breakdown, whether they can extend, what deposit is held, what to bring -- and four audiences, since a family back from the Gulf and someone on a work posting arrive with different worries. Their page does confirm one thing: it quotes no price either, only what affects the rate. So the no-figures approach is not a handicap against the site currently ranking first. This page lists five factors and offers a same-day quote. Both spellings of the vehicle class are present. The page said "seven-seater", but the search is typed "7 seater" -- the numeral now appears here and on the fleet page, where somebody looking for one would land. Monthly is linked from the NRI page's long-stay step, which is the natural path: a visitor reading about staying for weeks is the person who wants this. Verified in a browser: the page renders with its own h1 and title, mentions 7 seater, NRI, work postings, extra-KM, deposits and servicing, carries six Q&A entries, and contains no invented figure. All eleven routes still render an h1. No page errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Monthly hire, NRI visitors and weddings were reachable only through the nav or a small card in the services grid, where they read as four equal options. They are not equal: a month-long hire, a family flying in for a season and a wedding booking are each worth many times a weekend rental, and they are the bookings worth putting in front of someone before they leave the page. Each is now a band on navy rather than the page's sand, so it reads as an offer block instead of more body copy. The layout follows what works on the competitor's monthly section -- icon, heading, a gold line naming the question in the visitor's head, a paragraph, and a panel of four checked points ending in a call to action -- because it is a good pattern, not because it is theirs. Ours carries three of these where they have one. Still no figures anywhere in the band. The points say what decides the rate, not what the rate is; a test asserts no percentage or rupee figure appears. Verified in a browser: three blocks render, each call to action points at its own route and navigates, the band contains no invented price, and at 390px the blocks stack with no horizontal overflow. No page errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
The layout pattern only: two overlapping photographs with a circular badge across them, and beside it a pill label, a heading, a paragraph and numbered accordion rows. The content is ours. Nobody's founder story is borrowed, and nothing is claimed about the fleet that has not been established. The three rows say what is actually true and worth knowing before hiring: the rate, KM allowance, extra-KM rate and deposit are all stated before the vehicle is handed over; we cover the whole district including airport delivery; and cars, bikes, wedding vehicles and drivers come from one place rather than four. The circular badge is an SVG textPath around a circle, not letters rotated individually with transforms -- that approach drifts at different font sizes and falls apart when the font falls back. Only the svg spins, so the arrow in the middle stays upright, and it stops under prefers-reduced-motion. The accordion is buttons with aria-expanded rather than details and summary, because only one row should be open at a time and native details has no notion of a group. Clicking an open row closes it, so the section can be collapsed entirely. Reuses the two web-sized images already in the project. No new assets: the remaining photographs in public_html are 876K and 3.5M, too heavy to ship. Verified in a browser: three rows, the first open at load, opening the second closes the first, its panel becomes visible, clicking again closes it, the badge links to /contact, both images render, and at 390px there is no horizontal overflow. No page errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
The real logo, and a walk over every page
The footer signs off with the full lockup, on the navy directly. The white plate that was there is gone: it was written for a mark that might arrive with a dark background of its own, and this one has "CARS & BIKES" set in white, which the plate would have swallowed whole. The name is not written out beside it any more. The lockup says it, and the footer was the one place on the page where the same words appeared twice at two different sizes. It is still the picture's alt text, because a screen reader cannot read a picture -- which is the opposite of the header, where the name is in live text and the mark is hidden from it. The panel gets the emblem, in the sidebar and on the sign-in card, in place of a drawn car in a circle that was standing in for exactly this. The emblem rather than the full lockup in both: the sign-in card is white, and the white line in the lockup would vanish on it. Its own copy of the file rather than one from the site root, so the panel still works if it is ever installed on its own. Tested: 54 brand checks, now covering the footer's lockup and the panel's emblem -- that each loaded rather than merely being referenced, which file is where, the alt text on the one that needs it and the aria-hidden on the one that does not -- plus the full-site walk, which flags nothing, and the existing suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
The logo in the footer and in the panel
It carried the mark plus PREMIUM RENTALS over the business name in live text. That was right while there was no logo -- the words were the logo. It stopped being right the moment a real one arrived, because this one has the name drawn into it, so the header was saying "NiteSha Cars & Bikes" twice: once as artwork and once as type, in two faces at two sizes. The words are gone. The mark is the full lockup now rather than the emblem, precisely because they are: with nothing beside it, the mark has to carry the name itself, and it is set large enough that it does. That makes the header taller than a wordmark would, which is the cost of a logo with its name drawn in. It is alt text rather than aria-hidden for the same reason. There is no longer a written name for a screen reader to find, so the picture has to supply it -- the opposite of what was right an hour ago. Chromium's own accessibility tree says the link is announced as "NiteSha Cars & Bikes", and that none of the fifty-two links on the home page is unnamed. The footer already had the lockup alone; nothing there to remove. The monogram and the old wordmark stay as the fallback for a site with no logo at all, which is what this was before the file arrived. Three checks needed correcting rather than the code. One counted a link with no text as unnamed, which made the logo link look like a defect on every page -- a link whose only content is an image takes its name from that image's alt, and a false positive on every page is how a whole audit gets ignored. One measured the header on a narrow phone by counting the lines in a name that no longer exists, and measured its clearance against the Services dropdown rather than the menu button, which gave a negative gap. And one treated the logo appearing in both the header and the footer as a picture used twice, which is what a logo is for; it looks for a repeated photograph now, and separately checks the logo is in both places. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
The header is the logo, and only the logo
"Some buttons not taking to the pages": on the dashboard, Inquiries, All bookings and Finance work by setting location.hash. On a page that is already open that is a same-document navigation -- the browser changes the address bar and fires hashchange, and that is all. Nothing listened for it. shell.js read the hash once, at DOMContentLoaded, so the three buttons moved the address bar and left the Dashboard panel on screen. So the hash is now read on every change, not only on arrival, which also gives the back button something to do and makes any #panel link on the page work. An empty hash means the Dashboard, so backing out of #finance comes home instead of sitting there with the address bar disagreeing. The other half: the Inquiries button carried data-quick="enquiries" while the tab it wanted is called "inquiries", so even a working router would have found no such panel. And the tab now writes itself into the address bar -- with replaceState, not by assigning the hash, which would bounce straight back through the new listener and put a history entry behind every tab anyone glanced at. A refresh stays on the panel you were on, and a tab can be linked to. tools/test-nav-ui.js covers all of it: the sidebar, both kinds of quick action, a link straight to a panel, the address bar following the tab, a refresh, and the back button. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
The dashboard's quick actions actually go to the page now
"Save Booking" answered "The server returned an unreadable response." The endpoint had crashed: creating the customer wrote NULL into customer_type, which is NOT NULL with a default of 'New', so MySQL refused the insert and took the whole request with it. find_or_create_customer() wrote every optional column whether or not it had been given one, turning "no answer" into NULL. Leaving a column out is not the same thing: left out, it takes its own default. So it now writes only the columns it actually has values for. An enquiry has no customer type to send and should not have to invent one, which is why this only ever bit the Accept & Create Booking path. The unreadable response is the second half. A PHP fatal ends a request with an empty body, a warning prints ahead of the JSON, and either way the panel has nothing it can parse and says so -- while the message naming the real fault goes to a log nobody reads. Endpoints under api/ now answer JSON whatever happens to them: errors never reach the body, the detail is written to nitesha-storage/logs/panel-errors.log with a short reference, and the browser shows a message carrying that reference. tools/test-api-failure.php covers an exception, a fatal, and a notice ahead of a good reply. The suites found it, once they could run. test-ui.js had one machine's account and scratch directory written into it, so it never ran anywhere else; it takes them from the command line now, like its siblings, and looks for the signed-in name under .ns-who, which is where the redesigned shell puts it. The four browser suites also stop counting the web font's certificate as a failure -- it is fetched from Google and refused by any sandbox that proxies HTTPS with its own certificate. Run against a real MySQL, panel at the document root: 332 checks across the API, the ledger, expenses, enquiries, and the booking, enquiry, finance and navigation flows in a browser, plus 46 money checks and the 7 new ones. All pass; reverting the one-line cause turns test-enquiries.sh red again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Saving a booking from an inquiry no longer dies at the server
The panel has no way to delete a booking, and clearing test records by hand means working out what a booking is attached to: charges, payments, the deposit and its refund, the KM readings, extras, damages, files, reminders, approvals and documents all point at it, and the database refuses to let it go while any of them do. tools/tidy-records.sql does that in one pass, then renumbers what is left so the numbering starts at 1 again, and sets the counters to match -- without which the next booking is handed a number already in use and the save is refused. It will not act on a list that does not find what it says it will. A mistyped number would otherwise put every record in the delete set, so the lists are counted first and every statement asks that flag before it runs; the script reports that it stopped and changes nothing. Running it a second time is the same no-op, since by then the numbers it names are gone. Money and history are left alone: an expense keeps its amount and loses the booking, the audit trail keeps rows for records that no longer exist, and customers stay. The lines that would delete those instead are in the file, marked, and commented out. Checked against a real MySQL with five bookings and six enquiries carrying a full set of attached records: the right one survives and is renumbered, the kept inquiry keeps its link to it, one child row of each kind remains, the panel loads every tab with no errors, and the next booking and inquiry come out as 0002 and 0003. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
A script for keeping a few records and clearing the rest
Both were lists of cards that opened a long modal. They are now the shape of the ERP the owner asked them to look like: the counts across the top, the filters under them, a sortable table, and a record that opens in place of the list with its sections behind tabs. The counts, the chips and the rows all come from one fetch of the whole list, filtered in the browser. That is what lets a chip carry its own number -- a count taken from a list the server has already narrowed is a count of what is on screen, which is the one number nobody needs -- and it makes every chip and sort instant. A rental business has hundreds of bookings a year, not millions. Sorting is by any column, and clicking the one you are already sorted by turns it round. Paging is 10 to 100 a page. Search covers the number, the customer, the phone and the vehicle. A record was a modal because it started small. It carries payments, a deposit and its refunds, two odometer readings, documents, damage and a timeline, which is a page's worth of material: in a dialog it was a long scroll with the list stranded behind it. It now replaces the list, leads with a header carrying the number, both statuses -- the booking's and the money's -- and five figures, and puts the rest behind six tabs. Which tab you were on survives recording a payment, so you are not thrown back to Overview every time you do something. On a phone the columns a record opens with are dropped and the number rides under the name, so the table fits 390px with no sideways scroll. The layout follows the ERP; the colours do not. This is the navy and gold the panel and the website are built from, and a second palette inside one product reads as a bug. tools/test-records-ui.js is new and covers the list and the record: counts against the pager's total, a chip's count against the rows it shows, search, sorting and turning it round, per-page, Next and Back, the row numbers restarting, opening a record and coming back, both action buttons, and the unread mark clearing. 34 checks. The booking and enquiry suites follow the screens into their tabs. Run against a real MySQL: 366 checks across the API, the ledger, expenses, enquiries and six browser suites, plus 46 money checks and 7 on failures. All pass, no JS errors, nothing overflows at 390px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Bookings and Inquiries as a table, and a record you can open
A bin in the action column of both lists, and on the record itself, behind a dialog that says what is about to go. Deleting a booking now removes everything it works out. Its charges, the payments against it, the deposit and any refunds, both odometer readings, the extras, the damage notes, the attachments, the documents and the approvals raised for any of them -- so Finance and every report stop counting a booking that is not there. Before this it deleted six tables of the eleven, which the database would have refused anyway: an enquiry or an expense pointing at the booking is a foreign key, and the delete would have died halfway with a 500. Two things are kept on purpose. An expense is money that really left the business, so it keeps its amount and loses the booking; deleting it would quietly make the business look more profitable than it was. And the audit trail keeps its rows, now recording what the booking held when it went. Three other things follow the booking out: the enquiry it came from goes back to Accepted rather than staying Converted into something that no longer exists, a car left On Rental by a pickup that has just been deleted is freed, and the "only a cancelled booking can be deleted" rule is gone -- the dialog does that job better than an error after the fact. The dialog replaces confirm(). One line of unstyled browser text is not consent for something that removes payments: this names the record, lists what goes with it and what stays, and focuses Cancel, so the reflex press of enter on a dialog that just appeared deletes nothing. Escape and the backdrop both cancel. An enquiry that became a booking still cannot be deleted, and now the two rules compose: delete the booking and the enquiry goes back to Accepted, after which it can go too. Checked against a real MySQL: a booking with a payment, a deposit, readings, extras, damage, files, documents, approvals, a reminder, an expense and the enquiry it came from -- all of it gone or unlinked as described, and the income figure drops by exactly the payment it held. In the browser: the bin asks first, Escape and Cancel change nothing, confirming removes the row and the counts above the table agree. 46 checks in the records suite, 31 in the bookings suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Delete a booking or an inquiry, and take its arithmetic with it
The phone box was the width of the card for a value that is ten digits long. That room goes to a vehicle instead: two fifths for the number, three for a dropdown of what is actually on the fleet. It is the one question worth asking on a banner that was not being asked. Somebody who picks a car has told us the thing that decides the call back -- it turns "what would you like?" into "yes, that one is free on those dates" -- and it costs them a tap on a list that is already loaded for the page behind it. The choice travels both ways: the real vehicle id, so the panel shows the inquiry against the vehicle and counts it there rather than leaving the Asked-for column empty, and the name in the notes, which still reads correctly years later when that vehicle has been sold. The contact form sends the id now too; it had the vehicle in hand for the availability grid and was passing only the name. An id the panel does not recognise is ignored rather than refused, which is what makes this safe when the fleet is served from the file of placeholders after a failed request. In that case the dropdown does not appear at all -- an empty list asks a question with no answers -- and the number keeps the whole row. Checked against a real panel and database: the list is the live fleet, the boxes sit on one row at 135px and 208px on a laptop and 123px and 189px on a 390px phone with nothing overflowing, and a card filled in and sent arrives as an inquiry carrying vehicle_id 1 and "Vehicle of interest: Maruti Swift VXi". The home page suites still pass -- 71 on the banner, 16 on the floating button, 35 on image addresses, and the sixteen-page walk flags nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
A vehicle beside the number in the banner card
The logo slot in Website content cropped every upload square. That is from when the header showed a small mark beside the business name set in type -- a square plate was the right shape for it. The header has shown the full lockup and nothing else since the owner sent one, and a square frame around a wide lockup cuts the words off the sides of it: upload the real logo and what the site shows is "NiteSha CARS", with "& BIKES" and the line under it outside the crop. A logo is artwork whose margins are already decided by whoever drew it, so it is not framed at all now. Choosing one uploads it whole -- scaled down to fit 1280x800 if it is larger, never up, with the transparency kept because the header it sits on is navy. The slot says "Kept whole", shows the picture on its own shape, and offers no Edit button, there being nothing to frame. Every other slot still frames: a page banner has to be wide. Removing one was already there and stays there, which is what this leaves the owner to do once: Website content, the bin on the Logo slot, and the header falls back to the lockup that ships with the site. Also, while looking at why the panel logged ERR_TOO_MANY_REDIRECTS: index.php and logout.php sent a relative "Location: dashboard.php". Asked for a path the server has not been told about -- /site-images/logo.webp on a host without the rewrite -- the browser resolves that against the address it asked for, gets /site-images/dashboard.php, is told the same thing again, and goes round until it gives up. They now redirect from the panel's own root, wherever the panel is served from. tools/test-brand-ui.js is new: a 3:1 image uploaded to the logo slot comes back 3:1 rather than square, no frame is offered for it, a banner still frames, and the bin empties the slot. 13 checks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
The logo is not a badge, so stop framing it like one
The note above the pictures still told the owner what looks best "for the badge", and the file still described the slot as one. Neither is what it is any more, and the note now says the part that matters: the logo is kept as it was drawn, the banners are framed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Say "logo" where the panel used to say "badge"
They were stacked: the form's own button across the card, and WhatsApp in green across the card under it. Two full-width buttons in a column read as steps -- do this, then that -- and these are alternatives. Somebody who uses WhatsApp is not going to fill the form in first. So: one row, WhatsApp on the left in its own green, Check availability on the right where the thumb ends up. The label drops to "WhatsApp", which is what the green and the mark already say, and both fit on one line at every width down to 360px -- 141px each there, nothing clipped and nothing wrapped. The card is a row shorter for it, which is the part that matters on the screen it sits on. Measured at 1440, 1024, 768, 390 and 360: same row, WhatsApp left, neither label broken. The banner suite follows the change -- 72 checks -- and the floating-button, image-address, contrast and page-walk suites are unchanged and still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
The two ways to ask, side by side
The favicon was a navy disc with a thin gold ring and a drawn monogram in it. In a row of tabs that reads as a dark smudge: at sixteen pixels an icon has one colour, and the one this business has is gold. So the field is the gold now and the mark is the navy. The gold is the logo's rather than the site's flat token -- a light edge, a saturated middle, a dark foot, which is what makes the artwork look like metal -- and the letters lean forward the way the logo's do, with the sweep from the mark above them. At sixteen pixels the lean is most of what makes it recognisable as the one on the cars. Two things that were missing: iOS ignores an SVG icon entirely, so a phone that kept this site on its home screen drew a screenshot of the page. apple-touch-icon.png is the same mark, full bleed because iOS rounds the corners itself. And theme-color paints the browser's own furniture navy on a phone, so the top of the screen matches the header under it. The build now refuses an icon with two hyphens inside a comment. XML forbids it where HTML does not, the browser then draws nothing at all rather than complaining, and it is easy to type in a comment explaining a decision -- this file did exactly that, and the icon was invisible until it was measured. The check names the file, the line and the fix. Checked on the served site: both icons decode, the tab icon is 63% gold and 14% navy, the home-screen icon 83% and 14%. The page suites are unchanged -- banner 72, floating button 16, image addresses 35, and the sixteen-page walk flags nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
A gold tab icon, drawn from the logo
The owner changed the tab icon and still had the purple bolt this project shipped with, months after it stopped being referenced anywhere. Nothing was wrong with the file: /favicon.svg is the same address it has always been, and a browser caches a favicon by its address and holds it far harder than it holds a page. Chrome reads a new one when its own store forgets, which can be months, and an ordinary reload does not hurry it. So the prerender stamps both icons with a hash of the file: the address changes when the picture does, nothing cached can match it, and nobody has to know to clear anything. Every route and the SPA fallback are written from the same template, so all 26 carry it. Checked on the served site: the stamped addresses answer 200 and decode, the tab icon 63% gold and the home-screen one 83%, and the page walk and banner suite are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128YzbhrfGdegUSc9RrARRf
Address the icons by a hash of themselves
No description provided.