Sync queue management UI, sync/reboot/poweroff all btns

This commit is contained in:
ABelliqueux 2023-01-15 16:32:13 +01:00
parent a029b2e902
commit e44ff71a58
5 changed files with 143 additions and 61 deletions

10
app.py
View File

@ -38,6 +38,9 @@ gui_l10n = {"locale": LOCALE[:2],
"str_sync": _("Sync"), "str_sync": _("Sync"),
"str_poweroff": _("Poweroff"), "str_poweroff": _("Poweroff"),
"str_reboot": _("Reboot"), "str_reboot": _("Reboot"),
# TODO : add to l10n
"str_poweroff_all": _("Poweroff all"),
"str_reboot_all": _("Reboot all"),
"str_blink": _("Blink"), "str_blink": _("Blink"),
"str_link": _("Link"), "str_link": _("Link"),
"str_refresh": _("Refresh"), "str_refresh": _("Refresh"),
@ -631,11 +634,8 @@ def sync(host, arg0):
if arg0 == "stop": if arg0 == "stop":
return stop_upload() return stop_upload()
elif host == "all": elif host == "all":
reset_current_upload() # This is done in js for now.
return sync_media_folder(media_folder_local, media_folder_remote_expanded, host, cmd_port) pass
# TODO : figure it out for multiple hosts
# ~ for hostl in hosts_available:
# ~ sync_media_folder(media_folder_local, media_folder_remote_expanded, hostl, cmd_port)
else: else:
reset_current_upload() reset_current_upload()
return sync_media_folder(media_folder_local, media_folder_remote_expanded, host, cmd_port) return sync_media_folder(media_folder_local, media_folder_remote_expanded, host, cmd_port)

View File

@ -28,7 +28,7 @@ sha256 : 0fe3fe76d0e56e445124fa20646fa8b3d8c59568786b3ebc8a96d83d92f203e3
* Use nginx reverse proxy + SSL between server and clients ( https://medium.com/@antelle/how-to-generate-a-self-signed-ssl-certificate-for-an-ip-address-f0dd8dddf754 ) * Use nginx reverse proxy + SSL between server and clients ( https://medium.com/@antelle/how-to-generate-a-self-signed-ssl-certificate-for-an-ip-address-f0dd8dddf754 )
* Webgui beautifying * Webgui beautifying
## 0.4 : 2022-10-21-videopi.img.xz ## 0.4 : 2022-10-21-pilpil.img.xz
md5 : dee7af70135994169cab4f073ee51905 md5 : dee7af70135994169cab4f073ee51905
sha256 : ec3e17fc9b41f8c5181484e9866be2d1d92cab8403210e3d22f4f689edd4cfde sha256 : ec3e17fc9b41f8c5181484e9866be2d1d92cab8403210e3d22f4f689edd4cfde
@ -38,7 +38,7 @@ sha256 : ec3e17fc9b41f8c5181484e9866be2d1d92cab8403210e3d22f4f689edd4cfde
* Add media folder sync (scp, rsync, http upload) * Add media folder sync (scp, rsync, http upload)
* Install script ; Wifi setup, generate/install SSH keys/ nginx SSL cert/key fore each host, change hostname, static IPs * Install script ; Wifi setup, generate/install SSH keys/ nginx SSL cert/key fore each host, change hostname, static IPs
## 0.5 : 2022-11-17-videopi.img.xz ## 0.5 : 2022-11-17-pilpil.img.xz
md5 : d1dd7404ec05d16c8a4179370a214821 md5 : d1dd7404ec05d16c8a4179370a214821
sha256 : 401359a84c6d60902c05602bd52fae70f0b2ecac36d550b52d14e1e3230854a6 sha256 : 401359a84c6d60902c05602bd52fae70f0b2ecac36d550b52d14e1e3230854a6
@ -55,6 +55,31 @@ sha256 : 401359a84c6d60902c05602bd52fae70f0b2ecac36d550b52d14e1e3230854a6
* FR localisation for server, client and install script * FR localisation for server, client and install script
* pep8ification * pep8ification
## 0.6 : 2023-01-06-pilpil.img.xz
md5 :
sha256 :
* os update, cleanup
* server: upload in chunks
* server: file upload : only send new files, get upload status (/sync/status)
* server: pep8fied (except for line length)
* server: better overclock values for RPIs
* server: VLC file caching set to 5000ms for seamless playback
* server: BCM4345 workaround to solve wifi connectivity (https://wiki.arthus.net/?Rpi_wifi_-_BCM4345_and_CTRL-EVENT-ASSOC-REJECT)
* webgui: remove file from timeline (drag&drop to bin)
* webgui: upload progress dialog
* webgui: l10n html + js
* webgui: Add remote file enqueuing
* webgui: Fix timeline UI
* webgui: Add video thumbnails to timeline UI
* webgui: fix timeline/file sync with multiple clients
* webgui: file sync queuing UI
* webgui: sync all live hosts button
* webgui: Add remove from sync queue button
* webgui: Poweroff/Reboot all btn
* client: Only blink when running on rpi
* client: Generate video thumbnails on startup/upload with ffmpeg, serve SVG when file not found, create folder if needed
# FS checklist # FS checklist
@ -70,32 +95,27 @@ sha256 : 401359a84c6d60902c05602bd52fae70f0b2ecac36d550b52d14e1e3230854a6
# DOING NEXT : # DOING NEXT :
* server : remove incomplete files before upload * FIXME : playhead stuck after removing item from playlist
* webgui/server : interrupt current download ?
* webgui : upload status CSS transition ?
# DONE : # DONE :
* server : upload in chunks
* webgui: remove file from timeline (drag&drop to bin?) (check bugs)
* webgui: upload progress dialog
* server : file upload : only send new files, get upload status (/sync/status)
* webgui: l10n html + js
* webgui: Add remote file enqueuing
* webgui: Fix timeline UI
* webgui: Add video thumbnails to timeline UI
* client: Only blink when running on rpi
* client: Generate video thumbnail on start with ffmpegthumbnailer, serve SVG when file not found
* server: pep8fied (except for line length)
# TODO : # TODO :
* ~ Test with several rpis * check control from wifi client ( smartphone )
* ? Scripts hotspot linux : nmcli * playlist save/load ?
win : https://github.com/JamesCullum/Windows-Hotspot * webgui/server : interrupt current download ?
mac : https://apple.stackexchange.com/questions/2488/start-stop-internet-sharing-from-a-script * server : remove incomplete uploaded files before new upload ?
* webgui : upload status CSS transition ?
* ! Remove git personal details/resolv.conf, remove authorized_keys, ssh config, clean home, re-enable ssh pw login * ! Remove git personal details/resolv.conf, remove authorized_keys, ssh config, clean home, re-enable ssh pw login
* ~ Doc * ~ Doc
# OTHER: # OTHER:
* ? Scripts hotspot
linux : nmcli
win : https://github.com/JamesCullum/Windows-Hotspot
mac : https://apple.stackexchange.com/questions/2488/start-stop-internet-sharing-from-a-script
* get_client_rssi.sh on server * get_client_rssi.sh on server
# À FINANCER
* Shutters/motor control
* XLR support
* UDEV rules : keyboard/mouse = standalone GUI

View File

@ -435,6 +435,8 @@ function update_host_list(infos_array){
host_down.forEach(function(element){ host_down.forEach(function(element){
document.getElementById(element).style.display = 'none'; document.getElementById(element).style.display = 'none';
}); });
currentUser.hosts_up = host_up;
currentUser.hosts_down = host_down;
currentUser.status_all = host_up.length + " client(s)."; currentUser.status_all = host_up.length + " client(s).";
return host_up.length; return host_up.length;
} }
@ -449,10 +451,10 @@ function freeze_ui_host(host, html_attributes, inner_html=0, inner_text=0){
let upload_dialog_cont_element = add_HTML_element("div", html_attributes, 0, host); let upload_dialog_cont_element = add_HTML_element("div", html_attributes, 0, host);
container_element.insertBefore(upload_dialog_cont_element, siblings[0]); container_element.insertBefore(upload_dialog_cont_element, siblings[0]);
if (inner_html){ if (inner_html){
let child_node = add_HTML_element("div", inner_html, 0, host) let child_node = add_HTML_element("div", inner_html, 0, host);
let new_node = container_element.children.item(html_attributes.id + host).appendChild(child_node); let new_node = container_element.children.item(html_attributes.id + host).appendChild(child_node);
if (inner_text){ if (inner_text){
new_node.innerText = inner_text; new_node.innerHTML = inner_text;
} }
} }
} }
@ -461,17 +463,43 @@ function freeze_queued_container(command){
freeze_attributes = {"id":"ul_queued_freeze_", "class": "ul_queued_freeze"}; freeze_attributes = {"id":"ul_queued_freeze_", "class": "ul_queued_freeze"};
inner_html = {"id":"ul_queued_msg_", "class": "ul_queued_msg"}; inner_html = {"id":"ul_queued_msg_", "class": "ul_queued_msg"};
inner_text = t9n[LOCALE].item_queued; inner_text = t9n[LOCALE].item_queued;
freeze_ui_host(command.split("/")[2], freeze_attributes, inner_html, inner_text); let host = command.split("/")[2];
inner_text += '<buttton id="sync_unqueue_' + host + '" onclick="cancel_sync(this)" class="command btn btn-block btn-lg btn-default btn-unqueue" role="button">X</button';
freeze_ui_host(host, freeze_attributes, inner_html, inner_text);
} }
function unfreeze_queued_container(host){ function unfreeze_queued_container(host){
let container = document.getElementById(host); let container = document.getElementById(host);
let exists = container.querySelector("div.ul_queued_freeze"); let exists = container.querySelector("div.ul_queued_freeze");
exists? exists.remove(): false; if (exists){
exists.remove();
}
}
function unfreeze_all_containers(){
if (currentUser.hosts_up){
currentUser.hosts_up.forEach(function(host){
unfreeze_queued_container(host);
});
}
}
function exec_all_hosts(command){
let authorized_commands = ['reboot', 'poweroff'];
// Only send authorized commands
if (authorized_commands.indexOf(command) == -1 ){
return 0;
}
if ( confirm(t9n[LOCALE].confirmMessage) ) {
if (currentUser.hosts_up){
currentUser.hosts_up.forEach(function(host){
send_ajax_cmd("/" + host + "/" + command);
});
}
}
} }
function display_upload_status(command) { function display_upload_status(command) {
// TODO : First this should set the HTMl with empty values, then call update_upload...
let host = command.split("/")[2]; let host = command.split("/")[2];
let container_element = document.getElementById(host); let container_element = document.getElementById(host);
let siblings = container_element.children; let siblings = container_element.children;
@ -534,6 +562,7 @@ async function update_upload_status(current_upload) {
// Upload finished // Upload finished
console.log("Upload finished - Destroying dialog for " + currentUser.last_ul_host); console.log("Upload finished - Destroying dialog for " + currentUser.last_ul_host);
destroy_upload_status(currentUser.last_ul_host); destroy_upload_status(currentUser.last_ul_host);
// Remove first item from command queue // Remove first item from command queue
currentUser.ul_queue.shift(); currentUser.ul_queue.shift();
// Upload done, un-freeze // Upload done, un-freeze
@ -553,6 +582,7 @@ async function update_upload_status(current_upload) {
document.getElementById("ul_status_filename_" + currentUser.last_ul_host).innerHTML = t9n[LOCALE].filename + ": " + current_upload.filename; document.getElementById("ul_status_filename_" + currentUser.last_ul_host).innerHTML = t9n[LOCALE].filename + ": " + current_upload.filename;
document.getElementById("ul_status_filesize_" + currentUser.last_ul_host).innerHTML = t9n[LOCALE].size + ": " + current_upload.size + t9n[LOCALE].size_unit; document.getElementById("ul_status_filesize_" + currentUser.last_ul_host).innerHTML = t9n[LOCALE].size + ": " + current_upload.size + t9n[LOCALE].size_unit;
// Progress bar CSS // Progress bar CSS
// FIXME : cursor stops updating after upload ?
if (current_upload.transferred_percent) { if (current_upload.transferred_percent) {
document.getElementById("ul_progress_" + currentUser.last_ul_host).innerText = current_upload.transferred_percent + "%"; document.getElementById("ul_progress_" + currentUser.last_ul_host).innerText = current_upload.transferred_percent + "%";
document.getElementById("ul_progress_" + currentUser.last_ul_host).style.background = "linear-gradient(90deg, " + TIMELINE_COLOR_CURSOR + " " + current_upload.transferred_percent + "%, #fff " + current_upload.transferred_percent + "%)"; document.getElementById("ul_progress_" + currentUser.last_ul_host).style.background = "linear-gradient(90deg, " + TIMELINE_COLOR_CURSOR + " " + current_upload.transferred_percent + "%, #fff " + current_upload.transferred_percent + "%)";
@ -613,12 +643,7 @@ function parse_result(command, infos_array) {
//~ console.log("stopping upload..."); //~ console.log("stopping upload...");
//~ console.log(infos_array); //~ console.log(infos_array);
//~ destroy_upload_status(); //~ destroy_upload_status();
} //else { }
//~ console.log("displaying status");
//~ console.log(infos_array);
//~ if (infos_array.total_count) {
//~ display_upload_status(infos_array)
//}
} else { } else {
setTimeout(send_ajax_cmd, 180, "/all/list"); setTimeout(send_ajax_cmd, 180, "/all/list");
setTimeout(send_ajax_cmd, 180, "/all/status"); setTimeout(send_ajax_cmd, 180, "/all/status");
@ -685,7 +710,6 @@ function sync_host() {
// Update status // Update status
currentUser.status_all = t9n[LOCALE].sync; currentUser.status_all = t9n[LOCALE].sync;
// Create dialog // Create dialog
// TODO : display grayed out dialog when host is queued for upload
display_upload_status(command_q); display_upload_status(command_q);
// Set current host // Set current host
currentUser.last_ul_host = host; currentUser.last_ul_host = host;
@ -695,6 +719,7 @@ function sync_host() {
} else { } else {
console.log("Nothing else in the queue !"); console.log("Nothing else in the queue !");
// Make sure sane values are set back // Make sure sane values are set back
unfreeze_all_containers();
currentUser.last_ul_host = 0; currentUser.last_ul_host = 0;
return 0; return 0;
} }
@ -704,6 +729,20 @@ function sync_host() {
} }
function cancel_sync(host_btn_id){
let host = host_btn_id.id.split("_")[2];
let command = "/sync/" + host;
if (currentUser.ul_queue){
let item_index = currentUser.ul_queue.indexOf(command);
// Skip [0]
if (item_index > 0){
console.log(host + " unqueuing");
currentUser.ul_queue.splice(item_index, 1);
unfreeze_queued_container(host);
}
}
}
// UI // UI
addEventListener("DOMContentLoaded", function() { addEventListener("DOMContentLoaded", function() {
//~ adjust_timeline(); //~ adjust_timeline();
@ -716,11 +755,11 @@ addEventListener("DOMContentLoaded", function() {
let command = clickedButton.value; let command = clickedButton.value;
if ( command.indexOf("/reboot" ) > -1 || command.indexOf("/poweroff") > -1 ) { if ( command.indexOf("/reboot" ) > -1 || command.indexOf("/poweroff") > -1 ) {
// TODO : l10n if (command.indexOf("/all" ) == -1){
if ( !confirm(t9n[LOCALE].confirmMessage) ) { if ( !confirm(t9n[LOCALE].confirmMessage) ) {
return 0; return 0;
}
} }
} else if ( command == "/scan" ) { } else if ( command == "/scan" ) {
document.getElementById("status_all").innerHTML = currentUser.status_all; document.getElementById("status_all").innerHTML = currentUser.status_all;
@ -741,27 +780,36 @@ addEventListener("DOMContentLoaded", function() {
request.onload = send_ajax_cmd(command); request.onload = send_ajax_cmd(command);
} else if ( command.indexOf("/sync/") > -1 ) { } else if ( command.indexOf("/sync/") > -1 ) {
console.log("Sync command detected"); if ( command.indexOf("/all") > -1 ) {
console.log("Sync all request");
// If command is /sync/all, add live hosts to sync queue
if (currentUser.hosts_up){
currentUser.hosts_up.forEach(function(host){
let command = "/sync/" + host;
if (currentUser.ul_queue.indexOf(command) == -1){
console.log("Adding " + host + " to sync queue...");
currentUser.ul_queue.push(command);
freeze_queued_container(command);
}
});
}
if (currentUser.ul_queue.length > 1){
sync_host();
}
// Prevent request
return 0;
}
// If command not already in queue, add it // If command not already in queue, add it
if (!currentUser.ul_queue.includes(command)){ if (!currentUser.ul_queue.includes(command)){
console.log("Queuing command...");
currentUser.ul_queue.push(command); currentUser.ul_queue.push(command);
freeze_queued_container(command); freeze_queued_container(command);
} else { } else {
console.log("Command already in queue..."); console.log("Command already in queue...");
return 0;
} }
sync_host(); sync_host();
//~ if (currentUser.ul_queue.length > 1){ // Prevent request
//~ if (currentUser.freeze_ul){ return 1;
//~ console.log("UL freezed, wait for queue to complete.")
// Offload command sending to sync_host()
return 1;
//~ }
//~ currentUser.status_all = t9n[LOCALE].sync;
//~ // Display dialog
//~ display_upload_status(command);
//~ // Request values to fill dialog
//~ setTimeout(send_ajax_cmd, 1000, command + "/status");
} }
else if ( command.indexOf("/browse") > -1 ) { else if ( command.indexOf("/browse") > -1 ) {
request.onload = send_ajax_cmd("/all/browse"); request.onload = send_ajax_cmd("/all/browse");

View File

@ -63,7 +63,12 @@ tr:nth-child(2n+1) {background-color: #888;}
/* /*
.client_container .left_col{border-right: #a8a8a8 2px solid;} .client_container .left_col{border-right: #a8a8a8 2px solid;}
*/ */
.client_container .button{} .client_container .btn {
min-width:1em;
}
.client_container .right_col .btn {
min-width: 3em;
}
.client_container .upload_dialog_cont { .client_container .upload_dialog_cont {
position: absolute; position: absolute;
@ -122,6 +127,7 @@ tr:nth-child(2n+1) {background-color: #888;}
margin-top:1em; margin-top:1em;
background-color: #33333361; background-color: #33333361;
} }
[id^="tl_cont"] { [id^="tl_cont"] {
float: left; float: left;
width: 10%; width: 10%;
@ -152,7 +158,7 @@ tr:nth-child(2n+1) {background-color: #888;}
.buttons {width:75%;margin:auto;text-align: center;padding: 2em;} .buttons {width:75%;margin:auto;text-align: center;padding: 2em;}
.btn { .btn {
margin:auto; margin:auto;
width:3em; min-width:2em;
height: 4em; height: 4em;
display:inline-block; display:inline-block;
padding: 0; padding: 0;
@ -182,6 +188,11 @@ tr:nth-child(2n+1) {background-color: #888;}
} }
.btn_txt {display: block;font-size: small;} .btn_txt {display: block;font-size: small;}
.btn-unqueue {background: #bbb;
width: 1em;
height: 1em;
}
#delete_btn { #delete_btn {
width: 5em; width: 5em;
height: 3em; height: 3em;
@ -199,6 +210,7 @@ tr:nth-child(2n+1) {background-color: #888;}
/*Right column*/ /*Right column*/
.right_col {width: 71%;display: inline-block;} .right_col {width: 71%;display: inline-block;}
/*Left column*/ /*Left column*/
.left_col {width: 28%;display: inline-block;float: left;clear: left;} .left_col {width: 28%;display: inline-block;float: left;clear: left;}
.left_col button { .left_col button {

View File

@ -26,6 +26,8 @@
<button value="/all/clear" class="command btn btn-block btn-lg btn-default" role="button">X<span class="btn_txt">{{gui_l10n['str_clear']}}</span></button> <button value="/all/clear" class="command btn btn-block btn-lg btn-default" role="button">X<span class="btn_txt">{{gui_l10n['str_clear']}}</span></button>
<button value="/all/move/0/1" class="command btn btn-block btn-lg btn-default" role="button">&#x3b2;<span class="btn_txt">{{gui_l10n['str_sort']}}</span></button> <button value="/all/move/0/1" class="command btn btn-block btn-lg btn-default" role="button">&#x3b2;<span class="btn_txt">{{gui_l10n['str_sort']}}</span></button>
<button value="/sync/all" class="command btn btn-block btn-lg btn-default" role="button">&#x21ad;<span class="btn_txt">{{gui_l10n['str_sync']}}</span></button> <button value="/sync/all" class="command btn btn-block btn-lg btn-default" role="button">&#x21ad;<span class="btn_txt">{{gui_l10n['str_sync']}}</span></button>
<button value="/poweroff/all" onclick="exec_all_hosts('poweroff')" class="command btn btn-block btn-lg btn-default" role="button">&#x23fb;<span class="btn_txt">{{gui_l10n['str_poweroff_all']}}</span></button>
<button value="/reboot/all" onclick="exec_all_hosts('reboot')" class="command btn btn-block btn-lg btn-default" role="button">&#x21BA;<span class="btn_txt">{{gui_l10n['str_reboot_all']}}</span></button>
<button id="delete_btn" ondrop="drop(event, this)", ondragover="drag_over_bin(event)" ondragleave="drag_leave_bin(event)" class="delete_btn" role="button">&#x1F5D1;<span class="btn_txt">{{gui_l10n['str_delete']}}</span></button> <button id="delete_btn" ondrop="drop(event, this)", ondragover="drag_over_bin(event)" ondragleave="drag_leave_bin(event)" class="delete_btn" role="button">&#x1F5D1;<span class="btn_txt">{{gui_l10n['str_delete']}}</span></button>
</p> </p>
</div> </div>