@putkoff/abstract-securefiles 0.0.51 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ import type { SecureFilesApi } from "./api";
2
+ import type { Device } from "./types";
3
+ export declare function MyComputers({ api, devices, onChange, }: {
4
+ api: SecureFilesApi;
5
+ devices: Device[];
6
+ onChange: () => void;
7
+ }): import("react/jsx-runtime").JSX.Element;
package/dist/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BrowseResult, LinkRoot, RootChallenge, SecureFile, ShareUpdate } from "./types";
1
+ import type { BrowseResult, Device, LinkRoot, RootChallenge, SecureFile, ShareUpdate } from "./types";
2
2
  export declare const DEFAULT_API_BASE = "https://api.abstractendeavors.com";
3
3
  export declare class ApiError extends Error {
4
4
  status: number;
@@ -9,6 +9,17 @@ export declare function createApi(apiBase?: string): {
9
9
  user_name: string;
10
10
  is_admin: boolean;
11
11
  files: SecureFile[];
12
+ devices: Device[];
13
+ }>;
14
+ devices: () => Promise<{
15
+ devices: Device[];
16
+ }>;
17
+ pairDevice: () => Promise<{
18
+ code: string;
19
+ expires_in: number;
20
+ }>;
21
+ removeDevice: (id: number) => Promise<{
22
+ devices: Device[];
12
23
  }>;
13
24
  roots: () => Promise<{
14
25
  roots: LinkRoot[];
package/dist/index.cjs CHANGED
@@ -37,6 +37,9 @@ function createApi(apiBase = DEFAULT_API_BASE) {
37
37
  }
38
38
  return {
39
39
  list: () => json("/userInfo"),
40
+ devices: () => json("/devices"),
41
+ pairDevice: () => json("/devices/pair", { method: "POST", body: "{}" }),
42
+ removeDevice: (id) => json(`/devices/${id}`, { method: "DELETE" }),
40
43
  roots: () => json("/admin/roots"),
41
44
  requestRoot: (path) => json("/admin/roots", { method: "POST", body: JSON.stringify({ path }) }),
42
45
  confirmRoot: (nonce, signature, label) => json("/admin/roots/confirm", {
@@ -329,7 +332,7 @@ function formatSize(bytes) {
329
332
  }
330
333
  return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
331
334
  }
332
- const errText = (err) => (err instanceof Error ? err.message : String(err));
335
+ const errText$1 = (err) => (err instanceof Error ? err.message : String(err));
333
336
  function AddRoot({ api, onAdded }) {
334
337
  const [path, setPath] = react.useState("");
335
338
  const [label, setLabel] = react.useState("");
@@ -345,7 +348,7 @@ function AddRoot({ api, onAdded }) {
345
348
  setSignature("");
346
349
  }
347
350
  catch (err) {
348
- setError(errText(err));
351
+ setError(errText$1(err));
349
352
  }
350
353
  finally {
351
354
  setBusy(false);
@@ -364,7 +367,7 @@ function AddRoot({ api, onAdded }) {
364
367
  setSignature("");
365
368
  }
366
369
  catch (err) {
367
- setError(errText(err));
370
+ setError(errText$1(err));
368
371
  }
369
372
  finally {
370
373
  setBusy(false);
@@ -387,7 +390,7 @@ function Browser({ api, roots, folders, onLinked, }) {
387
390
  setPicked(new Set());
388
391
  }
389
392
  catch (err) {
390
- setError(errText(err));
393
+ setError(errText$1(err));
391
394
  }
392
395
  }, [api]);
393
396
  react.useEffect(() => {
@@ -415,7 +418,7 @@ function Browser({ api, roots, folders, onLinked, }) {
415
418
  await open(listing.path);
416
419
  }
417
420
  catch (err) {
418
- setResult(errText(err));
421
+ setResult(errText$1(err));
419
422
  }
420
423
  finally {
421
424
  setBusy(false);
@@ -439,7 +442,7 @@ function ServerFiles({ api, folders, onLinked, }) {
439
442
  setError(null);
440
443
  }
441
444
  catch (err) {
442
- setError(errText(err));
445
+ setError(errText$1(err));
443
446
  }
444
447
  }, [api]);
445
448
  return (jsxRuntime.jsxs("details", { className: "sf-admin", onToggle: (e) => e.currentTarget.open && !roots && load(), children: [jsxRuntime.jsx("summary", { children: "Server files (admin)" }), jsxRuntime.jsxs("div", { className: "sf-stack", children: [jsxRuntime.jsx("p", { className: "sf-muted", children: "Link files or whole folders that already live on the server instead of uploading copies. A linked folder downloads as one uncompressed .tar with an exact size, and interrupted downloads can be resumed. Only items inside allowed directories can be linked; removing a directory immediately disables its links. Deleting a linked entry never touches the original. Available from the local network only." }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), roots && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Allowed directories" }), roots.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "None yet." })) : (jsxRuntime.jsx("ul", { className: "sf-roots", children: roots.map((r) => (jsxRuntime.jsxs("li", { children: [jsxRuntime.jsxs("span", { children: [jsxRuntime.jsx("code", { children: r.path }), " ", r.label && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["\u2014 ", r.label] }), !r.exists && jsxRuntime.jsx("span", { className: "sf-error", children: " (missing)" })] }), jsxRuntime.jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
@@ -448,11 +451,72 @@ function ServerFiles({ api, folders, onLinked, }) {
448
451
  onLinked();
449
452
  }
450
453
  catch (err) {
451
- setError(errText(err));
454
+ setError(errText$1(err));
452
455
  }
453
456
  } })] }, r.id))) }))] }), jsxRuntime.jsx(AddRoot, { api: api, onAdded: setRoots }), jsxRuntime.jsx(Browser, { api: api, roots: roots, folders: folders, onLinked: onLinked })] }))] })] }));
454
457
  }
455
458
 
459
+ const errText = (err) => (err instanceof Error ? err.message : String(err));
460
+ const UNIX_CMD = "pipx install abstract-securefiles && pipx ensurepath && ~/.local/bin/securefiles setup";
461
+ const WIN_CMD = "py -m pip install --user pipx; py -m pipx install abstract-securefiles; py -m pipx ensurepath";
462
+ function ago(iso) {
463
+ if (!iso)
464
+ return "never";
465
+ const secs = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
466
+ if (secs < 90)
467
+ return "just now";
468
+ if (secs < 3600)
469
+ return `${Math.round(secs / 60)} min ago`;
470
+ if (secs < 86400)
471
+ return `${Math.round(secs / 3600)} h ago`;
472
+ return `${Math.round(secs / 86400)} d ago`;
473
+ }
474
+ function AddComputer({ api, onPaired }) {
475
+ const [code, setCode] = react.useState(null);
476
+ const [expiresAt, setExpiresAt] = react.useState(0);
477
+ const [, setTick] = react.useState(0);
478
+ const [error, setError] = react.useState(null);
479
+ react.useEffect(() => {
480
+ if (!code)
481
+ return;
482
+ const timer = setInterval(() => {
483
+ if (Date.now() > expiresAt)
484
+ setCode(null);
485
+ setTick((t) => t + 1);
486
+ onPaired();
487
+ }, 5000);
488
+ return () => clearInterval(timer);
489
+ }, [code, expiresAt, onPaired]);
490
+ const create = async () => {
491
+ setError(null);
492
+ try {
493
+ const res = await api.pairDevice();
494
+ setCode(res.code);
495
+ setExpiresAt(Date.now() + res.expires_in * 1000);
496
+ }
497
+ catch (err) {
498
+ setError(errText(err));
499
+ }
500
+ };
501
+ const minutesLeft = Math.max(1, Math.round((expiresAt - Date.now()) / 60000));
502
+ if (!code) {
503
+ return (jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", onClick: create, children: "Add a computer" }), error && jsxRuntime.jsx("span", { className: "sf-error", children: error })] }));
504
+ }
505
+ return (jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Connect a computer" }), jsxRuntime.jsxs("ol", { className: "sf-steps", children: [jsxRuntime.jsxs("li", { children: ["On that computer, install the agent (needs Python 3.9+):", jsxRuntime.jsxs("div", { className: "sf-os", children: [jsxRuntime.jsx("b", { children: "Linux / macOS" }), " (Terminal)", jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("code", { className: "sf-codeline", children: UNIX_CMD }), jsxRuntime.jsx(CopyButton, { text: UNIX_CMD, label: "Copy" })] }), jsxRuntime.jsxs("span", { className: "sf-muted", children: ["No pipx? ", jsxRuntime.jsx("code", { children: "python3 -m pip install --user pipx" }), " then use ", jsxRuntime.jsx("code", { children: "python3 -m pipx" }), ". On a Mac without Python, run ", jsxRuntime.jsx("code", { children: "python3" }), " once and accept Apple's developer-tools install."] })] }), jsxRuntime.jsxs("div", { className: "sf-os", children: [jsxRuntime.jsx("b", { children: "Windows" }), " (PowerShell; install Python from python.org first)", jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("code", { className: "sf-codeline", children: WIN_CMD }), jsxRuntime.jsx(CopyButton, { text: WIN_CMD, label: "Copy" })] }), jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Then open a new PowerShell window and run ", jsxRuntime.jsx("code", { children: "securefiles setup" }), "."] })] })] }), jsxRuntime.jsxs("li", { children: ["In the panel that opens (", jsxRuntime.jsx("code", { children: "http://127.0.0.1:47821" }), "), enter this code, or run", " ", jsxRuntime.jsxs("code", { children: ["securefiles pair ", code] }), ":", jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("span", { className: "sf-paircode", children: code }), jsxRuntime.jsx(CopyButton, { text: code, label: "Copy" })] }), jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Valid for about ", minutesLeft, " more minutes, once."] })] }), jsxRuntime.jsxs("li", { children: ["Pick files and folders to share in that panel (or ", jsxRuntime.jsx("code", { children: "securefiles share PATH" }), "). They appear here under ", jsxRuntime.jsx("code", { children: "devices/<computer>/" }), ", where you set links, passwords and limits."] })] }), jsxRuntime.jsx("div", { className: "sf-row", children: jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCode(null), children: "Done" }) })] }));
506
+ }
507
+ function MyComputers({ api, devices, onChange, }) {
508
+ const [error, setError] = react.useState(null);
509
+ return (jsxRuntime.jsxs("details", { className: "sf-admin", children: [jsxRuntime.jsxs("summary", { children: ["My computers", " ", jsxRuntime.jsxs("span", { className: "sf-muted", children: ["(", devices.filter((d) => d.online).length, " online / ", devices.length, ")"] })] }), jsxRuntime.jsxs("div", { className: "sf-stack", children: [jsxRuntime.jsx("p", { className: "sf-muted", children: "Share files and folders straight from your own computers \u2014 nothing is uploaded. Downloads stream from the computer through this site with the same links, passwords, limits and resume, while it's online." }), devices.length > 0 && (jsxRuntime.jsx("ul", { className: "sf-roots", children: devices.map((d) => (jsxRuntime.jsxs("li", { children: [jsxRuntime.jsxs("span", { children: [jsxRuntime.jsx("span", { className: `sf-dot${d.online ? " sf-dot--on" : ""}` }), " ", jsxRuntime.jsx("b", { children: d.name }), " ", jsxRuntime.jsxs("span", { className: "sf-muted", children: [d.online ? "online" : `offline · last seen ${ago(d.last_seen)}`, d.platform ? ` · ${d.platform}` : ""] })] }), jsxRuntime.jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
510
+ try {
511
+ await api.removeDevice(d.id);
512
+ onChange();
513
+ }
514
+ catch (err) {
515
+ setError(errText(err));
516
+ }
517
+ } })] }, d.id))) })), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), jsxRuntime.jsx(AddComputer, { api: api, onPaired: onChange })] })] }));
518
+ }
519
+
456
520
  function styleInject(css, ref) {
457
521
  if ( ref === void 0 ) ref = {};
458
522
  var insertAt = ref.insertAt;
@@ -480,7 +544,7 @@ function styleInject(css, ref) {
480
544
  }
481
545
  }
482
546
 
483
- var css_248z = ".sf-root{--sf-border:color-mix(in srgb,currentColor 22%,transparent);--sf-subtle:color-mix(in srgb,currentColor 6%,transparent);--sf-accent:#2563eb;--sf-danger:#dc2626;display:flex;flex-direction:column;font-size:14px;gap:16px;margin:0 auto;max-width:1100px;padding:16px;text-align:left}.sf-root *,.sf-root :after,.sf-root :before{box-sizing:border-box}.sf-head{align-items:baseline;display:flex;gap:12px;justify-content:space-between}.sf-head h2{font-size:1.4em;margin:0}.sf-muted{opacity:.7}.sf-error{color:var(--sf-danger)}.sf-row{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.sf-row--end{justify-content:flex-end}.sf-field{display:flex;flex:1;flex-direction:column;gap:4px;min-width:200px}.sf-input{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;font:inherit;min-width:0;padding:6px 10px;width:100%}.sf-row .sf-input{flex:1;width:auto}.sf-btn{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:5px 12px;white-space:nowrap}.sf-btn:hover:not(:disabled){border-color:var(--sf-accent)}.sf-btn:disabled{cursor:default;opacity:.45}.sf-btn--primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff}.sf-btn--armed,.sf-btn--danger:hover:not(:disabled){border-color:var(--sf-danger);color:var(--sf-danger)}.sf-btn--armed{background:color-mix(in srgb,var(--sf-danger) 12%,transparent)}.sf-icon-btn{background:none;border:1px solid transparent;border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:3px 6px}.sf-icon-btn:hover{border-color:var(--sf-border)}.sf-upload{display:flex;flex-direction:column;gap:10px}.sf-drop{align-items:center;border:2px dashed var(--sf-border);border-radius:12px;cursor:pointer;display:flex;gap:20px;padding:28px 24px;transition:border-color .15s,background .15s,box-shadow .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sf-drop:focus-visible,.sf-drop:hover{border-color:var(--sf-accent);box-shadow:0 4px 14px color-mix(in srgb,currentColor 10%,transparent);outline:none}.sf-drop p{margin:0}.sf-drop__icon{flex-shrink:0;height:72px;opacity:.6;width:72px}.sf-drop__title{font-size:1.1em;font-weight:600;margin-bottom:4px!important}.sf-drop__count{color:var(--sf-accent);font-weight:600;margin-top:6px!important}.sf-drop--active{background:color-mix(in srgb,var(--sf-accent) 8%,transparent);border-color:var(--sf-accent)}.sf-drop--active .sf-drop__icon{color:var(--sf-accent);opacity:1}.sf-mode{gap:14px}.sf-queue progress{width:120px}.sf-upload>.sf-row{align-items:flex-end}.sf-queue{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0;max-height:200px;overflow:auto;padding:0}.sf-queue li{display:flex;gap:12px;justify-content:space-between}.sf-queue__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-toolbar{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}.sf-search{max-width:320px}.sf-table-wrap{border:1px solid var(--sf-border);border-radius:8px;overflow-x:auto}.sf-table{border-collapse:collapse;width:100%}.sf-table td,.sf-table th{border-bottom:1px solid var(--sf-border);padding:8px 10px;text-align:left;vertical-align:middle}.sf-table tbody tr:last-child td{border-bottom:none}.sf-table th{background:var(--sf-subtle);font-weight:600}.sf-col-check{width:36px}.sf-col-actions{text-align:right!important}.sf-dir td{background:var(--sf-subtle)}.sf-dir__toggle{background:none;border:none;color:inherit;cursor:pointer;font:inherit;font-weight:600;padding:0}.sf-busy{opacity:.55}.sf-pill{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:999px;color:inherit;cursor:pointer;font:inherit;font-size:12px;padding:2px 10px}.sf-pill--on{background:color-mix(in srgb,#16a34a 18%,transparent);border-color:#16a34a}.sf-overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:16px;position:fixed;z-index:1000}.sf-dialog{--sf-border:color-mix(in srgb,CanvasText 22%,transparent);--sf-subtle:color-mix(in srgb,CanvasText 6%,transparent);background:Canvas;border-radius:10px;color:CanvasText;display:flex;flex-direction:column;gap:12px;max-height:90vh;overflow:auto;padding:18px 20px;text-align:left;width:min(520px,100%)}.sf-dialog__head{align-items:center;display:flex;justify-content:space-between}.sf-dialog__head h3{margin:0}.sf-dialog>.sf-muted{margin:0;word-break:break-all}.sf-dialog__foot{justify-content:flex-end}.sf-fieldset{border:none;display:flex;flex-direction:column;gap:12px;margin:0;padding:0}.sf-fieldset:disabled{opacity:.5}.sf-fieldset p{margin:0}.sf-check{align-items:center;cursor:pointer;display:flex;gap:8px}.sf-check--inline{display:inline-flex}.sf-stack{display:flex;flex-direction:column;gap:12px}.sf-stack p{margin:0}.sf-row--bottom{align-items:flex-end}.sf-field--narrow{flex:0 1 200px;min-width:140px}.sf-admin{border:1px solid var(--sf-border);border-radius:10px;padding:10px 14px}.sf-admin>summary{cursor:pointer;font-weight:600}.sf-admin[open]>summary{margin-bottom:12px}.sf-card{border:1px solid var(--sf-border);border-radius:8px;display:flex;flex-direction:column;gap:10px;padding:12px}.sf-card h4,.sf-card p{margin:0}.sf-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;resize:vertical}.sf-roots{display:flex;flex-direction:column;gap:6px;list-style:none;margin:0;padding:0}.sf-roots li{align-items:center;display:flex;gap:12px;justify-content:space-between}.sf-root-select{max-width:420px}.sf-browser{border:1px solid var(--sf-border);border-radius:6px;display:flex;flex-direction:column;max-height:320px;overflow:auto}.sf-browser>*{border-bottom:1px solid var(--sf-border);padding:5px 10px}.sf-browser>:last-child{border-bottom:none}.sf-browser__dir{background:none;border:none;border-bottom:1px solid var(--sf-border);color:inherit;cursor:pointer;font:inherit;text-align:left}.sf-browser__all,.sf-browser__dir:hover{background:var(--sf-subtle)}.sf-browser__all{font-weight:600}.sf-browser__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-tag{border:1px solid var(--sf-border);border-radius:999px;display:inline-block;font-size:11px;margin-left:8px;padding:0 7px;vertical-align:middle}.sf-tag--warn{border-color:var(--sf-danger);color:var(--sf-danger)}.sf-strike{opacity:.7;text-decoration:line-through}.sf-browser__row{align-items:center;display:flex;gap:8px}.sf-browser__row .sf-browser__dir{border-bottom:none;flex:1;padding:0}";
547
+ var css_248z = ".sf-root{--sf-border:color-mix(in srgb,currentColor 22%,transparent);--sf-subtle:color-mix(in srgb,currentColor 6%,transparent);--sf-accent:#2563eb;--sf-danger:#dc2626;display:flex;flex-direction:column;font-size:14px;gap:16px;margin:0 auto;max-width:1100px;padding:16px;text-align:left}.sf-root *,.sf-root :after,.sf-root :before{box-sizing:border-box}.sf-head{align-items:baseline;display:flex;gap:12px;justify-content:space-between}.sf-head h2{font-size:1.4em;margin:0}.sf-muted{opacity:.7}.sf-error{color:var(--sf-danger)}.sf-row{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.sf-row--end{justify-content:flex-end}.sf-field{display:flex;flex:1;flex-direction:column;gap:4px;min-width:200px}.sf-input{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;font:inherit;min-width:0;padding:6px 10px;width:100%}.sf-row .sf-input{flex:1;width:auto}.sf-btn{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:5px 12px;white-space:nowrap}.sf-btn:hover:not(:disabled){border-color:var(--sf-accent)}.sf-btn:disabled{cursor:default;opacity:.45}.sf-btn--primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff}.sf-btn--armed,.sf-btn--danger:hover:not(:disabled){border-color:var(--sf-danger);color:var(--sf-danger)}.sf-btn--armed{background:color-mix(in srgb,var(--sf-danger) 12%,transparent)}.sf-icon-btn{background:none;border:1px solid transparent;border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:3px 6px}.sf-icon-btn:hover{border-color:var(--sf-border)}.sf-upload{display:flex;flex-direction:column;gap:10px}.sf-drop{align-items:center;border:2px dashed var(--sf-border);border-radius:12px;cursor:pointer;display:flex;gap:20px;padding:28px 24px;transition:border-color .15s,background .15s,box-shadow .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sf-drop:focus-visible,.sf-drop:hover{border-color:var(--sf-accent);box-shadow:0 4px 14px color-mix(in srgb,currentColor 10%,transparent);outline:none}.sf-drop p{margin:0}.sf-drop__icon{flex-shrink:0;height:72px;opacity:.6;width:72px}.sf-drop__title{font-size:1.1em;font-weight:600;margin-bottom:4px!important}.sf-drop__count{color:var(--sf-accent);font-weight:600;margin-top:6px!important}.sf-drop--active{background:color-mix(in srgb,var(--sf-accent) 8%,transparent);border-color:var(--sf-accent)}.sf-drop--active .sf-drop__icon{color:var(--sf-accent);opacity:1}.sf-mode{gap:14px}.sf-queue progress{width:120px}.sf-upload>.sf-row{align-items:flex-end}.sf-queue{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0;max-height:200px;overflow:auto;padding:0}.sf-queue li{display:flex;gap:12px;justify-content:space-between}.sf-queue__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-toolbar{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}.sf-search{max-width:320px}.sf-table-wrap{border:1px solid var(--sf-border);border-radius:8px;overflow-x:auto}.sf-table{border-collapse:collapse;width:100%}.sf-table td,.sf-table th{border-bottom:1px solid var(--sf-border);padding:8px 10px;text-align:left;vertical-align:middle}.sf-table tbody tr:last-child td{border-bottom:none}.sf-table th{background:var(--sf-subtle);font-weight:600}.sf-col-check{width:36px}.sf-col-actions{text-align:right!important}.sf-dir td{background:var(--sf-subtle)}.sf-dir__toggle{background:none;border:none;color:inherit;cursor:pointer;font:inherit;font-weight:600;padding:0}.sf-busy{opacity:.55}.sf-pill{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:999px;color:inherit;cursor:pointer;font:inherit;font-size:12px;padding:2px 10px}.sf-pill--on{background:color-mix(in srgb,#16a34a 18%,transparent);border-color:#16a34a}.sf-overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:16px;position:fixed;z-index:1000}.sf-dialog{--sf-border:color-mix(in srgb,CanvasText 22%,transparent);--sf-subtle:color-mix(in srgb,CanvasText 6%,transparent);background:Canvas;border-radius:10px;color:CanvasText;display:flex;flex-direction:column;gap:12px;max-height:90vh;overflow:auto;padding:18px 20px;text-align:left;width:min(520px,100%)}.sf-dialog__head{align-items:center;display:flex;justify-content:space-between}.sf-dialog__head h3{margin:0}.sf-dialog>.sf-muted{margin:0;word-break:break-all}.sf-dialog__foot{justify-content:flex-end}.sf-fieldset{border:none;display:flex;flex-direction:column;gap:12px;margin:0;padding:0}.sf-fieldset:disabled{opacity:.5}.sf-fieldset p{margin:0}.sf-check{align-items:center;cursor:pointer;display:flex;gap:8px}.sf-check--inline{display:inline-flex}.sf-stack{display:flex;flex-direction:column;gap:12px}.sf-stack p{margin:0}.sf-row--bottom{align-items:flex-end}.sf-field--narrow{flex:0 1 200px;min-width:140px}.sf-admin{border:1px solid var(--sf-border);border-radius:10px;padding:10px 14px}.sf-admin>summary{cursor:pointer;font-weight:600}.sf-admin[open]>summary{margin-bottom:12px}.sf-card{border:1px solid var(--sf-border);border-radius:8px;display:flex;flex-direction:column;gap:10px;padding:12px}.sf-card h4,.sf-card p{margin:0}.sf-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;resize:vertical}.sf-roots{display:flex;flex-direction:column;gap:6px;list-style:none;margin:0;padding:0}.sf-roots li{align-items:center;display:flex;gap:12px;justify-content:space-between}.sf-root-select{max-width:420px}.sf-browser{border:1px solid var(--sf-border);border-radius:6px;display:flex;flex-direction:column;max-height:320px;overflow:auto}.sf-browser>*{border-bottom:1px solid var(--sf-border);padding:5px 10px}.sf-browser>:last-child{border-bottom:none}.sf-browser__dir{background:none;border:none;border-bottom:1px solid var(--sf-border);color:inherit;cursor:pointer;font:inherit;text-align:left}.sf-browser__all,.sf-browser__dir:hover{background:var(--sf-subtle)}.sf-browser__all{font-weight:600}.sf-browser__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-tag{border:1px solid var(--sf-border);border-radius:999px;display:inline-block;font-size:11px;margin-left:8px;padding:0 7px;vertical-align:middle}.sf-tag--warn{border-color:var(--sf-danger);color:var(--sf-danger)}.sf-strike{opacity:.7;text-decoration:line-through}.sf-browser__row{align-items:center;display:flex;gap:8px}.sf-browser__row .sf-browser__dir{border-bottom:none;flex:1;padding:0}.sf-dot{background:var(--sf-danger);border-radius:50%;display:inline-block;height:9px;vertical-align:middle;width:9px}.sf-dot--on{background:#16a34a}.sf-steps{display:flex;flex-direction:column;gap:10px;margin:0;padding-left:20px}.sf-steps li>.sf-row{margin:4px 0}.sf-codeline{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;font-size:12px;padding:4px 8px}.sf-codeline,.sf-paircode{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.sf-paircode{font-size:22px;font-weight:700;letter-spacing:.12em}.sf-small{font-size:12px}.sf-os{display:flex;flex-direction:column;gap:4px;margin-top:6px}";
484
548
  styleInject(css_248z);
485
549
 
486
550
  function buildTree(files) {
@@ -506,6 +570,7 @@ function SecureFiles({ apiBase }) {
506
570
  const [files, setFiles] = react.useState([]);
507
571
  const [user, setUser] = react.useState(null);
508
572
  const [isAdmin, setIsAdmin] = react.useState(false);
573
+ const [devices, setDevices] = react.useState([]);
509
574
  const [loading, setLoading] = react.useState(true);
510
575
  const [error, setError] = react.useState(null);
511
576
  const [notice, setNotice] = react.useState(null);
@@ -520,6 +585,7 @@ function SecureFiles({ apiBase }) {
520
585
  const data = await api.list();
521
586
  setUser(data.user_name);
522
587
  setIsAdmin(data.is_admin);
588
+ setDevices(data.devices || []);
523
589
  setFiles(data.files);
524
590
  setSelected((prev) => new Set([...prev].filter((id) => data.files.some((f) => f.id === id))));
525
591
  }
@@ -602,7 +668,7 @@ function SecureFiles({ apiBase }) {
602
668
  const searching = search.trim().length > 0;
603
669
  const renderFile = (f, depth) => {
604
670
  const busy = busyIds.has(f.id);
605
- return (jsxRuntime.jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsxRuntime.jsx("td", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxRuntime.jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsxRuntime.jsx("span", { className: f.available ? undefined : "sf-strike", children: f.filename }), f.kind === "folder" && (jsxRuntime.jsx("span", { className: "sf-tag", title: "Downloads as one uncompressed, resumable .tar of the whole folder", children: "folder \u00B7 .tar" })), f.linked && (jsxRuntime.jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), !f.available && (jsxRuntime.jsx("span", { className: "sf-tag sf-tag--warn", title: f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk", children: "missing" }))] }), jsxRuntime.jsxs("td", { children: [jsxRuntime.jsx("button", { type: "button", className: `sf-pill${f.shareable ? " sf-pill--on" : ""}`, disabled: busy || !f.available, onClick: () => setShared([f], !f.shareable), title: f.shareable ? "Click to stop sharing" : "Click to share", children: f.shareable ? "Shared" : "Private" }), f.shareable && f.has_password && jsxRuntime.jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxRuntime.jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsxRuntime.jsx("td", { className: "sf-col-actions", children: jsxRuntime.jsxs("div", { className: "sf-row sf-row--end", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && f.share_url && jsxRuntime.jsx(CopyButton, { text: f.share_url }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), jsxRuntime.jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) })] }) })] }, f.id));
671
+ return (jsxRuntime.jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsxRuntime.jsx("td", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxRuntime.jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsxRuntime.jsx("span", { className: f.available || f.device ? undefined : "sf-strike", children: f.filename }), f.kind === "folder" && (jsxRuntime.jsx("span", { className: "sf-tag", title: "Downloads as one uncompressed, resumable .tar of the whole folder", children: "folder \u00B7 .tar" })), f.linked && (jsxRuntime.jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), f.device && (jsxRuntime.jsxs("span", { className: `sf-tag${f.device.online ? "" : " sf-tag--warn"}`, title: "Served from your computer through the relay", children: ["on ", f.device.name, " \u00B7 ", f.device.online ? "online" : "offline"] })), !f.available && !f.device && (jsxRuntime.jsx("span", { className: "sf-tag sf-tag--warn", title: f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk", children: "missing" }))] }), jsxRuntime.jsxs("td", { children: [jsxRuntime.jsx("button", { type: "button", className: `sf-pill${f.shareable ? " sf-pill--on" : ""}`, disabled: busy || !f.available, onClick: () => setShared([f], !f.shareable), title: f.shareable ? "Click to stop sharing" : "Click to share", children: f.shareable ? "Shared" : "Private" }), f.shareable && f.has_password && jsxRuntime.jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxRuntime.jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsxRuntime.jsx("td", { className: "sf-col-actions", children: jsxRuntime.jsxs("div", { className: "sf-row sf-row--end", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && f.share_url && jsxRuntime.jsx(CopyButton, { text: f.share_url }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), f.device ? (jsxRuntime.jsxs("span", { className: "sf-muted sf-small", title: "Stop sharing it from that computer's Secure Files panel", children: ["managed on ", f.device.name] })) : (jsxRuntime.jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) }))] }) })] }, f.id));
606
672
  };
607
673
  const renderFolder = (node, depth) => {
608
674
  const rows = [];
@@ -619,7 +685,7 @@ function SecureFiles({ apiBase }) {
619
685
  rows.push(...ownFiles.map((f) => renderFile(f, depth)));
620
686
  return rows;
621
687
  };
622
- return (jsxRuntime.jsxs("div", { className: "sf-root", children: [jsxRuntime.jsxs("header", { className: "sf-head", children: [jsxRuntime.jsx("h2", { children: "Secure Files" }), user && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsxRuntime.jsx(UploadPanel, { api: api, folders: folders, onUploaded: load }), isAdmin && jsxRuntime.jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxRuntime.jsxs("section", { className: "sf-toolbar", children: [jsxRuntime.jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsxRuntime.jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsxRuntime.jsx("div", { className: "sf-table-wrap", children: jsxRuntime.jsxs("table", { className: "sf-table", children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsxRuntime.jsx("th", { children: "Name" }), jsxRuntime.jsx("th", { children: "Sharing" }), jsxRuntime.jsx("th", { children: "Downloads" }), jsxRuntime.jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsxRuntime.jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsxRuntime.jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
688
+ return (jsxRuntime.jsxs("div", { className: "sf-root", children: [jsxRuntime.jsxs("header", { className: "sf-head", children: [jsxRuntime.jsx("h2", { children: "Secure Files" }), user && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsxRuntime.jsx(UploadPanel, { api: api, folders: folders.filter((d) => d !== "devices" && !d.startsWith("devices/")), onUploaded: load }), jsxRuntime.jsx(MyComputers, { api: api, devices: devices, onChange: load }), isAdmin && jsxRuntime.jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxRuntime.jsxs("section", { className: "sf-toolbar", children: [jsxRuntime.jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsxRuntime.jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsxRuntime.jsx("div", { className: "sf-table-wrap", children: jsxRuntime.jsxs("table", { className: "sf-table", children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsxRuntime.jsx("th", { children: "Name" }), jsxRuntime.jsx("th", { children: "Sharing" }), jsxRuntime.jsx("th", { children: "Downloads" }), jsxRuntime.jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsxRuntime.jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsxRuntime.jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
623
689
  }
624
690
 
625
691
  exports.ApiError = ApiError;