Give identity its due: native enrollment, honest deletion, and sign-in that stays home

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 18:55:38 -04:00
parent 5a0643357f
commit 4ec8bd94d9
25 changed files with 4713 additions and 407 deletions
@@ -25,6 +25,12 @@ Singleton {
property bool scanned: false
property string lastError: ""
// The pictures the distribution ships, as [{name, path}]. Read once and
// kept: it is a directory listing that does not change while a session is
// running, and the gallery it fills is opened and closed repeatedly.
property var stockAvatars: []
property bool stockAvatarsLoaded: false
// Guards read the Process objects rather than a derived "busy" binding. A
// binding hands back its cached value inside the handler that changes it,
// which silently turns a refresh after a successful change into a no-op --
@@ -115,7 +121,27 @@ Singleton {
String(Math.round(x)), String(Math.round(y)), String(Math.round(size))]);
}
function setAccountType(userName: string, kind: string): void {
// Clearing the picture is the same call with nothing in it -- there is no
// separate method for it in accountsservice, and there is none here either.
// No user name: this is the hero card's own avatar, and an account you are
// not signed in to has no avatar surface to remove it from.
function removeIcon(): void {
root.settingIcon = true;
root.run(["set-icon", root.currentUser, ""]);
}
// Called when the gallery is about to be shown, not at startup: most
// sessions never open it, and it is a directory listing either way.
function loadStockAvatars(): void {
if (root.stockAvatarsLoaded || stock.running)
return;
stock.running = true;
}
// Any account, including one that is not signed in. The helper keeps the
// last-administrator refusal, so a page that forgets the guard still
// cannot leave the machine unadministrable.
function setAccountTypeFor(userName: string, kind: string): void {
root.run(["set-account-type", userName, kind]);
}
@@ -123,12 +149,28 @@ Singleton {
root.run(["set-automatic-login", userName, enabled ? "true" : "false"]);
}
// Locked accounts cannot sign in at all. Unlocking is the only half of this
// the page offers, because locking someone out is not a settings gesture.
function setLocked(userName: string, locked: bool): void {
root.run(["set-locked", userName, locked ? "true" : "false"]);
}
// No password of any kind is involved: accountsservice is told the account
// must choose one at the next sign-in, and the login screen collects it
// from the person who will use it.
function resetPassword(userName: string): void {
root.run(["reset-password", userName]);
}
function createUser(userName: string, realName: string, kind: string): void {
root.run(["create-user", userName, realName, kind]);
}
function deleteUser(userName: string, removeFiles: bool): void {
root.run(["delete-user", userName, removeFiles ? "remove-files" : "keep-files"]);
// keepFiles, not removeFiles: the page asks "Keep the files" or "Remove
// everything", and a service that inverted the sentence on its way to the
// helper is how the destructive answer gets chosen by accident.
function deleteUser(userName: string, keepFiles: bool): void {
root.run(["delete-user", userName, keepFiles ? "keep" : "remove"]);
}
// The password goes to the helper's stdin and nowhere else: never an
@@ -177,6 +219,25 @@ Singleton {
onExited: root.pendingPassword = ""
}
Process {
id: stock
command: [root.helperPath, "stock-avatars"]
stdout: StdioCollector {
onStreamFinished: {
try {
const parsed = JSON.parse(this.text);
root.stockAvatars = Array.isArray(parsed.avatars) ? parsed.avatars : [];
} catch (error) {
// A machine with no gallery is normal; an empty one reads
// the same to the page, and nothing else here depends on it.
root.stockAvatars = [];
console.warn("Accounts: could not read the stock avatars:", error);
}
root.stockAvatarsLoaded = true;
}
}
}
Process {
id: mutation
// The helper answers with the fresh state, so the page updates from the