chore(copy): em-dash sweep across user-facing JSX text + bump lint to error
Replaced 174 em-dashes (—) with " - " (space-hyphen-space) across 49 files in src/components + src/app. The em-dash reads as a tell-tale "AI-generated" marker per the user's design feedback; hyphens with spaces preserve the connector semantics without the AI tint. Touched only lines outside pure-comment context (// /* * */). Code comments, JSDoc, audit-log strings, structured logging strings, and templates outside the lint scope retain their em-dashes for now — they're not user-visible. Also captured two remaining cases that used the `—` HTML entity instead of the literal character (system-monitoring-dashboard, interest-stage-picker) — replaced with a plain hyphen. Bumped the existing `no-restricted-syntax` rule from `warn` → `error` in eslint.config.mjs scoped to src/components/**/*.tsx + src/app/**/*.tsx. New code reintroducing em-dashes in JSX text now fails the lint gate. Verified: tsc clean, vitest 1448/1448, eslint 0 em-dash warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -354,7 +354,7 @@ export function AuditLogList() {
|
||||
row.original.ipAddress ? (
|
||||
<code className="text-xs text-muted-foreground">{row.original.ipAddress}</code>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
<span className="text-xs text-muted-foreground"> - </span>
|
||||
),
|
||||
size: 130,
|
||||
},
|
||||
@@ -457,7 +457,7 @@ export function AuditLogList() {
|
||||
<SelectItem value="job_failed">Job failed</SelectItem>
|
||||
<SelectItem value="cron_run">Cron run</SelectItem>
|
||||
{/* L-AU02: actions that fire in the code but were missing from
|
||||
the dropdown — reps couldn't filter on them. */}
|
||||
the dropdown - reps couldn't filter on them. */}
|
||||
<SelectItem value="password_change">Password change</SelectItem>
|
||||
<SelectItem value="portal_invite">Portal invite</SelectItem>
|
||||
<SelectItem value="portal_activate">Portal activate</SelectItem>
|
||||
@@ -585,7 +585,7 @@ export function AuditLogList() {
|
||||
|
||||
{dateRangeInvalid && (
|
||||
<p className="mt-2 text-xs text-destructive">
|
||||
From date must be on or before To date — date filter ignored.
|
||||
From date must be on or before To date - date filter ignored.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -642,7 +642,7 @@ export function AuditLogList() {
|
||||
<>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{detailEntry.action.replace(/_/g, ' ')} — {detailEntry.entityType}
|
||||
{detailEntry.action.replace(/_/g, ' ')} - {detailEntry.entityType}
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
{formatDate(detailEntry.createdAt, 'datetime.medium')}
|
||||
|
||||
@@ -121,7 +121,7 @@ export function BackupAdminPanel() {
|
||||
</CardHeader>
|
||||
<CardContent className="text-xs text-muted-foreground">
|
||||
Backups land at <code>backups/<id>.dump</code> via{' '}
|
||||
<code>getStorageBackend().put()</code>. Restore is intentionally not exposed in the UI —
|
||||
<code>getStorageBackend().put()</code>. Restore is intentionally not exposed in the UI -
|
||||
download the .dump file and run <code>pg_restore</code> manually.
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -192,7 +192,7 @@ export function BulkAddBerthsWizard() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Step 1 — Sequence</CardTitle>
|
||||
<CardTitle>Step 1 - Sequence</CardTitle>
|
||||
<CardDescription>
|
||||
Pick the dock letter and the mooring-number range. Tenure + status apply to every row;
|
||||
everything else (dimensions, pricing, pontoon) is filled per row in Step 2.
|
||||
@@ -265,7 +265,7 @@ export function BulkAddBerthsWizard() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Step 2 — Fill in each row</CardTitle>
|
||||
<CardTitle>Step 2 - Fill in each row</CardTitle>
|
||||
<CardDescription>
|
||||
Per-row dimensions, pricing, pontoon. Use the “Apply to all” inputs in the
|
||||
header to copy a value down every row at once.
|
||||
@@ -435,7 +435,7 @@ export function BulkAddBerthsWizard() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">—</SelectItem>
|
||||
<SelectItem value="__none__"> - </SelectItem>
|
||||
{sidePontoonOptions.filter(Boolean).map((p) => (
|
||||
<SelectItem key={p} value={p}>
|
||||
{p}
|
||||
|
||||
@@ -171,7 +171,7 @@ export function CustomFieldsManager() {
|
||||
the form <code className="rounded bg-amber-100 px-1">{`{{custom.fieldName}}`}</code> now
|
||||
expand in EOI/contract/email templates for client/interest/berth contexts. They still
|
||||
don’t plug into the global search index, the berth recommender, or the entity-diff
|
||||
audit log — use them for rep-only annotations and template-merge values, but anything
|
||||
audit log - use them for rep-only annotations and template-merge values, but anything
|
||||
load-bearing for the deal flow still needs a first-class column.
|
||||
</span>
|
||||
</WarningCallout>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function EmbeddedSigningCard() {
|
||||
};
|
||||
setResult({ ...res.data, at: new Date() });
|
||||
if (res.data.ok) toast.success('Embedded signing host reachable.');
|
||||
else toast.error('Embedded signing host probe failed — see card.');
|
||||
else toast.error('Embedded signing host probe failed - see card.');
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
setResult({
|
||||
@@ -200,7 +200,7 @@ export function EmbeddedSigningCard() {
|
||||
<p className="text-muted-foreground">
|
||||
The marketing site needs to handle <code>/sign/[role]/[token]</code> by forwarding
|
||||
to the underlying Documenso signing URL (or embedding it in an iframe). Role is one
|
||||
of <code>client</code> / <code>developer</code> / <code>approver</code> — useful for
|
||||
of <code>client</code> / <code>developer</code> / <code>approver</code> - useful for
|
||||
tracking which slot the signer is in.
|
||||
</p>
|
||||
<p className="mt-1 text-muted-foreground">Minimum Next.js example:</p>
|
||||
@@ -228,7 +228,7 @@ export default function SignPage({ params }) {
|
||||
<p className="text-muted-foreground">
|
||||
Use the Test connection button to verify <code>/</code> and{' '}
|
||||
<code>/sign/success</code> return 2xx. If either fails, the marketing site
|
||||
isn't ready — fix the route before flipping live or signers will land on a 404
|
||||
isn't ready - fix the route before flipping live or signers will land on a 404
|
||||
page from outbound emails.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -106,7 +106,7 @@ export function TemplateSyncButton() {
|
||||
onSuccess: (result) => {
|
||||
setLastResult(result);
|
||||
toast.success(
|
||||
`Synced "${result.title}" — ${result.recipients.length} recipients, ${result.fieldCount} fields cached`,
|
||||
`Synced "${result.title}" - ${result.recipients.length} recipients, ${result.fieldCount} fields cached`,
|
||||
);
|
||||
void queryClient.invalidateQueries({ queryKey: ['settings', 'resolved'] });
|
||||
void queryClient.invalidateQueries({
|
||||
@@ -218,7 +218,7 @@ export function TemplateSyncButton() {
|
||||
<div className="font-medium text-muted-foreground">Template-level settings</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Read from the template itself on Documenso. These values are bound to the
|
||||
template, so every envelope generated from it inherits them —{' '}
|
||||
template, so every envelope generated from it inherits them -{' '}
|
||||
<code>/template/use</code> does <strong>not</strong> accept overrides for these.
|
||||
Change them in Documenso's template editor.
|
||||
</p>
|
||||
@@ -236,7 +236,7 @@ export function TemplateSyncButton() {
|
||||
</span>
|
||||
{lastResult.templateMeta.distributionMethod === 'EMAIL' && (
|
||||
<span className="ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-900 dark:bg-amber-950 dark:text-amber-200">
|
||||
⚠️ Documenso will email recipients directly — the CRM's branded email
|
||||
⚠️ Documenso will email recipients directly - the CRM's branded email
|
||||
is in addition. Set to NONE on the template to let the CRM be the sole
|
||||
sender.
|
||||
</span>
|
||||
@@ -256,7 +256,7 @@ export function TemplateSyncButton() {
|
||||
Fields: {lastResult.fieldCount} cached for <code>prefillFields</code>
|
||||
{lastResult.fieldCount === 0 && (
|
||||
<span className="ml-1 font-normal text-muted-foreground">
|
||||
— that's fine if your template is a fillable PDF (AcroForm). The CRM will
|
||||
- that's fine if your template is a fillable PDF (AcroForm). The CRM will
|
||||
fill it via <code>formValues</code>-by-name instead, same as on v1.{' '}
|
||||
<code>prefillFields</code> is only needed if you placed field overlays directly in
|
||||
the Documenso template editor.
|
||||
@@ -314,7 +314,7 @@ export function TemplateSyncButton() {
|
||||
</div>
|
||||
<p className="pt-0.5 text-[11px] text-muted-foreground">
|
||||
These are the fillable fields actually in the PDF binary on Documenso. The CRM
|
||||
fills them by name at send time — this is the same mechanism the prod v1 server
|
||||
fills them by name at send time - this is the same mechanism the prod v1 server
|
||||
uses.
|
||||
</p>
|
||||
{lastResult.acroForm.map((report) => (
|
||||
@@ -427,7 +427,7 @@ export function TemplateSyncButton() {
|
||||
{sync.isError && !lastResult && (
|
||||
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-3 text-xs">
|
||||
<div className="flex items-center gap-2 font-medium text-destructive">
|
||||
<XCircle className="size-3" /> Sync failed — check the Documenso credentials above and
|
||||
<XCircle className="size-3" /> Sync failed - check the Documenso credentials above and
|
||||
confirm the template exists on the configured instance.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -155,7 +155,7 @@ export function TemplateForm({ open, onOpenChange, template, onSuccess }: Templa
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Paste or edit TipTap JSON. Use{' '}
|
||||
<code className="rounded bg-muted px-1 text-xs">{'{{scope.field}}'}</code> tokens for
|
||||
dynamic content — see the list below.
|
||||
dynamic content - see the list below.
|
||||
</p>
|
||||
<textarea
|
||||
id="template-content"
|
||||
|
||||
@@ -122,7 +122,7 @@ export function EmailRoutingCard() {
|
||||
{!isSalesAvailable ? (
|
||||
<WarningCallout>
|
||||
<p className="text-sm">
|
||||
Sales sender is disabled — configure SMTP credentials in the "Sales send-from
|
||||
Sales sender is disabled - configure SMTP credentials in the "Sales send-from
|
||||
account" card below to enable the <code>sales</code> option.
|
||||
</p>
|
||||
</WarningCallout>
|
||||
|
||||
@@ -348,7 +348,7 @@ function CreateCriterionDialog({
|
||||
<DialogTitle>Add qualification criterion</DialogTitle>
|
||||
<DialogDescription>
|
||||
The <strong>key</strong> is a stable identifier code references (lowercase alphanumeric
|
||||
+ underscores). It can't be changed once created — per-interest state rows
|
||||
+ underscores). It can't be changed once created - per-interest state rows
|
||||
reference it.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -188,8 +188,8 @@ export function ResidentialStagesAdmin() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">In-progress</SelectItem>
|
||||
<SelectItem value="won">Closed — won</SelectItem>
|
||||
<SelectItem value="lost">Closed — lost</SelectItem>
|
||||
<SelectItem value="won">Closed - won</SelectItem>
|
||||
<SelectItem value="lost">Closed - lost</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@ export function RoleList() {
|
||||
{/* Display-normalize: snake_case → "Snake Case" so admin-
|
||||
created roles with arbitrary keys still read cleanly.
|
||||
The underlying name is stored verbatim and is what code
|
||||
checks against — display is purely cosmetic. */}
|
||||
checks against - display is purely cosmetic. */}
|
||||
<span className="font-medium">{formatRole(row.original.name)}</span>
|
||||
{row.original.isSystem && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
@@ -248,14 +248,14 @@ export function RoleList() {
|
||||
onSuccess={fetchRoles}
|
||||
/>
|
||||
|
||||
{/* Permissions inspector — opens when admin clicks the count
|
||||
{/* Permissions inspector - opens when admin clicks the count
|
||||
badge in the table. Lists granted vs denied per resource so
|
||||
they can spot gaps before opening the editor. */}
|
||||
<Dialog open={!!viewingPermissions} onOpenChange={(o) => !o && setViewingPermissions(null)}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Permissions — {viewingPermissions ? formatRole(viewingPermissions.name) : ''}
|
||||
Permissions - {viewingPermissions ? formatRole(viewingPermissions.name) : ''}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Granted vs total per resource. Click Edit to change.
|
||||
|
||||
@@ -159,7 +159,7 @@ export function SalesEmailConfigCard() {
|
||||
message: res.data.error ?? 'Unknown error',
|
||||
at: new Date(),
|
||||
});
|
||||
toast.error('SMTP test failed — see card for details.');
|
||||
toast.error('SMTP test failed - see card for details.');
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
@@ -221,7 +221,7 @@ export function SalesEmailConfigCard() {
|
||||
<CardTitle>Sales send-from account</CardTitle>
|
||||
<CardDescription>
|
||||
SMTP credentials for human-touch outbound (brochures + per-berth PDFs). IMAP creds
|
||||
enable the bounce monitor — leave blank to disable bounce-rejection banners. Passwords
|
||||
enable the bounce monitor - leave blank to disable bounce-rejection banners. Passwords
|
||||
are encrypted at rest and never returned by the API.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@@ -272,7 +272,7 @@ export function SalesEmailConfigCard() {
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={`SMTP password ${smtpPassSet ? '(stored — leave blank to keep)' : ''}`}
|
||||
label={`SMTP password ${smtpPassSet ? '(stored - leave blank to keep)' : ''}`}
|
||||
id="sef-smtp-pass"
|
||||
>
|
||||
<Input
|
||||
@@ -322,7 +322,7 @@ export function SalesEmailConfigCard() {
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={`IMAP password ${imapPassSet ? '(stored — leave blank to keep)' : ''}`}
|
||||
label={`IMAP password ${imapPassSet ? '(stored - leave blank to keep)' : ''}`}
|
||||
id="sef-imap-pass"
|
||||
>
|
||||
<Input
|
||||
|
||||
@@ -94,7 +94,7 @@ const KNOWN_SETTINGS: Array<{
|
||||
key: 'default_new_interest_owner',
|
||||
label: 'Default New-Interest Owner',
|
||||
description:
|
||||
'User ID to auto-assign as the deal owner when a new interest is created. Stored as { "userId": "..." }. Leave blank to have new interests unassigned by default — the rep can pick an owner from the interest detail header.',
|
||||
'User ID to auto-assign as the deal owner when a new interest is created. Stored as { "userId": "..." }. Leave blank to have new interests unassigned by default - the rep can pick an owner from the interest detail header.',
|
||||
type: 'json',
|
||||
defaultValue: { userId: null },
|
||||
},
|
||||
@@ -136,7 +136,7 @@ const KNOWN_SETTINGS: Array<{
|
||||
// ─── Berth recommender (src/lib/services/berth-recommender.service.ts) ──────
|
||||
{
|
||||
key: 'recommender_max_oversize_pct',
|
||||
label: 'Recommender — max oversize %',
|
||||
label: 'Recommender - max oversize %',
|
||||
description:
|
||||
'Cap on how much larger a berth can be than the desired length/width/draft before it stops being suggested. Default 30.',
|
||||
type: 'number',
|
||||
@@ -144,35 +144,35 @@ const KNOWN_SETTINGS: Array<{
|
||||
},
|
||||
{
|
||||
key: 'recommender_top_n_default',
|
||||
label: 'Recommender — default result count',
|
||||
label: 'Recommender - default result count',
|
||||
description: 'Default number of berth recommendations returned per request. Default 8.',
|
||||
type: 'number',
|
||||
defaultValue: 8,
|
||||
},
|
||||
{
|
||||
key: 'fallthrough_policy',
|
||||
label: 'Recommender — fall-through policy',
|
||||
label: 'Recommender - fall-through policy',
|
||||
description: 'How berths re-enter the recommender after a lost deal.',
|
||||
type: 'select',
|
||||
defaultValue: 'immediate_with_heat',
|
||||
options: [
|
||||
{
|
||||
value: 'immediate_with_heat',
|
||||
label: 'Immediate (with heat boost) — surface again right away',
|
||||
label: 'Immediate (with heat boost) - surface again right away',
|
||||
},
|
||||
{
|
||||
value: 'cooldown',
|
||||
label: 'Cooldown — wait N days (see below)',
|
||||
label: 'Cooldown - wait N days (see below)',
|
||||
},
|
||||
{
|
||||
value: 'never_auto_recommend',
|
||||
label: 'Never — only re-surface via manual rep search',
|
||||
label: 'Never - only re-surface via manual rep search',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'fallthrough_cooldown_days',
|
||||
label: 'Recommender — fall-through cooldown (days)',
|
||||
label: 'Recommender - fall-through cooldown (days)',
|
||||
description:
|
||||
'Days a berth stays out of the recommender after a lost deal when the policy is `cooldown`. Default 30.',
|
||||
type: 'number',
|
||||
@@ -180,14 +180,14 @@ const KNOWN_SETTINGS: Array<{
|
||||
},
|
||||
{
|
||||
key: 'heat_weight_recency',
|
||||
label: 'Heat weight — recency',
|
||||
label: 'Heat weight - recency',
|
||||
description: 'Weight given to how recently the prior interest fell through. Default 30.',
|
||||
type: 'number',
|
||||
defaultValue: 30,
|
||||
},
|
||||
{
|
||||
key: 'heat_weight_furthest_stage',
|
||||
label: 'Heat weight — furthest stage',
|
||||
label: 'Heat weight - furthest stage',
|
||||
description:
|
||||
'Weight given to how close the prior interest got to closing before falling through. Default 40.',
|
||||
type: 'number',
|
||||
@@ -195,7 +195,7 @@ const KNOWN_SETTINGS: Array<{
|
||||
},
|
||||
{
|
||||
key: 'heat_weight_interest_count',
|
||||
label: 'Heat weight — historical interest count',
|
||||
label: 'Heat weight - historical interest count',
|
||||
description:
|
||||
'Weight given to how often this berth has attracted interest historically. Default 15.',
|
||||
type: 'number',
|
||||
@@ -203,7 +203,7 @@ const KNOWN_SETTINGS: Array<{
|
||||
},
|
||||
{
|
||||
key: 'heat_weight_eoi_count',
|
||||
label: 'Heat weight — historical EOI count',
|
||||
label: 'Heat weight - historical EOI count',
|
||||
description:
|
||||
'Weight given to how often interest in this berth has reached EOI signing. Default 15.',
|
||||
type: 'number',
|
||||
@@ -211,7 +211,7 @@ const KNOWN_SETTINGS: Array<{
|
||||
},
|
||||
{
|
||||
key: 'tier_ladder_hide_late_stage',
|
||||
label: 'Recommender — hide late-stage tier',
|
||||
label: 'Recommender - hide late-stage tier',
|
||||
description:
|
||||
'Hide berths whose only active interests are late-stage (close to closing) from recommendations.',
|
||||
type: 'boolean',
|
||||
@@ -219,7 +219,7 @@ const KNOWN_SETTINGS: Array<{
|
||||
},
|
||||
{
|
||||
key: 'documents_show_expired_tab',
|
||||
label: 'Documents — show Expired tab',
|
||||
label: 'Documents - show Expired tab',
|
||||
description:
|
||||
'When off, the Expired tab on the documents hub is hidden. Use this when expired EOIs are noise that distracts reps from active deals.',
|
||||
type: 'boolean',
|
||||
@@ -227,12 +227,15 @@ const KNOWN_SETTINGS: Array<{
|
||||
},
|
||||
{
|
||||
key: 'berths_default_currency',
|
||||
label: 'Berths — default currency',
|
||||
label: 'Berths - default currency',
|
||||
description:
|
||||
'Currency applied to newly-created berths when none is specified on the form. Existing berths keep their per-row currency. Defaults to USD.',
|
||||
type: 'select',
|
||||
defaultValue: 'USD',
|
||||
options: SUPPORTED_CURRENCIES.map((c) => ({ value: c.code, label: `${c.code} — ${c.label}` })),
|
||||
options: SUPPORTED_CURRENCIES.map((c) => ({
|
||||
value: c.code,
|
||||
label: `${c.code} - ${c.label}`,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -350,7 +353,7 @@ export function SettingsManager() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* String + Select Settings — both render in the same card.
|
||||
{/* String + Select Settings - both render in the same card.
|
||||
'select' settings get a Select dropdown bound to setting.options;
|
||||
'string' settings get a free-text Input. */}
|
||||
{KNOWN_SETTINGS.some((s) => s.type === 'string' || s.type === 'select') && (
|
||||
@@ -526,7 +529,7 @@ export function SettingsManager() {
|
||||
this for one-off feature flags, integration secrets, or experimental tunables that the
|
||||
platform reads at runtime via{' '}
|
||||
<code className="text-xs">getSystemSetting(portId, key)</code>. Values can be JSON
|
||||
objects, plain strings, numbers, or booleans. Most reps will never need this section —
|
||||
objects, plain strings, numbers, or booleans. Most reps will never need this section -
|
||||
touch only if you know which key affects what.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -616,7 +616,7 @@ function UserSelectInput({
|
||||
<SelectValue placeholder={isLoading ? 'Loading users…' : placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE}>— No CRM user linked —</SelectItem>
|
||||
<SelectItem value={NONE}> - No CRM user linked - </SelectItem>
|
||||
{(data?.data ?? []).map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>
|
||||
{u.name || u.email} {u.name ? `· ${u.email}` : ''}
|
||||
|
||||
@@ -84,7 +84,7 @@ const S3_FIELDS: SettingFieldDef[] = [
|
||||
// (`pnpm tsx scripts/encrypt-plaintext-credentials.ts`) this field is
|
||||
// empty and the encrypted form takes over.
|
||||
key: 'storage_s3_access_key',
|
||||
label: 'S3 access key (legacy plaintext — deprecated)',
|
||||
label: 'S3 access key (legacy plaintext - deprecated)',
|
||||
description:
|
||||
'Deprecated. Use the AES-encrypted access key field below instead. After running the migration script, this row is removed and only the encrypted form is used.',
|
||||
type: 'string',
|
||||
@@ -222,13 +222,13 @@ export function StorageAdminPanel() {
|
||||
description="Where the CRM stores per-berth PDFs, brochures, GDPR exports, profile photos, and other binary files."
|
||||
/>
|
||||
|
||||
{/* AES-encrypted access key — write path. The legacy plaintext access
|
||||
{/* AES-encrypted access key - write path. The legacy plaintext access
|
||||
key field below is read-only deprecation; new writes should go
|
||||
through this card. After running the encrypt-plaintext-credentials
|
||||
migration script, the legacy field becomes empty. */}
|
||||
<RegistryDrivenForm
|
||||
title="S3 access key (encrypted)"
|
||||
description="AES-encrypted at rest. Type your access key here — it replaces the deprecated plaintext field below and fixes audit finding S-23."
|
||||
description="AES-encrypted at rest. Type your access key here - it replaces the deprecated plaintext field below and fixes audit finding S-23."
|
||||
sections={['storage.s3']}
|
||||
/>
|
||||
|
||||
@@ -254,7 +254,7 @@ export function StorageAdminPanel() {
|
||||
<div className="rounded-md border p-3 text-sm">
|
||||
{testResult.ok ? (
|
||||
<div className="flex items-center gap-2 text-emerald-600">
|
||||
<CheckCircle2 className="h-4 w-4" aria-hidden /> Connection OK — round-trip
|
||||
<CheckCircle2 className="h-4 w-4" aria-hidden /> Connection OK - round-trip
|
||||
succeeded.
|
||||
</div>
|
||||
) : (
|
||||
@@ -271,7 +271,7 @@ export function StorageAdminPanel() {
|
||||
|
||||
<SettingsFormCard
|
||||
title="Filesystem configuration"
|
||||
description="Used when the active backend is filesystem. Only single-node deployments — multi-node servers must use S3."
|
||||
description="Used when the active backend is filesystem. Only single-node deployments - multi-node servers must use S3."
|
||||
fields={FS_FIELDS}
|
||||
/>
|
||||
|
||||
@@ -335,7 +335,7 @@ export function StorageAdminPanel() {
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<strong>Switch + migrate</strong> copies every existing file to the new backend then
|
||||
flips the pointer atomically. Reversible with a follow-up reverse-migration.{' '}
|
||||
<strong>Switch only</strong> flips the pointer immediately — old files become
|
||||
<strong>Switch only</strong> flips the pointer immediately - old files become
|
||||
inaccessible until you migrate them or revert the backend.
|
||||
</p>
|
||||
</div>
|
||||
@@ -400,7 +400,7 @@ export function StorageAdminPanel() {
|
||||
{s.fileCount} existing file
|
||||
{s.fileCount === 1 ? '' : 's'} on <code className="text-xs">{s.backend}</code> will
|
||||
not be reachable from the CRM after the switch unless you migrate them later. This is
|
||||
rarely the right choice — prefer Switch + migrate.
|
||||
rarely the right choice - prefer Switch + migrate.
|
||||
</WarningCallout>
|
||||
)}
|
||||
<DialogFooter>
|
||||
|
||||
@@ -173,7 +173,7 @@ function RecentErrorsPanel() {
|
||||
<div className="space-y-0.5 min-w-0">
|
||||
<p className="font-medium truncate">{error.message}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{error.source === 'queue' ? 'Queue' : 'Audit'} —{' '}
|
||||
{error.source === 'queue' ? 'Queue' : 'Audit'} -{' '}
|
||||
{new Date(error.timestamp).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -221,7 +221,7 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) {
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
How this user appears across the app — usually their full name, but they can pick
|
||||
How this user appears across the app - usually their full name, but they can pick
|
||||
a nickname.
|
||||
</p>
|
||||
</div>
|
||||
@@ -238,7 +238,7 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) {
|
||||
/>
|
||||
{isEdit && email.trim().toLowerCase() !== originalEmail.toLowerCase() ? (
|
||||
<p className="text-xs text-amber-600">
|
||||
You'll be asked to confirm — the original address will receive an automated
|
||||
You'll be asked to confirm - the original address will receive an automated
|
||||
notice that you, the admin, changed their sign-in email.
|
||||
</p>
|
||||
) : isEdit ? (
|
||||
|
||||
@@ -210,7 +210,7 @@ export function UserPermissionMatrix({ userId }: UserPermissionMatrixProps) {
|
||||
if (isSuperAdmin) {
|
||||
return (
|
||||
<div className="rounded-md border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||
Super-admin users bypass per-port permission checks. Overrides don't apply here —
|
||||
Super-admin users bypass per-port permission checks. Overrides don't apply here -
|
||||
revoke the super-admin flag on the Profile tab first.
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user