verynicegui 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ from verynicegui.app import VeryNiceGUI
2
+ from verynicegui.layout import VeryNiceLayout, RightDrawer
3
+ from verynicegui.table import VeryNiceTable
4
+ from verynicegui.card import VeryNiceCard
5
+ from verynicegui.i18n import tr, set_language
6
+ from verynicegui.auth import (
7
+ get_current_user,
8
+ is_admin,
9
+ has_permission,
10
+ do_login,
11
+ do_logout,
12
+ load_users,
13
+ save_users,
14
+ )
15
+
16
+ __all__ = [
17
+ "VeryNiceGUI",
18
+ "VeryNiceLayout",
19
+ "RightDrawer",
20
+ "VeryNiceTable",
21
+ "VeryNiceCard",
22
+ "tr",
23
+ "set_language",
24
+ "get_current_user",
25
+ "is_admin",
26
+ "has_permission",
27
+ "do_login",
28
+ "do_logout",
29
+ "load_users",
30
+ "save_users",
31
+ ]
@@ -0,0 +1,4 @@
1
+ from verynicegui.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
verynicegui/admin.py ADDED
@@ -0,0 +1,351 @@
1
+ from nicegui import ui
2
+ from verynicegui.auth import (
3
+ load_users,
4
+ save_users,
5
+ hash_password,
6
+ is_admin,
7
+ )
8
+
9
+
10
+ def _role_badge(role: str) -> None:
11
+ """Renders a colorful badge representing the user's role."""
12
+ if role == "admin":
13
+ color = "background:#6366f1;color:white"
14
+ label = "Admin"
15
+ icon_name = "verified"
16
+ else:
17
+ color = "background:#0ea5e9;color:white"
18
+ label = "Kullanıcı"
19
+ icon_name = "person"
20
+
21
+ with ui.element("div").style(
22
+ f"display:inline-flex;align-items:center;gap:4px;padding:4px 12px;"
23
+ f"border-radius:20px;font-size:12px;font-weight:600;{color}"
24
+ ):
25
+ ui.icon(icon_name, size="14px")
26
+ ui.label(label).style("font-size:11px;font-weight:600")
27
+
28
+
29
+ def _perm_chip(label: str) -> None:
30
+ """Renders a custom permission chip styled for dark/light themes."""
31
+ ui.element("span").classes("mn-perm-chip").text = label
32
+
33
+
34
+ def get_available_categories(app_instance) -> list[dict]:
35
+ """Retrieves all distinct categories defined in the menu hierarchy."""
36
+ categories = []
37
+ menu = app_instance.left_drawer_config.get("menu", [])
38
+ for item in menu:
39
+ category = item.get("category")
40
+ if category and category != "admin":
41
+ categories.append({
42
+ "category": category,
43
+ "title": item.get("title", category),
44
+ "icon": item.get("icon", "circle")
45
+ })
46
+ return categories
47
+
48
+
49
+ def admin_page(app_instance) -> None:
50
+ """Renders the built-in Admin User Management Dashboard."""
51
+ if not is_admin():
52
+ ui.navigate.to("/403")
53
+ return
54
+
55
+ card_style = "background-color:var(--mn-card-bg); border:var(--mn-card-border)"
56
+ text_primary = "color:var(--mn-text-primary)"
57
+ text_secondary = "color:var(--mn-text-secondary)"
58
+
59
+ # Page Header
60
+ with ui.row().classes("items-center justify-between w-full mb-2"):
61
+ with ui.column().classes("gap-1"):
62
+ ui.label("Kullanıcı Yönetimi").classes("text-3xl font-bold").style(text_primary)
63
+ ui.label("SaaS platformu kullanıcılarını oluşturun, düzenleyin ve sayfa izinlerini yönetin.").style(text_secondary + ";font-size:14px")
64
+
65
+ ui.button(
66
+ "Yeni Kullanıcı",
67
+ icon="person_add",
68
+ on_click=lambda: create_dialog.open(),
69
+ ).props("unelevated no-caps color=indigo").style("border-radius:12px; padding: 10px 20px")
70
+
71
+ # Refreshable Statistics Cards
72
+ @ui.refreshable
73
+ def stats_row():
74
+ users = load_users()
75
+ admin_count = sum(1 for u in users.values() if u.get("role") == "admin" or "*" in u.get("permissions", []))
76
+ total_count = len(users)
77
+
78
+ stat_cards = [
79
+ ("group", str(total_count), "Toplam Kullanıcı", "#6366f1"),
80
+ ("admin_panel_settings", str(admin_count), "Yönetici (Admin)", "#a855f7"),
81
+ ("shield", str(total_count - admin_count), "Standart Kullanıcı", "#0ea5e9"),
82
+ ]
83
+
84
+ with ui.row().classes("w-full gap-4 mb-4"):
85
+ for icon_name, value, label_text, color in stat_cards:
86
+ with ui.element("div").style(
87
+ f"{card_style};border-radius:16px;"
88
+ f"padding:20px 24px;flex:1;display:flex;align-items:center;gap:20px"
89
+ ):
90
+ with ui.element("div").style(
91
+ f"width:52px;height:52px;border-radius:14px;"
92
+ f"background:{color}15;display:flex;align-items:center;justify-content:center;flex-shrink:0"
93
+ ):
94
+ ui.icon(icon_name, size="28px").style(f"color:{color}")
95
+ with ui.column().classes("gap-0"):
96
+ ui.label(value).style(f"{text_primary};font-size:28px;font-weight:800;line-height:1")
97
+ ui.label(label_text).style(f"{text_secondary};font-size:13px;font-weight:500;margin-top:4px")
98
+
99
+ stats_row()
100
+
101
+ # Refreshable Users List
102
+ @ui.refreshable
103
+ def user_list():
104
+ users = load_users()
105
+ categories = get_available_categories(app_instance)
106
+
107
+ if not users:
108
+ ui.label("Henüz kayıtlı kullanıcı bulunmamaktadır.").style(text_secondary)
109
+ return
110
+
111
+ with ui.column().classes("w-full gap-4"):
112
+ for username, user_data in users.items():
113
+ _user_card(username, user_data, categories, card_style, text_primary, text_secondary)
114
+
115
+ def _user_card(uname, udata, categories, card_style, text_primary, text_secondary):
116
+ with ui.element("div").style(
117
+ f"{card_style};border-radius:16px;padding:20px 24px;"
118
+ f"display:flex;align-items:center;gap:20px;transition:box-shadow 0.2s"
119
+ ):
120
+ # Initials Avatar
121
+ display_name = udata.get("display_name") or uname
122
+ initials = (display_name[:1] or "?").upper()
123
+ av_color = "#6366f1" if udata.get("role") == "admin" else "#0ea5e9"
124
+ with ui.element("div").style(
125
+ f"width:48px;height:48px;border-radius:50%;"
126
+ f"background:{av_color};display:flex;align-items:center;"
127
+ f"justify-content:center;flex-shrink:0;font-size:18px;font-weight:700;color:white"
128
+ ):
129
+ ui.label(initials)
130
+
131
+ # Details Area
132
+ with ui.column().classes("gap-1").style("flex:1;min-width:0"):
133
+ with ui.row().classes("items-center gap-3 flex-wrap"):
134
+ ui.label(display_name).style(f"{text_primary};font-size:16px;font-weight:700")
135
+ ui.label(f"@{uname}").style(f"{text_secondary};font-size:12px")
136
+ _role_badge(udata.get("role", "user"))
137
+
138
+ # Permissions Chip Display
139
+ with ui.row().classes("items-center gap-1 flex-wrap mt-1"):
140
+ perms = udata.get("permissions", [])
141
+ if "*" in perms:
142
+ _perm_chip("Tüm Sayfalar (Admin)")
143
+ else:
144
+ rendered_any = False
145
+ for cat in categories:
146
+ if cat["category"] in perms:
147
+ _perm_chip(cat["title"])
148
+ rendered_any = True
149
+ if not rendered_any:
150
+ ui.label("Erişim Yetkisi Yok").style(f"{text_secondary};font-size:12px;font-style:italic")
151
+
152
+ # Actions Button Group
153
+ with ui.row().classes("items-center gap-2 flex-shrink-0"):
154
+ ui.button(icon="edit", on_click=lambda u=uname, d=udata: open_edit_dialog(u, d)).props(
155
+ "flat round color=indigo dense"
156
+ ).tooltip("Düzenle")
157
+
158
+ if uname != "admin": # Protect primary admin user
159
+ ui.button(
160
+ icon="delete",
161
+ on_click=lambda u=uname: confirm_delete(u),
162
+ ).props("flat round color=negative dense").tooltip("Sil")
163
+
164
+ user_list()
165
+
166
+ # ─── CREATE USER DIALOG ────────────────────────────────────────────────
167
+ with ui.dialog() as create_dialog, ui.card().style(
168
+ f"{card_style};border-radius:20px;padding:32px;width:480px;max-width:95vw"
169
+ ):
170
+ ui.label("Yeni Kullanıcı").style(f"{text_primary};font-size:20px;font-weight:700")
171
+ ui.label("Kullanıcı profil bilgilerini ve sayfa yetkilerini ayarlayın.").style(
172
+ f"{text_secondary};font-size:13px;margin-top:-8px"
173
+ )
174
+
175
+ new_uname = ui.input("Kullanıcı Adı").classes("w-full").props("outlined")
176
+ new_dname = ui.input("Görünen Ad").classes("w-full").props("outlined")
177
+ new_pwd = ui.input("Şifre", password=True, password_toggle_button=True).classes("w-full").props("outlined")
178
+
179
+ ui.label("Sayfa Kategorileri İzinleri").style(
180
+ f"{text_primary};font-size:14px;font-weight:600;margin-top:8px"
181
+ )
182
+
183
+ categories = get_available_categories(app_instance)
184
+ perm_switches: dict[str, ui.switch] = {}
185
+
186
+ with ui.column().classes("w-full gap-2"):
187
+ for cat in categories:
188
+ with ui.row().classes("items-center justify-between w-full"):
189
+ with ui.row().classes("items-center gap-2"):
190
+ ui.icon(cat["icon"], size="18px").style("color:#6366f1")
191
+ ui.label(cat["title"]).style(f"{text_primary};font-size:14px")
192
+ sw = ui.switch().props("color=indigo")
193
+ perm_switches[cat["category"]] = sw
194
+
195
+ ui.separator().style("margin:8px 0")
196
+
197
+ with ui.row().classes("justify-end gap-2 w-full"):
198
+ ui.button("İptal", on_click=create_dialog.close).props("flat no-caps color=grey")
199
+
200
+ def do_create():
201
+ uname = (new_uname.value or "").strip()
202
+ pwd = new_pwd.value or ""
203
+ dname = (new_dname.value or "").strip() or uname
204
+
205
+ if not uname:
206
+ ui.notify("Kullanıcı adı gereklidir.", type="warning")
207
+ return
208
+ if len(pwd) < 4:
209
+ ui.notify("Şifre en az 4 karakter olmalıdır.", type="warning")
210
+ return
211
+
212
+ users = load_users()
213
+ if uname in users:
214
+ ui.notify(f"'{uname}' kullanıcısı zaten mevcut.", type="warning")
215
+ return
216
+
217
+ perms = [cat for cat, sw in perm_switches.items() if sw.value]
218
+ users[uname] = {
219
+ "password_hash": hash_password(pwd),
220
+ "role": "user",
221
+ "permissions": perms,
222
+ "display_name": dname,
223
+ "theme": "light"
224
+ }
225
+ save_users(users)
226
+
227
+ # Reset form fields
228
+ new_uname.value = ""
229
+ new_dname.value = ""
230
+ new_pwd.value = ""
231
+ for sw in perm_switches.values():
232
+ sw.value = False
233
+
234
+ create_dialog.close()
235
+ user_list.refresh()
236
+ stats_row.refresh()
237
+ ui.notify(f"'{uname}' kullanıcısı oluşturuldu.", type="positive", icon="check_circle")
238
+
239
+ ui.button("Oluştur", on_click=do_create).props("unelevated no-caps color=indigo").style("border-radius:10px")
240
+
241
+ # ─── EDIT USER DIALOG ──────────────────────────────────────────────────
242
+ edit_dialog = ui.dialog()
243
+
244
+ def open_edit_dialog(uname: str, udata: dict):
245
+ edit_dialog.clear()
246
+
247
+ with edit_dialog, ui.card().style(
248
+ f"{card_style};border-radius:20px;padding:32px;width:480px;max-width:95vw"
249
+ ):
250
+ ui.label(f"Düzenle: @{uname}").style(f"{text_primary};font-size:20px;font-weight:700")
251
+
252
+ categories = get_available_categories(app_instance)
253
+ current_perms = udata.get("permissions", [])
254
+ is_super = "*" in current_perms
255
+
256
+ edit_dname = ui.input("Görünen Ad", value=udata.get("display_name", uname)).classes("w-full").props("outlined")
257
+ edit_pwd = ui.input("Yeni Şifre (boş bırakılırsa değişmez)", password=True, password_toggle_button=True).classes("w-full").props("outlined")
258
+
259
+ if is_super:
260
+ with ui.row().classes("items-center gap-3 w-full").style(
261
+ "background:rgba(99,102,241,0.1);border-radius:12px;padding:12px 16px;margin-top:8px"
262
+ ):
263
+ ui.icon("verified", size="20px").style("color:#6366f1")
264
+ ui.label("Bu hesap tüm yetkilere sahiptir (Admin).").style(f"{text_secondary};font-size:13px")
265
+ edit_perm_switches = {}
266
+ else:
267
+ ui.label("Sayfa Kategorileri İzinleri").style(
268
+ f"{text_primary};font-size:14px;font-weight:600;margin-top:8px"
269
+ )
270
+ edit_perm_switches: dict[str, ui.switch] = {}
271
+
272
+ with ui.column().classes("w-full gap-2"):
273
+ for cat in categories:
274
+ with ui.row().classes("items-center justify-between w-full"):
275
+ with ui.row().classes("items-center gap-2"):
276
+ ui.icon(cat["icon"], size="18px").style("color:#6366f1")
277
+ ui.label(cat["title"]).style(f"{text_primary};font-size:14px")
278
+ sw = ui.switch(value=cat["category"] in current_perms).props("color=indigo")
279
+ edit_perm_switches[cat["category"]] = sw
280
+
281
+ ui.separator().style("margin:8px 0")
282
+
283
+ with ui.row().classes("justify-end gap-2 w-full"):
284
+ ui.button("İptal", on_click=edit_dialog.close).props("flat no-caps color=grey")
285
+
286
+ def do_save(un=uname, is_sup=is_super, sw_map=edit_perm_switches):
287
+ users = load_users()
288
+ if un not in users:
289
+ ui.notify("Kullanıcı bulunamadı.", type="negative")
290
+ return
291
+
292
+ dname = (edit_dname.value or "").strip()
293
+ if dname:
294
+ users[un]["display_name"] = dname
295
+
296
+ pwd = edit_pwd.value or ""
297
+ if pwd:
298
+ if len(pwd) < 4:
299
+ ui.notify("Şifre en az 4 karakter olmalıdır.", type="warning")
300
+ return
301
+ users[un]["password_hash"] = hash_password(pwd)
302
+
303
+ if not is_sup:
304
+ users[un]["permissions"] = [
305
+ cat for cat, sw in sw_map.items() if sw.value
306
+ ]
307
+
308
+ save_users(users)
309
+ edit_dialog.close()
310
+ user_list.refresh()
311
+ ui.notify("Değişiklikler kaydedildi.", type="positive", icon="check_circle")
312
+
313
+ ui.button("Kaydet", on_click=do_save).props("unelevated no-caps color=indigo").style("border-radius:10px")
314
+
315
+ edit_dialog.open()
316
+
317
+ # ─── CONFIRM DELETE DIALOG ─────────────────────────────────────────────
318
+ def confirm_delete(uname: str):
319
+ with ui.dialog() as del_dialog, ui.card().style(
320
+ f"{card_style};border-radius:20px;padding:28px;width:380px"
321
+ ):
322
+ with ui.row().classes("items-center gap-3 mb-2"):
323
+ with ui.element("div").style(
324
+ "width:44px;height:44px;background:rgba(239,68,68,0.12);"
325
+ "border-radius:12px;display:flex;align-items:center;justify-content:center"
326
+ ):
327
+ ui.icon("warning", size="24px").style("color:#ef4444")
328
+ ui.label("Kullanıcıyı Sil").style(f"{text_primary};font-size:18px;font-weight:700")
329
+
330
+ ui.label(
331
+ f"'{uname}' kullanıcısını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz."
332
+ ).style(f"{text_secondary};font-size:14px")
333
+
334
+ ui.separator().style("margin:12px 0")
335
+
336
+ with ui.row().classes("justify-end gap-2 w-full"):
337
+ ui.button("Vazgeç", on_click=del_dialog.close).props("flat no-caps color=grey")
338
+
339
+ def do_delete(u=uname):
340
+ users = load_users()
341
+ if u in users:
342
+ del users[u]
343
+ save_users(users)
344
+ del_dialog.close()
345
+ user_list.refresh()
346
+ stats_row.refresh()
347
+ ui.notify(f"'{u}' kullanıcısı başarıyla silindi.", type="info", icon="delete")
348
+
349
+ ui.button("Evet, Sil", on_click=do_delete).props("unelevated no-caps color=negative").style("border-radius:10px")
350
+
351
+ del_dialog.open()
verynicegui/app.py ADDED
@@ -0,0 +1,209 @@
1
+ import inspect
2
+ from nicegui import ui
3
+ from verynicegui.layout import VeryNiceLayout, RightDrawer
4
+
5
+
6
+ class VeryNiceGUI:
7
+ """The main application wrapper for verynicegui.
8
+
9
+ Handles configuration, default page routing (login, 403, admin),
10
+ and automatic page layout injection via the @app.page decorator.
11
+ """
12
+
13
+ def __init__(self, title: str = "VeryNiceGUI App", header: dict = None, left_drawer: dict = None, footer: dict = None, translations: dict = None):
14
+ # Configure Header
15
+ self.header_config = {
16
+ "logo": "radar",
17
+ "title": title,
18
+ "show_theme_toggle": True
19
+ }
20
+ if header:
21
+ self.header_config.update(header)
22
+
23
+ # Configure Left Drawer
24
+ self.left_drawer_config = {
25
+ "menu": [],
26
+ "show_settings": True,
27
+ "title": "NAVİGASYON"
28
+ }
29
+ if left_drawer:
30
+ self.left_drawer_config.update(left_drawer)
31
+
32
+ # Configure Footer
33
+ self.footer_config = {
34
+ "left": "",
35
+ "middle": "",
36
+ "right": ""
37
+ }
38
+ if footer:
39
+ self.footer_config.update(footer)
40
+
41
+ # Add global head script and styles for preventing theme loading flashes
42
+ ui.add_head_html("""
43
+ <script>
44
+ (function() {
45
+ var theme = 'light';
46
+ var match = document.cookie.match(new RegExp('(^| )theme=([^;]+)'));
47
+ if (match) theme = match[2];
48
+ if (theme === 'dark') {
49
+ document.documentElement.classList.add('body--dark');
50
+ } else {
51
+ document.documentElement.classList.remove('body--dark');
52
+ }
53
+ })();
54
+ </script>
55
+ <style>
56
+ html, body, .q-layout, .q-page-container {
57
+ background-color: #f8fafc !important;
58
+ }
59
+ html.body--dark, html.body--dark body, html.body--dark .q-layout, html.body--dark .q-page-container,
60
+ body.body--dark, body.body--dark .q-layout, body.body--dark .q-page-container {
61
+ background-color: #0f172a !important;
62
+ }
63
+ </style>
64
+ <meta name="darkreader-lock">
65
+ """, shared=True)
66
+
67
+ # Register custom translations
68
+ if translations:
69
+ from verynicegui.i18n import custom_translations
70
+ for lang, trans_dict in translations.items():
71
+ if lang not in custom_translations:
72
+ custom_translations[lang] = {}
73
+ custom_translations[lang].update(trans_dict)
74
+
75
+ self.registered_routes = set()
76
+
77
+ def page(self, route: str, title: str = None, allowed_categories: list[str] = None, standalone: bool = False):
78
+ """Decorator to register a page route.
79
+
80
+ Automatically wraps pages with authentication checks, authorization (RBAC),
81
+ and layout injection (SaaSLayout).
82
+ If the page function signature contains a parameter named 'drawer' or 'right_drawer',
83
+ a toggleable RightDrawer component is instantiated and injected.
84
+ """
85
+ self.registered_routes.add(route)
86
+
87
+ def decorator(func):
88
+ # Inspect signature to see if page function requests the right drawer or request
89
+ sig = inspect.signature(func)
90
+ drawer_param_name = None
91
+ for param in sig.parameters.values():
92
+ if param.name in ("drawer", "right_drawer"):
93
+ drawer_param_name = param.name
94
+ break
95
+
96
+ from fastapi import Request
97
+
98
+ @ui.page(route)
99
+ def page_wrapper(request: Request):
100
+ # Resolve arguments for user-defined function
101
+ func_kwargs = {}
102
+ if "request" in sig.parameters:
103
+ func_kwargs["request"] = request
104
+
105
+ if standalone:
106
+ func(**func_kwargs)
107
+ return
108
+
109
+ # Auth Guard
110
+ from verynicegui.auth import require_login
111
+ if not require_login():
112
+ return
113
+
114
+ # RBAC Category Guard
115
+ if allowed_categories:
116
+ from verynicegui.auth import has_permission
117
+ authorized = any(has_permission(cat) for cat in allowed_categories)
118
+ if not authorized:
119
+ ui.navigate.to("/403")
120
+ return
121
+
122
+ # Instantiate right drawer if requested by the page function
123
+ right_drawer = None
124
+ if drawer_param_name:
125
+ right_drawer = RightDrawer()
126
+ func_kwargs[drawer_param_name] = right_drawer
127
+
128
+ # Render page inside standard VeryNiceLayout
129
+ with VeryNiceLayout(
130
+ title=title or self.header_config.get("title", "Page"),
131
+ app_instance=self,
132
+ right_drawer=right_drawer,
133
+ current_route=route
134
+ ):
135
+ func(**func_kwargs)
136
+
137
+ return page_wrapper
138
+ return decorator
139
+
140
+ def _register_default_routes(self):
141
+ """Auto-registers system routes (login, logout, 403, and admin) if not overridden."""
142
+ from verynicegui.templates import login_page, forbidden_page
143
+ from verynicegui.auth import do_logout, require_login, is_admin
144
+ from verynicegui.admin import admin_page
145
+
146
+ # Login page (Standalone)
147
+ if "/login" not in self.registered_routes:
148
+ @ui.page("/login")
149
+ def login_route():
150
+ login_page(
151
+ app_title=self.header_config.get("title", "VeryNiceGUI"),
152
+ logo_icon=self.header_config.get("logo", "radar")
153
+ )
154
+
155
+ # Forbidden page (Standalone)
156
+ if "/403" not in self.registered_routes:
157
+ @ui.page("/403")
158
+ def forbidden_route():
159
+ if not require_login():
160
+ return
161
+ forbidden_page(app_title=self.header_config.get("title", "VeryNiceGUI"))
162
+
163
+ # Logout route (API / Action redirect)
164
+ if "/logout" not in self.registered_routes:
165
+ @ui.page("/logout")
166
+ def logout_route():
167
+ do_logout()
168
+ ui.navigate.to("/login")
169
+
170
+ # Admin route (Built-in user management with standard layout)
171
+ if "/admin" not in self.registered_routes:
172
+ @ui.page("/admin")
173
+ def admin_route():
174
+ if not require_login():
175
+ return
176
+ if not is_admin():
177
+ ui.navigate.to("/403")
178
+ return
179
+
180
+ with VeryNiceLayout(
181
+ title="Ayarlar / Admin",
182
+ app_instance=self,
183
+ current_route="/admin"
184
+ ):
185
+ admin_page(app_instance=self)
186
+
187
+ def run(self, **kwargs):
188
+ """Initializes default system routes and runs NiceGUI."""
189
+ # Auto-serve assets directory relative to the running entrypoint script
190
+ import sys
191
+ from pathlib import Path
192
+ if sys.argv and sys.argv[0]:
193
+ entry_dir = Path(sys.argv[0]).parent.resolve()
194
+ assets_path = entry_dir / "assets"
195
+ if assets_path.exists() and assets_path.is_dir():
196
+ from nicegui import app as nicegui_app
197
+ try:
198
+ nicegui_app.add_static_files("/assets", str(assets_path))
199
+ except Exception:
200
+ pass
201
+
202
+ self._register_default_routes()
203
+
204
+ # Merge defaults
205
+ kwargs.setdefault("port", 8080)
206
+ kwargs.setdefault("reload", True)
207
+ kwargs.setdefault("storage_secret", "very-nice-gui-secret-key-123456")
208
+
209
+ ui.run(**kwargs)