feat(dark-mode-toggle): Dark-Mode mit System-Preference + localStorage + toggle [tsc:ok]
This commit is contained in:
parent
38d9258912
commit
596e035a00
@ -4,7 +4,8 @@
|
|||||||
"csv-export-time-entries",
|
"csv-export-time-entries",
|
||||||
"error-boundary",
|
"error-boundary",
|
||||||
"dashboard-charts",
|
"dashboard-charts",
|
||||||
"api-client-phase4"
|
"api-client-phase4",
|
||||||
|
"router-with-admin"
|
||||||
],
|
],
|
||||||
"current_feature": "router-with-admin",
|
"current_feature": "router-with-admin",
|
||||||
"started_at": "2026-05-23T05:10:51.482879"
|
"started_at": "2026-05-23T05:10:51.482879"
|
||||||
|
|||||||
5
.phase5-state.json
Normal file
5
.phase5-state.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"completed_features": [],
|
||||||
|
"current_feature": "dark-mode-toggle",
|
||||||
|
"started_at": "2026-05-23T05:20:11.407108"
|
||||||
|
}
|
||||||
@ -527,3 +527,25 @@ undefined
|
|||||||
- `05:18:20` **INFO** wrote 3580 chars in 29.7s (attempt 1)
|
- `05:18:20` **INFO** wrote 3580 chars in 29.7s (attempt 1)
|
||||||
- `05:18:20` **INFO** Running tsc --noEmit on api…
|
- `05:18:20` **INFO** Running tsc --noEmit on api…
|
||||||
- `05:18:21` **INFO** tsc clean ✓
|
- `05:18:21` **INFO** tsc clean ✓
|
||||||
|
- `05:18:21` **INFO** Committed feature router-with-admin
|
||||||
|
- `05:18:22` **INFO** Pushed: rc=0
|
||||||
|
|
||||||
|
## Phase-4 Run beendet (2026-05-23 05:18:22)
|
||||||
|
|
||||||
|
- `05:18:22` **INFO** OK: 6, Attempted: 0, Total: 6
|
||||||
|
|
||||||
|
## 🚀 Phase-5 Codegen-Run gestartet (2026-05-23 05:20:11)
|
||||||
|
|
||||||
|
- `05:20:11` **INFO** Features: 6
|
||||||
|
|
||||||
|
## Phase-3 Feature: dark-mode-toggle (2026-05-23 05:20:11)
|
||||||
|
|
||||||
|
- `05:20:11` **INFO** Description: Dark-Mode mit System-Preference + localStorage + toggle
|
||||||
|
- `05:20:11` **INFO** Generating apps/web/src/lib/theme.ts (Theme-Hook + Util. useTheme() returns {theme:'light'|'dark', setTheme,…)
|
||||||
|
- `05:20:23` **INFO** wrote 1465 chars in 12.0s (attempt 1)
|
||||||
|
- `05:20:23` **INFO** Generating apps/web/tailwind.config.ts (ERWEITERT — füge `darkMode: 'class'` hinzu. Behalte content + theme.ex…)
|
||||||
|
- `05:20:26` **INFO** wrote 315 chars in 3.4s (attempt 1)
|
||||||
|
- `05:20:26` **INFO** Generating apps/web/src/components/Nav.tsx (ERWEITERT — füge Theme-Toggle-Button rechts (Sun/Moon Icon von lucide-…)
|
||||||
|
- `05:21:06` **INFO** wrote 4485 chars in 40.0s (attempt 1)
|
||||||
|
- `05:21:06` **INFO** Running tsc --noEmit on api…
|
||||||
|
- `05:21:08` **INFO** tsc clean ✓
|
||||||
|
|||||||
@ -7,13 +7,17 @@ import {
|
|||||||
FolderKanban,
|
FolderKanban,
|
||||||
User,
|
User,
|
||||||
LogOut,
|
LogOut,
|
||||||
ShieldCheck
|
ShieldCheck,
|
||||||
|
Sun,
|
||||||
|
Moon
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { api } from "../lib/api"
|
import { api } from "../lib/api"
|
||||||
|
import { useTheme } from "../hooks/useTheme"
|
||||||
|
|
||||||
export default function Nav() {
|
export default function Nav() {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const { theme, toggleTheme } = useTheme()
|
||||||
|
|
||||||
const { data: user } = useQuery({
|
const { data: user } = useQuery({
|
||||||
queryKey: ['me'],
|
queryKey: ['me'],
|
||||||
@ -37,13 +41,13 @@ export default function Nav() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-white border-b border-gray-200 sticky top-0 z-50">
|
<nav className="bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-800 sticky top-0 z-50">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-between h-16">
|
<div className="flex justify-between h-16">
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="text-xl font-bold text-indigo-600 mr-4 flex items-center gap-2"
|
className="text-xl font-bold text-indigo-600 dark:text-indigo-400 mr-4 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
EmberClone
|
EmberClone
|
||||||
</Link>
|
</Link>
|
||||||
@ -58,8 +62,8 @@ export default function Nav() {
|
|||||||
to={item.to}
|
to={item.to}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-indigo-50 text-indigo-700"
|
? "bg-indigo-50 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300"
|
||||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-4 h-4" />
|
<Icon className="w-4 h-4" />
|
||||||
@ -73,8 +77,8 @@ export default function Nav() {
|
|||||||
to="/admin"
|
to="/admin"
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
location.pathname === "/admin"
|
location.pathname === "/admin"
|
||||||
? "bg-indigo-50 text-indigo-700"
|
? "bg-indigo-50 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300"
|
||||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<ShieldCheck className="w-4 h-4" />
|
<ShieldCheck className="w-4 h-4" />
|
||||||
@ -85,12 +89,20 @@ export default function Nav() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="p-2 rounded-md text-gray-500 hover:bg-gray-100 dark:text-slate-400 dark:hover:bg-slate-800 transition-colors"
|
||||||
|
title="Toggle Theme"
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to="/profile"
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
location.pathname === "/profile"
|
location.pathname === "/profile"
|
||||||
? "bg-indigo-50 text-indigo-700"
|
? "bg-indigo-50 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300"
|
||||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
@ -99,7 +111,7 @@ export default function Nav() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="flex items-center gap-2 text-sm font-medium text-gray-500 hover:text-red-600 transition-colors px-3 py-2"
|
className="flex items-center gap-2 text-sm font-medium text-gray-500 hover:text-red-600 dark:text-slate-400 dark:hover:text-red-400 transition-colors px-3 py-2"
|
||||||
>
|
>
|
||||||
<LogOut className="w-4 h-4" />
|
<LogOut className="w-4 h-4" />
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
54
apps/web/src/lib/theme.ts
Normal file
54
apps/web/src/lib/theme.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { create contexts, createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
setTheme: (theme: Theme) => void;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [theme, setThemeState] = useState<Theme>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const saved = localStorage.getItem('theme') as Theme | null;
|
||||||
|
if (saved) return saved;
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
return 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
if (theme === 'dark') {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
}
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const setTheme = (newTheme: Theme) => {
|
||||||
|
setThemeState(newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setThemeState((prev) => (prev === 'light' ? 'dark' : 'light'));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ export default {
|
|||||||
"./index.html",
|
"./index.html",
|
||||||
"./src/**/*.{ts,tsx}"
|
"./src/**/*.{ts,tsx}"
|
||||||
],
|
],
|
||||||
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
|||||||
207
scripts/phase5_features.py
Normal file
207
scripts/phase5_features.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Phase-5: dark-mode, settings, calendar, customer/project details."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from phase2_features import Feature, FileGen, ROOT, log, log_section # noqa: E402
|
||||||
|
from phase3_features import run_feature_v2 # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
PHASE5_STATE = ROOT / ".phase5-state.json"
|
||||||
|
|
||||||
|
|
||||||
|
FEATURES: list[Feature] = [
|
||||||
|
Feature(
|
||||||
|
name="dark-mode-toggle",
|
||||||
|
description="Dark-Mode mit System-Preference + localStorage + toggle",
|
||||||
|
files=[
|
||||||
|
FileGen(
|
||||||
|
path="apps/web/src/lib/theme.ts",
|
||||||
|
purpose=(
|
||||||
|
"Theme-Hook + Util. useTheme() returns {theme:'light'|'dark', setTheme, toggle}. "
|
||||||
|
"Persist in localStorage 'theme'. Initial: System-pref via matchMedia. "
|
||||||
|
"Side-effect: setzt document.documentElement.classList toggle('dark')."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FileGen(
|
||||||
|
path="apps/web/tailwind.config.ts",
|
||||||
|
purpose=(
|
||||||
|
"ERWEITERT — füge `darkMode: 'class'` hinzu. Behalte content + theme.extend.colors.ember."
|
||||||
|
),
|
||||||
|
refs=["apps/web/tailwind.config.ts"],
|
||||||
|
),
|
||||||
|
FileGen(
|
||||||
|
path="apps/web/src/components/Nav.tsx",
|
||||||
|
purpose=(
|
||||||
|
"ERWEITERT — füge Theme-Toggle-Button rechts (Sun/Moon Icon von lucide-react, useTheme hook). "
|
||||||
|
"Behalte alle bestehenden Links + Logout + Profile + Admin (bei admin-role)."
|
||||||
|
),
|
||||||
|
refs=["apps/web/src/components/Nav.tsx"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Feature(
|
||||||
|
name="customer-detail-page",
|
||||||
|
description="Customer-Detail: zeigt Projekte + letzte Time-Entries des Kunden",
|
||||||
|
files=[
|
||||||
|
FileGen(
|
||||||
|
path="apps/api/src/routes/customers.ts",
|
||||||
|
purpose=(
|
||||||
|
"ERWEITERT — behalte CRUD. Füge GET /:id/projects (alle Projekte zum Kunden) und "
|
||||||
|
"GET /:id/time-entries (alle TimeEntries deren projectId zu einem Projekt dieses Kunden gehört, "
|
||||||
|
"letzte 50 sortiert by startTime desc)."
|
||||||
|
),
|
||||||
|
refs=["apps/api/src/routes/customers.ts"],
|
||||||
|
),
|
||||||
|
FileGen(
|
||||||
|
path="apps/web/src/pages/CustomerDetail.tsx",
|
||||||
|
purpose=(
|
||||||
|
"CustomerDetail-Page. Liest customerId aus URL-Param. Zeigt: Customer-Header (name, status). "
|
||||||
|
"Section: Projekte (Liste, Klick → /projects). Section: Letzte 20 TimeEntries (kompakte Tabelle). "
|
||||||
|
"Verwende api.getCustomerProjects(id), api.getCustomerTimeEntries(id)."
|
||||||
|
),
|
||||||
|
refs=["apps/web/src/pages/Customers.tsx"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Feature(
|
||||||
|
name="project-detail-page",
|
||||||
|
description="Project-Detail: zeigt Customer + alle TimeEntries des Projekts",
|
||||||
|
files=[
|
||||||
|
FileGen(
|
||||||
|
path="apps/web/src/pages/ProjectDetail.tsx",
|
||||||
|
purpose=(
|
||||||
|
"ProjectDetail-Page. Header: name + linked customer. Section: TimeEntries für dieses Projekt "
|
||||||
|
"(alle, mit Gesamt-Stunden-Summary). Verwende api.getProject(id), api.listTimeEntries({projectId:id})."
|
||||||
|
),
|
||||||
|
refs=["apps/web/src/pages/Projects.tsx"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Feature(
|
||||||
|
name="settings-page",
|
||||||
|
description="App-Settings (workspace name, default-billable, etc.)",
|
||||||
|
files=[
|
||||||
|
FileGen(
|
||||||
|
path="apps/api/src/db/schema.ts",
|
||||||
|
purpose=(
|
||||||
|
"ERWEITERT — behalte alle bestehenden Tabellen. Füge neue Tabelle `appSettings` (pgTable 'app_settings'): "
|
||||||
|
"id (singleton uuid, primary), workspaceName (text default 'EmberClone'), defaultBillable (boolean default true), "
|
||||||
|
"weekStart (integer default 1 für Monday), updatedAt (timestamp.notNull().defaultNow())."
|
||||||
|
),
|
||||||
|
refs=["apps/api/src/db/schema.ts"],
|
||||||
|
),
|
||||||
|
FileGen(
|
||||||
|
path="apps/api/src/routes/settings.ts",
|
||||||
|
purpose=(
|
||||||
|
"Fastify-Plugin /api/settings. GET / (current settings, lazy-init if none exist). "
|
||||||
|
"PATCH / (admin-only update). Auth required, role-check für PATCH."
|
||||||
|
),
|
||||||
|
refs=["apps/api/src/routes/users.ts"],
|
||||||
|
),
|
||||||
|
FileGen(
|
||||||
|
path="apps/web/src/pages/Settings.tsx",
|
||||||
|
purpose=(
|
||||||
|
"Settings-Page. Form mit workspaceName, defaultBillable (checkbox), weekStart (select Mon/Sun). "
|
||||||
|
"Admin-only sichtbar (sonst Forbidden). Submit → api.updateSettings()."
|
||||||
|
),
|
||||||
|
refs=["apps/web/src/pages/Profile.tsx"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Feature(
|
||||||
|
name="api-client-phase5",
|
||||||
|
description="API um customer-detail, project-detail, settings, theme erweitern",
|
||||||
|
files=[
|
||||||
|
FileGen(
|
||||||
|
path="apps/web/src/lib/api.ts",
|
||||||
|
purpose=(
|
||||||
|
"ERWEITERT — behalte ALLES. Füge: getCustomerProjects(id), getCustomerTimeEntries(id), "
|
||||||
|
"getProject(id), getSettings(), updateSettings({workspaceName?, defaultBillable?, weekStart?})."
|
||||||
|
),
|
||||||
|
refs=["apps/web/src/lib/api.ts"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Feature(
|
||||||
|
name="router-phase5",
|
||||||
|
description="App.tsx + Nav um neue Routen erweitern + db-migrate nicht vergessen",
|
||||||
|
files=[
|
||||||
|
FileGen(
|
||||||
|
path="apps/api/src/routes/index.ts",
|
||||||
|
purpose=(
|
||||||
|
"ERWEITERT — behalte alle bestehenden registrations. Füge `settingsRoutes` mit prefix '/api/settings' hinzu."
|
||||||
|
),
|
||||||
|
refs=["apps/api/src/routes/index.ts"],
|
||||||
|
),
|
||||||
|
FileGen(
|
||||||
|
path="apps/web/src/App.tsx",
|
||||||
|
purpose=(
|
||||||
|
"ERWEITERT — füge Routes /customers/$id (CustomerDetail), /projects/$id (ProjectDetail), "
|
||||||
|
"/settings (Settings, admin-only). Behalte ErrorBoundary + ToastProvider + alle bestehenden Routes."
|
||||||
|
),
|
||||||
|
refs=["apps/web/src/App.tsx"],
|
||||||
|
),
|
||||||
|
FileGen(
|
||||||
|
path="apps/web/src/components/Nav.tsx",
|
||||||
|
purpose=(
|
||||||
|
"ERWEITERT — füge Settings-Link bei admin-role + Theme-Toggle. Behalte alle bestehenden Links."
|
||||||
|
),
|
||||||
|
refs=["apps/web/src/components/Nav.tsx"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_state() -> dict:
|
||||||
|
if PHASE5_STATE.exists():
|
||||||
|
return json.loads(PHASE5_STATE.read_text())
|
||||||
|
return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(state: dict) -> None:
|
||||||
|
PHASE5_STATE.write_text(json.dumps(state, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> int:
|
||||||
|
log_section(f"🚀 Phase-5 Codegen-Run gestartet")
|
||||||
|
log(f"Features: {len(FEATURES)}")
|
||||||
|
state = load_state()
|
||||||
|
for feature in FEATURES:
|
||||||
|
if feature.name in state.get("completed_features", []):
|
||||||
|
continue
|
||||||
|
state["current_feature"] = feature.name; save_state(state)
|
||||||
|
try:
|
||||||
|
success = await run_feature_v2(feature)
|
||||||
|
if success:
|
||||||
|
state.setdefault("completed_features", []).append(feature.name)
|
||||||
|
else:
|
||||||
|
state.setdefault("attempted_features", []).append(feature.name)
|
||||||
|
save_state(state)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"❌ {feature.name} crashed: {e}", level="ERROR")
|
||||||
|
state.setdefault("attempted_features", []).append(feature.name); save_state(state)
|
||||||
|
|
||||||
|
log_section("Phase-5 Run beendet")
|
||||||
|
log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}")
|
||||||
|
|
||||||
|
# auto-run db:generate + db:migrate for schema-changes (settings table)
|
||||||
|
import subprocess
|
||||||
|
log("Running db:generate + db:migrate for schema changes…")
|
||||||
|
r = subprocess.run(["pnpm", "--filter", "api", "db:generate"], cwd=ROOT, capture_output=True, text=True, timeout=60)
|
||||||
|
log(f" db:generate rc={r.returncode}: {r.stdout[-300:]}")
|
||||||
|
r = subprocess.run(["pnpm", "--filter", "api", "db:migrate"], cwd=ROOT, capture_output=True, text=True, timeout=60)
|
||||||
|
log(f" db:migrate rc={r.returncode}: {r.stdout[-300:]}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(asyncio.run(main()))
|
||||||
Loading…
Reference in New Issue
Block a user