App logo (icon set, welcome screen, sidebar, page icon); despawn N bots to ease the load
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
No files matched your search
@@ -115,6 +115,11 @@ impl Ra {
|
||||
Ok(first_number(&self.bot_command("botcmd despawnall")?))
|
||||
}
|
||||
|
||||
/// Log one bot out (the character stays saved). False when that bot was not online.
|
||||
pub fn despawn_bot(&mut self, guid: u64) -> Result<bool> {
|
||||
Ok(self.bot_command(&format!("botcmd despawn {guid}"))?.to_lowercase().contains("despawned"))
|
||||
}
|
||||
|
||||
/// Log every bot out and delete every bot character for good (the caller saves a recovery point first).
|
||||
pub fn purge_all(&mut self) -> Result<String> {
|
||||
let out = self.bot_command("botcmd purgeall")?;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CoA Server Manager</title>
|
||||
<link rel="icon" type="image/png" href="/src/assets/logo.png" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 499 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.7 MiB |
@@ -539,6 +539,34 @@ async fn companions_take_offline(state: State<'_, AppState>, id: String) -> std:
|
||||
.await
|
||||
}
|
||||
|
||||
/// Log `count` randomly chosen online companions out to lower the load. Uses the per-bot command that every server
|
||||
/// build has, so it works on older builds too.
|
||||
#[tauri::command]
|
||||
async fn companions_despawn_some(state: State<'_, AppState>, id: String, count: u32) -> std::result::Result<CompanionAction, UiError> {
|
||||
let root = path_of(&state, &id)?;
|
||||
let _guard = BusyGuard::acquire(&state, &id)?;
|
||||
blocking(move || {
|
||||
if !(1..=5000).contains(&count) {
|
||||
return Err(Error::Invalid("Choose a number between 1 and 5000.".into()));
|
||||
}
|
||||
let prefix = coa_core::population::bot_account_prefix(&root).to_uppercase();
|
||||
let db = coa_core::db::Db::from_repack(&root, coa_core::db::Account::App)?;
|
||||
let rows = db.query(&format!(
|
||||
"SELECT c.guid FROM acore_characters.characters c JOIN acore_auth.account a ON a.id=c.account WHERE c.online=1 AND UPPER(a.username) LIKE '{prefix}%' ORDER BY RAND() LIMIT {count};"
|
||||
))?;
|
||||
let guids: Vec<u64> = rows.lines().filter_map(|l| l.trim().parse().ok()).collect();
|
||||
let mut ra = Ra::connect(&root)?;
|
||||
let mut done = 0;
|
||||
for g in guids {
|
||||
if ra.despawn_bot(g).unwrap_or(false) {
|
||||
done += 1;
|
||||
}
|
||||
}
|
||||
Ok(CompanionAction { count: done })
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Delete every companion for good. A recovery point of the characters and accounts is saved first.
|
||||
#[tauri::command]
|
||||
async fn companions_delete_all(app: AppHandle, state: State<'_, AppState>, id: String) -> std::result::Result<CompanionAction, UiError> {
|
||||
@@ -976,6 +1004,7 @@ pub fn run() {
|
||||
companions_stop_spawning,
|
||||
companions_take_offline,
|
||||
companions_delete_all,
|
||||
companions_despawn_some,
|
||||
open_link,
|
||||
companion_sizes,
|
||||
add_companions,
|
||||
|
||||
|
After Width: | Height: | Size: 143 KiB |
@@ -562,6 +562,12 @@ const de: Partial<Record<keyof typeof en, string>> = {
|
||||
"comp.deleteDone": "Alle Begleiter wurden gelöscht. Zuvor wurde ein Wiederherstellungspunkt gespeichert.",
|
||||
"err.companion_command_unsupported.title": "Diese Serverversion kann das noch nicht",
|
||||
"err.companion_command_unsupported.message": "Das Bot-Modul dieses Server-Builds kennt diesen Befehl nicht. Aktualisiere den Server oder starte ihn neu, um Laufendes zu stoppen.",
|
||||
|
||||
// Despawn some
|
||||
"comp.despawnSome": "{n} Bots entfernen",
|
||||
"comp.despawnCount": "Wie viele abmelden",
|
||||
"comp.despawnHint": "Entlastet sofort: Jeder Bot online kostet Serverzeit. Sie bleiben gespeichert und kommen beim nächsten Start zurück, wenn die automatische Anmeldung an ist.",
|
||||
"comp.despawnSomeDone": "{n} Begleiter abgemeldet. Sie bleiben gespeichert.",
|
||||
};
|
||||
|
||||
export default de;
|
||||
@@ -562,6 +562,12 @@ const en = {
|
||||
"comp.deleteDone": "All companions were deleted. A recovery point was saved first.",
|
||||
"err.companion_command_unsupported.title": "This server version can not do that yet",
|
||||
"err.companion_command_unsupported.message": "The bot module of this server build has no such command. Update the server, or restart it to stop what is running.",
|
||||
|
||||
// Despawn some
|
||||
"comp.despawnSome": "Despawn {n} bots",
|
||||
"comp.despawnCount": "How many to log out",
|
||||
"comp.despawnHint": "Eases the load right away: every online bot costs server time. They stay saved and return at the next start if automatic login is on.",
|
||||
"comp.despawnSomeDone": "{n} companions logged out. They stay saved.",
|
||||
} as const;
|
||||
|
||||
export default en;
|
||||
@@ -562,6 +562,12 @@ const es: Partial<Record<keyof typeof en, string>> = {
|
||||
"comp.deleteDone": "Se eliminaron todos los compañeros. Antes se guardó un punto de recuperación.",
|
||||
"err.companion_command_unsupported.title": "Esta versión del servidor aún no puede hacerlo",
|
||||
"err.companion_command_unsupported.message": "El módulo de bots de esta versión del servidor no tiene ese comando. Actualiza el servidor o reinícialo para detener lo que esté en marcha.",
|
||||
|
||||
// Despawn some
|
||||
"comp.despawnSome": "Retirar {n} bots",
|
||||
"comp.despawnCount": "Cuántos desconectar",
|
||||
"comp.despawnHint": "Alivia la carga al instante: cada bot conectado cuesta tiempo de servidor. Quedan guardados y vuelven en el próximo inicio si el inicio de sesión automático está activado.",
|
||||
"comp.despawnSomeDone": "{n} compañeros desconectados. Quedan guardados.",
|
||||
};
|
||||
|
||||
export default es;
|
||||
@@ -562,6 +562,12 @@ const fr: Partial<Record<keyof typeof en, string>> = {
|
||||
"comp.deleteDone": "Tous les compagnons ont été supprimés. Un point de restauration a été enregistré d’abord.",
|
||||
"err.companion_command_unsupported.title": "Cette version du serveur ne sait pas encore le faire",
|
||||
"err.companion_command_unsupported.message": "Le module de bots de cette version du serveur n’a pas cette commande. Mettez le serveur à jour ou redémarrez-le pour arrêter ce qui est en cours.",
|
||||
|
||||
// Despawn some
|
||||
"comp.despawnSome": "Retirer {n} bots",
|
||||
"comp.despawnCount": "Combien déconnecter",
|
||||
"comp.despawnHint": "Allège aussitôt la charge : chaque bot en ligne coûte du temps serveur. Ils restent enregistrés et reviennent au prochain démarrage si la connexion automatique est activée.",
|
||||
"comp.despawnSomeDone": "{n} compagnons déconnectés. Ils restent enregistrés.",
|
||||
};
|
||||
|
||||
export default fr;
|
||||
@@ -562,6 +562,12 @@ const ru: Partial<Record<keyof typeof en, string>> = {
|
||||
"comp.deleteDone": "Все компаньоны удалены. Сначала была сохранена точка восстановления.",
|
||||
"err.companion_command_unsupported.title": "Эта версия сервера пока этого не умеет",
|
||||
"err.companion_command_unsupported.message": "В модуле ботов этой сборки сервера нет такой команды. Обновите сервер или перезапустите его, чтобы остановить текущую работу.",
|
||||
|
||||
// Despawn some
|
||||
"comp.despawnSome": "Убрать ботов: {n}",
|
||||
"comp.despawnCount": "Сколько убрать",
|
||||
"comp.despawnHint": "Сразу снижает нагрузку: каждый бот в сети отнимает время сервера. Они остаются сохранёнными и вернутся при следующем запуске, если включён автоматический вход.",
|
||||
"comp.despawnSomeDone": "Убрано из игры компаньонов: {n}. Они остаются сохранёнными.",
|
||||
};
|
||||
|
||||
export default ru;
|
||||
@@ -291,6 +291,7 @@ export const api = {
|
||||
population: (id: string) => invoke<Population | null>("get_population", { id }),
|
||||
stopSpawning: (id: string) => invoke<{ count: number }>("companions_stop_spawning", { id }),
|
||||
takeOffline: (id: string) => invoke<{ count: number }>("companions_take_offline", { id }),
|
||||
despawnSome: (id: string, count: number) => invoke<{ count: number }>("companions_despawn_some", { id, count }),
|
||||
deleteAllCompanions: (id: string) => invoke<{ count: number }>("companions_delete_all", { id }),
|
||||
openLink: (url: string) => invoke<void>("open_link", { url }),
|
||||
performance: (id: string) => invoke<Performance | null>("get_performance", { id }),
|
||||
|
||||
@@ -23,6 +23,7 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
const [job, setJob] = useState<{ baseline: number; target: number; since: number } | null>(null);
|
||||
const [startNote, setStartNote] = useState<string | null>(null);
|
||||
const [startBusy, setStartBusy] = useState(false);
|
||||
const [despawnN, setDespawnN] = useState("10");
|
||||
|
||||
useEffect(() => {
|
||||
void api
|
||||
@@ -67,6 +68,14 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
});
|
||||
};
|
||||
|
||||
const dn = Number(despawnN);
|
||||
const dnValid = Number.isInteger(dn) && dn >= 1 && dn <= 5000;
|
||||
const despawnSome = () =>
|
||||
act(async () => {
|
||||
const r = await api.despawnSome(serverId, dn);
|
||||
return t("comp.despawnSomeDone", { n: r.count });
|
||||
});
|
||||
|
||||
const deleteAll = () => {
|
||||
if (!window.confirm(t("comp.deleteConfirm", { n: pop?.bots_total ?? 0 }))) return;
|
||||
void act(async () => {
|
||||
@@ -213,7 +222,22 @@ export function CompanionsCard({ serverId }: { serverId: string }) {
|
||||
{running && pop && pop.bots_total > 0 && (
|
||||
<div className="mt-5 border-t border-line pt-4">
|
||||
<h3 className="text-sm font-semibold">{t("comp.manageTitle")}</h3>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-sm">
|
||||
<label htmlFor="despawn-n" className="text-muted">{t("comp.despawnCount")}</label>
|
||||
<input
|
||||
id="despawn-n"
|
||||
value={despawnN}
|
||||
onChange={(e) => setDespawnN(e.target.value)}
|
||||
inputMode="numeric"
|
||||
aria-invalid={!dnValid}
|
||||
className="w-20 rounded-md border border-line bg-bg px-3 py-1.5 text-right outline-none focus:border-gold"
|
||||
/>
|
||||
<Button size="sm" variant="secondary" disabled={busy || !dnValid} onClick={() => void despawnSome()}>
|
||||
{t("comp.despawnSome", { n: dnValid ? dn : "…" })}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted">{t("comp.despawnHint")}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="secondary" disabled={busy} onClick={takeOffline}>
|
||||
{t("comp.offline")}
|
||||
</Button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { Archive, Bot, Gauge, Globe, Server, Settings, Terminal, Users, type LucideIcon } from "lucide-react";
|
||||
import { type ServerSummary } from "@/lib/api";
|
||||
import { useT, type Key } from "@/i18n";
|
||||
import logo from "@/assets/logo.png";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Overview } from "@/screens/Overview";
|
||||
import { Placeholder } from "@/screens/Placeholder";
|
||||
@@ -41,7 +42,10 @@ export function Shell(props: {
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<nav className="flex w-60 shrink-0 flex-col border-r border-line bg-[#0b0c0e] p-3" aria-label={t("nav.main")}>
|
||||
<div className="px-3 pb-4 pt-2 text-sm font-semibold tracking-wide text-gold">{t("app.name")}</div>
|
||||
<div className="flex items-center gap-2 px-3 pb-4 pt-2 text-sm font-semibold tracking-wide text-gold">
|
||||
<img src={logo} alt="" aria-hidden className="h-7 w-7" />
|
||||
{t("app.name")}
|
||||
</div>
|
||||
<ul className="flex flex-col gap-0.5">
|
||||
{NAV.map(({ id, label, icon: Icon }) => (
|
||||
<li key={id}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FolderSearch, Download } from "lucide-react";
|
||||
import { useT } from "@/i18n";
|
||||
import logo from "@/assets/logo.png";
|
||||
import { LanguagePicker } from "@/components/LanguagePicker";
|
||||
|
||||
export function Welcome({ onImport, onInstall, onBack }: { onImport: () => void; onInstall: () => void; onBack?: () => void }) {
|
||||
@@ -8,7 +9,7 @@ export function Welcome({ onImport, onInstall, onBack }: { onImport: () => void;
|
||||
<main className="relative flex h-full flex-col items-center justify-center px-8">
|
||||
<LanguagePicker className="absolute right-6 top-5" />
|
||||
<div className="mb-10 text-center">
|
||||
<div className="mx-auto mb-5 h-12 w-12 rounded-full border-2 border-gold/80 bg-card-2" aria-hidden />
|
||||
<img src={logo} alt="" aria-hidden className="mx-auto mb-5 h-24 w-24 drop-shadow-[0_6px_24px_rgb(201_162_74/0.35)]" />
|
||||
<h1 className="text-3xl font-semibold tracking-tight">{t("welcome.title")}</h1>
|
||||
<p className="mt-2 text-muted">{t("welcome.subtitle")}</p>
|
||||
</div>
|
||||
|
||||