Add branding splash and simulator cycle timing
This commit is contained in:
@@ -331,19 +331,23 @@ def run_simulator_scenario(payload: SimulatorScenarioCreate, db: Session = Depen
|
|||||||
elif payload.scenario == "cycle_signal_edge":
|
elif payload.scenario == "cycle_signal_edge":
|
||||||
current_value = machine.cycle_signal_edge != "falling"
|
current_value = machine.cycle_signal_edge != "falling"
|
||||||
previous_value = not current_value
|
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(
|
events.extend(
|
||||||
[
|
[
|
||||||
SimulatorEventCreate(
|
SimulatorEventCreate(
|
||||||
machine_id=machine.code,
|
machine_id=machine.code,
|
||||||
event_type="digital_input_changed",
|
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_name="cycle_signal",
|
||||||
input_value=previous_value,
|
input_value=previous_value,
|
||||||
),
|
),
|
||||||
SimulatorEventCreate(
|
SimulatorEventCreate(
|
||||||
machine_id=machine.code,
|
machine_id=machine.code,
|
||||||
event_type="digital_input_changed",
|
event_type="digital_input_changed",
|
||||||
timestamp=now,
|
timestamp=current_event_time,
|
||||||
input_name="cycle_signal",
|
input_name="cycle_signal",
|
||||||
input_value=current_value,
|
input_value=current_value,
|
||||||
cycle_time_sec=cycle_time_sec,
|
cycle_time_sec=cycle_time_sec,
|
||||||
|
|||||||
@@ -195,3 +195,23 @@ def test_simulator_debounce_noise_is_ignored(db_session: Session) -> None:
|
|||||||
assert result["scenario"] == "debounce_noise"
|
assert result["scenario"] == "debounce_noise"
|
||||||
assert len(result["events"]) == 2
|
assert len(result["events"]) == 2
|
||||||
assert cycles == []
|
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
BIN
frontend/public/KSS.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -39,6 +39,7 @@ function FieldLabel({ label, error, children }: { label: string; error?: ReactNo
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [showSplash, setShowSplash] = useState(true);
|
||||||
const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null);
|
const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null);
|
||||||
const [orders, setOrders] = useState<ProductionOrder[]>([]);
|
const [orders, setOrders] = useState<ProductionOrder[]>([]);
|
||||||
const [unqualifiedDowntimes, setUnqualifiedDowntimes] = useState<Downtime[]>([]);
|
const [unqualifiedDowntimes, setUnqualifiedDowntimes] = useState<Downtime[]>([]);
|
||||||
@@ -77,6 +78,11 @@ export default function App() {
|
|||||||
return language === "fr" ? fr : en;
|
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 }> = [
|
const moduleTabs: Array<{ key: ModuleKey; label: string; description: string }> = [
|
||||||
{ key: "atelier", label: t("Workshop", "Atelier"), description: t("Machines view", "Vue machines") },
|
{ key: "atelier", label: t("Workshop", "Atelier"), description: t("Machines view", "Vue machines") },
|
||||||
{ key: "machine", label: "Machine", description: t("Cycles and events", "Cycles et evenements") },
|
{ 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 (
|
return (
|
||||||
<div className={`app-shell ${isWorkshopMode && activeModule === "atelier" ? "app-shell--workshop" : ""}`}>
|
<div className={`app-shell ${isWorkshopMode && activeModule === "atelier" ? "app-shell--workshop" : ""}`}>
|
||||||
<main className="workspace">
|
<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">
|
<header className="hero">
|
||||||
<div>
|
<div className="brand-lockup">
|
||||||
<p className="eyebrow">Plast Track MVP</p>
|
<img className="brand-lockup__logo" src="/KSS.png" alt="KSS" />
|
||||||
<h1>{t("Workshop supervision", "Supervision atelier")}</h1>
|
<div>
|
||||||
|
<p className="eyebrow">Plast Track MVP</p>
|
||||||
|
<h1>{t("Workshop supervision", "Supervision atelier")}</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hero__controls">
|
<div className="hero__controls">
|
||||||
<label className="language-select">
|
<label className="language-select">
|
||||||
|
|||||||
@@ -47,6 +47,19 @@ body {
|
|||||||
background: var(--bg-base);
|
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,
|
button,
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
@@ -71,6 +84,7 @@ button:disabled {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell--workshop .demo-notice,
|
||||||
.app-shell--workshop .hero,
|
.app-shell--workshop .hero,
|
||||||
.app-shell--workshop .summary-ribbon,
|
.app-shell--workshop .summary-ribbon,
|
||||||
.app-shell--workshop .module-tabs {
|
.app-shell--workshop .module-tabs {
|
||||||
@@ -97,6 +111,30 @@ button:disabled {
|
|||||||
align-items: start;
|
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,
|
.hero,
|
||||||
.panel,
|
.panel,
|
||||||
.machine-card,
|
.machine-card,
|
||||||
@@ -115,6 +153,20 @@ button:disabled {
|
|||||||
padding: 20px;
|
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 {
|
.hero h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Space Grotesk", sans-serif;
|
font-family: "Space Grotesk", sans-serif;
|
||||||
@@ -1066,6 +1118,8 @@ tr.is-selected-row td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero,
|
.hero,
|
||||||
|
.demo-notice,
|
||||||
|
.brand-lockup,
|
||||||
.hero__controls,
|
.hero__controls,
|
||||||
.machine-card__top,
|
.machine-card__top,
|
||||||
.machine-card__hero,
|
.machine-card__hero,
|
||||||
@@ -1080,6 +1134,10 @@ tr.is-selected-row td {
|
|||||||
justify-items: start;
|
justify-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-lockup__logo {
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-actions {
|
.panel-actions {
|
||||||
justify-content: stretch;
|
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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
|
|||||||
BIN
simulator-ui/public/KSS.png
Normal file
BIN
simulator-ui/public/KSS.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -120,6 +120,7 @@ function parseInputValue(value: string): boolean | number | string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [showSplash, setShowSplash] = useState(true);
|
||||||
const [language, setLanguage] = useState<Language>(() => {
|
const [language, setLanguage] = useState<Language>(() => {
|
||||||
const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY);
|
const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY);
|
||||||
return savedLanguage === "en" || savedLanguage === "fr" ? savedLanguage : "fr";
|
return savedLanguage === "en" || savedLanguage === "fr" ? savedLanguage : "fr";
|
||||||
@@ -141,6 +142,11 @@ function App() {
|
|||||||
return value[language];
|
return value[language];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setTimeout(() => setShowSplash(false), 900);
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiRequest<Machine[]>("/api/machines")
|
apiRequest<Machine[]>("/api/machines")
|
||||||
.then((items) => {
|
.then((items) => {
|
||||||
@@ -220,18 +226,29 @@ function App() {
|
|||||||
const selectedMachineInfo = machines.find((machine) => machine.code === selectedMachine);
|
const selectedMachineInfo = machines.find((machine) => machine.code === selectedMachine);
|
||||||
const machinePowerLabel = selectedMachineInfo?.powered_on ? t("Powered ON", "Sous tension") : t("Powered OFF", "Hors tension");
|
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 (
|
return (
|
||||||
<main className="shell">
|
<main className="shell">
|
||||||
<section className="hero">
|
<section className="hero">
|
||||||
<div>
|
<div className="brand-lockup">
|
||||||
<p className="eyebrow">{t("Separate test console", "Console de test separee")}</p>
|
<img className="brand-lockup__logo" src="/KSS.png" alt="KSS" />
|
||||||
<h1>{t("Passive Signal Simulator", "Simulateur signaux passifs")}</h1>
|
<div>
|
||||||
<p className="hero__copy">
|
<p className="eyebrow">{t("Separate test console", "Console de test separee")}</p>
|
||||||
{t(
|
<h1>{t("Passive Signal Simulator", "Simulateur signaux passifs")}</h1>
|
||||||
"Simulate cycles, power states, stops, alarms, signal edges, debounce noise, and raw digital inputs without adding test controls to the production dashboard.",
|
<p className="hero__copy">
|
||||||
"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.",
|
{t(
|
||||||
)}
|
"Simulate cycles, power states, stops, alarms, signal edges, debounce noise, and raw digital inputs without adding test controls to the production dashboard.",
|
||||||
</p>
|
"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>
|
||||||
<div className="hero__side">
|
<div className="hero__side">
|
||||||
<label className="language-select">
|
<label className="language-select">
|
||||||
@@ -285,7 +302,7 @@ function App() {
|
|||||||
|
|
||||||
<div className="numeric-grid">
|
<div className="numeric-grid">
|
||||||
<label className="field">
|
<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
|
<input
|
||||||
min="1"
|
min="1"
|
||||||
step="0.1"
|
step="0.1"
|
||||||
|
|||||||
@@ -22,6 +22,19 @@ body {
|
|||||||
min-width: 320px;
|
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,
|
button,
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
@@ -54,6 +67,21 @@ button:disabled {
|
|||||||
max-width: 1280px;
|
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 {
|
.eyebrow {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@@ -371,10 +399,20 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero,
|
.hero,
|
||||||
|
.brand-lockup,
|
||||||
.control-grid {
|
.control-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-lockup {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-lockup__logo {
|
||||||
|
width: 72px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.connection-card {
|
.connection-card {
|
||||||
min-width: 0;
|
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) {
|
@media (max-width: 620px) {
|
||||||
.scenario-grid,
|
.scenario-grid,
|
||||||
.numeric-grid {
|
.numeric-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user