# SHELL_REPORT.md — §13.2 step 1 (Shell) + brand layer

**Spec:** NOVACARES_REDESIGN_SPEC.md **v3.2** · **Brand:** NOVACares Brand Package **v1.0**
**Built:** 4 Aug 2026 · **Target:** `/addons/novacares3/`, docroot `…/novacares3/public` (dev.novacares.my)
**Production:** not touched. No file under `/addons/novacares` was opened for writing at any point.

---

## 1. What was built

### Directory layout (§2 Directory layout, §12.9)
```
novacares3/
├── app/        code            ─┐
├── config/     secrets          │ above the docroot,
├── docs/brand/ brand package    │ each with a deny-all .htaccess
├── setup/      migrations+tools │ (defence in depth — see §3 finding 13)
├── storage/    uploads/logs/…  ─┘
└── public/     ← THE ONLY web-servable folder
```
Every path resolves from a single **`APP_ROOT`** constant derived from
`app/lib/bootstrap.php`'s own location. Verified: zero occurrences of
`novacares3`, `dev`, or `/home/…` in code (comments excluded). The tree copies
to `novacares-prod/` at cutover unmodified.

### Brand layer (§2 Branding, §5)
| Item | Status |
|---|---|
| `docs/brand/` | Guidelines (PDF+DOCX), Copy Library, brand token CSS, package README, **full logo kit** (34 files) + kit ReadMe/audit |
| `public/assets/css/tokens.css` | Supplied file, **two values corrected** (§2 below) — the single source of truth |
| `public/assets/img/logo/` | Horizontal, Horizontal_Reversed, Primary_NoTagline(+Reversed), Symbol (SVG + PNG), favicons 32/180/512 |
| `public/assets/fonts/` | `@font-face` wired; woff2 files pending (§5 open questions) |
| **Legacy crest removed** | `nova-logo*.png` (MRSM Langkawi + MARA crest) **deleted** from this tree — §2 Identity/§5.4 forbid it, and the brand README forbids mixing NOVACares with the student crest |

### Design system (§5)
`nova.css` rewritten from scratch: **every declaration references a `var(--…)`**.
Components built per §5.3 — card (module/story), notice-banner (info/action/
external), status-badge, form field set, button (primary/secondary/external ↗),
empty-state, page header (both variants), share-kit block, callout.
Two visual languages as body classes: `.t-editorial` (720 px) / `.t-app` (960 px),
nav 1140 px. Mobile-first from 360 px; 44 px tap targets.
Enforcement: **`setup/check-tokens.sh`** — adversarially probed, catches 3/4/6/8-digit
hex, `rgb()/rgba()/hsl()/hsla()`, `font-family:` and the `font:` shorthand,
`border-radius` plus all four longhand corners, and colours inside PWA manifests.

### IA, nav, templates (§4–5)
Five nav items, capped structurally (Home · Stories · Apps · Community ·
**About NOVACares**). Home is the **v3.2 four-slot layout**: notice strip →
recent stories (3 cards) → quick-action tiles → this week. *(The v1/v2 featured-story
hero is gone.)* About carries the independence statement and standard
description **verbatim from the Copy Library** — no invented identity copy.

### Module registry (§8)
`setup/migrate-v21.sql` → `modules` + `module_audit`. Five modules seeded;
transport/carpool/challenges as **wrap pointers** at the current live pages,
volunteers/events as placeholders. All `hidden` (soft-launch). One route guard
in the front controller implements the whole §8.3 status table, and a known
module **never** returns 404 (§8.4).

### Matomo (§11)
`app/lib/analytics.php` — cookieless, DNT honoured, link tracking on. Emits
**nothing** until `MATOMO_URL`/`MATOMO_SITE_ID` are set, and never tracks dev
unless `MATOMO_TRACK_DEV` is explicitly true.

### Nightly sanitised sync (§13.1)
`setup/sync-staging.sh` — mysqldump prod → private temp file → import → mask →
verify. **Eight guards (S0–S7)**, the important one being S0: the write target
must contain a `_staging_marker` table, so transposed config values are caught
*before* any write (name-inequality alone cannot catch a swap).

### §10.7 preservation
All seven contract rows reserved in `public/.htaccess` **before** the front
controller, plus `setup/url-contract.md` as the per-upgrade checklist. Both PWA
manifests and both service workers exist at their exact URLs with scopes `/`
and `/admin/` intact.

---

## 2. Deviations from the spec (each deliberate)

| # | Spec says | Built | Why |
|---|---|---|---|
| D1 | "Brand Guidelines **v1.1** (31 Jul 2026)" (§2) | Followed **v1.0** | No v1.1 exists on disk; the package README states v1.0. §2's palette reproduces v1.0 exactly (including Ink `#173A46`), so they agree. **Spec should be corrected to v1.0** or v1.1 supplied. |
| D2 | `--c-ink: #1A1A1A` in the supplied tokens.css | **`#173A46`** | The file's own TODO asked for verification. Brand source `--nc-ink` and spec §2 both say `#173A46`. |
| D3 | `--text-muted: #5C6B70` in the supplied tokens.css | **`#5E747C`** | Brand source `--nc-muted` is `#5E747C`. Not flagged in the file, found by comparing against the brand's token CSS. |
| D4 | Staging DB `dalecarn_logistics_dev` (§13.1) | **`dalecarn_novacares`** | Your confirmed choice — provisioned, imported, 35 tables, already serving dev. DB name is config-driven, so this is a one-line change if reverted. |
| D5 | Staging host `beta.novacares.my` (v3.1 §13.1) / `dev.` (v3.2) | **`dev.novacares.my`** | v3.2 already says `dev.`; noting it because earlier drafts said `beta.`. |
| D6 | Self-hosted Nunito Sans (§2) | `@font-face` wired, **files absent** | Nunito Sans is in neither the brand package nor anywhere on disk; brand v1.0 specifies "Avenir Next, Arial fallback" — Nunito enters via spec v3.1. Non-Apple devices currently render **Arial**, the brand's own named fallback, so nothing is broken. Drop three woff2 files in `public/assets/fonts/` and it works with no code change. |
| D7 | `modules.committee_id INT NOT NULL` (§8.2) | **NULLable** | Appendix A lists volunteers/events with committee "(varies)". NOT NULL would make them unseedable. |
| D8 | `config/config.staging.php` (§12.7) | Lookup order `config.production.php` → `config.staging.php` → `config.php` | Supports the spec's names without hard-coding an environment (§12.9). **Production wins outright** — see finding 2. |

---

## 3. Review findings (3 verifiers, 22 findings — 21 fixed, 1 accepted)

**High**
1. **`.htaccess` §10.7 rules matched only extensionless URLs.** The live app emits
   `.php`-suffixed links to outsiders — `RETURN_URL` is literally `/thankyou.php`
   (registered with ToyyibPay), carpool emails carry `carpool-manage.php?token=`.
   Those would have 404'd after cutover, **breaking payment returns and every
   carpool link in parents' inboxes**. *Fixed:* every contract rule now accepts
   an optional `.php` and trailing slash (21 forms tested); `url-contract.md`
   corrected. Also added a **host guard** so the wrap redirects cannot loop once
   this tree becomes novacares.my.
2. **Config shadowing at cutover.** A leftover `config.php` would have silently
   beaten `config.production.php`, running production with `NOVA_ENV=dev`,
   sandbox payment keys and `display_errors` on. *Fixed:* production wins
   outright; multiple configs log a warning instead of resolving by luck.
3. **Sync script defeatable by transposed config values.** Only name-inequality
   guarded the write side. *Fixed:* S0 marker-table proof + non-empty checks on
   all six credentials.

**Medium** — masthead used the taglined lockup below the kit's 280 px threshold
(now `Primary_NoTagline_Reversed`) · token checker's manifest assertion was inert
(`theme-color` vs `theme_color`, and `.json` unscanned) · **`MAIL_CATCHALL` was a
promise with no mechanism** — the footer said "mail is redirected" but nothing
read the constant; now `app/lib/mail.php` enforces it and **fails closed** if it's
unset in dev · `/admin/` had no index so the admin PWA's `start_url` 403'd ·
**`docs/brand/` had no deny-guard** — the guidelines and logo masters would have
been reachable if the primary domain serves `public_html` · dump file world-readable
and orphaned on kill (now `umask 077`, 0600, EXIT trap) · cron failures were
log-only so cPanel never emailed (now stdout too).

**Low** — token-checker blind spots (all six probed and now caught) · gold-on-white
badge text at 2.2:1 contrast (gold is now border-only, text Deep Teal) · trailing
slashes on contract URLs · wrap forward dropped the query string · `file.php` leaked
existence via 404-vs-403 (authorises first now) · mask SQL is a static allowlist
(maintenance rule added in-file) · sync header doc drift.

**Accepted, not fixed:** the `.htaccess` wrap redirects hard-code
`https://novacares.my`. `.htaccess` cannot read config, the in-file comment and
`url-contract.md` mark them as replace-at-upgrade, and the host guard prevents
looping. Assessed as documentation, not a §12.9 violation.

---

## 4. Decisions I made

1. **Removed the legacy crest** rather than carrying it forward. The identity rule
   is unambiguous and the brand README forbids combining NOVACares with the
   student crest.
2. **Verbatim brand copy.** Independence statement, standard description and
   tagline are copied exactly; I wrote no identity language of my own.
3. **`file.php` fails closed for `financial`.** No committee auth exists yet, so
   receipts are served to nobody rather than guessed at.
4. **Wrap redirects are 302, news will be 301.** Temporary vs permanent, so
   caches don't pin the wrong answer.
5. **`_staging_marker` as the sync guard.** A positive proof of identity beats a
   negative name comparison.
6. **Derived tokens kept, documented:** `--border-subtle #DDE5E6` and
   `--status-error #B3261E` have no brand equivalent (the palette contains no
   red). Flagged here for brand-owner confirmation.

---

## 5. Open questions

1. **Brand Guidelines version** — is v1.1 real? If so it supersedes the v1.0 I
   built against; if not, the spec's §2 reference should read v1.0. *(D1)*
2. **Nunito Sans** — confirm it's wanted (brand v1.0 says Avenir Next/Arial). If
   yes, supply/generate the three woff2 files. *(D6)*
3. **`--border-subtle` and `--status-error`** — brand owner to confirm these two
   derived hexes, or supply brand equivalents.
4. **Staging DB name** — spec §13.1 still says `dalecarn_logistics_dev`; update it
   to `dalecarn_novacares` or tell me to switch. *(D4)*
5. **Read-only production MySQL user** for the sync job — recommended; the staging
   config currently needs prod-read credentials.
6. **Matomo** — install location/subdomain and its own DB (a SuperAdmin action);
   then set `MATOMO_URL`/`MATOMO_SITE_ID`.
7. **Phase 0.5 item 5** — extension-less `error_log` deny on the **live** site is
   still open (the new tree already handles it).

---

## 6. To deploy this shell

1. **Sync `Website-dev/` → `/addons/novacares3/`** (Dreamweaver Put; `config/config.php`
   is cloaked/excluded).
2. **Run `setup/migrate-v21.sql`** on `dalecarn_novacares` — additive only; safe to
   run on production too, where the current app ignores it.
3. **Create the sync marker** on the staging DB only:
   `CREATE TABLE _staging_marker (note VARCHAR(64)); INSERT INTO _staging_marker VALUES ('safe to overwrite nightly');`
4. **Fill `SYNC_PROD_*`** in `config/config.php`, then add the nightly cron:
   `/bin/sh /home/dalecarn/public_html/addons/novacares3/setup/sync-staging.sh`
5. **Visit dev.novacares.my** — click the five nav items, then flip
   `modules.status='enabled'` for `transport` and watch the card, the home tile
   and the route appear with no code change.

### Verification performed
`check-tokens.sh` PASSED (and adversarially probed) · 20 PHP files brace-balanced ·
`sh -n` on both scripts · `node --check` on both service workers · both manifests
valid JSON · §12.9 path grep clean · 21 §10.7 URL forms regex-tested.
**Not performed:** live browser rendering at 360 px, and any execution against a
database — no PHP binary or DB access in this environment.
