refactor(ts-migration): Phase 11 batch 10 — the authoring and settings pages

Nine admin pages: `AgentDetail`, both skill editors, the three Workspace
pages, `SkillDevelopment`, `Settings` and `Profile`. 9/9 byte-identical,
bundle hash unchanged.

Six errors, five of them the same shape this phase has met repeatedly:
`Object.values(skill.ui || {}).flatMap((page) => page.sections)` yields
`unknown` because inference into the union parameter of `values` does not
distribute. Three copies of that line across the two editors and
`WorkspaceSkills`, plus one `Object.entries` and one `new Set` whose
element type reached a React `key`, where `unknown` is not allowed.

The sixth is worth its own line. `WorkspaceSkills.toggleSkill` builds a
preferences patch as `{ disabledSkills }` and then adds `customSkills` to
it conditionally, several lines later, when re-enabling a skill flips an
inactive definition back to active. The literal's inferred type does not
carry a key assigned after the fact, so the later write looked wrong.
The annotation names both keys and marks the conditional one optional,
which is what the function does.

Measured against `b99dc7c`:

  typecheck   6 errors, unchanged; no new error anywhere
  lint        exit 0, 0 errors, 289 warnings
  npm test    1684/1691, the same 7 failures verbatim
  build       exit 0, identical bundle hash 74d17e2d…
  type erasure  113/113 byte-identical across Phase 11 so far

Nine files left in Phase 11, all data pages. No baseline artifact
touched — three of those nine are the pages the baselines cover, so the
recapture question arrives with the next batch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
2026-09-18 15:40:35 +05:30
parent b99dc7c576
commit a0f3f77900
9 changed files with 6 additions and 6 deletions

498
src/pages/admin/Profile.tsx Normal file
View File

@@ -0,0 +1,498 @@
import React, { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Building2, Check, KeyRound, LogOut, Pencil, Settings as SettingsIcon, Shield,
ShieldCheck, Smartphone, CheckCircle2, Lock, Eye, Sparkles, Clock, Laptop,
Activity, Zap, AlertCircle, Key
} from 'lucide-react';
import { Button, Field, Input, Modal, Toggle, toast,
} from '@/components/ds';
import { base44 } from '@/api/base44Client';
import { useCurrentUser, useUpdateProfile, useUserActivity } from '@/lib/krowHooks';
import { AdminPage } from '@/components/admin/PageShell';
import { endAdminSession } from '@/lib/admin/session';
import { PERMISSIONS } from '@/lib/admin/permissions';
import { SkillSurface } from '@/components/skills/SkillSurface';
/**
* Admin Profile — Premium enterprise account & security control center.
*/
export default function AdminProfile() {
const navigate = useNavigate();
const { data: user } = useCurrentUser();
const updateProfile = useUpdateProfile();
const [editOpen, setEditOpen] = useState(false);
const [passwordOpen, setPasswordOpen] = useState(false);
const [twoFactor, setTwoFactor] = useState(true);
const [form, setForm] = useState({ full_name: '', email: '' });
const [nameError, setNameError] = useState('');
const saving = updateProfile.isPending;
/* Stamped once on mount */
const [sessionStarted] = useState(() =>
new Date().toLocaleString(undefined, {
weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
})
);
/** Recognise device without fingerprinting */
const deviceLabel = useMemo(() => {
const ua = navigator.userAgent;
const os = /Mac/i.test(ua) ? 'macOS' : /Windows/i.test(ua) ? 'Windows'
: /Android/i.test(ua) ? 'Android' : /iPhone|iPad/i.test(ua) ? 'iOS' : 'Unknown OS';
const browser = /Edg\//i.test(ua) ? 'Edge' : /Chrome\//i.test(ua) ? 'Chrome'
: /Safari\//i.test(ua) ? 'Safari' : /Firefox\//i.test(ua) ? 'Firefox' : 'Unknown browser';
return `${browser} on ${os}`;
}, []);
const { data: activity = [] } = useUserActivity();
React.useEffect(() => {
if (!editOpen || !user) return;
setForm({ full_name: user.full_name || '', email: user.email || '' });
setNameError('');
}, [editOpen, user]);
const myActivity = useMemo(
() => activity.filter((e) => e.user_email === user?.email).slice(0, 6),
[activity, user]
);
const save = async () => {
const fullName = form.full_name.trim();
if (!fullName) {
setNameError('Name cannot be empty.');
return;
}
if (fullName.length > 60) {
setNameError('Name must be 60 characters or fewer.');
return;
}
setNameError('');
try {
await updateProfile.mutateAsync({ full_name: fullName });
setEditOpen(false);
toast.success('Profile updated');
} catch {
toast.error('Could not save your profile');
}
};
const fullName = user?.full_name || 'Admin User';
const email = user?.email || 'admin@krow.app';
const memberSince = user
? new Date(user.created_date).toLocaleDateString(undefined, { month: 'long', year: 'numeric' })
: 'June 2026';
return (
<AdminPage
title="Admin Profile"
subtitle="Manage your platform credentials, system permissions and security governance."
/* Three buttons need 295px; a 320px phone has 273 after the page gutters,
so the row wraps instead of pushing Sign Out off the edge. */
actions={
<div className="flex flex-wrap items-center gap-2">
<button
onClick={() => navigate('/admin/settings')}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-white border border-[#E0E4EA] text-[#101820] text-[13px] font-semibold hover:bg-[#F8F8F8] transition-all shadow-xs"
>
<SkillSurface page="profile" placement="after-header" />
<SettingsIcon className="w-4 h-4 text-[#687386]" />
Settings
</button>
<button
onClick={() => setEditOpen(true)}
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-[#0838E0] hover:bg-[#062BAF] text-white text-[13px] font-semibold transition-all shadow-md shadow-blue-500/20 hover:scale-[1.02]"
>
<Pencil className="w-4 h-4" />
Edit Profile
</button>
<button
onClick={() => { endAdminSession(); base44.auth.logout('/admin/login'); }}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-red-50 hover:bg-red-100 text-red-600 border border-red-200/60 text-[13px] font-semibold transition-all"
>
<LogOut className="w-4 h-4" />
Sign Out
</button>
</div>
}
>
<div className="space-y-6">
{/* ── 1. Hero Identity Banner ────────────────────────────────────────────── */}
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-[#0838E0] via-[#1E40AF] to-[#0284C7] p-6 sm:p-8 text-white shadow-xl">
{/* Subtle background ambient mesh */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-white/15 via-transparent to-transparent pointer-events-none" />
<div className="absolute -right-16 -bottom-16 w-64 h-64 rounded-full bg-white/5 blur-3xl pointer-events-none" />
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="flex items-center gap-5">
{/* Avatar with status ring */}
<div className="relative shrink-0">
<div className="w-20 h-20 sm:w-24 sm:h-24 rounded-2xl bg-white/20 backdrop-blur-md border-2 border-white/40 shadow-2xl flex items-center justify-center text-white text-3xl font-extrabold overflow-hidden">
{user?.avatar_url ? (
<img src={user.avatar_url} alt={fullName} className="w-full h-full object-cover" />
) : (
fullName.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase()
)}
</div>
<span className="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-[#16A765] text-white ring-4 ring-[#0838E0]">
<Check className="w-3.5 h-3.5 stroke-[3]" />
</span>
</div>
{/* User details */}
<div className="space-y-1.5">
<div className="flex items-center gap-2 flex-wrap">
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-white">{fullName}</h1>
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full bg-white/20 backdrop-blur-md text-white text-[11px] font-semibold border border-white/30">
<ShieldCheck className="w-3.5 h-3.5 text-sky-300" />
Verified Admin
</span>
</div>
<p className="text-[14px] text-blue-100 font-normal">{email}</p>
<div className="flex items-center gap-2 pt-1 flex-wrap">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-white/10 backdrop-blur-md text-white text-[12px] font-medium border border-white/20">
<Shield className="w-3.5 h-3.5 text-yellow-300" />
Platform Administrator
</span>
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-[#16A765]/20 backdrop-blur-md text-emerald-200 text-[12px] font-semibold border border-[#16A765]/40">
<span className="w-2 h-2 rounded-full bg-[#16A765] animate-ping shrink-0" />
Active · Verified
</span>
{twoFactor && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-purple-500/20 backdrop-blur-md text-purple-200 text-[12px] font-medium border border-purple-400/30">
<Zap className="w-3.5 h-3.5 text-purple-300" />
2FA Protected
</span>
)}
</div>
</div>
</div>
{/* Quick stats in Hero */}
<div className="flex flex-wrap items-center gap-3 border-t md:border-t-0 md:border-l md:flex-nowrap border-white/20 pt-4 md:pt-0 md:pl-6">
<div className="flex-1 md:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Scopes</p>
<p className="text-2xl font-bold text-white mt-0.5">{PERMISSIONS.length}</p>
<span className="text-[10px] text-blue-200">Full Control</span>
</div>
<div className="flex-1 md:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Security Level</p>
<p className="text-2xl font-bold text-emerald-300 mt-0.5">Tier 1</p>
<span className="text-[10px] text-blue-200">Max Privileges</span>
</div>
</div>
</div>
</div>
{/* ── 2. Information Cards Grid ────────────────────────────────────────── */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-4 shadow-xs hover:shadow-md transition-all group">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-[#687386]">System Role</span>
<div className="w-8 h-8 rounded-xl bg-blue-50 text-[#0838E0] flex items-center justify-center group-hover:scale-110 transition-transform">
<Shield className="w-4 h-4" />
</div>
</div>
<p className="text-[15px] font-bold text-[#101820]">Platform Administrator</p>
<p className="text-[12px] text-[#687386] mt-0.5">Full System Governance</p>
</div>
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-4 shadow-xs hover:shadow-md transition-all group">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-[#687386]">Department</span>
<div className="w-8 h-8 rounded-xl bg-indigo-50 text-indigo-600 flex items-center justify-center group-hover:scale-110 transition-transform">
<Building2 className="w-4 h-4" />
</div>
</div>
<p className="text-[15px] font-bold text-[#101820]">Workforce Operations</p>
<p className="text-[12px] text-[#687386] mt-0.5">Enterprise Management</p>
</div>
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-4 shadow-xs hover:shadow-md transition-all group">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-[#687386]">Account Status</span>
<div className="w-8 h-8 rounded-xl bg-emerald-50 text-[#16A765] flex items-center justify-center group-hover:scale-110 transition-transform">
<ShieldCheck className="w-4 h-4" />
</div>
</div>
<p className="text-[15px] font-bold text-[#101820]">Active · Verified</p>
<p className="text-[12px] text-[#16A765] font-medium mt-0.5">All services active</p>
</div>
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-4 shadow-xs hover:shadow-md transition-all group">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-[#687386]">Member Since</span>
<div className="w-8 h-8 rounded-xl bg-violet-50 text-violet-600 flex items-center justify-center group-hover:scale-110 transition-transform">
<Clock className="w-4 h-4" />
</div>
</div>
<p className="text-[15px] font-bold text-[#101820]">{memberSince}</p>
<p className="text-[12px] text-[#687386] mt-0.5">Authenticated Account</p>
</div>
</div>
{/* ── 3. Main Split View: Left (Permissions & Activity) | Right (Security & Session) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column (2 Cols) */}
<div className="lg:col-span-2 space-y-6">
{/* Permissions Matrix. `id` is the anchor Settings → Users &
permissions links to, so that row lands on this card. */}
<div id="perms" className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-[16px] font-bold text-[#101820] flex items-center gap-2">
<Key className="w-4 h-4 text-[#0838E0]" />
Access & Scope Permissions
</h2>
<p className="text-[13px] text-[#687386]">Operational scopes granted to your administrative account.</p>
</div>
<span className="px-3 py-1 rounded-full bg-blue-50 text-[#0838E0] text-[12px] font-semibold border border-blue-200/60">
{PERMISSIONS.length} Active Scopes
</span>
</div>
<div className="divide-y divide-[#E0E4EA]">
{PERMISSIONS.map((p) => {
const isFull = p.level === 'Full access';
return (
<div key={p.scope} className="py-3.5 first:pt-0 last:pb-0 flex items-center justify-between gap-4 hover:bg-[#F8F8F8] px-3 -mx-3 rounded-xl transition-colors">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="text-[14px] font-semibold text-[#101820]">{p.scope}</p>
</div>
<p className="text-[12px] text-[#687386] mt-0.5">{p.detail}</p>
</div>
<div className="flex items-center gap-3 shrink-0">
{/* Status badge */}
{isFull ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#EAF8F0] text-[#16A765] text-[12px] font-bold border border-[#16A765]/30">
<CheckCircle2 className="w-3.5 h-3.5" />
Full Access
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#E8F0F8] text-[#0838E0] text-[12px] font-bold border border-[#0838E0]/20">
<Eye className="w-3.5 h-3.5" />
Read Only
</span>
)}
</div>
</div>
);
})}
</div>
</div>
{/* Recent Activity */}
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-[16px] font-bold text-[#101820] flex items-center gap-2">
<Activity className="w-4 h-4 text-[#0838E0]" />
Recent Account Activity
</h2>
<p className="text-[13px] text-[#687386]">Audit log of administrative actions performed on your profile.</p>
</div>
<span className="px-3 py-1 rounded-full bg-[#F8F8F8] text-[#687386] text-[12px] font-medium border border-[#E0E4EA]">
{myActivity.length} Events Logged
</span>
</div>
{myActivity.length ? (
<div className="space-y-3">
{myActivity.map((e) => (
<div key={e.id} className="flex items-start gap-3 p-3 rounded-xl border border-[#E0E4EA] bg-[#F8F8F8]/50 hover:bg-white hover:shadow-xs transition-all">
<div className="w-8 h-8 rounded-lg bg-blue-50 text-[#0838E0] flex items-center justify-center shrink-0 mt-0.5">
<Sparkles className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[13px] font-semibold text-[#101820] capitalize">
{e.event_type.replace(/_/g, ' ')}
</p>
{e.details && <p className="text-[12px] text-[#687386] truncate mt-0.5">{e.details}</p>}
</div>
<span className="text-[11px] font-medium text-[#8B94A5] shrink-0">
{new Date(e.created_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
</span>
</div>
))}
</div>
) : (
<div className="text-center py-8 bg-[#F8F8F8] rounded-xl border border-dashed border-[#E0E4EA]">
<Activity className="w-8 h-8 text-[#8B94A5] mx-auto mb-2" />
<p className="text-[13px] text-[#687386]">No recent activity recorded for this account.</p>
</div>
)}
</div>
</div>
{/* Right Column (1 Col) - Security & Session */}
<div className="space-y-6">
{/* Security Control Card. `id` is the anchor the account menu's
Security item and Settings → Security both point at. */}
<div id="security" className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs space-y-5">
<div className="flex items-center gap-2 border-b border-[#E0E4EA] pb-3">
<ShieldCheck className="w-5 h-5 text-[#0838E0]" />
<h2 className="text-[16px] font-bold text-[#101820]">Security & 2FA</h2>
</div>
{/* Last Sign-In */}
<div className="p-3.5 rounded-xl bg-[#F8F8F8] border border-[#E0E4EA]">
<div className="flex items-center justify-between">
<span className="text-[11px] font-bold uppercase tracking-wider text-[#687386]">Last Sign-in</span>
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-[#16A765] bg-[#EAF8F0] px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-[#16A765]" />
Active Device
</span>
</div>
<p className="text-[14px] font-bold text-[#101820] mt-1">
{new Date().toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}
</p>
<p className="text-[12px] text-[#687386] mt-0.5">IP: 10.24.11.86 · {deviceLabel}</p>
</div>
{/* 2FA Toggle */}
<div className="p-3.5 rounded-xl border border-[#E0E4EA] bg-white">
<Toggle
label="Two-Factor Authentication"
description="Require code verification for all admin logins"
checked={twoFactor}
onCheckedChange={(v) => {
setTwoFactor(v);
toast[v ? 'success' : 'error'](v ? '2FA enabled' : '2FA disabled');
}}
/>
</div>
{/* Action Buttons */}
<div className="space-y-2.5">
<button
onClick={() => setPasswordOpen(true)}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-[#E0E4EA] bg-white hover:bg-[#F8F8F8] text-[#101820] text-[13px] font-semibold transition-all shadow-xs"
>
<KeyRound className="w-4 h-4 text-[#0838E0]" />
Change Password
</button>
<button
onClick={() => toast.info('Session management is active for this device.')}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-[#E0E4EA] bg-white hover:bg-[#F8F8F8] text-[#101820] text-[13px] font-semibold transition-all shadow-xs"
>
<Smartphone className="w-4 h-4 text-[#687386]" />
Manage Active Sessions
</button>
</div>
{/* Password expiry note */}
<div className="flex items-start gap-2.5 p-3 rounded-xl bg-amber-50/80 border border-amber-200/60 text-amber-900 text-[12px] leading-relaxed">
<AlertCircle className="w-4 h-4 text-amber-600 shrink-0 mt-0.5" />
<p>
Password last changed <strong>42 days ago</strong>. Administrator credentials expire every 90 days.
</p>
</div>
</div>
{/* Current Session Card */}
<div className="bg-white rounded-2xl border border-[#E0E4EA] p-6 shadow-xs space-y-4">
<div className="flex items-center gap-2 border-b border-[#E0E4EA] pb-3">
<Laptop className="w-5 h-5 text-[#0838E0]" />
<h2 className="text-[16px] font-bold text-[#101820]">Current Active Session</h2>
</div>
<dl className="space-y-3 text-[13px]">
<div className="flex justify-between items-center border-b border-[#F1F4F7] pb-2.5">
<dt className="text-[#687386] font-medium">Session Started</dt>
<dd className="font-semibold text-[#101820]">{sessionStarted}</dd>
</div>
<div className="flex justify-between items-center border-b border-[#F1F4F7] pb-2.5">
<dt className="text-[#687386] font-medium">Session Type</dt>
<dd className="font-semibold text-[#101820]">Browser Tab</dd>
</div>
<div className="flex justify-between items-center border-b border-[#F1F4F7] pb-2.5">
<dt className="text-[#687386] font-medium">Platform / OS</dt>
<dd className="font-semibold text-[#101820]">{deviceLabel}</dd>
</div>
<div className="flex justify-between items-center">
<dt className="text-[#687386] font-medium">Token Security</dt>
<dd className="font-semibold text-[#16A765] inline-flex items-center gap-1">
<Lock className="w-3.5 h-3.5" /> Encrypted
</dd>
</div>
</dl>
<div className="pt-2">
<p className="text-[11px] text-[#8B94A5] leading-relaxed bg-[#F8F8F8] p-3 rounded-xl border border-[#E0E4EA]">
Administrator sessions expire automatically on browser close to enforce zero-trust security.
</p>
</div>
</div>
</div>
</div>
{/* Edit Profile Modal */}
<Modal
open={editOpen}
onOpenChange={setEditOpen}
title="Edit Admin Profile"
description="Update your display name and administrative credentials."
icon={Pencil}
size="sm"
busy={saving}
footer={
<>
<Button variant="outline" onClick={() => setEditOpen(false)} disabled={saving}>Cancel</Button>
<Button onClick={save} loading={saving}>Save Changes</Button>
</>
}
>
<div className="space-y-4">
<Field label="Full Name" required error={nameError}>
<Input
value={form.full_name}
onChange={(e) => {
setForm({ ...form, full_name: e.target.value });
if (nameError) setNameError('');
}}
/>
</Field>
<Field label="Email Address" hint="Email changes require system verification.">
<Input value={form.email} readOnly disabled />
</Field>
</div>
</Modal>
{/* Change Password Modal */}
<Modal
open={passwordOpen}
onOpenChange={setPasswordOpen}
title="Change Password"
description="Update your administrator login password."
icon={KeyRound}
size="sm"
footer={
<>
<Button variant="outline" onClick={() => setPasswordOpen(false)}>Cancel</Button>
<Button onClick={() => { setPasswordOpen(false); toast.success('Password updated'); }}>
Update Password
</Button>
</>
}
>
<div className="space-y-4">
<Field label="Current Password"><Input type="password" autoComplete="current-password" /></Field>
<Field label="New Password" hint="At least 12 characters, with numbers and symbols.">
<Input type="password" autoComplete="new-password" />
</Field>
<Field label="Confirm New Password"><Input type="password" autoComplete="new-password" /></Field>
</div>
</Modal>
</div>
<SkillSurface page="profile" placement="before-footer" />
</AdminPage>
);
}