mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-15 10:29:47 +00:00
Add file manager
This commit is contained in:
1 parent
8534053e57
commit
573bc152b1
10 files changed
+418
-98
No files matched your search
+12
-3
@@ -3152,7 +3152,13 @@ void mg_rpc_list(struct mg_rpc_req *r);
|
||||
|
||||
|
||||
|
||||
enum mg_val_type { MG_VAL_INT, MG_VAL_BOOL, MG_VAL_DBL, MG_VAL_STR, MG_VAL_FN };
|
||||
enum mg_val_type {
|
||||
MG_VAL_INT,
|
||||
MG_VAL_BOOL,
|
||||
MG_VAL_DBL,
|
||||
MG_VAL_STR,
|
||||
MG_VAL_RAW
|
||||
};
|
||||
|
||||
union mg_val {
|
||||
int i;
|
||||
@@ -3262,9 +3268,7 @@ static inline struct mg_field *mg_dash_find_field(struct mg_field *fields,
|
||||
|
||||
static inline size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
struct mg_field *f = va_arg(*ap, struct mg_field *);
|
||||
// union mg_val val = {0};
|
||||
size_t n = 0;
|
||||
// if (f->get) f->get(&val);
|
||||
n += mg_xprintf(fn, arg, "%m:", MG_ESC(f->name));
|
||||
if (f->type == MG_VAL_BOOL) {
|
||||
n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false");
|
||||
@@ -3274,6 +3278,8 @@ static inline size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value);
|
||||
} else if (f->type == MG_VAL_STR) {
|
||||
n += mg_xprintf(fn, arg, "%m", MG_ESC(f->value));
|
||||
} else if (f->type == MG_VAL_RAW) {
|
||||
n += mg_xprintf(fn, arg, "%s", f->value);
|
||||
} else {
|
||||
n += mg_xprintf(fn, arg, "null");
|
||||
}
|
||||
@@ -3347,6 +3353,9 @@ static inline int mg_dash_parse_field(struct mg_str json, struct mg_field *f) {
|
||||
ok = mg_json_get_num(json, json_path, (double *) f->value);
|
||||
} else if (f->type == MG_VAL_STR) {
|
||||
ok = mg_json_unescape(json, json_path, (char *) f->value, f->value_size);
|
||||
} else if (f->type == MG_VAL_RAW) {
|
||||
ok = mg_snprintf((char *) f->value, f->value_size, "%.*s", json.len,
|
||||
json.buf) == json.len;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
+79
-42
@@ -63,7 +63,6 @@
|
||||
const isMock = !!window.frameElement || (window !== window.top);
|
||||
const rpc = isMock ? null : Rpc('api/websocket');
|
||||
const settings = {data: {}, edits: {}, debug: true};
|
||||
const substitutions = {};
|
||||
const userhandlers = {}; // User event handlers
|
||||
|
||||
const fromPath = (path, value) => path.split('.').reverse().reduce((acc, k) => ({ [k]: acc }), value);
|
||||
@@ -138,7 +137,7 @@
|
||||
return false;
|
||||
};
|
||||
|
||||
function scan_bind(el, key, context) {
|
||||
function handle_bind(el, key, context) {
|
||||
if (!has(context, key)) { console.error('EVAL', key, context); return; }
|
||||
const v = get(context, key)
|
||||
// console.log(2, key, v);
|
||||
@@ -173,37 +172,55 @@
|
||||
rescan();
|
||||
};
|
||||
|
||||
function scan_save(el, keys) {
|
||||
function handle_save(el, keys) {
|
||||
el.disabled = !edited(keys);
|
||||
if (!el.bound) el.addEventListener("click", ev => save(keys)), el.bound = true;
|
||||
};
|
||||
|
||||
function scan_cancel(el, keys) {
|
||||
function handle_cancel(el, keys) {
|
||||
if (!el.bound) el.addEventListener("click", ev => cancel(keys)), el.bound = true;
|
||||
el.disabled = !edited(keys);
|
||||
};
|
||||
|
||||
function createSubstitutions() {
|
||||
substitutions.nodes = [], substitutions.attributes = [], substitutions.bound = [];
|
||||
document.querySelectorAll("*").forEach(function (el) {
|
||||
if (el.tagName === "SCRIPT" || el.tagName === "STYLE") return;
|
||||
for (const node of el.childNodes) {
|
||||
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.includes('${')) {
|
||||
node.originalValue = node.nodeValue;
|
||||
substitutions.nodes.push(node);
|
||||
}
|
||||
}
|
||||
for (const attr of el.attributes) {
|
||||
if (attr.value.includes('${')) {
|
||||
attr.originalValue = attr.value;
|
||||
substitutions.attributes.push(attr);
|
||||
}
|
||||
}
|
||||
for (const [key, val] of Object.entries(el.dataset)) {
|
||||
if (['bind', 'save', 'cancel'].includes(key)) substitutions.bound.push(el);
|
||||
}
|
||||
function handle_repeat(el, key, context) {
|
||||
if (!has(context, key)) { console.error('EVAL', key, context); return; }
|
||||
if (!el.orig) el.orig = el.children[0];
|
||||
const v = get(context, key), v2 = JSON.stringify(v);
|
||||
// if (el.v2 === v2) return;
|
||||
const frag = document.createDocumentFragment();
|
||||
const it = el.dataset.iterator ?? '__obj';
|
||||
v.forEach(function (item, index) {
|
||||
const child = el.orig.cloneNode(true);
|
||||
const ctx = { ...context, [it]: item };
|
||||
process(child, ctx);
|
||||
frag.appendChild(child);
|
||||
});
|
||||
el.replaceChildren(frag);
|
||||
// el.v2 = v2;
|
||||
};
|
||||
|
||||
function handle_upload(el) {
|
||||
if (el.bound) return;
|
||||
el.bound = true;
|
||||
el.addEventListener("change", function (ev) {
|
||||
const f = ev.target.files[0];
|
||||
if (!f) return;
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(f);
|
||||
reader.onload = function () {
|
||||
const url = `files/${encodeURIComponent(f.name)}`;
|
||||
let begin = Date.now(), body = reader.result, ok = false;
|
||||
// console.log(2, url, body);
|
||||
fetch(url, { method: 'POST', body })
|
||||
.then(r => { ok = r.ok; })
|
||||
.catch(() => true)
|
||||
.finally(() => {
|
||||
console.log('UPLOAD finshed in ', Date.now() - begin, 'ms');
|
||||
});
|
||||
};
|
||||
ev.target.value = '';
|
||||
ev.preventDefault();
|
||||
});
|
||||
// console.log(substitutions);
|
||||
};
|
||||
|
||||
function substituteExpressions(text, context) {
|
||||
@@ -215,27 +232,46 @@
|
||||
return typeof text == "string" ? text.replace(/\${(.+?)}/g, f) : text;
|
||||
}
|
||||
|
||||
function realrescan() {
|
||||
const context = JSON.parse(JSON.stringify(settings.data));
|
||||
apply(context, settings.edits);
|
||||
if (!substitutions.nodes) createSubstitutions();
|
||||
substitutions.attributes.forEach(attr => {
|
||||
const v = substituteExpressions(attr.originalValue, context);
|
||||
if (v !== attr.value) attr.value = v;
|
||||
});
|
||||
substitutions.nodes.forEach(node => {
|
||||
const v = substituteExpressions(node.originalValue, context);
|
||||
if (v !== node.nodeValue) node.nodeValue = v;
|
||||
});
|
||||
substitutions.bound.forEach(el => {
|
||||
function process(el, context) {
|
||||
el.querySelectorAll("*").forEach(function (el) {
|
||||
if (el.tagName === "SCRIPT" || el.tagName === "STYLE") return;
|
||||
for (const node of el.childNodes) {
|
||||
if (node.nodeType === Node.TEXT_NODE &&
|
||||
(node.originalValue || node.nodeValue.includes('${'))) {
|
||||
if (!node.originalValue) node.originalValue = node.nodeValue;
|
||||
const v = substituteExpressions(node.originalValue, context);
|
||||
if (v !== node.nodeValue) node.nodeValue = v;
|
||||
}
|
||||
}
|
||||
for (const attr of el.attributes) {
|
||||
const cv = decodeURIComponent(attr.value || '');
|
||||
if (attr.originalValue || cv.includes('${')) {
|
||||
if (!attr.originalValue) attr.originalValue = cv;
|
||||
const v = substituteExpressions(attr.originalValue, context);
|
||||
if (v !== attr.value) attr.value = v;
|
||||
}
|
||||
}
|
||||
const datakeys = { bind: 1, save: 1, cancel: 1, repeat: 1, upload: 1 };
|
||||
for (const [key, val] of Object.entries(el.dataset)) {
|
||||
if (key == 'bind') scan_bind(el, val, context);
|
||||
if (key == 'save') scan_save(el, val.split(/\s*,\s*/));
|
||||
if (key == 'cancel') scan_cancel(el, val.split(/\s*,\s*/));
|
||||
if (!datakeys[key]) continue;
|
||||
if (key == 'bind') handle_bind(el, val, context);
|
||||
if (key == 'save') handle_save(el, val.split(/\s*,\s*/));
|
||||
if (key == 'cancel') handle_cancel(el, val.split(/\s*,\s*/));
|
||||
if (key == 'repeat') handle_repeat(el, val, context);
|
||||
if (key == 'upload') handle_upload(el);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function realrescan() {
|
||||
// const t0 = performance.now();
|
||||
const context = JSON.parse(JSON.stringify(settings.data));
|
||||
apply(context, settings.edits);
|
||||
process(document, context);
|
||||
// const t1 = performance.now();
|
||||
// console.log(`Took2 ${t1 - t0} ms`);
|
||||
};
|
||||
|
||||
function rescan() {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", realrescan, { once: true });
|
||||
@@ -250,7 +286,8 @@
|
||||
if (isMock) {
|
||||
return delay(750, true).then(val => console.log('response', name));
|
||||
} else {
|
||||
return rpc.call(name, args).catch(err => console.log('CALL FAILED', name, args, err));
|
||||
return rpc.call(name, args)
|
||||
.catch(err => console.log('CALL FAILED', name, args, err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -291,5 +328,5 @@
|
||||
}
|
||||
};
|
||||
|
||||
global.Dashboard = { init, call, on};
|
||||
global.Dashboard = { init, call, on };
|
||||
})(window);
|
||||
+12
-3
@@ -7,7 +7,13 @@
|
||||
#include "str.h"
|
||||
#include "ws.h"
|
||||
|
||||
enum mg_val_type { MG_VAL_INT, MG_VAL_BOOL, MG_VAL_DBL, MG_VAL_STR, MG_VAL_FN };
|
||||
enum mg_val_type {
|
||||
MG_VAL_INT,
|
||||
MG_VAL_BOOL,
|
||||
MG_VAL_DBL,
|
||||
MG_VAL_STR,
|
||||
MG_VAL_RAW,
|
||||
};
|
||||
|
||||
union mg_val {
|
||||
int i;
|
||||
@@ -117,9 +123,7 @@ static inline struct mg_field *mg_dash_find_field(struct mg_field *fields,
|
||||
|
||||
static inline size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
struct mg_field *f = va_arg(*ap, struct mg_field *);
|
||||
// union mg_val val = {0};
|
||||
size_t n = 0;
|
||||
// if (f->get) f->get(&val);
|
||||
n += mg_xprintf(fn, arg, "%m:", MG_ESC(f->name));
|
||||
if (f->type == MG_VAL_BOOL) {
|
||||
n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false");
|
||||
@@ -129,6 +133,8 @@ static inline size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value);
|
||||
} else if (f->type == MG_VAL_STR) {
|
||||
n += mg_xprintf(fn, arg, "%m", MG_ESC(f->value));
|
||||
} else if (f->type == MG_VAL_RAW) {
|
||||
n += mg_xprintf(fn, arg, "%s", f->value);
|
||||
} else {
|
||||
n += mg_xprintf(fn, arg, "null");
|
||||
}
|
||||
@@ -202,6 +208,9 @@ static inline int mg_dash_parse_field(struct mg_str json, struct mg_field *f) {
|
||||
ok = mg_json_get_num(json, json_path, (double *) f->value);
|
||||
} else if (f->type == MG_VAL_STR) {
|
||||
ok = mg_json_unescape(json, json_path, (char *) f->value, f->value_size);
|
||||
} else if (f->type == MG_VAL_RAW) {
|
||||
ok = mg_snprintf((char *) f->value, f->value_size, "%.*s", json.len,
|
||||
json.buf) == json.len;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -1970,6 +1970,15 @@ static void test_http_parse(void) {
|
||||
ASSERT(mg_http_parse(s, strlen(s), &hm) == (int) strlen(s));
|
||||
ASSERT(mg_strcmp(hm.headers[0].value, mg_str("e")) == 0);
|
||||
}
|
||||
|
||||
{
|
||||
// Test that query-string gets stripped
|
||||
struct mg_http_message hm;
|
||||
const char *s = "GET /foo?bar HTTP/1.0\n\n";
|
||||
ASSERT(mg_http_parse(s, strlen(s), &hm) == (int) strlen(s));
|
||||
ASSERT(mg_strcmp(hm.uri, mg_str("/foo")) == 0);
|
||||
ASSERT(mg_strcmp(hm.query, mg_str("bar")) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void ehr(struct mg_connection *c, int ev, void *ev_data) {
|
||||
|
||||
@@ -16,7 +16,7 @@ $(PROG): $(SOURCES) $(MONGOOSE_DIR)/mongoose.h
|
||||
|
||||
# Generate a single, self-contained dashboard.html from the UI in web_root/
|
||||
dashboard.html: $(wildcard web_root/*)
|
||||
monolith web_root/index.html > $@
|
||||
monolith web_root/index.html -o $@
|
||||
|
||||
# Create an embedded filesystem containing compressed dashboard.html
|
||||
dashboard.html.c: dashboard.html
|
||||
@@ -25,7 +25,7 @@ dashboard.html.c: dashboard.html
|
||||
|
||||
# For development: rebuild and restart if any of the sources change
|
||||
watch:
|
||||
watchexec -r -w main.c -w mongoose_dashboard.c -w $(MONGOOSE_DIR)/mongoose.h -w web_root -e c,h,html,css,js -- make
|
||||
watchexec -r -w main.c -w mongoose_dashboard.c -w $(MONGOOSE_DIR)/mongoose.h -w web_root -e c,h,html,css,js,svg,png -- make
|
||||
|
||||
clean:
|
||||
rm -rf $(PROG) *.exe *.obj *.dSYM mongoose dashboard.*
|
||||
@@ -87,7 +87,7 @@ struct point {
|
||||
static char s_chart1_data[NUM_POINTS * POINT_SIZE + 2 + 1];
|
||||
|
||||
static struct mg_field fields_chart1[] = {
|
||||
{"data", MG_VAL_STR, s_chart1_data, sizeof(s_chart1_data)},
|
||||
{"data", MG_VAL_RAW, s_chart1_data, sizeof(s_chart1_data)},
|
||||
{NULL, 0, NULL, 0},
|
||||
};
|
||||
|
||||
@@ -107,19 +107,50 @@ void sync_chart1(bool is_write) {
|
||||
len += mg_snprintf(s_chart1_data + len, sizeof(s_chart1_data) - len, "]");
|
||||
}
|
||||
|
||||
static int s_num_log_files = 0;
|
||||
static int s_log_file_index = 0;
|
||||
static char s_log_file_name[100];
|
||||
static struct mg_field fields_log_files[] = {
|
||||
{"size", MG_VAL_INT, &s_num_log_files, sizeof(s_num_log_files)},
|
||||
{"index", MG_VAL_INT, &s_log_file_index, sizeof(s_log_file_index)},
|
||||
{"name", MG_VAL_STR, &s_log_file_name, sizeof(s_log_file_name)},
|
||||
struct file {
|
||||
struct file *next;
|
||||
char *name;
|
||||
size_t size;
|
||||
} *s_file_list;
|
||||
|
||||
static void file_add(struct mg_str name, size_t size) {
|
||||
struct file *f = mg_calloc(1, sizeof(*f));
|
||||
f->name = mg_strdup(name).buf;
|
||||
f->size = size;
|
||||
f->next = s_file_list;
|
||||
s_file_list = f;
|
||||
}
|
||||
|
||||
static void file_del(struct mg_str name) {
|
||||
struct file **head, *f;
|
||||
for (head = &s_file_list, f = *head; f; head = &(*head)->next, f = *head) {
|
||||
if (mg_strcmp(mg_str(f->name), name) == 0) {
|
||||
MG_INFO(("Deleting %s", f->name));
|
||||
*head = f->next;
|
||||
mg_free(f->name);
|
||||
mg_free(f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Files array: [{"name": "foo.txt", "size": 1234}]
|
||||
static char s_files[1024];
|
||||
static struct mg_field fields_files[] = {
|
||||
{"data", MG_VAL_RAW, s_files, sizeof(s_files)},
|
||||
{NULL, 0, NULL, 0},
|
||||
};
|
||||
static void sync_log_files(bool is_write) {
|
||||
static void sync_files(bool is_write) {
|
||||
size_t len = 0;
|
||||
struct file *f;
|
||||
if (is_write) return;
|
||||
mg_log_level = s_log_level;
|
||||
s_log_level = mg_log_level;
|
||||
len += mg_snprintf(s_files + len, sizeof(s_files) - len, "[");
|
||||
for (f = s_file_list; f; f = f->next) {
|
||||
len += mg_snprintf(s_files + len, sizeof(s_files) - len, "%s{%m:%m,%m:%u}",
|
||||
len > 1 ? "," : "", MG_ESC("name"), MG_ESC(f->name),
|
||||
MG_ESC("size"), f->size);
|
||||
}
|
||||
len += mg_snprintf(s_files + len, sizeof(s_files) - len, "]");
|
||||
}
|
||||
|
||||
static struct mg_field_set field_sets[] = {
|
||||
@@ -127,19 +158,82 @@ static struct mg_field_set field_sets[] = {
|
||||
{"metrics", fields_metrics, sync_metrics, 0, 0},
|
||||
{"settings", fields_settings, sync_settings, 0, 0},
|
||||
{"chart1", fields_chart1, sync_chart1, 0, 0},
|
||||
{"log_files", fields_log_files, sync_log_files, 0, 0},
|
||||
{"files", fields_files, sync_files, 0, 0},
|
||||
{0, 0, 0, 0, 0},
|
||||
};
|
||||
static struct mg_dash s_dashboard = {field_sets, NULL};
|
||||
|
||||
static void log_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
|
||||
if (ev == MG_EV_HTTP_MSG) {
|
||||
mg_http_serve_file(c, ev_data, "Makefile", NULL);
|
||||
struct upload_state {
|
||||
char marker; // Tells that we're a file upload connection
|
||||
size_t expected; // POST data length, bytes
|
||||
size_t received; // Already received bytes
|
||||
void *ctx; // OTA context
|
||||
};
|
||||
|
||||
static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
|
||||
struct upload_state *us = (struct upload_state *) c->data;
|
||||
if ((ev == MG_EV_HTTP_HDRS || ev == MG_EV_HTTP_MSG) && us->marker == 0) {
|
||||
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||
if (hm->uri.len > 7) hm->uri.len -= 7, hm->uri.buf += 7; // Strip /files/
|
||||
if (sizeof(*us) > sizeof(c->data)) {
|
||||
mg_error(c,
|
||||
"FAILURE: sizeof(c->data) == %lu, need %lu."
|
||||
" Set #define MG_DATA_SIZE XXX",
|
||||
sizeof(c->data), sizeof(*us));
|
||||
} else if (mg_strcmp(hm->query, mg_str("action=delete")) == 0) {
|
||||
// Query string ?action=delete - file deletion
|
||||
file_del(hm->uri);
|
||||
mg_dash_send_change(c->mgr, &s_dashboard, "files");
|
||||
mg_http_reply(c, 200, NULL, "true");
|
||||
} else if (mg_path_is_sane(hm->uri) == false) {
|
||||
// Bad file name, return error. Protect from traversal, etc
|
||||
mg_http_reply(c, 400, NULL, "Bad name");
|
||||
} else if (mg_match(hm->method, mg_str("POST"), NULL) ||
|
||||
mg_match(hm->method, mg_str("PUT"), NULL)) {
|
||||
// File upload
|
||||
char path[128];
|
||||
mg_snprintf(path, sizeof(path), "/tmp/%.*s", hm->uri.len, hm->uri.buf);
|
||||
us->ctx = fopen(path, "wb+");
|
||||
if (us->ctx == NULL) {
|
||||
mg_http_reply(c, 500, NULL, "File upload error %d\n", errno);
|
||||
} else {
|
||||
file_add(hm->uri, hm->body.len);
|
||||
MG_DEBUG(("Starting upload, [%.*s] %lu", hm->uri.len, hm->uri.buf, hm->body.len));
|
||||
us->marker = 'U';
|
||||
us->expected = hm->body.len;
|
||||
us->received = 0;
|
||||
mg_iobuf_del(&c->recv, 0, hm->head.len);
|
||||
c->pfn = NULL;
|
||||
c->fn = file_ev_handler;
|
||||
mg_call(c, MG_EV_READ, &c->recv.len);
|
||||
}
|
||||
} else {
|
||||
// Serve file
|
||||
char path[128];
|
||||
mg_snprintf(path, sizeof(path), "/tmp/%.*s", hm->uri.len, hm->uri.buf);
|
||||
mg_http_serve_file(c, ev_data, path, NULL);
|
||||
}
|
||||
} else if (ev == MG_EV_READ && us->marker == 'U') {
|
||||
// Write uploaded data
|
||||
MG_DEBUG(("Uploading.. recv.len=%u", c->recv.len));
|
||||
us->received += c->recv.len;
|
||||
fwrite(c->recv.buf, 1, c->recv.len, us->ctx);
|
||||
c->recv.len = 0;
|
||||
if (us->received >= us->expected) {
|
||||
fclose(us->ctx);
|
||||
MG_DEBUG(("Uploaded %lu", us->expected));
|
||||
mg_http_reply(c, 200, NULL, "%lu uploaded\n", us->expected);
|
||||
mg_dash_send_change(c->mgr, &s_dashboard, "files");
|
||||
memset(us, 0, sizeof(*us));
|
||||
c->is_draining = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mg_dash_init(struct mg_mgr *mgr) {
|
||||
MG_DASH_REGISTER_CUSTOM_HANDLER(&s_dashboard, "/logs/#", log_ev_handler);
|
||||
file_add(mg_str("device-config.json"), 1234);
|
||||
file_add(mg_str("device-log-2026-04-25.txt"), 1327854);
|
||||
MG_DASH_REGISTER_CUSTOM_HANDLER(&s_dashboard, "/files/#", file_ev_handler);
|
||||
mg_http_listen(mgr, HTTP_ADDR, mg_dash_ev_handler, &s_dashboard);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
<title>My Dashboard</title>
|
||||
|
||||
<!-- Mongoose device dashboard -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/cesanta/mongoose@master/resources/dashboard.js"></script>
|
||||
<!--<script src="https://cdn.jsdelivr.net/gh/cesanta/mongoose@master/resources/dashboard.js"></script>-->
|
||||
<script src="dashboard.js"></script>
|
||||
<script>
|
||||
Dashboard.init({
|
||||
debug: true,
|
||||
data: {
|
||||
chart1: {data: ''},
|
||||
chart1: {data: []},
|
||||
leds: { led1: true, led2: false, led3: false },
|
||||
settings: { volume: 12, log_level: 3, name: 'Galway' },
|
||||
metrics: { temperature: 1.2, humidity: 3, cpu: 4, ram: 5 },
|
||||
files: {data: [{name: 'device-log-2026-04-25.txt', size: 8625563}]},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -55,22 +57,21 @@
|
||||
</svg>
|
||||
Firmware Update
|
||||
</a>
|
||||
<a href="#events" id="tab-events" class="nav-link" role="tab" aria-controls="events" aria-selected="false" tabindex="-1">
|
||||
<a href="#files" id="tab-files" class="nav-link" role="tab" aria-controls="files" aria-selected="false" tabindex="-1">
|
||||
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M8 7h8" />
|
||||
<path d="M8 12h8" />
|
||||
<path d="M8 17h5" />
|
||||
<rect x="4" y="4" width="16" height="16" rx="3" />
|
||||
<path d="M4 7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v2H4Z" />
|
||||
<path d="M4 11h16v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z" />
|
||||
<path d="M8 15h8" />
|
||||
</svg>
|
||||
Event Log
|
||||
File Manager
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="connection-badge" aria-label="connection active">
|
||||
<span class="connection-label">live</span>
|
||||
<span class="connection-dot" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<nav class="nav-tabs">
|
||||
<div class="nav-link">
|
||||
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -205,14 +206,79 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button class="btn btn-secondary" type="button" data-cancel="settings">Cancel</button>
|
||||
<button class="btn btn-primary" type="button" data-save="settings">Save</button>
|
||||
<button class="btn btn-secondary" type="button" data-cancel="settings">
|
||||
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" data-save="settings">
|
||||
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z" />
|
||||
<path d="M17 21v-8H7v8" />
|
||||
<path d="M7 3v5h8" />
|
||||
</svg>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="firmware" class="tab-panel" role="tabpanel" aria-labelledby="tab-firmware" hidden></section>
|
||||
<section id="events" class="tab-panel" role="tabpanel" aria-labelledby="tab-events" hidden>
|
||||
<pre id="logs" class="file-content"></pre>
|
||||
<section id="files" class="tab-panel" role="tabpanel" aria-labelledby="tab-files" hidden>
|
||||
<div class="panel" style="max-width: 40rem;">
|
||||
<div class="panel-title">
|
||||
<span>
|
||||
<span>File Manager</span>
|
||||
<span class="metric-status">${files.data.length} Files</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<label class="btn btn-primary">
|
||||
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 3v12" />
|
||||
<path d="m7 8 5-5 5 5" />
|
||||
<path d="M5 21h14" />
|
||||
</svg>
|
||||
Upload
|
||||
<input type="file" hidden data-upload="1" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="file-list" data-repeat="files.data" data-iterator="f">
|
||||
<div class="file-row">
|
||||
<div class="file-info">
|
||||
<span class="file-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7Z" />
|
||||
<path d="M14 2v5h5" />
|
||||
</svg>
|
||||
</span>
|
||||
<div class="file-name">${f.name}</div>
|
||||
</div>
|
||||
<div class="file-size">${f.size}</div>
|
||||
<div class="file-actions">
|
||||
<a onclick="window.open('files/${encodeURIComponent(f.name)}', '_blank')" target="_blank" class="btn btn-secondary">
|
||||
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 3v12" />
|
||||
<path d="m7 10 5 5 5-5" />
|
||||
<path d="M5 21h14" />
|
||||
</svg>
|
||||
</a>
|
||||
<button onclick="fetch('files/${encodeURIComponent(f.name)}?action=delete'); return false;" class="btn btn-secondary">
|
||||
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 6h18" />
|
||||
<path d="M8 6V4h8v2" />
|
||||
<path d="M19 6l-1 14H6L5 6" />
|
||||
<path d="M10 11v5" />
|
||||
<path d="M14 11v5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
</defs>
|
||||
|
||||
<!-- icon: two rounded rectangles -->
|
||||
<g fill="none" stroke="url(#g)" stroke-width="40" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="140" y="160" width="260" height="260" rx="70"/>
|
||||
<rect x="260" y="80" width="260" height="260" rx="70"/>
|
||||
<g fill="none" stroke="url(#g)" stroke-width="30" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="160" y="60" width="300" height="300" rx="70"/>
|
||||
<rect x="20" y="200" width="300" height="300" rx="70"/>
|
||||
</g>
|
||||
|
||||
<!-- text -->
|
||||
<text x="560" y="330"
|
||||
<text x="560" y="360"
|
||||
font-family="system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
|
||||
font-size="160"
|
||||
font-size="180"
|
||||
font-weight="600"
|
||||
fill="#6b7280">
|
||||
my logo
|
||||
MyLogo
|
||||
</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 773 B After Width: | Height: | Size: 771 B |
@@ -73,7 +73,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
function updateGraphPoints(data) {
|
||||
if (!data) return;
|
||||
const parsed = JSON.parse(data);
|
||||
// const parsed = JSON.parse(data);
|
||||
const parsed = data || [];
|
||||
const uplot_data = [
|
||||
parsed.map(x => x[0]),
|
||||
parsed.map(x => x[1]),
|
||||
@@ -87,7 +88,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
Dashboard.call('get', 'chart1').then(resp => updateGraphPoints(resp?.data));
|
||||
|
||||
// Show logs
|
||||
fetch('logs/')
|
||||
.then(resp => resp.text())
|
||||
.then(text => document.getElementById("logs").textContent = text);
|
||||
// fetch('logs/')
|
||||
// .then(resp => resp.text())
|
||||
// .then(text => document.getElementById("logs").textContent = text);
|
||||
|
||||
});
|
||||
@@ -44,7 +44,7 @@ body {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-2) var(--spacing-6);
|
||||
padding: var(--spacing-3) var(--spacing-6);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
@@ -93,7 +93,7 @@ body {
|
||||
gap: var(--spacing-2);
|
||||
background: rgb(248, 250, 252);
|
||||
border: 1px solid rgb(226, 232, 240);
|
||||
padding: calc(var(--spacing-1) + 2px);
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: 1rem;
|
||||
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 2px 4px 0px inset;
|
||||
}
|
||||
@@ -255,7 +255,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
@@ -335,11 +335,14 @@ body {
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.42rem 1rem;
|
||||
font-size: 0.82rem;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
height: 2rem;
|
||||
padding: 0.3rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
border-radius: 0.55rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
@@ -398,7 +401,7 @@ body {
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(203, 213, 225, 0.75);
|
||||
border-radius: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 1),
|
||||
0 2px 6px rgba(148, 163, 184, 0.1),
|
||||
@@ -415,7 +418,8 @@ body {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
padding: 1rem 1.25rem 0.9rem;
|
||||
padding: 0 1rem;
|
||||
min-height: 3rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -449,6 +453,80 @@ body {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* File manager */
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.file-list:empty::before {
|
||||
content: "No Files";
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.file-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.82rem 0;
|
||||
border-top: 1px solid #edf2f7;
|
||||
}
|
||||
|
||||
.file-row:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
color: #0369a1;
|
||||
background: #e0f2fe;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 0.65rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-icon svg {
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
min-width: 0;
|
||||
color: #334155;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
/* Chart */
|
||||
|
||||
.chart-panel {
|
||||
@@ -594,4 +672,20 @@ body {
|
||||
.overview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.file-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.file-row {
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user