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>
245 lines
6.0 KiB
TypeScript
245 lines
6.0 KiB
TypeScript
// Wire types for the Transmission RPC surface this sidecar uses.
|
|
//
|
|
// Field names are Transmission's OWN, verbatim — including its inconsistent casing (`percentDone` next to
|
|
// `begin_piece`, `peer-limit`, `sequential_download`). That is deliberate: renaming them would mean every
|
|
// future field needs a translation entry, and a name that doesn't appear in Transmission's rpc-spec.md is a
|
|
// name you cannot grep for when something misbehaves. The only fields we invent are the `officer*` derived
|
|
// ones at the bottom of Torrent, which are prefixed so they can never be mistaken for upstream data.
|
|
|
|
/** Transmission's torrent status enum (rpc-spec.md §3.3). */
|
|
export const TorrentStatus = {
|
|
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;
|
|
clientIsChoked: boolean;
|
|
clientIsInterested: boolean;
|
|
flagStr: string;
|
|
isDownloadingFrom: boolean;
|
|
isEncrypted: boolean;
|
|
isIncoming: boolean;
|
|
isUploadingTo: boolean;
|
|
isUTP: boolean;
|
|
peerIsChoked: boolean;
|
|
peerIsInterested: 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;
|
|
// Per-torrent limits — the fields the "Other settings" dialog reads and writes.
|
|
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 fields — present when fetched through /_officer/torrents/:id.
|
|
hashString?: string;
|
|
comment?: string;
|
|
creator?: string;
|
|
dateCreated?: number;
|
|
maxConnectedPeers?: number;
|
|
files?: TorrentFile[];
|
|
fileStats?: TorrentFileStat[];
|
|
peers?: TorrentPeer[];
|
|
peersFrom?: PeersFrom;
|
|
// ── Derived by the sidecar, never sent by Transmission ──
|
|
/**
|
|
* The primary tracker's host with its port and any ignorable subdomain prefix stripped
|
|
* (`udp://tr.example.org:1337/announce` → `example.org`). The UI groups by this, so the normalisation
|
|
* has to be one implementation, not one per caller.
|
|
*/
|
|
officerTrackerHost: string;
|
|
/** Human-readable announce state of the primary tracker, for the "Tracker Status" column. */
|
|
officerTrackerStatus: string;
|
|
/** Count of trackers that have reported an error, so the UI can flag a torrent without walking the array. */
|
|
officerTrackerErrors: number;
|
|
};
|
|
|
|
export type SessionSettings = Record<string, unknown> & {
|
|
version: string;
|
|
'rpc-version': number;
|
|
'download-dir': string;
|
|
};
|
|
|
|
export type SessionStats = {
|
|
activeTorrentCount: number;
|
|
pausedTorrentCount: number;
|
|
torrentCount: number;
|
|
downloadSpeed: number;
|
|
uploadSpeed: number;
|
|
'cumulative-stats': StatsBucket;
|
|
'current-stats': StatsBucket;
|
|
};
|
|
|
|
export type StatsBucket = {
|
|
downloadedBytes: number;
|
|
uploadedBytes: number;
|
|
filesAdded: number;
|
|
sessionCount: number;
|
|
secondsActive: number;
|
|
};
|
|
|
|
/** The fields the torrent list needs. Mirrors what the list UI actually renders — nothing speculative. */
|
|
export const LIST_FIELDS = [
|
|
'activityDate',
|
|
'addedDate',
|
|
'bandwidthPriority',
|
|
'corruptEver',
|
|
'doneDate',
|
|
'downloadDir',
|
|
'downloadedEver',
|
|
'downloadLimit',
|
|
'downloadLimited',
|
|
'error',
|
|
'errorString',
|
|
'eta',
|
|
'file-count',
|
|
'group',
|
|
'haveValid',
|
|
'honorsSessionLimits',
|
|
'id',
|
|
'isPrivate',
|
|
'labels',
|
|
'leftUntilDone',
|
|
'magnetLink',
|
|
'metadataPercentComplete',
|
|
'name',
|
|
'peer-limit',
|
|
'peersGettingFromUs',
|
|
'peersSendingToUs',
|
|
'percentDone',
|
|
'pieceCount',
|
|
'pieceSize',
|
|
'queuePosition',
|
|
'rateDownload',
|
|
'rateUpload',
|
|
'secondsSeeding',
|
|
'seedIdleLimit',
|
|
'seedIdleMode',
|
|
'seedRatioLimit',
|
|
'seedRatioMode',
|
|
'sequential_download',
|
|
'sizeWhenDone',
|
|
'status',
|
|
'totalSize',
|
|
'trackerList',
|
|
'trackerStats',
|
|
'uploadedEver',
|
|
'uploadLimit',
|
|
'uploadLimited',
|
|
'uploadRatio',
|
|
] as const;
|
|
|
|
/** Extra fields only the detail pane needs — big arrays we refuse to pull for every row on every poll. */
|
|
export const DETAIL_FIELDS = [
|
|
'comment',
|
|
'creator',
|
|
'dateCreated',
|
|
'fileStats',
|
|
'files',
|
|
'hashString',
|
|
'maxConnectedPeers',
|
|
'peers',
|
|
'peersFrom',
|
|
'recheckProgress',
|
|
] as const;
|