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>
This commit is contained in:
+193
@@ -0,0 +1,193 @@
|
||||
// Shared types/constants for the /transmission workspace panels. The wire shapes mirror what the
|
||||
// officer-transmission sidecar returns under /api/transmission/_officer/* — which in turn keeps
|
||||
// Transmission's own field names, casing warts and all (`percentDone` beside `peer-limit` and
|
||||
// `sequential_download`). Renaming them here would mean this file and Transmission's rpc-spec.md disagree
|
||||
// on what a field is called, which is exactly the confusion you don't want at 2am.
|
||||
//
|
||||
// The `officer*` fields are the sidecar's own additions — see src/servers/sidecar/transmission/rpc.ts.
|
||||
|
||||
export const TRANSMISSION_SECTIONS = [
|
||||
{ id: 'torrents', label: 'Torrents' },
|
||||
{ id: 'stats', label: 'Statistics' },
|
||||
{ id: 'settings', label: 'Settings' },
|
||||
{ id: 'connection', label: 'Connection' },
|
||||
] as const;
|
||||
|
||||
export type TransmissionSectionId = (typeof TRANSMISSION_SECTIONS)[number]['id'];
|
||||
|
||||
/** Where /transmission lands, and where an unrecognised section redirects to. */
|
||||
export const DEFAULT_TRANSMISSION_SECTION: TransmissionSectionId = 'torrents';
|
||||
|
||||
export const isTransmissionSection = (value: string | undefined): value is TransmissionSectionId =>
|
||||
TRANSMISSION_SECTIONS.some((s) => s.id === value);
|
||||
|
||||
/** The one place the section URL is spelled, so the nav, the guard and any deep link cannot drift apart. */
|
||||
export const transmissionSectionPath = (id: TransmissionSectionId) => `/transmission/${id}`;
|
||||
|
||||
// ── Domain objects ────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Transmission's torrent status enum (rpc-spec.md §3.3). */
|
||||
export const STATUS = {
|
||||
Stopped: 0,
|
||||
CheckWait: 1,
|
||||
Check: 2,
|
||||
DownloadWait: 3,
|
||||
Download: 4,
|
||||
SeedWait: 5,
|
||||
Seed: 6,
|
||||
} as const;
|
||||
|
||||
export type TrackerStat = {
|
||||
announce: string;
|
||||
announceState: number;
|
||||
downloadCount: number;
|
||||
hasAnnounced: boolean;
|
||||
hasScraped: boolean;
|
||||
host: string;
|
||||
id: number;
|
||||
isBackup: boolean;
|
||||
lastAnnouncePeerCount: number;
|
||||
lastAnnounceResult: string;
|
||||
lastAnnounceSucceeded: boolean;
|
||||
lastAnnounceTime: number;
|
||||
lastScrapeResult: string;
|
||||
lastScrapeSucceeded: boolean;
|
||||
lastScrapeTime: number;
|
||||
leecherCount: number;
|
||||
nextAnnounceTime: number;
|
||||
scrape: string;
|
||||
seederCount: number;
|
||||
tier: number;
|
||||
};
|
||||
|
||||
export type TorrentFile = { bytesCompleted: number; length: number; name: string };
|
||||
export type TorrentFileStat = { bytesCompleted: number; priority: number; wanted: boolean };
|
||||
|
||||
export type TorrentPeer = {
|
||||
address: string;
|
||||
clientName: string;
|
||||
flagStr: string;
|
||||
isEncrypted: boolean;
|
||||
isIncoming: boolean;
|
||||
isUTP: boolean;
|
||||
port: number;
|
||||
progress: number;
|
||||
rateToClient: number;
|
||||
rateToPeer: number;
|
||||
};
|
||||
|
||||
export type PeersFrom = {
|
||||
fromCache: number;
|
||||
fromDht: number;
|
||||
fromIncoming: number;
|
||||
fromLpd: number;
|
||||
fromLtep: number;
|
||||
fromPex: number;
|
||||
fromTracker: number;
|
||||
};
|
||||
|
||||
export type Torrent = {
|
||||
id: number;
|
||||
name: string;
|
||||
status: number;
|
||||
totalSize: number;
|
||||
sizeWhenDone: number;
|
||||
leftUntilDone: number;
|
||||
haveValid: number;
|
||||
percentDone: number;
|
||||
metadataPercentComplete: number;
|
||||
recheckProgress?: number;
|
||||
downloadedEver: number;
|
||||
uploadedEver: number;
|
||||
corruptEver?: number;
|
||||
uploadRatio: number;
|
||||
rateDownload: number;
|
||||
rateUpload: number;
|
||||
eta: number;
|
||||
peersSendingToUs: number;
|
||||
peersGettingFromUs: number;
|
||||
addedDate: number;
|
||||
doneDate: number;
|
||||
activityDate: number;
|
||||
secondsSeeding: number;
|
||||
downloadDir: string;
|
||||
bandwidthPriority: number;
|
||||
queuePosition: number;
|
||||
isPrivate: boolean;
|
||||
labels: string[];
|
||||
error: number;
|
||||
errorString: string;
|
||||
pieceCount: number;
|
||||
pieceSize: number;
|
||||
magnetLink: string;
|
||||
group: string;
|
||||
'file-count': number;
|
||||
trackerStats: TrackerStat[];
|
||||
trackerList?: string;
|
||||
honorsSessionLimits: boolean;
|
||||
downloadLimited: boolean;
|
||||
downloadLimit: number;
|
||||
uploadLimited: boolean;
|
||||
uploadLimit: number;
|
||||
'peer-limit': number;
|
||||
seedRatioMode: number;
|
||||
seedRatioLimit: number;
|
||||
seedIdleMode: number;
|
||||
seedIdleLimit: number;
|
||||
sequential_download?: boolean;
|
||||
// Detail-only — present when fetched through useTorrentDetail.
|
||||
hashString?: string;
|
||||
comment?: string;
|
||||
creator?: string;
|
||||
dateCreated?: number;
|
||||
maxConnectedPeers?: number;
|
||||
files?: TorrentFile[];
|
||||
fileStats?: TorrentFileStat[];
|
||||
peers?: TorrentPeer[];
|
||||
peersFrom?: PeersFrom;
|
||||
// Derived by the sidecar.
|
||||
officerTrackerHost: string;
|
||||
officerTrackerStatus: string;
|
||||
officerTrackerErrors: number;
|
||||
};
|
||||
|
||||
export type SessionSettings = Record<string, unknown> & {
|
||||
version: string;
|
||||
'rpc-version': number;
|
||||
'download-dir': string;
|
||||
};
|
||||
|
||||
export type StatsBucket = {
|
||||
downloadedBytes: number;
|
||||
uploadedBytes: number;
|
||||
filesAdded: number;
|
||||
sessionCount: number;
|
||||
secondsActive: number;
|
||||
};
|
||||
|
||||
export type SessionStats = {
|
||||
activeTorrentCount: number;
|
||||
pausedTorrentCount: number;
|
||||
torrentCount: number;
|
||||
downloadSpeed: number;
|
||||
uploadSpeed: number;
|
||||
'cumulative-stats': StatsBucket;
|
||||
'current-stats': StatsBucket;
|
||||
};
|
||||
|
||||
/** The ids the sidecar accepts at POST /_officer/torrents/action. */
|
||||
export type TorrentAction =
|
||||
| 'start'
|
||||
| 'start-now'
|
||||
| 'stop'
|
||||
| 'verify'
|
||||
| 'reannounce'
|
||||
| 'queue-top'
|
||||
| 'queue-up'
|
||||
| 'queue-down'
|
||||
| 'queue-bottom';
|
||||
|
||||
export const PRIORITY = { Low: -1, Normal: 0, High: 1 } as const;
|
||||
|
||||
/** Transmission's seedRatioMode / seedIdleMode share this vocabulary. */
|
||||
export const LIMIT_MODE = { Global: 0, Single: 1, Unlimited: 2 } as const;
|
||||
Reference in New Issue
Block a user