Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bec5eaa
Two surfaces that were telling the reader something untrue (IA-89, IA…
important-new Jul 29, 2026
22013b3
A date range you can read, and a popover that stays with its trigger
important-new Jul 29, 2026
8594edd
Communication A1 server side: thread by contact, one endpoint over on…
important-new Jul 29, 2026
0ac9f43
Communication A1 UI: the inspector can finally read what the client said
important-new Jul 29, 2026
dedd629
Remove a stray seed script committed by accident
important-new Jul 29, 2026
e2f6e4b
Track D: the company Messages inbox, the mention, and the sidebar badge
important-new Jul 29, 2026
13dd29f
IA-111: one word stops meaning two things
important-new Jul 29, 2026
5641bf7
Full-gate pass: knip dead exports, window-capped test fixtures, purge…
important-new Jul 29, 2026
fbf88a9
Role model Phase 1 + the purity gate (Tasks 1, 2, 13)
important-new Jul 29, 2026
ed398ca
Role model Phase 2: /me carries capabilities, the portal stops guessing
important-new Jul 29, 2026
5d9673b
Role model Phase 3: per-profile overrides, seeded explicit, read ever…
important-new Jul 29, 2026
adfc396
Role model Phase 4 (server): the referrer is a column, the portal is …
important-new Jul 29, 2026
af94276
Role model Phase 4 (UI): the referrer picker on Order details
important-new Jul 30, 2026
265e122
Role model Task 10: edit per-profile capabilities, with an audited diff
important-new Jul 30, 2026
c4ff1c1
Role model Task 11: a capability matrix behind ? on the Roles page
important-new Jul 30, 2026
389e3b5
Role model Task 12: declared capability must equal mounted guard (IA-98)
important-new Jul 30, 2026
1d09178
Role model Tasks 14-16: the gates, with re-measured honesty on 16
important-new Jul 30, 2026
1f03fbb
Role type label + knip cleanup from the role-model waves
important-new Jul 30, 2026
268ea4e
Ripples: inspections tail gains referred_by_contact_id, MCP snapshot …
important-new Jul 30, 2026
e8ea080
Communication A2: manual sends become visible
important-new Jul 30, 2026
df9bb7e
feat: per-tenant Privacy/Terms plus SMS consent and send path
important-new Jul 30, 2026
24f576e
Communication C0: viewCommunication gets teeth
important-new Jul 30, 2026
5182807
T1: the erasure gate stops trusting its own silence (#88)
important-new Jul 30, 2026
cb9a487
T2: the calendar drag now actually reschedules, and the instant moves…
important-new Jul 30, 2026
7c5dead
C1 server side: the notice gets a header, and the ledger rows belong …
important-new Jul 30, 2026
27be2c6
C1 app side: the Outbox groups on the header
important-new Jul 30, 2026
f34792e
C3 server side: a Notices inbox addressed by contact, not by address
important-new Jul 30, 2026
2a2725a
C3 server side: make "Turn on texts" lead somewhere real
important-new Jul 30, 2026
98a0cd1
C3 server side: name the company on every notice
important-new Jul 30, 2026
77cfb6f
C3/C4 app side: one Notices panel, three portals
important-new Jul 30, 2026
ad6c276
Notices: a delayed automation must not surface early
important-new Jul 30, 2026
c3a2e38
Auth pages: consistent by construction, not by copy-paste
important-new Jul 30, 2026
ceb2f6c
DS: text on a primary fill comes from the token, not from "white"
important-new Jul 30, 2026
c9c6f0a
B1: the in_app channel, and the join that was hiding rows
important-new Jul 30, 2026
066dd07
B2: a rule can address the workspace's staff
important-new Jul 30, 2026
e0d241e
B3 step 1: a notice's wording can come from a template
important-new Jul 30, 2026
f77edc1
B3 + C4: the office alert becomes a rule; one NoticeList, guarded
important-new Jul 30, 2026
cabad76
Backfill scripts: escape the backslash too, not just the quote
important-new Jul 30, 2026
603bd7b
STOP binds every recipient, not just consumers
important-new Jul 30, 2026
9e02e06
The Automations editor never learned about in_app or staff
important-new Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 8 additions & 4 deletions .dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,18 @@ TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
GOOGLE_CLIENT_ID=your_google_client_id_here
GOOGLE_CLIENT_SECRET=your_google_client_secret_here

# Optional: Public base URL for this worker (used to build OAuth redirect URIs)
APP_BASE_URL=http://localhost:8788
# Optional: Public base URL for this worker (OAuth redirects, email links,
# hosted Privacy/Terms absolute URLs, sidebar legal links). Must match the
# origin you open in the browser — including port (wrangler often uses 8787
# or 8788). Without this, in-process API calls may emit `http://localhost/...`
# without a port and TFV / footer links break locally.
APP_BASE_URL=http://127.0.0.1:8787

# Optional: Legal document links (self-host operators). When either URL is set, account-creating
# public forms (agent signup, guest join) require an acceptance checkbox, and the acceptance blob
# (URL + timestamp + IP + country) is stored on the user row. Unset = feature is entirely off.
# TERMS_URL=https://yourdomain.com/terms
# PRIVACY_URL=https://yourdomain.com/privacy
# TERMS_URL / PRIVACY_URL removed — configure per-tenant Privacy & Terms in
# Settings → Compliance (hosted /legal/:slug/… or custom website URLs).

# Optional: Cloudflare account credentials — required for silo mode (per-tenant D1 databases)
# Create an API token at dash.cloudflare.com → My Profile → API Tokens with D1:Edit permission
Expand Down
3 changes: 1 addition & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,7 @@ OpenInspection runs as ONE Cloudflare Worker (cloudflare/react-router-hono-fulls
| `INSPECTION_DOC` | No | Durable Object binding (`class_name: InspectionDocDO`) for collaborative inspection editing (#181 — Yjs CRDT host; one DO per inspection, tenant-scoped `idFromName`). Declared in `wrangler.jsonc` (committed) and must be added to `wrangler.saas.jsonc` (gitignored) with the matching `v2` SQLite-class migration. When absent the collab routes return `501` and fail closed — editing falls back to a single-client Y.Doc with no realtime sync. See `docs/developers/collab-editing.md`. |
| `TURNSTILE_SECRET_KEY` | No | Server-side Turnstile verification — `POST /api/book` enforces this when set. Use test secret `1x0000000000000000000000000000000AA` for local dev. |
| `APP_BASE_URL` | No | Public URL for OAuth and link generation |
| `TERMS_URL` | No | Optional URL of the operator's Terms of Service. When set (with or without `PRIVACY_URL`), account-creating public forms require an acceptance checkbox and stamp an acceptance record on the user row. |
| `PRIVACY_URL` | No | Optional URL of the operator's Privacy Policy. When set, public pages render a privacy-notice footer link. |
| `APP_BASE_URL` | No | Public origin used when building absolute links (reports, hosted `/legal/:tenant/…` Privacy & Terms). |
| `RESEND_API_KEY`| No | Platform-default email delivery (Resend). Tenants may switch to their OWN Resend key + verified sender via Settings → Communication (per-tenant override; the email pipeline resolves own-vs-platform explicitly). |
| `GEMINI_API_KEY`| No | DEPRECATED as a platform key — AI assistance is strictly bring-your-own-key: `AIService` reads the tenant's own stored key (Settings → Advanced) and ignores this env. AI features stay off until a tenant configures a key. |
| `APP_MODE` | No | `standalone` (default) or `saas` — controls tenant resolution |
Expand Down
2 changes: 1 addition & 1 deletion app/components/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function ConfirmDialog({
const confirmClass =
tone === "danger"
? "bg-ih-bad-fg text-white hover:opacity-90"
: "bg-ih-primary text-white hover:opacity-90";
: "bg-ih-primary text-ih-fg-inverse hover:opacity-90";
return (
<Modal
open={open}
Expand Down
4 changes: 2 additions & 2 deletions app/components/DocumentsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Shared per-inspection documents area (unified client portal, section ⑦).
*
* Used by BOTH the inspector inspection-hub AND the client portal Hub. Pure
* Used by BOTH the inspector portal AND the client portal Hub. Pure
* presentational + helpers — NO data fetching inside. All upload / delete /
* download actions are passed in as callbacks/hrefs by the host route.
*
Expand Down Expand Up @@ -266,7 +266,7 @@ export default function DocumentsSection({
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="rounded-md bg-ih-primary px-3 py-1.5 text-[13px] font-bold text-white transition-opacity hover:opacity-90"
className="rounded-md bg-ih-primary px-3 py-1.5 text-[13px] font-bold text-ih-fg-inverse transition-opacity hover:opacity-90"
>
{m.documents_choose_file()}
</button>
Expand Down
26 changes: 0 additions & 26 deletions app/components/LegalCheckbox.tsx

This file was deleted.

31 changes: 31 additions & 0 deletions app/components/PublicLegalFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { m } from "~/paraglide/messages";

/** Compact Privacy · Terms footer for public tenant surfaces. */
export function PublicLegalFooter({
privacyUrl,
termsUrl,
className = "",
}: {
privacyUrl?: string | null;
termsUrl?: string | null;
className?: string;
}) {
if (!privacyUrl && !termsUrl) return null;
return (
<footer
className={`mt-10 pt-6 border-t border-ih-border text-center text-[11px] text-ih-fg-4 ${className}`}
>
{privacyUrl && (
<a href={privacyUrl} target="_blank" rel="noreferrer" className="hover:underline hover:text-ih-fg-3">
{m.booking_link_privacy_policy()}
</a>
)}
{privacyUrl && termsUrl && <span className="mx-2">·</span>}
{termsUrl && (
<a href={termsUrl} target="_blank" rel="noreferrer" className="hover:underline hover:text-ih-fg-3">
{m.legal_checkbox_terms()}
</a>
)}
</footer>
);
}
15 changes: 10 additions & 5 deletions app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useState, useRef } from "react";
import { NavLink, useRouteLoaderData } from "react-router";
import { useSessionContext } from "~/hooks/useSessionContext";
import { useSessionContext, useUnreadMessages } from "~/hooks/useSessionContext";
import { writeSidebarCookie, type UiPrefs } from "~/lib/ui-prefs";
import { IC, WORKSPACE_ITEMS } from "~/components/sidebar/nav-items";
import { SidebarGroup } from "~/components/sidebar/SidebarGroup";
import { UserMenuPopover } from "~/components/sidebar/UserMenuPopover";
import { MobileHeader } from "~/components/sidebar/MobileHeader";
import { useCommandPalette } from "~/components/CommandPalette";
import { StaffNoticeBell } from "~/components/notices/StaffNoticeBell";
import { Avatar } from "@core/shared-ui";
import { m } from "~/paraglide/messages";

Expand All @@ -23,6 +24,7 @@ export function Sidebar() {
const [userMenuOpen, setUserMenuOpen] = useState(false);
const userMenuRef = useRef<HTMLDivElement>(null);
const ctx = useSessionContext();
const unreadMessages = useUnreadMessages();
const { openPalette } = useCommandPalette();

const companyName = ctx?.branding?.companyName || "OpenInspection";
Expand Down Expand Up @@ -65,9 +67,12 @@ export function Sidebar() {
{!collapsed && (
<>
<span className="text-[14px] font-bold text-ih-fg-1 tracking-tight leading-tight truncate">{companyName}</span>
<NavLink to="/notifications" className="ml-auto relative flex items-center justify-center w-7 h-7 rounded-ih-button text-ih-fg-4 hover:bg-ih-bg-muted hover:text-ih-primary transition-all" aria-label={m.nav_action_notifications()}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /></svg>
</NavLink>
{/* Notices — the same bell + panel the client and agent portals
use (design §3.15). It was a link to /notifications; "sent to
me" is a glance, and the page stays for the full history. */}
<span className="ml-auto shrink-0">
<StaffNoticeBell />
</span>
</>
)}
</div>
Expand All @@ -93,7 +98,7 @@ export function Sidebar() {

{/* Nav */}
<nav className="flex-1 px-2 py-1 overflow-y-auto">
<SidebarGroup label={m.nav_section_workspace()} items={WORKSPACE_ITEMS} collapsed={collapsed} />
<SidebarGroup label={m.nav_section_workspace()} items={WORKSPACE_ITEMS.map((i) => (i.to === "/messages" ? { ...i, badge: unreadMessages } : i))} collapsed={collapsed} />
{/* ds-allow: compact sidebar nav rhythm (10/7/14px), no semantic spacing token */}
<div className="mb-[14px]">
<NavLink
Expand Down
2 changes: 1 addition & 1 deletion app/components/SignaturePad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function SignaturePad({ onSubmit, onCancel, label = "Sign" }: Props) {
)}
<button
type="button"
className="text-sm font-semibold bg-ih-primary text-white px-3 py-1 rounded"
className="text-sm font-semibold bg-ih-primary text-ih-fg-inverse px-3 py-1 rounded"
onClick={submit}
disabled={busy}
>
Expand Down
60 changes: 60 additions & 0 deletions app/components/agent/AgentNoticeBell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* The agent portal's Notices bell, wired.
*
* Same shared <NoticeBell> the client Hub uses — after C1 a notice is one
* entity regardless of who reads it (design §3.11), so a second component
* would only be a copy waiting to drift. Two honest differences, both props:
*
* - Rows name the sending COMPANY. An agent's inbox spans every company that
* has them as a contact, so "who sent this" is information the client's
* single-company inbox does not need.
* - No email remedy. It opens a message composer, and the agent portal has
* none yet (Track D). A button with nothing behind it is worse than the
* plain "Not delivered" the reader gets instead.
*/
import { useEffect } from "react";
import { useFetcher, useRevalidator } from "react-router";
import { m } from "~/paraglide/messages";
import { NoticeBell } from "~/components/notices/NoticeBell";
import type { NoticeRowData, NoticeRemedy } from "~/lib/notice-view";

export function AgentNoticeBell({ notices, unread }: { notices: NoticeRowData[]; unread: number }) {
const fetcher = useFetcher<{ ok?: boolean; intent?: string; url?: string }>();
const revalidator = useRevalidator();

useEffect(() => {
const data = fetcher.data;
if (!data?.ok) return;
// Full navigation for the opt-in link: the sealed token's base64 can
// contain "/", so a decoded client-side match sees two path segments and
// renders nothing (same reason as PortalNoticeBell — verified in Chrome).
if (data.intent === "notice-optin-link" && data.url) window.location.assign(data.url);
else revalidator.revalidate();
// Keyed on the fetcher payload so each completed write acts once; the
// revalidator's identity changes on every state transition.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher.data]);

const submit = (intent: string, noticeId?: string) =>
fetcher.submit(
{ intent, ...(noticeId ? { noticeId } : {}) },
{ method: "post", action: "/resources/agent-notices" },
);

const onRemedy = (remedy: NoticeRemedy) => {
if (remedy.kind === "sms-consent") submit("notice-optin-link", remedy.noticeId);
};

return (
<NoticeBell
notices={notices}
unread={unread}
emailComposer={false}
showCompany
emptyBody={m.notice_empty_body_agent()}
onMarkAllRead={() => submit("notice-mark-all-read")}
onDismiss={(id) => submit("notice-dismiss", id)}
onRemedy={onRemedy}
/>
);
}
2 changes: 1 addition & 1 deletion app/components/agreements/AgreementRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { m } from "~/paraglide/messages";
* Library → Agreements table rows.
*
* IA-65 — the signing-request row that used to live beside this one moved to
* the inspection workspace (`inspection-hub/SigningRequests`), where the
* the inspection workspace (`inspector-portal/SigningRequests`), where the
* envelope it describes actually belongs. What remains is the template row.
*/
export function TemplateRow({ t }: { t: { id: string; name?: string; updatedAt?: string; createdAt?: string } }) {
Expand Down
2 changes: 1 addition & 1 deletion app/components/agreements/SignerList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { m } from "~/paraglide/messages";
* Track I-a Task 9 — shared multi-signer list for an agreement envelope.
*
* Self-contained: takes a signers array + per-signer action callbacks; it has
* NO route coupling so the #111 inspection-hub session can mount it later. The
* NO route coupling so the #111 inspector-portal session can mount it later. The
* admin Signing tab wires the callbacks to BFF loaders/fetchers (no client
* fetch('/api/...')). DS tokens only; admin surface so it inherits light/dark
* via data-color-scheme (no hardcoded colors).
Expand Down
34 changes: 29 additions & 5 deletions app/components/booking/BookingShell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { brandTokens, type TenantBrand } from "~/lib/brand";
import { PublicLegalFooter } from "~/components/PublicLegalFooter";
import type { CompanyProfile } from "./booking-constants";
import { m } from "~/paraglide/messages";

Expand All @@ -18,15 +19,30 @@ export function BookingErrorState({ error }: { error: string | null }) {

// B-16 — the company hasn't configured working hours yet: show an honest
// not-open state instead of a wizard whose submit can only fail.
export function BookingNotOpenState({ profile, brand }: { profile: CompanyProfile; brand: TenantBrand }) {
export function BookingNotOpenState({
profile,
brand,
privacyUrl,
termsUrl,
}: {
profile: CompanyProfile;
brand: TenantBrand;
privacyUrl?: string | null;
termsUrl?: string | null;
}) {
return (
<div className="min-h-screen flex items-center justify-center bg-ih-bg-app" style={brandTokens(brand.primaryColor)}>
<div className="max-w-md text-center p-8 bg-ih-bg-card border border-ih-border rounded-xl">
<div className="min-h-screen flex flex-col items-center justify-center bg-ih-bg-app px-4" style={brandTokens(brand.primaryColor)}>
<div className="max-w-md w-full text-center p-8 bg-ih-bg-card border border-ih-border rounded-xl">
<h1 className="text-xl font-bold text-ih-fg-1">{m.booking_not_open_heading()}</h1>
<p className="text-[14px] text-ih-fg-3 mt-3 leading-relaxed">
{m.booking_not_open_body({ company: profile.company })}
</p>
</div>
<PublicLegalFooter
privacyUrl={privacyUrl ?? brand.privacyUrl}
termsUrl={termsUrl ?? brand.termsUrl}
className="mt-6 pt-0 border-t-0"
/>
</div>
);
}
Expand All @@ -35,11 +51,13 @@ export function BookingShell({
profile,
brand,
privacyUrl,
termsUrl = null,
children,
}: {
profile: CompanyProfile;
brand: TenantBrand;
privacyUrl: string | null;
termsUrl?: string | null;
children: ReactNode;
}) {
return (
Expand All @@ -64,9 +82,15 @@ export function BookingShell({
<p className="text-center text-[11px] text-ih-fg-4 mt-6">
{m.booking_powered_by()}
</p>
{privacyUrl && (
{(privacyUrl || termsUrl) && (
<p className="mt-8 text-center text-xs text-ih-fg-3">
<a href={privacyUrl} target="_blank" rel="noreferrer" className="hover:underline">{m.booking_link_privacy_policy()}</a>
{privacyUrl && (
<a href={privacyUrl} target="_blank" rel="noreferrer" className="hover:underline">{m.booking_link_privacy_policy()}</a>
)}
{privacyUrl && termsUrl && <span className="mx-1.5">·</span>}
{termsUrl && (
<a href={termsUrl} target="_blank" rel="noreferrer" className="hover:underline">{m.legal_checkbox_terms()}</a>
)}
</p>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/components/calendar/BlockTimeDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function BlockTimeDrawer({
type="submit"
form="block-time-form"
disabled={submitting}
className="h-9 px-4 rounded-md bg-ih-primary text-[13px] font-bold text-white hover:bg-ih-primary-600 disabled:opacity-50"
className="h-9 px-4 rounded-md bg-ih-primary text-[13px] font-bold text-ih-fg-inverse hover:bg-ih-primary-600 disabled:opacity-50"
>
{submitting ? m.calendar_block_saving() : block ? m.calendar_block_save_changes() : m.calendar_block_time()}
</button>
Expand Down
2 changes: 1 addition & 1 deletion app/components/calendar/CalendarEventModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function CalendarEventModal({ event, open, displayTz, locale, onClose }:
navigate(`/inspections/${typeof inspectionId === "string" ? inspectionId : event.id}`);
onClose();
}}
className="h-8 px-4 rounded-md bg-ih-primary text-white font-bold text-[13px] hover:bg-ih-primary-600"
className="h-8 px-4 rounded-md bg-ih-primary text-ih-fg-inverse font-bold text-[13px] hover:bg-ih-primary-600"
>
{m.calendar_event_open_inspection()}
</button>
Expand Down
4 changes: 2 additions & 2 deletions app/components/calendar/CalendarScopeToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function CalendarScopeToolbar({
onClick={() => onScopeChange("my")}
aria-pressed={scope === "my"}
className={`h-8 rounded px-3 text-[13px] font-bold ${
scope === "my" ? "bg-ih-primary text-white" : "text-ih-fg-3 hover:bg-ih-bg-muted"
scope === "my" ? "bg-ih-primary text-ih-fg-inverse" : "text-ih-fg-3 hover:bg-ih-bg-muted"
}`}
>
{m.calendar_scope_my()}
Expand All @@ -51,7 +51,7 @@ export function CalendarScopeToolbar({
onClick={() => onScopeChange("team")}
aria-pressed={scope === "team"}
className={`h-8 rounded px-3 text-[13px] font-bold ${
scope === "team" ? "bg-ih-primary text-white" : "text-ih-fg-3 hover:bg-ih-bg-muted"
scope === "team" ? "bg-ih-primary text-ih-fg-inverse" : "text-ih-fg-3 hover:bg-ih-bg-muted"
}`}
>
{m.calendar_scope_team()}
Expand Down
2 changes: 1 addition & 1 deletion app/components/calendar/MonthView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function MonthView({
setDragTarget(null);
}}
>
<span className={`inline-flex items-center justify-center w-6 h-6 rounded-full text-[12px] font-medium ${isToday(day) ? "bg-ih-primary text-white" : "text-ih-fg-2"}`}>
<span className={`inline-flex items-center justify-center w-6 h-6 rounded-full text-[12px] font-medium ${isToday(day) ? "bg-ih-primary text-ih-fg-inverse" : "text-ih-fg-2"}`}>
{day}
</span>
<div className="mt-0.5 space-y-0.5">
Expand Down
Loading
Loading