media/ gets its shape: user/ and treated/
<root>/
website/ the checkout
media/
user/ what people upload, untouched
treated/ what comes back out of conversion
ONE list drives both provisioning and reporting. two lists is how a status comes to
say "ready" about a structure missing a folder the creator quietly stopped making.
THE BUTTON REPAIRS AS READILY AS IT CREATES. `mkdir -p` on every path, every time,
so the same press fixes a `media/` somebody made by hand with nothing in it and a
structure that lost a folder — and pressing it on a complete one changes nothing
and fails at nothing. that is what makes it safe to offer beside a status a few
seconds stale; the version that refuses when the parent exists is the version
somebody has to finish in a terminal.
three states, not two. `media/` with one child missing looked identical to a
finished one, and that is the state most worth seeing — so the row says complete,
partly there, or absent, and lists both children whether or not they exist. the
missing ones are the reason to show the list at all.
RECORDED BEFORE ANY RULE DEPENDS ON IT: the two halves are not symmetric. `user/`
is original — nothing regenerates it, and losing it means asking somebody for a
file they may no longer have. `treated/` is derived — every file in it can be made
again from the one it came from, so it is the half that is safe to delete, safe to
rebuild, and safe for a job to overwrite without asking. nothing enforces that yet;
it is the fact that should decide every rule written about them later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+37
-14
@@ -13,11 +13,19 @@ import { readableError } from './errors';
|
||||
* No console. Making a directory either works or gives one line about why not, and a black transcript
|
||||
* box for a single sentence would make a small thing look like a large one.
|
||||
*
|
||||
* What goes INSIDE is not decided yet — see `api/media.ts`. This provisions the directory and nothing
|
||||
* else on purpose.
|
||||
* ── The children are listed, and the button repairs ──
|
||||
*
|
||||
* `user/` and `treated/` are shown whether or not they are there, so a structure that is missing one
|
||||
* reads as INCOMPLETE rather than as fine. The button follows: "Create" when there is nothing, "Create
|
||||
* missing" when part of it exists. It is `mkdir -p` on every path either way, so pressing it can only
|
||||
* add and can never fail on something already correct.
|
||||
*/
|
||||
|
||||
type Status = { path: string; present: boolean; entries: number | null; blocked: boolean };
|
||||
type Child = { name: string; hint: string; present: boolean; entries: number | null };
|
||||
type Status = { path: string; present: boolean; blocked: boolean; children: Child[]; complete: boolean };
|
||||
|
||||
const count = (entries: number | null) =>
|
||||
entries === null ? '' : entries === 0 ? 'empty' : `${entries} item${entries === 1 ? '' : 's'}`;
|
||||
|
||||
export const MediaFolder = ({ rootPath }: { rootPath: string }) => {
|
||||
const client = useClient();
|
||||
@@ -63,8 +71,10 @@ export const MediaFolder = ({ rootPath }: { rootPath: string }) => {
|
||||
|
||||
if (!status) return null;
|
||||
|
||||
const Icon = status.blocked ? CircleAlert : status.present ? CircleCheck : CircleDashed;
|
||||
const tone = status.blocked ? 'text-destructive' : status.present ? 'text-success' : 'text-muted-foreground';
|
||||
// Three states and not two: complete, partly there, and absent. A `media/` with one folder missing
|
||||
// looked identical to a finished one under `present`, which is the state most worth seeing.
|
||||
const Icon = status.blocked ? CircleAlert : status.complete ? CircleCheck : CircleDashed;
|
||||
const tone = status.blocked ? 'text-destructive' : status.complete ? 'text-success' : 'text-muted-foreground';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
@@ -75,23 +85,36 @@ export const MediaFolder = ({ rootPath }: { rootPath: string }) => {
|
||||
|
||||
{status.blocked ? (
|
||||
<p className="text-xs text-destructive">Something is already here, and it is not a directory.</p>
|
||||
) : status.complete ? (
|
||||
<p className="text-xs text-muted-foreground">Ready.</p>
|
||||
) : status.present ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{status.entries === null
|
||||
? 'Ready.'
|
||||
: status.entries === 0
|
||||
? 'Ready — empty.'
|
||||
: `Ready — ${status.entries} item${status.entries === 1 ? '' : 's'}.`}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Partly there.</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">Not created yet.</p>
|
||||
)}
|
||||
|
||||
{/* The structure, always — the folders that are missing are the point of showing it. Named
|
||||
with what they hold, because "user" and "treated" say who and what happened rather than
|
||||
what goes in them. */}
|
||||
<ul className="pt-1">
|
||||
{status.children.map((child) => (
|
||||
<li key={child.name} className="flex items-baseline gap-2 text-xs">
|
||||
<span className={`font-mono ${child.present ? '' : 'text-muted-foreground/60 line-through'}`}>
|
||||
{child.name}/
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-muted-foreground/70">
|
||||
{child.hint}
|
||||
{child.present && count(child.entries) ? ` · ${count(child.entries)}` : ''}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{!status.present && (
|
||||
{!status.blocked && !status.complete && (
|
||||
<Button size="sm" className="shrink-0" onClick={() => void create()} disabled={busy}>
|
||||
{busy ? <Loader2 className="size-3.5 animate-spin" /> : <FolderPlus className="size-3.5" />}
|
||||
Create
|
||||
{status.present ? 'Create missing' : 'Create'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user