mirror of
https://github.com/skidoodle/hostinfo
synced 2026-04-28 09:37:37 +02:00
Refactor ServerInfo and add icon and tab fallbacks
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-62
@@ -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-5">
|
||||||
|
<InfoRow
|
||||||
|
icon={CpuChipIcon}
|
||||||
|
label="Type"
|
||||||
|
value="Local Browser Page"
|
||||||
|
iconColor="text-orange-500"
|
||||||
|
/>
|
||||||
|
<InfoRow
|
||||||
|
icon={GlobeAltIcon}
|
||||||
|
label="URL"
|
||||||
|
value={data.url}
|
||||||
|
iconColor="text-gray-400"
|
||||||
/>
|
/>
|
||||||
<div className="p-6 flex flex-col items-center justify-center text-center">
|
|
||||||
<div className="w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-3">
|
|
||||||
<CpuChipIcon className="w-6 h-6 text-gray-400" />
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500">No network information available for this page.</p>
|
<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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user