AzraelH commited on
Commit
40ba97e
·
1 Parent(s): 73908b6

Serve web assets as separate files

Browse files
Files changed (1) hide show
  1. server/app.py +296 -282
server/app.py CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
5
  from textwrap import dedent
6
 
7
  import uvicorn
8
- from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
9
  from openenv.core.env_server.http_server import create_fastapi_app
10
 
11
  try:
@@ -29,6 +29,289 @@ app = create_fastapi_app(
29
  max_concurrent_envs=2,
30
  )
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  WEB_PAGE = dedent(
33
  """\
34
  <!doctype html>
@@ -37,179 +320,7 @@ WEB_PAGE = dedent(
37
  <meta charset="utf-8">
38
  <meta name="viewport" content="width=device-width, initial-scale=1">
39
  <title>Engineer Manager</title>
40
- <style>
41
- :root {
42
- color-scheme: dark;
43
- --bg: #07111f;
44
- --panel: rgba(10, 21, 38, 0.88);
45
- --panel-2: rgba(14, 28, 48, 0.96);
46
- --line: rgba(124, 231, 255, 0.18);
47
- --text: #eaf4ff;
48
- --muted: #99abc7;
49
- --accent: #7ce7ff;
50
- --accent-2: #86f0ca;
51
- --meeting: #f5c76a;
52
- --empty: rgba(255, 255, 255, 0.06);
53
- --danger: #ff8d8d;
54
- }
55
- * { box-sizing: border-box; }
56
- body {
57
- margin: 0;
58
- min-height: 100vh;
59
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
60
- color: var(--text);
61
- background:
62
- radial-gradient(circle at top, rgba(124, 231, 255, 0.12), transparent 30rem),
63
- linear-gradient(180deg, #09111d 0%, #07111f 100%);
64
- }
65
- .shell {
66
- max-width: 1180px;
67
- margin: 0 auto;
68
- padding: 32px 20px 40px;
69
- }
70
- .hero {
71
- padding: 24px;
72
- border: 1px solid var(--line);
73
- border-radius: 24px;
74
- background: linear-gradient(135deg, rgba(9, 20, 38, 0.95), rgba(12, 31, 54, 0.92));
75
- box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
76
- }
77
- .eyebrow {
78
- color: var(--accent);
79
- font-size: 12px;
80
- letter-spacing: 0.18em;
81
- text-transform: uppercase;
82
- margin-bottom: 10px;
83
- }
84
- h1 {
85
- margin: 0 0 10px;
86
- font-size: clamp(2rem, 5vw, 3.8rem);
87
- line-height: 0.95;
88
- }
89
- .sub {
90
- margin: 0;
91
- color: var(--muted);
92
- max-width: 62ch;
93
- line-height: 1.5;
94
- }
95
- .grid {
96
- display: grid;
97
- grid-template-columns: 300px minmax(0, 1fr);
98
- gap: 18px;
99
- margin-top: 18px;
100
- }
101
- .panel {
102
- border: 1px solid var(--line);
103
- border-radius: 22px;
104
- background: var(--panel);
105
- box-shadow: 0 12px 36px rgba(0, 0, 0, 0.22);
106
- backdrop-filter: blur(10px);
107
- }
108
- .panel-inner { padding: 18px; }
109
- .section-title {
110
- margin: 0 0 14px;
111
- font-size: 0.92rem;
112
- letter-spacing: 0.12em;
113
- text-transform: uppercase;
114
- color: var(--accent);
115
- }
116
- .metrics {
117
- display: grid;
118
- grid-template-columns: repeat(4, minmax(0, 1fr));
119
- gap: 12px;
120
- margin-bottom: 16px;
121
- }
122
- .metric {
123
- padding: 14px;
124
- border-radius: 18px;
125
- border: 1px solid var(--line);
126
- background: var(--panel-2);
127
- }
128
- .metric small {
129
- display: block;
130
- color: var(--muted);
131
- margin-bottom: 8px;
132
- text-transform: uppercase;
133
- letter-spacing: 0.08em;
134
- font-size: 0.7rem;
135
- }
136
- .metric strong {
137
- font-size: 1.35rem;
138
- }
139
- label {
140
- display: block;
141
- margin-bottom: 10px;
142
- color: var(--muted);
143
- font-size: 0.92rem;
144
- }
145
- input, select, button {
146
- width: 100%;
147
- border-radius: 14px;
148
- border: 1px solid var(--line);
149
- background: rgba(255, 255, 255, 0.04);
150
- color: var(--text);
151
- padding: 12px 14px;
152
- font: inherit;
153
- }
154
- button {
155
- cursor: pointer;
156
- transition: transform 120ms ease, background 120ms ease;
157
- background: linear-gradient(135deg, rgba(124, 231, 255, 0.22), rgba(134, 240, 202, 0.16));
158
- }
159
- button:hover { transform: translateY(-1px); }
160
- .actions {
161
- display: grid;
162
- grid-template-columns: repeat(2, minmax(0, 1fr));
163
- gap: 10px;
164
- margin-top: 12px;
165
- }
166
- .actions button[data-op="0"] { grid-column: span 2; }
167
- .timeline {
168
- display: grid;
169
- grid-template-columns: repeat(auto-fit, minmax(58px, 1fr));
170
- gap: 8px;
171
- }
172
- .slot {
173
- border-radius: 16px;
174
- min-height: 84px;
175
- padding: 10px 8px;
176
- border: 1px solid transparent;
177
- background: var(--empty);
178
- display: flex;
179
- flex-direction: column;
180
- justify-content: space-between;
181
- }
182
- .slot.current { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(124, 231, 255, 0.25) inset; }
183
- .slot.work { background: linear-gradient(180deg, rgba(124, 240, 202, 0.22), rgba(124, 240, 202, 0.10)); }
184
- .slot.meeting { background: linear-gradient(180deg, rgba(245, 199, 106, 0.28), rgba(245, 199, 106, 0.10)); }
185
- .slot small { color: var(--muted); }
186
- .slot strong { font-size: 0.82rem; }
187
- .task-list, pre {
188
- margin: 0;
189
- padding: 14px;
190
- border-radius: 18px;
191
- border: 1px solid var(--line);
192
- background: rgba(255, 255, 255, 0.03);
193
- }
194
- .task-list li { margin: 0 0 8px; color: var(--muted); }
195
- .task-list li:last-child { margin-bottom: 0; }
196
- pre {
197
- overflow: auto;
198
- max-height: 260px;
199
- color: #d7e6ff;
200
- }
201
- .status {
202
- margin-top: 12px;
203
- min-height: 24px;
204
- color: var(--muted);
205
- }
206
- .status.error { color: var(--danger); }
207
- @media (max-width: 920px) {
208
- .grid, .metrics { grid-template-columns: 1fr; }
209
- .actions { grid-template-columns: 1fr; }
210
- .actions button[data-op="0"] { grid-column: span 1; }
211
- }
212
- </style>
213
  </head>
214
  <body>
215
  <div class="shell">
@@ -263,114 +374,7 @@ WEB_PAGE = dedent(
263
  </main>
264
  </div>
265
  </div>
266
-
267
- <script>
268
- const statusEl = document.getElementById("status");
269
- const targetSlotEl = document.getElementById("targetSlot");
270
- const timelineEl = document.getElementById("timeline");
271
- const tasksEl = document.getElementById("tasks");
272
- const payloadEl = document.getElementById("payload");
273
- const metrics = {
274
- time: document.getElementById("m-time"),
275
- slot: document.getElementById("m-slot"),
276
- flow: document.getElementById("m-flow"),
277
- debt: document.getElementById("m-debt"),
278
- };
279
-
280
- function setStatus(message, isError = false) {
281
- statusEl.textContent = message;
282
- statusEl.className = isError ? "status error" : "status";
283
- }
284
-
285
- function kindLabel(value) {
286
- if (value === 1) return "Deep Work";
287
- if (value === 2) return "Meeting";
288
- return "Open";
289
- }
290
-
291
- function renderObservation(response) {
292
- const obs = response.observation || {};
293
- metrics.time.textContent = obs.current_time ?? "-";
294
- metrics.slot.textContent = String(obs.current_slot ?? "-");
295
- metrics.flow.textContent = Number(obs.flow_score ?? 0).toFixed(2);
296
- metrics.debt.textContent = Number(obs.social_debt ?? 0).toFixed(2);
297
-
298
- targetSlotEl.max = Math.max(0, (obs.timeline || []).length - 1);
299
-
300
- timelineEl.innerHTML = "";
301
- (obs.timeline || []).forEach((value, index) => {
302
- const slot = document.createElement("div");
303
- slot.className = "slot " + (value === 1 ? "work" : value === 2 ? "meeting" : "");
304
- if (index === obs.current_slot) slot.classList.add("current");
305
- slot.innerHTML = "<small>Slot " + index + "</small><strong>" + kindLabel(value) + "</strong>";
306
- timelineEl.appendChild(slot);
307
- });
308
-
309
- tasksEl.innerHTML = "";
310
- const tasks = obs.task_buffer || [];
311
- if (!tasks.length) {
312
- const empty = document.createElement("li");
313
- empty.textContent = "No queued tasks.";
314
- tasksEl.appendChild(empty);
315
- } else {
316
- tasks.forEach((task, index) => {
317
- const item = document.createElement("li");
318
- item.textContent = "Task " + (index + 1) + ": " + task.duration + " slots, complexity " + task.hidden_complexity;
319
- tasksEl.appendChild(item);
320
- });
321
- }
322
-
323
- payloadEl.textContent = JSON.stringify(response, null, 2);
324
- }
325
-
326
- async function callJson(url, body) {
327
- const response = await fetch(url, {
328
- method: "POST",
329
- headers: { "Content-Type": "application/json" },
330
- body: JSON.stringify(body),
331
- });
332
- const data = await response.json();
333
- if (!response.ok) {
334
- throw new Error(JSON.stringify(data));
335
- }
336
- return data;
337
- }
338
-
339
- async function resetEnv() {
340
- setStatus("Resetting environment...");
341
- try {
342
- const data = await callJson("/reset", {});
343
- renderObservation(data);
344
- setStatus("Environment reset.");
345
- } catch (error) {
346
- setStatus("Reset failed: " + error.message, true);
347
- }
348
- }
349
-
350
- async function stepEnv(operation) {
351
- setStatus("Applying action...");
352
- try {
353
- const payload = {
354
- action: {
355
- target_slot: Number(targetSlotEl.value || 0),
356
- operation: Number(operation),
357
- },
358
- };
359
- const data = await callJson("/step", payload);
360
- renderObservation(data);
361
- setStatus("Action applied.");
362
- } catch (error) {
363
- setStatus("Action failed: " + error.message, true);
364
- }
365
- }
366
-
367
- document.getElementById("resetBtn").addEventListener("click", resetEnv);
368
- document.querySelectorAll("[data-op]").forEach((button) => {
369
- button.addEventListener("click", () => stepEnv(button.dataset.op));
370
- });
371
-
372
- resetEnv();
373
- </script>
374
  </body>
375
  </html>
376
  """
@@ -392,6 +396,16 @@ def web_ui() -> HTMLResponse:
392
  return HTMLResponse(WEB_PAGE)
393
 
394
 
 
 
 
 
 
 
 
 
 
 
395
  @app.get("/manifest.json", include_in_schema=False)
396
  def manifest() -> JSONResponse:
397
  return JSONResponse(
 
5
  from textwrap import dedent
6
 
7
  import uvicorn
8
+ from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse
9
  from openenv.core.env_server.http_server import create_fastapi_app
10
 
11
  try:
 
29
  max_concurrent_envs=2,
30
  )
31
 
32
+ WEB_CSS = dedent(
33
+ """\
34
+ :root {
35
+ color-scheme: dark;
36
+ --bg: #07111f;
37
+ --panel: rgba(10, 21, 38, 0.88);
38
+ --panel-2: rgba(14, 28, 48, 0.96);
39
+ --line: rgba(124, 231, 255, 0.18);
40
+ --text: #eaf4ff;
41
+ --muted: #99abc7;
42
+ --accent: #7ce7ff;
43
+ --accent-2: #86f0ca;
44
+ --meeting: #f5c76a;
45
+ --empty: rgba(255, 255, 255, 0.06);
46
+ --danger: #ff8d8d;
47
+ }
48
+ * { box-sizing: border-box; }
49
+ body {
50
+ margin: 0;
51
+ min-height: 100vh;
52
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
53
+ color: var(--text);
54
+ background:
55
+ radial-gradient(circle at top, rgba(124, 231, 255, 0.12), transparent 30rem),
56
+ linear-gradient(180deg, #09111d 0%, #07111f 100%);
57
+ }
58
+ .shell {
59
+ max-width: 1180px;
60
+ margin: 0 auto;
61
+ padding: 32px 20px 40px;
62
+ }
63
+ .hero {
64
+ padding: 24px;
65
+ border: 1px solid var(--line);
66
+ border-radius: 24px;
67
+ background: linear-gradient(135deg, rgba(9, 20, 38, 0.95), rgba(12, 31, 54, 0.92));
68
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
69
+ }
70
+ .eyebrow {
71
+ color: var(--accent);
72
+ font-size: 12px;
73
+ letter-spacing: 0.18em;
74
+ text-transform: uppercase;
75
+ margin-bottom: 10px;
76
+ }
77
+ h1 {
78
+ margin: 0 0 10px;
79
+ font-size: clamp(2rem, 5vw, 3.8rem);
80
+ line-height: 0.95;
81
+ }
82
+ .sub {
83
+ margin: 0;
84
+ color: var(--muted);
85
+ max-width: 62ch;
86
+ line-height: 1.5;
87
+ }
88
+ .grid {
89
+ display: grid;
90
+ grid-template-columns: 300px minmax(0, 1fr);
91
+ gap: 18px;
92
+ margin-top: 18px;
93
+ }
94
+ .panel {
95
+ border: 1px solid var(--line);
96
+ border-radius: 22px;
97
+ background: var(--panel);
98
+ box-shadow: 0 12px 36px rgba(0, 0, 0, 0.22);
99
+ backdrop-filter: blur(10px);
100
+ }
101
+ .panel-inner { padding: 18px; }
102
+ .section-title {
103
+ margin: 0 0 14px;
104
+ font-size: 0.92rem;
105
+ letter-spacing: 0.12em;
106
+ text-transform: uppercase;
107
+ color: var(--accent);
108
+ }
109
+ .metrics {
110
+ display: grid;
111
+ grid-template-columns: repeat(4, minmax(0, 1fr));
112
+ gap: 12px;
113
+ margin-bottom: 16px;
114
+ }
115
+ .metric {
116
+ padding: 14px;
117
+ border-radius: 18px;
118
+ border: 1px solid var(--line);
119
+ background: var(--panel-2);
120
+ }
121
+ .metric small {
122
+ display: block;
123
+ color: var(--muted);
124
+ margin-bottom: 8px;
125
+ text-transform: uppercase;
126
+ letter-spacing: 0.08em;
127
+ font-size: 0.7rem;
128
+ }
129
+ .metric strong {
130
+ font-size: 1.35rem;
131
+ }
132
+ label {
133
+ display: block;
134
+ margin-bottom: 10px;
135
+ color: var(--muted);
136
+ font-size: 0.92rem;
137
+ }
138
+ input, select, button {
139
+ width: 100%;
140
+ border-radius: 14px;
141
+ border: 1px solid var(--line);
142
+ background: rgba(255, 255, 255, 0.04);
143
+ color: var(--text);
144
+ padding: 12px 14px;
145
+ font: inherit;
146
+ }
147
+ button {
148
+ cursor: pointer;
149
+ transition: transform 120ms ease, background 120ms ease;
150
+ background: linear-gradient(135deg, rgba(124, 231, 255, 0.22), rgba(134, 240, 202, 0.16));
151
+ }
152
+ button:hover { transform: translateY(-1px); }
153
+ .actions {
154
+ display: grid;
155
+ grid-template-columns: repeat(2, minmax(0, 1fr));
156
+ gap: 10px;
157
+ margin-top: 12px;
158
+ }
159
+ .actions button[data-op="0"] { grid-column: span 2; }
160
+ .timeline {
161
+ display: grid;
162
+ grid-template-columns: repeat(auto-fit, minmax(58px, 1fr));
163
+ gap: 8px;
164
+ }
165
+ .slot {
166
+ border-radius: 16px;
167
+ min-height: 84px;
168
+ padding: 10px 8px;
169
+ border: 1px solid transparent;
170
+ background: var(--empty);
171
+ display: flex;
172
+ flex-direction: column;
173
+ justify-content: space-between;
174
+ }
175
+ .slot.current { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(124, 231, 255, 0.25) inset; }
176
+ .slot.work { background: linear-gradient(180deg, rgba(124, 240, 202, 0.22), rgba(124, 240, 202, 0.10)); }
177
+ .slot.meeting { background: linear-gradient(180deg, rgba(245, 199, 106, 0.28), rgba(245, 199, 106, 0.10)); }
178
+ .slot small { color: var(--muted); }
179
+ .slot strong { font-size: 0.82rem; }
180
+ .task-list, pre {
181
+ margin: 0;
182
+ padding: 14px;
183
+ border-radius: 18px;
184
+ border: 1px solid var(--line);
185
+ background: rgba(255, 255, 255, 0.03);
186
+ }
187
+ .task-list li { margin: 0 0 8px; color: var(--muted); }
188
+ .task-list li:last-child { margin-bottom: 0; }
189
+ pre {
190
+ overflow: auto;
191
+ max-height: 260px;
192
+ color: #d7e6ff;
193
+ }
194
+ .status {
195
+ margin-top: 12px;
196
+ min-height: 24px;
197
+ color: var(--muted);
198
+ }
199
+ .status.error { color: var(--danger); }
200
+ @media (max-width: 920px) {
201
+ .grid, .metrics { grid-template-columns: 1fr; }
202
+ .actions { grid-template-columns: 1fr; }
203
+ .actions button[data-op="0"] { grid-column: span 1; }
204
+ }
205
+ """
206
+ )
207
+
208
+ WEB_JS = dedent(
209
+ """\
210
+ const statusEl = document.getElementById("status");
211
+ const targetSlotEl = document.getElementById("targetSlot");
212
+ const timelineEl = document.getElementById("timeline");
213
+ const tasksEl = document.getElementById("tasks");
214
+ const payloadEl = document.getElementById("payload");
215
+ const metrics = {
216
+ time: document.getElementById("m-time"),
217
+ slot: document.getElementById("m-slot"),
218
+ flow: document.getElementById("m-flow"),
219
+ debt: document.getElementById("m-debt"),
220
+ };
221
+
222
+ function setStatus(message, isError = false) {
223
+ statusEl.textContent = message;
224
+ statusEl.className = isError ? "status error" : "status";
225
+ }
226
+
227
+ function kindLabel(value) {
228
+ if (value === 1) return "Deep Work";
229
+ if (value === 2) return "Meeting";
230
+ return "Open";
231
+ }
232
+
233
+ function renderObservation(response) {
234
+ const obs = response.observation || {};
235
+ metrics.time.textContent = obs.current_time ?? "-";
236
+ metrics.slot.textContent = String(obs.current_slot ?? "-");
237
+ metrics.flow.textContent = Number(obs.flow_score ?? 0).toFixed(2);
238
+ metrics.debt.textContent = Number(obs.social_debt ?? 0).toFixed(2);
239
+ targetSlotEl.max = Math.max(0, (obs.timeline || []).length - 1);
240
+
241
+ timelineEl.innerHTML = "";
242
+ (obs.timeline || []).forEach((value, index) => {
243
+ const slot = document.createElement("div");
244
+ slot.className = "slot " + (value === 1 ? "work" : value === 2 ? "meeting" : "");
245
+ if (index === obs.current_slot) slot.classList.add("current");
246
+ slot.innerHTML = "<small>Slot " + index + "</small><strong>" + kindLabel(value) + "</strong>";
247
+ timelineEl.appendChild(slot);
248
+ });
249
+
250
+ tasksEl.innerHTML = "";
251
+ const tasks = obs.task_buffer || [];
252
+ if (!tasks.length) {
253
+ const empty = document.createElement("li");
254
+ empty.textContent = "No queued tasks.";
255
+ tasksEl.appendChild(empty);
256
+ } else {
257
+ tasks.forEach((task, index) => {
258
+ const item = document.createElement("li");
259
+ item.textContent = "Task " + (index + 1) + ": " + task.duration + " slots, complexity " + task.hidden_complexity;
260
+ tasksEl.appendChild(item);
261
+ });
262
+ }
263
+
264
+ payloadEl.textContent = JSON.stringify(response, null, 2);
265
+ }
266
+
267
+ async function callJson(url, body) {
268
+ const response = await fetch(url, {
269
+ method: "POST",
270
+ headers: { "Content-Type": "application/json" },
271
+ body: JSON.stringify(body),
272
+ });
273
+ const data = await response.json();
274
+ if (!response.ok) throw new Error(JSON.stringify(data));
275
+ return data;
276
+ }
277
+
278
+ async function resetEnv() {
279
+ setStatus("Resetting environment...");
280
+ try {
281
+ const data = await callJson("/reset", {});
282
+ renderObservation(data);
283
+ setStatus("Environment reset.");
284
+ } catch (error) {
285
+ setStatus("Reset failed: " + error.message, true);
286
+ }
287
+ }
288
+
289
+ async function stepEnv(operation) {
290
+ setStatus("Applying action...");
291
+ try {
292
+ const payload = {
293
+ action: {
294
+ target_slot: Number(targetSlotEl.value || 0),
295
+ operation: Number(operation),
296
+ },
297
+ };
298
+ const data = await callJson("/step", payload);
299
+ renderObservation(data);
300
+ setStatus("Action applied.");
301
+ } catch (error) {
302
+ setStatus("Action failed: " + error.message, true);
303
+ }
304
+ }
305
+
306
+ document.getElementById("resetBtn").addEventListener("click", resetEnv);
307
+ document.querySelectorAll("[data-op]").forEach((button) => {
308
+ button.addEventListener("click", () => stepEnv(button.dataset.op));
309
+ });
310
+
311
+ resetEnv();
312
+ """
313
+ )
314
+
315
  WEB_PAGE = dedent(
316
  """\
317
  <!doctype html>
 
320
  <meta charset="utf-8">
321
  <meta name="viewport" content="width=device-width, initial-scale=1">
322
  <title>Engineer Manager</title>
323
+ <link rel="stylesheet" href="/assets/app.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  </head>
325
  <body>
326
  <div class="shell">
 
374
  </main>
375
  </div>
376
  </div>
377
+ <script src="/assets/app.js" defer></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  </body>
379
  </html>
380
  """
 
396
  return HTMLResponse(WEB_PAGE)
397
 
398
 
399
+ @app.get("/assets/app.css", include_in_schema=False)
400
+ def web_css() -> PlainTextResponse:
401
+ return PlainTextResponse(WEB_CSS, media_type="text/css")
402
+
403
+
404
+ @app.get("/assets/app.js", include_in_schema=False)
405
+ def web_js() -> PlainTextResponse:
406
+ return PlainTextResponse(WEB_JS, media_type="application/javascript")
407
+
408
+
409
  @app.get("/manifest.json", include_in_schema=False)
410
  def manifest() -> JSONResponse:
411
  return JSONResponse(