Skip to content

Commit 7dd8f90

Browse files
committed
feat: password-protect diagnostics, fix stale toast and autocomplete timing
- Move the donation wall behind the diagnostics page instead of the public Main page; reachable via a new "Donation Wall" button there. - Gate diagnostics access behind an optional diagnostics_password config value, entered on a new password page before the 5-tap gesture reaches Diagnostics. Unset (default) keeps today's no-password behavior. - Recompute username autocomplete on focus, not just on keystrokes, so tapping a field with leftover partial text shows its suggestion immediately. - Reset last-added-amount when starting a fresh InsertMoney/InsertCoins session — it lives on MainWindow and outlives those page instances, so a bill/coin toast that hadn't auto-hidden yet could replay at the start of the next donation.
1 parent c44aaba commit 7dd8f90

8 files changed

Lines changed: 165 additions & 35 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ nix-shell
1212
cargo run
1313
```
1414

15-
The app starts fullscreen. Tap the logo **5 times** to open the diagnostics panel.
15+
The app starts fullscreen. Tap the logo **5 times** to open the diagnostics panel (password-protected if `diagnostics_password` is set — see below).
1616

1717
---
1818

@@ -22,6 +22,7 @@ Create `.config/dramma.toml` next to the binary (or in the working directory you
2222

2323
```toml
2424
token = "your-bearer-token" # For Bot donates
25+
diagnostics_password = "your-password" # Optional — gates the diagnostics panel (and donation wall) if set
2526

2627
# Optional overrides (these are the defaults):
2728
home_assistant_url = "https://ha.hackem.cc/web-dramma/0?BrowserID=dramma"

src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub struct GameEntry {
2727
#[serde(default)]
2828
pub struct Config {
2929
pub token: Option<String>,
30+
pub diagnostics_password: Option<String>,
3031
pub home_assistant_url: String,
3132
pub hass_api_port: u16,
3233
pub cashcode_serial_port: String,
@@ -42,6 +43,7 @@ impl Default for Config {
4243
fn default() -> Self {
4344
Self {
4445
token: None,
46+
diagnostics_password: None,
4547
home_assistant_url: "https://ha.hackem.cc/web-dramma/0?BrowserID=dramma".to_string(),
4648
hass_api_port: 8321,
4749
cashcode_serial_port:

src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ pub fn main() {
5555
// Enable fullscreen mode for kiosk deployment
5656
main_window.window().set_fullscreen(true);
5757

58+
main_window.set_diagnostics_password(
59+
config
60+
.diagnostics_password
61+
.clone()
62+
.unwrap_or_default()
63+
.into(),
64+
);
65+
5866
virtual_keyboard::init(&main_window);
5967
autocomplete_handler::init(&main_window);
6068
let cashcode_tx = bill_acceptor::init(&main_window, &config);

ui/autocomplete_line_edit.slint

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,20 @@ export component AutocompleteLineEdit inherits Rectangle {
6666
}
6767
}
6868

69+
// Recompute the suggestion for whatever text is already in the field —
70+
// otherwise re-focusing a field left with partial text shows no
71+
// suggestion until the user types another character.
72+
function refresh-suggestion() {
73+
root.current-suggestion = AutocompleteHandler.find-suggestion(root.text, root.suggestions);
74+
root.is-valid = AutocompleteHandler.is-valid-input(root.text, root.suggestions);
75+
}
76+
6977
// Reset cursor visibility when focus changes
7078
changed has-focus => {
7179
root.cursor-visible = true;
80+
if (root.has-focus) {
81+
root.refresh-suggestion();
82+
}
7283
AutocompleteHandler.return-key-active = root.has-focus && root.current-suggestion != "";
7384
}
7485
// React to autocomplete trigger from virtual keyboard
@@ -154,8 +165,7 @@ export component AutocompleteLineEdit inherits Rectangle {
154165
text <=> root.text;
155166

156167
edited(value) => {
157-
root.current-suggestion = AutocompleteHandler.find-suggestion(value, root.suggestions);
158-
root.is-valid = AutocompleteHandler.is-valid-input(value, root.suggestions);
168+
root.refresh-suggestion();
159169
root.edited(value);
160170
}
161171

ui/main_window.slint

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { InsertMoney } from "pages/insert_money.slint";
88
import { InsertCoins } from "pages/insert_coins.slint";
99
import { HomeAssistant } from "pages/home_assistant.slint";
1010
import { Diagnostics, LogEntry } from "pages/diagnostics.slint";
11+
import { DiagnosticsAuth } from "pages/diagnostics_auth.slint";
1112
import { Logs, DonationLogItem } from "pages/logs.slint";
1213

1314
export { VirtualKeyboardHandler, KeyModel, AutocompleteHandler }
@@ -18,6 +19,7 @@ enum Page {
1819
InsertMoney,
1920
InsertCoins,
2021
HomeAssistant,
22+
DiagnosticsAuth,
2123
Diagnostics,
2224
Logs,
2325
Top,
@@ -48,6 +50,9 @@ export component MainWindow inherits Window {
4850
in-out property <int> last-added-amount: 0;
4951

5052
// diagnostics
53+
/// Password required to enter Diagnostics, set once from Rust config at
54+
/// startup. Empty string means no password is configured — gate skipped.
55+
in-out property <string> diagnostics-password: "";
5156
in-out property <[LogEntry]> diag-logs: [];
5257
in-out property <LogEntry> diag-bill-status: { level: 0, text: "Initializing..." };
5358
in-out property <LogEntry> diag-coin-status: { level: 0, text: "Initializing..." };
@@ -127,16 +132,13 @@ export component MainWindow inherits Window {
127132

128133
play-clicked => {
129134
root.session-amount = 0;
135+
root.last-added-amount = 0;
130136
root.start-accepting-money();
131137
root.current-page = Page.InsertCoins;
132138
}
133139

134-
logs-clicked => {
135-
root.current-page = Page.Logs;
136-
}
137-
138140
secret-tapped => {
139-
root.current-page = Page.Diagnostics;
141+
root.current-page = root.diagnostics-password == "" ? Page.Diagnostics : Page.DiagnosticsAuth;
140142
}
141143
}
142144
if current-page == Page.Donate: Donate {
@@ -164,6 +166,7 @@ export component MainWindow inherits Window {
164166
root.session-fund-id = fund-id;
165167
root.session-fund-name = self.selected-fund-index >= 0 ? self.fund-items[self.selected-fund-index] : "";
166168
root.session-amount = 0; // reset session amount
169+
root.last-added-amount = 0; // clear any stale toast from a previous session
167170
root.start-accepting-money(); // enable bill acceptor
168171
root.current-page = Page.InsertMoney;
169172
root.enter-insert-money(); // start inactivity timer
@@ -234,6 +237,15 @@ export component MainWindow inherits Window {
234237
root.current-page = Page.Main;
235238
}
236239
}
240+
if current-page == Page.DiagnosticsAuth: DiagnosticsAuth {
241+
expected-password: root.diagnostics-password;
242+
back-clicked => {
243+
root.current-page = Page.Main;
244+
}
245+
unlocked => {
246+
root.current-page = Page.Diagnostics;
247+
}
248+
}
237249
if current-page == Page.Diagnostics: Diagnostics {
238250
log-lines: root.diag-logs;
239251
bill-status: root.diag-bill-status;
@@ -256,14 +268,17 @@ export component MainWindow inherits Window {
256268
check-backend => {
257269
root.diag-check-backend();
258270
}
271+
open-logs => {
272+
root.current-page = Page.Logs;
273+
}
259274
}
260275
if current-page == Page.Logs: Logs {
261276
entries: root.donation-logs;
262277
fetch-logs => {
263278
root.fetch-logs();
264279
}
265280
back-clicked => {
266-
root.current-page = Page.Main;
281+
root.current-page = Page.Diagnostics;
267282
}
268283
}
269284

ui/pages/diagnostics.slint

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export component Diagnostics inherits Rectangle {
1212
callback reenumerate-coins();
1313
callback play-sound();
1414
callback check-backend();
15+
callback open-logs();
1516

1617
in-out property <[LogEntry]> log-lines: [];
1718
in property <LogEntry> bill-status: { level: 0, text: "Initializing..." };
@@ -141,6 +142,18 @@ export component Diagnostics inherits Rectangle {
141142
root.play-sound();
142143
}
143144
}
145+
146+
Button {
147+
text: "Donation Wall";
148+
width: 200px;
149+
enabled: !root.guard;
150+
clicked => {
151+
inactivity-timer.running = false;
152+
inactivity-timer.running = true;
153+
root.seconds-left = 120;
154+
root.open-logs();
155+
}
156+
}
144157
}
145158

146159
// ── Status panel + camera preview ────────────────────────────────
@@ -178,10 +191,9 @@ export component Diagnostics inherits Rectangle {
178191
height: 10px;
179192
border-radius: 5px;
180193
y: (parent.height - self.height) / 2;
181-
background: root.bill-status.level == 1 ? #4caf50 :
182-
root.bill-status.level == 2 ? #ff8c00 :
183-
root.bill-status.level == 3 ? #f44336 : #808080;
194+
background: root.bill-status.level == 1 ? #4caf50 : root.bill-status.level == 2 ? #ff8c00 : root.bill-status.level == 3 ? #f44336 : #808080;
184195
}
196+
185197
Text {
186198
text: root.bill-status.text;
187199
font-size: 13px;
@@ -204,15 +216,15 @@ export component Diagnostics inherits Rectangle {
204216
width: 130px;
205217
vertical-alignment: center;
206218
}
219+
207220
Rectangle {
208221
width: 10px;
209222
height: 10px;
210223
border-radius: 5px;
211224
y: (parent.height - self.height) / 2;
212-
background: root.coin-status.level == 1 ? #4caf50 :
213-
root.coin-status.level == 2 ? #ff8c00 :
214-
root.coin-status.level == 3 ? #f44336 : #808080;
225+
background: root.coin-status.level == 1 ? #4caf50 : root.coin-status.level == 2 ? #ff8c00 : root.coin-status.level == 3 ? #f44336 : #808080;
215226
}
227+
216228
Text {
217229
text: root.coin-status.text;
218230
font-size: 13px;
@@ -235,15 +247,15 @@ export component Diagnostics inherits Rectangle {
235247
width: 130px;
236248
vertical-alignment: center;
237249
}
250+
238251
Rectangle {
239252
width: 10px;
240253
height: 10px;
241254
border-radius: 5px;
242255
y: (parent.height - self.height) / 2;
243-
background: root.backend-status.level == 1 ? #4caf50 :
244-
root.backend-status.level == 2 ? #ff8c00 :
245-
root.backend-status.level == 3 ? #f44336 : #808080;
256+
background: root.backend-status.level == 1 ? #4caf50 : root.backend-status.level == 2 ? #ff8c00 : root.backend-status.level == 3 ? #f44336 : #808080;
246257
}
258+
247259
Text {
248260
text: root.backend-status.text;
249261
font-size: 13px;
@@ -252,6 +264,7 @@ export component Diagnostics inherits Rectangle {
252264
horizontal-stretch: 1;
253265
overflow: elide;
254266
}
267+
255268
Button {
256269
text: "Check";
257270
width: 80px;

ui/pages/diagnostics_auth.slint

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Button, LineEdit, Palette } from "std-widgets.slint";
2+
import { VirtualKeyboardHandler, VirtualKeyboard } from "../virtual_keyboard.slint";
3+
4+
export component DiagnosticsAuth inherits Rectangle {
5+
callback back-clicked();
6+
callback unlocked();
7+
in property <string> expected-password: "";
8+
9+
property <bool> wrong: false;
10+
11+
init => {
12+
VirtualKeyboardHandler.open = true;
13+
password-input.focus();
14+
}
15+
16+
function try-unlock() {
17+
if password-input.text == root.expected-password {
18+
VirtualKeyboardHandler.open = false;
19+
root.wrong = false;
20+
password-input.text = "";
21+
root.unlocked();
22+
} else {
23+
root.wrong = true;
24+
password-input.text = "";
25+
}
26+
}
27+
28+
background: Palette.background;
29+
30+
VerticalLayout {
31+
alignment: center;
32+
spacing: 20px;
33+
padding: 32px;
34+
35+
Text {
36+
text: "🔒 Diagnostics";
37+
font-size: 28px;
38+
font-weight: 700;
39+
color: Palette.foreground;
40+
horizontal-alignment: center;
41+
}
42+
43+
Text {
44+
text: root.wrong ? "Wrong password, try again" : "Enter password to continue";
45+
font-size: 16px;
46+
color: root.wrong ? #e53935 : Palette.foreground;
47+
opacity: root.wrong ? 1.0 : 0.6;
48+
horizontal-alignment: center;
49+
}
50+
51+
HorizontalLayout {
52+
alignment: center;
53+
54+
password-input := LineEdit {
55+
width: 320px;
56+
height: 60px;
57+
font-size: 22px;
58+
input-type: password;
59+
horizontal-alignment: center;
60+
accepted => {
61+
root.try-unlock();
62+
}
63+
}
64+
}
65+
66+
HorizontalLayout {
67+
alignment: center;
68+
spacing: 16px;
69+
70+
Button {
71+
text: "← Back";
72+
width: 140px;
73+
height: 56px;
74+
clicked => {
75+
VirtualKeyboardHandler.open = false;
76+
password-input.text = "";
77+
root.wrong = false;
78+
root.back-clicked();
79+
}
80+
}
81+
82+
Button {
83+
text: "Unlock";
84+
primary: true;
85+
width: 140px;
86+
height: 56px;
87+
clicked => {
88+
root.try-unlock();
89+
}
90+
}
91+
}
92+
}
93+
94+
keyboard := VirtualKeyboard {
95+
y: VirtualKeyboardHandler.open ? parent.height - self.height : parent.height;
96+
}
97+
}

0 commit comments

Comments
 (0)