Files
pastilhasandClaude Opus 5 864dd16355 transmission, extracted from the platform into its own repository
The sixth plugin out, immediately after photos. A sidecar, a proxy router and
29 web files; no db/ directory, because there are no tables.

── The one thing that is not self-contained ──

The connection lives in `service_connections`, a CORE table, reached through
getServiceConnection/saveServiceConnection/getServiceCredentials. This plugin
declares no schema.

Deliberate: that table is the app store's shared credential store, keyed on
(user_id, service), already encrypted at rest and already read by
app-store/effects.ts. A private copy of the same three columns would be a
second place the same fact could live, and the runbook is explicit that a move
plus a data-format change gives any failure two causes.

The cost, stated rather than hidden: this plugin depends on a core facility it
does not own, and would break if that table changed shape. Photos, by contrast,
owns photos_config outright.

── 38 palette classes paid down, mapped by meaning ──

  blue     downloading / download speed  -> info
  cyan     checking (verifying)          -> busy
  emerald  seeding / complete            -> success
  amber    queued / stalled / warnings   -> warning

Collapsing blue and cyan into one tone would have lost a real distinction in
the status dot, so `checking` went to `busy` — in-progress work, which is what
that tone is for.

── A near-miss worth recording ──

The first copy globbed *.tsx and *.ts and silently missed detail/ and dialogs/
— ten components. tsgo caught it as ten "cannot find module" errors. Worth
stating because it is quiet in the other direction: had those been imported
lazily, the typecheck would have passed and the panels would have broken at
runtime. Check the directory tree, not the file glob.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:19:30 +00:00

129 lines
5.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReactNode } from 'react';
import type { Torrent } from '../shared';
import { Check, Copy } from 'lucide-react';
import { useState } from 'react';
import {
formatDate,
formatDuration,
formatEta,
formatPercent,
formatRatio,
formatSize,
formatSpeed,
PRIORITY_LABELS,
statusLabel,
} from '../format';
import { copyToClipboard } from 'helpers/clipboard';
// The "what is this torrent" tab: transfer numbers, then the immutable facts from the metainfo.
type GeneralTabProps = { torrent: Torrent };
export const GeneralTab = ({ torrent: t }: GeneralTabProps) => (
<div className="space-y-5 p-4">
{t.error !== 0 && t.errorString && (
<p className="rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive">{t.errorString}</p>
)}
<Section title="Transfer">
<Field label="Status" value={statusLabel(t)} />
<Field label="Progress" value={formatPercent(t.percentDone)} />
<Field label="Downloaded" value={formatSize(t.downloadedEver)} />
<Field label="Uploaded" value={formatSize(t.uploadedEver)} />
<Field label="Ratio" value={formatRatio(t.uploadRatio)} />
<Field label="Remaining" value={formatSize(t.leftUntilDone)} />
<Field label="Down speed" value={formatSpeed(t.rateDownload) || '—'} />
<Field label="Up speed" value={formatSpeed(t.rateUpload) || '—'} />
<Field label="ETA" value={t.percentDone >= 1 ? '—' : formatEta(t.eta)} />
<Field label="Seeds / peers" value={`${t.peersSendingToUs} / ${t.peersGettingFromUs}`} />
<Field label="Seeding time" value={formatDuration(t.secondsSeeding)} />
{/* Corrupt bytes are re-downloaded silently, so this is the only place a failing disk or a bad peer
shows up before the ratio quietly goes wrong. */}
<Field label="Corrupt" value={t.corruptEver ? formatSize(t.corruptEver) : '—'} />
</Section>
<Section title="Dates">
<Field label="Added" value={formatDate(t.addedDate)} />
<Field label="Completed" value={formatDate(t.doneDate)} />
<Field label="Last active" value={formatDate(t.activityDate)} />
<Field label="Created" value={formatDate(t.dateCreated)} />
</Section>
<Section title="Torrent">
<Field label="Total size" value={formatSize(t.totalSize)} />
<Field label="Files" value={String(t['file-count'] ?? '—')} />
<Field label="Pieces" value={`${t.pieceCount} × ${formatSize(t.pieceSize)}`} />
<Field label="Privacy" value={t.isPrivate ? 'Private torrent' : 'Public torrent'} />
<Field label="Priority" value={PRIORITY_LABELS[t.bandwidthPriority] ?? '—'} />
<Field label="Queue position" value={String(t.queuePosition)} />
<Field label="Creator" value={t.creator || '—'} />
<Field label="Peer limit" value={String(t['peer-limit'] ?? '—')} />
</Section>
<Section title="Location" columns={1}>
<Field label="Download directory" value={t.downloadDir} mono />
<Field label="Hash" value={t.hashString ?? '—'} mono copy={t.hashString} />
<Field label="Magnet" value={t.magnetLink || '—'} mono truncate copy={t.magnetLink} />
{t.labels.length > 0 && <Field label="Labels" value={t.labels.join(', ')} />}
{t.comment && <Field label="Comment" value={t.comment} />}
</Section>
{t.peersFrom && (
<Section title="Peer sources">
<Field label="Tracker" value={String(t.peersFrom.fromTracker)} />
<Field label="DHT" value={String(t.peersFrom.fromDht)} />
<Field label="PEX" value={String(t.peersFrom.fromPex)} />
<Field label="Incoming" value={String(t.peersFrom.fromIncoming)} />
<Field label="Local discovery" value={String(t.peersFrom.fromLpd)} />
<Field label="Cache" value={String(t.peersFrom.fromCache)} />
</Section>
)}
</div>
);
const Section = ({ title, children, columns = 2 }: { title: string; children: ReactNode; columns?: number }) => (
<section>
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">{title}</h3>
<dl className={`grid gap-x-6 gap-y-1.5 ${columns === 1 ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2'}`}>
{children}
</dl>
</section>
);
type FieldProps = { label: string; value: string; mono?: boolean; truncate?: boolean; copy?: string };
const Field = ({ label, value, mono, truncate, copy }: FieldProps) => (
<div className="flex min-w-0 items-baseline gap-3 text-xs">
<dt className="w-32 shrink-0 text-muted-foreground">{label}</dt>
<dd className={`min-w-0 flex-1 ${mono ? 'font-mono' : ''} ${truncate ? 'truncate' : 'break-words'}`} title={value}>
{value}
</dd>
{copy && <CopyButton value={copy} />}
</div>
);
const CopyButton = ({ value }: { value: string }) => {
const [copied, setCopied] = useState(false);
return (
<button
type="button"
className="shrink-0 text-muted-foreground hover:text-foreground"
onClick={() => {
// copyToClipboard handles the insecure-origin case — navigator.clipboard does not exist over
// plain http, which Officer IS reached over on a tailnet. This comment used to say "Officer is
// always behind HTTPS"; it was not, and the button silently copied nothing.
void copyToClipboard(value).then(
() => {
setCopied(true);
setTimeout(() => setCopied(false), 1200);
},
() => undefined,
);
}}
aria-label="Copy"
>
{copied ? <Check className="h-3 w-3 text-success" /> : <Copy className="h-3 w-3" />}
</button>
);
};