Add branding splash and simulator cycle timing

This commit is contained in:
masterdev
2026-06-17 13:51:12 +01:00
parent d2ddcb2b20
commit 6d5e11e433
8 changed files with 203 additions and 15 deletions

View File

@@ -331,19 +331,23 @@ def run_simulator_scenario(payload: SimulatorScenarioCreate, db: Session = Depen
elif payload.scenario == "cycle_signal_edge":
current_value = machine.cycle_signal_edge != "falling"
previous_value = not current_value
last_cycle_at = None
if machine.last_cycle_at is not None:
last_cycle_at = machine.last_cycle_at.replace(tzinfo=UTC) if machine.last_cycle_at.tzinfo is None else machine.last_cycle_at.astimezone(UTC)
current_event_time = last_cycle_at + timedelta(seconds=cycle_time_sec) if last_cycle_at is not None else now
events.extend(
[
SimulatorEventCreate(
machine_id=machine.code,
event_type="digital_input_changed",
timestamp=now - timedelta(milliseconds=max(machine.debounce_ms + 100, 500)),
timestamp=current_event_time - timedelta(milliseconds=max(machine.debounce_ms + 100, 500)),
input_name="cycle_signal",
input_value=previous_value,
),
SimulatorEventCreate(
machine_id=machine.code,
event_type="digital_input_changed",
timestamp=now,
timestamp=current_event_time,
input_name="cycle_signal",
input_value=current_value,
cycle_time_sec=cycle_time_sec,

View File

@@ -195,3 +195,23 @@ def test_simulator_debounce_noise_is_ignored(db_session: Session) -> None:
assert result["scenario"] == "debounce_noise"
assert len(result["events"]) == 2
assert cycles == []
def test_simulator_passive_edge_uses_requested_real_cycle_time(db_session: Session) -> None:
machine = seed_running_machine(db_session)
machine.last_cycle_at = datetime(2026, 6, 14, 8, 0, tzinfo=timezone.utc)
db_session.commit()
result = run_simulator_scenario(
SimulatorScenarioCreate(
machine_id=machine.code,
scenario="cycle_signal_edge",
cycle_time_sec=12,
),
db_session,
)
cycle = db_session.execute(select(Cycle).where(Cycle.machine_id == machine.id)).scalar_one()
assert result["scenario"] == "cycle_signal_edge"
assert len(result["events"]) == 2
assert cycle.cycle_time_sec == 12

BIN
frontend/public/KSS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -39,6 +39,7 @@ function FieldLabel({ label, error, children }: { label: string; error?: ReactNo
}
export default function App() {
const [showSplash, setShowSplash] = useState(true);
const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null);
const [orders, setOrders] = useState<ProductionOrder[]>([]);
const [unqualifiedDowntimes, setUnqualifiedDowntimes] = useState<Downtime[]>([]);
@@ -77,6 +78,11 @@ export default function App() {
return language === "fr" ? fr : en;
}
useEffect(() => {
const timer = window.setTimeout(() => setShowSplash(false), 900);
return () => window.clearTimeout(timer);
}, []);
const moduleTabs: Array<{ key: ModuleKey; label: string; description: string }> = [
{ key: "atelier", label: t("Workshop", "Atelier"), description: t("Machines view", "Vue machines") },
{ key: "machine", label: "Machine", description: t("Cycles and events", "Cycles et evenements") },
@@ -1118,13 +1124,34 @@ export default function App() {
);
}
if (showSplash) {
return (
<div className="splash-screen" aria-label="Plast Track">
<img src="/KSS.png" alt="KSS" />
</div>
);
}
return (
<div className={`app-shell ${isWorkshopMode && activeModule === "atelier" ? "app-shell--workshop" : ""}`}>
<main className="workspace">
<aside className="demo-notice" role="note">
<strong>{t("Simulation and demonstration only", "Simulation et demonstration uniquement")}</strong>
<span>
{t(
"This web application is for simulation and demonstration. It must not be used as a validated production MES or safety system.",
"Cette application web est destinee a la simulation et a la demonstration. Elle ne doit pas etre utilisee comme MES de production valide ni comme systeme de securite.",
)}
</span>
</aside>
<header className="hero">
<div>
<p className="eyebrow">Plast Track MVP</p>
<h1>{t("Workshop supervision", "Supervision atelier")}</h1>
<div className="brand-lockup">
<img className="brand-lockup__logo" src="/KSS.png" alt="KSS" />
<div>
<p className="eyebrow">Plast Track MVP</p>
<h1>{t("Workshop supervision", "Supervision atelier")}</h1>
</div>
</div>
<div className="hero__controls">
<label className="language-select">

View File

@@ -47,6 +47,19 @@ body {
background: var(--bg-base);
}
.splash-screen {
display: grid;
min-height: 100vh;
place-items: center;
background: #FFFFFF;
}
.splash-screen img {
width: min(220px, 58vw);
height: auto;
animation: splash-enter 420ms ease both;
}
button,
input,
select,
@@ -71,6 +84,7 @@ button:disabled {
padding: 12px;
}
.app-shell--workshop .demo-notice,
.app-shell--workshop .hero,
.app-shell--workshop .summary-ribbon,
.app-shell--workshop .module-tabs {
@@ -97,6 +111,30 @@ button:disabled {
align-items: start;
}
.demo-notice {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid color-mix(in srgb, var(--accent) 46%, #FFFFFF);
border-radius: 12px;
background: var(--accent-light);
color: var(--text-primary);
box-shadow: 0 1px 3px rgba(10, 47, 90, 0.06), 0 4px 12px rgba(10, 47, 90, 0.04);
}
.demo-notice strong {
flex: 0 0 auto;
font-family: "Space Grotesk", sans-serif;
font-size: var(--text-base);
}
.demo-notice span {
color: var(--text-secondary);
font-size: var(--text-sm);
line-height: 1.45;
}
.hero,
.panel,
.machine-card,
@@ -115,6 +153,20 @@ button:disabled {
padding: 20px;
}
.brand-lockup {
display: flex;
min-width: 0;
align-items: center;
gap: 16px;
}
.brand-lockup__logo {
width: 74px;
max-height: 58px;
flex: 0 0 auto;
object-fit: contain;
}
.hero h1 {
margin: 0;
font-family: "Space Grotesk", sans-serif;
@@ -1066,6 +1118,8 @@ tr.is-selected-row td {
}
.hero,
.demo-notice,
.brand-lockup,
.hero__controls,
.machine-card__top,
.machine-card__hero,
@@ -1080,6 +1134,10 @@ tr.is-selected-row td {
justify-items: start;
}
.brand-lockup__logo {
width: 64px;
}
.panel-actions {
justify-content: stretch;
}
@@ -1105,6 +1163,18 @@ tr.is-selected-row td {
}
}
@keyframes splash-enter {
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,

BIN
simulator-ui/public/KSS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -120,6 +120,7 @@ function parseInputValue(value: string): boolean | number | string {
}
function App() {
const [showSplash, setShowSplash] = useState(true);
const [language, setLanguage] = useState<Language>(() => {
const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY);
return savedLanguage === "en" || savedLanguage === "fr" ? savedLanguage : "fr";
@@ -141,6 +142,11 @@ function App() {
return value[language];
}
useEffect(() => {
const timer = window.setTimeout(() => setShowSplash(false), 900);
return () => window.clearTimeout(timer);
}, []);
useEffect(() => {
apiRequest<Machine[]>("/api/machines")
.then((items) => {
@@ -220,18 +226,29 @@ function App() {
const selectedMachineInfo = machines.find((machine) => machine.code === selectedMachine);
const machinePowerLabel = selectedMachineInfo?.powered_on ? t("Powered ON", "Sous tension") : t("Powered OFF", "Hors tension");
if (showSplash) {
return (
<main className="splash-screen" aria-label="Plast Track Simulator">
<img src="/KSS.png" alt="KSS" />
</main>
);
}
return (
<main className="shell">
<section className="hero">
<div>
<p className="eyebrow">{t("Separate test console", "Console de test separee")}</p>
<h1>{t("Passive Signal Simulator", "Simulateur signaux passifs")}</h1>
<p className="hero__copy">
{t(
"Simulate cycles, power states, stops, alarms, signal edges, debounce noise, and raw digital inputs without adding test controls to the production dashboard.",
"Simuler cycles, etats alimentation, arrets, alarmes, fronts signal, bruit anti-rebond et entrees digitales brutes sans ajouter de controles de test au tableau de production.",
)}
</p>
<div className="brand-lockup">
<img className="brand-lockup__logo" src="/KSS.png" alt="KSS" />
<div>
<p className="eyebrow">{t("Separate test console", "Console de test separee")}</p>
<h1>{t("Passive Signal Simulator", "Simulateur signaux passifs")}</h1>
<p className="hero__copy">
{t(
"Simulate cycles, power states, stops, alarms, signal edges, debounce noise, and raw digital inputs without adding test controls to the production dashboard.",
"Simuler cycles, etats alimentation, arrets, alarmes, fronts signal, bruit anti-rebond et entrees digitales brutes sans ajouter de controles de test au tableau de production.",
)}
</p>
</div>
</div>
<div className="hero__side">
<label className="language-select">
@@ -285,7 +302,7 @@ function App() {
<div className="numeric-grid">
<label className="field">
<span>{t("Cycle time sec", "Temps cycle sec")}</span>
<span>{t("Real cycle time sec", "Temps de cycle reel sec")}</span>
<input
min="1"
step="0.1"

View File

@@ -22,6 +22,19 @@ body {
min-width: 320px;
}
.splash-screen {
display: grid;
min-height: 100vh;
place-items: center;
background: #ffffff;
}
.splash-screen img {
width: min(220px, 58vw);
height: auto;
animation: splash-enter 420ms ease both;
}
button,
input,
select {
@@ -54,6 +67,21 @@ button:disabled {
max-width: 1280px;
}
.brand-lockup {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 18px;
}
.brand-lockup__logo {
width: 82px;
max-height: 68px;
flex: 0 0 auto;
object-fit: contain;
margin-top: 4px;
}
.eyebrow {
margin: 0 0 8px;
color: var(--accent);
@@ -371,10 +399,20 @@ pre {
}
.hero,
.brand-lockup,
.control-grid {
grid-template-columns: 1fr;
}
.brand-lockup {
display: grid;
}
.brand-lockup__logo {
width: 72px;
margin-top: 0;
}
.connection-card {
min-width: 0;
}
@@ -384,6 +422,18 @@ pre {
}
}
@keyframes splash-enter {
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@media (max-width: 620px) {
.scenario-grid,
.numeric-grid {