Refactor ServerInfo and add icon and tab fallbacks

This commit is contained in:
2026-02-03 05:03:55 +01:00
parent 2973e038d6
commit 9292a4a6e2
5 changed files with 107 additions and 83 deletions
-7
View File
@@ -7,13 +7,6 @@ export default function Error({ error }: { error: string }) {
<ExclamationTriangleIcon className="w-6 h-6 text-red-600 dark:text-red-400" /> <ExclamationTriangleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />
</div> </div>
<h3 className="text-sm font-bold text-gray-900 dark:text-white mb-1">Unable to Load</h3> <h3 className="text-sm font-bold text-gray-900 dark:text-white mb-1">Unable to Load</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-6 max-w-50 leading-relaxed">{error}</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 text-xs font-medium text-gray-700 dark:text-gray-300 rounded-md transition-colors shadow-sm"
>
Try Again
</button>
</div> </div>
); );
} }
+79 -64
View File
@@ -4,13 +4,13 @@ import {
MapPinIcon, MapPinIcon,
GlobeAltIcon, GlobeAltIcon,
BuildingOfficeIcon, BuildingOfficeIcon,
CpuChipIcon,
ClipboardDocumentIcon, ClipboardDocumentIcon,
CheckIcon, CheckIcon,
ArrowTopRightOnSquareIcon CpuChipIcon
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import type { HostInfo } from '@/utils/types'; import type { HostInfo } from '@/utils/types';
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
import Error from './Error';
const CopyButton = ({ text }: { text: string }) => { const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -26,7 +26,7 @@ const CopyButton = ({ text }: { text: string }) => {
return ( return (
<button <button
onClick={handleCopy} onClick={handleCopy}
className="ml-2 p-1 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-800 transition-all" className="ml-2 p-1 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-800 transition-all opacity-0 group-hover:opacity-100 focus:opacity-100 cursor-pointer"
title="Copy to clipboard" title="Copy to clipboard"
> >
{copied ? <CheckIcon className="w-3.5 h-3.5 text-green-500" /> : <ClipboardDocumentIcon className="w-3.5 h-3.5" />} {copied ? <CheckIcon className="w-3.5 h-3.5 text-green-500" /> : <ClipboardDocumentIcon className="w-3.5 h-3.5" />}
@@ -39,19 +39,21 @@ const InfoRow = ({
label, label,
value, value,
href, href,
canCopy canCopy,
iconColor = "text-gray-400 dark:text-gray-500"
}: { }: {
icon: any, icon: any,
label: string, label: string,
value: string | null, value: string | null,
href?: string, href?: string,
canCopy?: boolean canCopy?: boolean,
iconColor?: string
}) => { }) => {
if (!value) return null; if (!value) return null;
return ( return (
<div className="flex items-start py-2.5 border-b border-gray-100 dark:border-gray-800 last:border-0"> <div className="group flex items-start py-3 border-b border-gray-100 dark:border-gray-800 last:border-0">
<div className="mt-0.5 mr-3 text-gray-400 dark:text-gray-500"> <div className={`mt-0.5 mr-3 ${iconColor}`}>
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -62,13 +64,12 @@ const InfoRow = ({
href={href} href={href}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-sm text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 truncate transition-colors flex items-center gap-1" className="text-sm font-medium text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 truncate transition-colors flex items-center gap-1.5"
> >
<span className="truncate">{value}</span> <span className="truncate">{value}</span>
<ArrowTopRightOnSquareIcon className="w-3 h-3 opacity-50" />
</a> </a>
) : ( ) : (
<span className="text-sm text-gray-900 dark:text-gray-100 truncate select-all">{value}</span> <span className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate select-all">{value}</span>
)} )}
{canCopy && <CopyButton text={value} />} {canCopy && <CopyButton text={value} />}
</div> </div>
@@ -80,14 +81,33 @@ const InfoRow = ({
export default function ServerInfo({ data }: { data: HostInfo }) { export default function ServerInfo({ data }: { data: HostInfo }) {
const { network, location, domain, isBrowserResource } = data; const { network, location, domain, isBrowserResource } = data;
// URL generation for flags
const getFlagUrl = (code?: string | null) => {
if (!code) return '';
try {
const path = `/${code.toLowerCase()}.webp`;
return browser.runtime.getURL(path as any);
} catch {
return '';
}
};
// Header Component // Header Component
const Header = ({ title, subtitle, icon: Icon }: { title: string, subtitle?: string, icon?: any }) => ( const Header = ({ title, flagCode }: { title: string, flagCode?: string | null }) => (
<div className="px-5 py-4 bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800 flex items-center gap-3"> <div className="px-5 py-4 bg-gray-50/50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
{Icon && <Icon className="w-6 h-6 text-gray-500" />} <div className="min-w-0 pr-3">
<div> <h1 className="text-base font-bold text-gray-900 dark:text-white truncate" title={title}>
<h2 className="text-base font-semibold text-gray-900 dark:text-white leading-tight">{title}</h2> {title}
{subtitle && <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{subtitle}</p>} </h1>
</div> </div>
{flagCode && (
<img
src={getFlagUrl(flagCode)}
alt={flagCode}
className="w-8 h-auto rounded shadow-sm border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"
onError={(e) => (e.currentTarget.style.display = 'none')}
/>
)}
</div> </div>
); );
@@ -97,46 +117,55 @@ export default function ServerInfo({ data }: { data: HostInfo }) {
<div className="w-80 bg-white dark:bg-gray-950 font-sans"> <div className="w-80 bg-white dark:bg-gray-950 font-sans">
<Header <Header
title="System Resource" title="System Resource"
subtitle="Local browser page" flagCode="unknown"
icon={CpuChipIcon}
/> />
<div className="p-6 flex flex-col items-center justify-center text-center"> <div className="p-5">
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-3"> <InfoRow
<CpuChipIcon className="w-6 h-6 text-gray-400" /> icon={CpuChipIcon}
</div> label="Type"
<p className="text-sm text-gray-500">No network information available for this page.</p> value="Local Browser Page"
iconColor="text-orange-500"
/>
<InfoRow
icon={GlobeAltIcon}
label="URL"
value={data.url}
iconColor="text-gray-400"
/>
</div>
<div className="px-5 pb-5 text-xs text-gray-400 text-center">
This page is generated locally by your browser.
</div> </div>
</div> </div>
); );
} }
if (!network) return null; // Fallback if network data is missing
if (!network) {
const flagUrl = location?.countryCode return <Error error="Host information unavailable." />;
? `/${location.countryCode.toLowerCase()}.webp` }
: '';
// Local Network View // Local Network View
if (network.isLocal) { if (network.isLocal) {
return ( return (
<div className="w-80 bg-white dark:bg-gray-950 font-sans"> <div className="w-80 bg-white dark:bg-gray-950 font-sans">
<Header <Header
title="Private Network" title={domain}
subtitle="Local or internal resource" flagCode="unknown"
icon={ServerIcon}
/> />
<div className="p-5"> <div className="p-5">
<InfoRow
icon={CpuChipIcon}
label="Type"
value="Local / Private Network"
iconColor="text-orange-500"
/>
<InfoRow <InfoRow
icon={ServerIcon} icon={ServerIcon}
label="IP Address" label="IP Address"
value={network.ip} value={network.ip}
canCopy canCopy
/> iconColor="text-blue-500"
<InfoRow
icon={GlobeAltIcon}
label="Hostname"
value={network.hostname || 'Localhost'}
canCopy
/> />
</div> </div>
</div> </div>
@@ -146,64 +175,50 @@ export default function ServerInfo({ data }: { data: HostInfo }) {
// Public Internet View // Public Internet View
return ( return (
<div className="w-80 bg-white dark:bg-gray-950 font-sans text-gray-900 dark:text-gray-100"> <div className="w-80 bg-white dark:bg-gray-950 font-sans text-gray-900 dark:text-gray-100">
<div className="px-5 py-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between bg-gray-50 dark:bg-gray-900/50"> <Header
<div className="min-w-0 pr-2"> title="Host Information"
<h1 className="text-base font-bold text-gray-900 dark:text-white truncate" title={domain}> flagCode={location?.countryCode || 'unknown'}
{domain} />
</h1>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-green-500"></span>
Publicly Accessible
</p>
</div>
{location?.countryCode && (
<img
src={flagUrl}
alt={location.countryCode}
className="w-8 h-auto rounded shadow-sm border border-gray-200 dark:border-gray-700"
onError={(e) => (e.currentTarget.style.display = 'none')}
/>
)}
</div>
<div className="p-5 space-y-1"> <div className="p-5 space-y-0.5">
<InfoRow <InfoRow
icon={ServerIcon} icon={ServerIcon}
label="IP Address" label="IP Address"
value={network.ip} value={network.ip}
href={`https://ip.albert.lol/${network.ip}`} href={`https://ip.albert.lol/${network.ip}`}
canCopy canCopy
iconColor="text-blue-500"
/> />
<InfoRow <InfoRow
icon={GlobeAltIcon} icon={GlobeAltIcon}
label="Hostname" label="Hostname"
value={network.hostname} value={network.hostname}
canCopy canCopy
iconColor="text-indigo-500"
/> />
<InfoRow <InfoRow
icon={MapPinIcon} icon={MapPinIcon}
label="Location" label="Location"
value={location?.countryName || 'Unknown'} value={location?.countryName || 'Unknown Location'}
iconColor="text-emerald-500"
/> />
<InfoRow <InfoRow
icon={BuildingOfficeIcon} icon={BuildingOfficeIcon}
label="Organization / ASN" label="Organization / ASN"
value={network.org} value={network.org}
href={network.asn ? `https://bgp.he.net/${network.asn}` : undefined} href={network.asn ? `https://bgp.he.net/${network.asn}` : undefined}
iconColor="text-violet-500"
/> />
</div> </div>
<div className="px-5 pb-5 pt-1"> <div className="px-5 pb-5 pt-2">
<a <a
href={`https://platform.censys.io/search?q=${network.hostname || domain}`} href={`https://platform.censys.io/search?q=${network.hostname || domain}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="flex items-center justify-center w-full py-2 px-4 bg-gray-900 dark:bg-gray-100 hover:bg-gray-800 dark:hover:bg-gray-200 text-white dark:text-gray-900 rounded-md transition-colors text-xs font-medium shadow-sm" className="flex items-center justify-center w-full py-2 px-4 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-md transition-all text-xs font-medium shadow-sm hover:shadow cursor-pointer"
> >
<GlobeAltIcon className="w-3.5 h-3.5 mr-2" /> <GlobeAltIcon className="w-3.5 h-3.5 mr-2 text-gray-400" />
Analyze on Censys Analyze on Censys
</a> </a>
</div> </div>
+2 -2
View File
@@ -4,7 +4,7 @@ import { StorageService } from '@/utils/storage';
export default defineBackground({ export default defineBackground({
main() { main() {
// Listen for Network Responses (Source of Truth for IPs) // Listen for Network Responses
browser.webRequest.onResponseStarted.addListener( browser.webRequest.onResponseStarted.addListener(
async (details) => { async (details) => {
if (details.tabId === -1 || details.type !== 'main_frame' || !details.ip) return; if (details.tabId === -1 || details.type !== 'main_frame' || !details.ip) return;
@@ -27,7 +27,7 @@ export default defineBackground({
} }
}); });
// 3. Cleanup // Cleanup
browser.tabs.onRemoved.addListener(async (tabId) => { browser.tabs.onRemoved.addListener(async (tabId) => {
await StorageService.remove(tabId); await StorageService.remove(tabId);
}); });
+2 -8
View File
@@ -5,15 +5,9 @@ const ICON_CACHE = new Map<string, Record<string, ImageData>>();
export const IconService = { export const IconService = {
async update(tabId: number, countryCode: string | null, isLocal: boolean) { async update(tabId: number, countryCode: string | null, isLocal: boolean) {
try { try {
if (isLocal) { const code = isLocal ? 'unknown' : (countryCode ? countryCode.toLowerCase() : 'unknown');
const path = '/icon/128.png' as string;
await browser.action.setIcon({ tabId, path });
return;
}
const code = countryCode ? countryCode.toLowerCase() : 'unknown';
const imageData = await this.getIconData(code); const imageData = await this.getIconData(code);
await browser.action.setIcon({ tabId, imageData }); await browser.action.setIcon({ tabId, imageData });
} catch (e) { } catch (e) {
console.warn('Failed to update icon', e); console.warn('Failed to update icon', e);
@@ -37,7 +31,6 @@ export const IconService = {
ICON_CACHE.set(code, data); ICON_CACHE.set(code, data);
return data; return data;
} catch { } catch {
// Fallback to unknown if specific flag fails
if (code !== 'unknown') return this.getIconData('unknown'); if (code !== 'unknown') return this.getIconData('unknown');
throw new Error('Failed to load fallback icon'); throw new Error('Failed to load fallback icon');
} }
@@ -52,6 +45,7 @@ export const IconService = {
const offsetX = (canvas.width - bitmap.width * ratio) / 2; const offsetX = (canvas.width - bitmap.width * ratio) / 2;
const offsetY = (canvas.height - bitmap.height * ratio) / 2; const offsetY = (canvas.height - bitmap.height * ratio) / 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, offsetX, offsetY, bitmap.width * ratio, bitmap.height * ratio); ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, offsetX, offsetY, bitmap.width * ratio, bitmap.height * ratio);
const sizes = [16, 32, 48, 128]; const sizes = [16, 32, 48, 128];
+24 -2
View File
@@ -32,7 +32,7 @@ export const Tab = {
return; return;
} }
// Handle Local/Private IPs // Handle Local/Private IPs (Bogons)
if (IpUtils.isLocalOrBogon(ip)) { if (IpUtils.isLocalOrBogon(ip)) {
const localInfo: HostInfo = { const localInfo: HostInfo = {
...initialState, ...initialState,
@@ -55,8 +55,30 @@ export const Tab = {
// Fetch Public Data // Fetch Public Data
const geoData = await GeoService.resolve(ip); const geoData = await GeoService.resolve(ip);
// If Geo lookup fails (e.g. LAN domain with public IP, or API down),
// we still want to show the IP we captured.
if (!geoData) { if (!geoData) {
await StorageService.set(tabId, { ...initialState, loading: false, error: 'Failed to fetch host info' }); const fallbackInfo: HostInfo = {
...initialState,
loading: false,
network: {
ip,
hostname: null,
asn: null,
org: 'Unknown',
isLocal: false,
isBogon: false
},
location: {
countryCode: null,
countryName: 'Unknown Location',
city: null,
region: null,
timezone: null
}
};
await StorageService.set(tabId, fallbackInfo);
await IconService.update(tabId, 'unknown', false);
return; return;
} }