Skip to content

Commit

Permalink
add ajax loading and bug fixes, multi user support
Browse files Browse the repository at this point in the history
  • Loading branch information
mrjones-plip authored Feb 18, 2023
2 parents 512fccd + 8893eb6 commit c6d5e22
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 96 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,13 @@ By design, this system is very secure as far as controlling clients over SSH, bu

## Todo

* [ ] enable more than one user per machine
* [ ] add PIN protection in web GUI
* [ ] add "refresh" button per client in web GUI
* [ ] better error handling when SSH fails etc.
* [ ] support ssh keys
* [ ] support ssh keys
* [X] enable more than one user per machine
* [X] better error handling when SSH fails etc.
* [X] AJAX async loading
* [X] handle identically named users on diff servers

## Related projects

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ services:
restart: unless-stopped
ports:
- "${TIMEKPR_IP:-0.0.0.0}:${TIMEKPR_PORT:-8080}:8080"
environment:
TZ: ${TIMEKPR_TZ:-America/Los_Angeles}
35 changes: 24 additions & 11 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import conf, re
from fabric import Connection
from paramiko.ssh_exception import AuthenticationException
from paramiko.ssh_exception import NoValidConnectionsError


def get_config():
Expand All @@ -12,25 +13,37 @@ def get_config():

def get_usage(user, computer, ssh):
# to do - maybe check if user is in timekpr first? (/usr/bin/timekpra --userlist)
timekpra_userinfo_output = str(ssh.run(
conf.ssh_timekpra_bin + ' --userinfo ' + user,
hide=True
))
global timekpra_userinfo_output
fail_json = {'time_left': 0, 'time_spent': 0, 'result': 'fail'}
try:
timekpra_userinfo_output = str(ssh.run(
conf.ssh_timekpra_bin + ' --userinfo ' + user,
hide=True
))
except NoValidConnectionsError as e:
print(f"Cannot connect to SSH server on host '{computer}'. "
f"Check address in conf.py or try again later.")
return fail_json
except AuthenticationException as e:
print(f"Wrong credentials for user '{conf.ssh_user}' on host '{computer}'. "
f"Check `ssh_user` and `ssh_password` credentials in conf.py.")
return fail_json
except Exception as e:
quit(f"Error logging in as user '{conf.ssh_user}' on host '{computer}', check conf.py. \n\n\t" + str(e))
return fail_json
search = r"(TIME_LEFT_DAY: )([0-9]+)"
time_left = re.search(search, timekpra_userinfo_output)
search = r"(TIME_SPENT_DAY: )([0-9]+)"
time_spent = re.search(search, timekpra_userinfo_output)
# todo - better handle "else" when we can't find time remaining
if not time_left or not time_left.group(2):
print(f"Error getting time left, setting to 0. ssh call result: " + str(timekpra_userinfo_output))
time_left = '0'
time_spent = '0'
return fail_json
else:
time_left = str(time_left.group(2))
time_spent = str(time_spent.group(2))

print(f"Time left for {user} at {computer}: {time_spent}")
return {'time_left': time_left, 'time_spent': time_spent}
print(f"Time left for {user} at {computer}: {time_left}")
return {'time_left': time_left, 'time_spent': time_spent, 'result': 'success'}


def get_connection(computer):
Expand All @@ -49,9 +62,9 @@ def get_connection(computer):
)
except AuthenticationException as e:
quit(f"Wrong credentials for user '{conf.ssh_user}' on host '{computer}'. "
f"Check `ssh_user` and `ssh_password` credentials in config.py.")
f"Check `ssh_user` and `ssh_password` credentials in conf.py.")
except Exception as e:
quit(f"Error logging in as user '{conf.ssh_user}' on host '{computer}', check config. \n\n\t" + str(e))
quit(f"Error logging in as user '{conf.ssh_user}' on host '{computer}', check conf.py. \n\n\t" + str(e))
finally:
return connection

Expand Down
218 changes: 137 additions & 81 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@
color: blue;
margin-left: 4px;
}
.loading {
color: lightgray;
}
.error {
color: red;
margin-left: 4px;
}
.user {
padding-bottom: 15px;
display: block;
}
.disabled {
border: 1px solid #ddd !important;
color: #ccc !important;
}
.failed_load {
height: 62px;
}

/* ==========================================================================
Fork Me on Github Stuffs
Expand Down Expand Up @@ -103,105 +121,143 @@
</header>

<div class="b-example-divider"></div>
<div id="people"></div>
<div id="people"></div>
</div>
<script src="{{ url_for('static', filename='bootstrap.5.1.3.min.js') }}"></script>
<script src="{{ url_for('static', filename='jquery-3.6.3.min.js') }}"></script>
<script>
$.getJSON( "/config", function( data ) {
const items = [];
const second_options = [300, 900, 1800, 2700, 3600];
$.each( data, function( computer, user ) {
let usage = $.parseJSON(get_usage(user, computer));
items.push(
"<span class='user' id='"+ user + "'>" +
"<p class='h3'>" + user + "<span class='notify'>Updated!</span><p>" +
"<span>Unused: <span class='time' id='time_left'>" + secondsToHms(usage.time_left) + "</span> </span>" +
"<span>Used: <span class='time' id='time_spent'>" + secondsToHms(usage.time_spent) + "</span></span><br/>" +
"<div class='btn-group' role='group' aria-label='Action'>" +
"<input type='radio' class='btn-check' id='add' value='add' name='action' />" +
"<label class='btn btn-outline-primary' for='add'>Add</label>" +
"<input type='radio' class='btn-check' id='remove' value='remove' name='action' />" +
"<label class='btn btn-outline-primary' for='remove'>Remove</label> " +
"</div> " +
"<div class='btn-group' role='group' aria-label='Times'>"
$.getJSON( "/config", function( data ) {
$.each( data, function( computer, user ) {
let rand_dom = '_' + uuidv4();
let times = get_times(rand_dom);
// todo - click doesn't work and radio spans accross users'
$("#people").append(
"<span class='user' id='" + user + rand_dom + "'>" +
"<p class='h3'>" + user + "<span class='notify loading'></span></p>" +
"<span class='time_wrapper'>" +
"Unused: <span class='time time_left'>...</span> " +
"Used: " +
"<span class='time time_spent'>...</span>" +
"</span><br/>" +
"<div class='btn-group' role='group' aria-label='Action'>" +
"<input type='radio' class='btn-check' id='add" + rand_dom + "' value='add' name='action' />" +
"<label class='btn btn-outline-primary' for='add" + rand_dom + "'>Add</label>" +
"<input type='radio' class='btn-check' id='remove" + rand_dom + "' value='remove' name='action' />" +
"<label class='btn btn-outline-primary' for='remove" + rand_dom + "'>Remove</label> " +
"</div> " +
"<div class='btn-group' role='group' aria-label='Times'>" +
times +
"</div>" +
"<div class='btn-group' role='group' aria-label='save'>" +
"<button class='btn btn-outline-primary save_me' " +
"value='save' user='" + user + "' computer='" + computer + "' >Save</button>" +
"</div> " +
"</span>"
);
$.each(second_options, function(index, seconds) {
items.push(
"<input type='radio' class='btn-check' value='" + seconds + "' id='the" + seconds + "' name='seconds' />" +
"<label class='btn btn-outline-primary' for='the" + seconds + "'>" + secondsToHms(seconds) + "</label>");

$('#' + user + rand_dom + " .notify").show().html("Loading...");
$('#' + user + rand_dom + ' .btn').addClass("disabled");
$('#' + user + rand_dom + ' .btn-check').addClass("disabled");

setInterval(function () {
$('#' + user + rand_dom + " .loading").fadeIn(1300).fadeOut(1300);
}, 1400);
$.getJSON( "/get_usage/" + computer + "/" + user, function( usage ) {
render_user_to_dom(user, computer, usage, rand_dom);
});
});
items.push(
"</div>" +
"<div class='btn-group' role='group' aria-label='save'>" +
"<button class='btn btn-outline-primary' " +
"id='save_me' value='save' user='" + user + "' computer='" + computer + "' >Save</button>" +
"</div> " +
"</span>"
);
get_usage();
});

$("#people").html(items.join( "" ));

$("#save_me" ).click(function() {
const user = $( this ).attr('user');
const computer = $( this ).attr('computer');
const action = $( '#' + user + ' input[name="action"]:checked').val();
const seconds = $( '#' + user + ' input[name="seconds"]:checked').val();
if(action !== undefined && seconds !== undefined){
$('input').prop('checked', false);
add_or_remove_time(user, computer, action, seconds);
$( '#' + user + ' .notify').fadeIn(function() {
function get_times(rand_dom){
const second_options = [300, 900, 1800, 2700, 3600];
let times = '';
$.each(second_options, function (index, seconds) {
times = times + "<input type='radio' class='btn-check' value='" + seconds + "' id='" + seconds + rand_dom + "' name='seconds' />" +
"<label class='btn btn-outline-primary' for='" + seconds + rand_dom + "'>" + secondsToHms(seconds) + "</label>";
});
return times;
}
function uuidv4() {
return 'xxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}

function add_click_handler(user, rand_dom){
$("#" + user + rand_dom + " .save_me" ).click(function() {
const computer = $( this ).attr('computer');
const action = $( '#' + user + rand_dom + ' input[name="action"]:checked').val();
const seconds = $( '#' + user + rand_dom + ' input[name="seconds"]:checked').val();
if(action !== undefined && seconds !== undefined){
$('input').prop('checked', false);
add_or_remove_time(user, computer, action, seconds, rand_dom);
$( '#' + user + rand_dom + ' .notify').html('Updated!').fadeIn(function() {
setTimeout(function() {
$('#' + user + rand_dom + ' .notify').fadeOut("slow");
}, 1000);
});
} else {
alert("Choose 'Add' or 'Remove' and choose an amount of time to add.");
}
});
}

function render_user_to_dom(user, computer, usage, rand_dom){
if(usage.result === 'success') {
$( '#' + user + rand_dom + ' .notify').fadeOut().html('Loaded!').fadeIn(function() {
setTimeout(function() {
$('#' + user + ' .notify').fadeOut("slow");
$('#' + user + rand_dom + ' .notify').fadeOut("slow");
}, 1000);
});
$('#' + user + rand_dom + " .time_left").html(secondsToHms(usage.time_left));
$('#' + user + rand_dom + " .time_spent").html(secondsToHms(usage.time_spent));
$('#' + user + rand_dom + " .loading").removeClass('loading');
$('#' + user + rand_dom + ' .btn').removeClass("disabled");
$('#' + user + rand_dom + ' .btn-check').removeClass("disabled");
add_click_handler(user, rand_dom);
} else {
alert("Choose 'Add' or 'Remove' and choose an amount of time to add.");
$('#' + user + rand_dom + " .time_wrapper").html(computer + " failed. Off? Bad Auth?");
$('#' + user + rand_dom + " .notify").addClass("error").html("Error");
}
});
});

function add_or_remove_time(user, computer, action, seconds){
let form;
if(action === 'add'){
form = 'increase_time'
} else {
form = 'decrease_time'
}
console.log("add_or_remove_time callded: form", form,"computer", computer,"user",user,"seconds",seconds);
$.getJSON("/" + form + "/" + computer + "/" + user + "/" + seconds, function( data ) {
$('#' + user + " #time_left").html(secondsToHms(data.time_left));
$('#' + user + " #time_spent").html(secondsToHms(data.time_spent));
});
}

function get_usage(user, computer){
return $.ajax({
type: "GET",
url: "/get_usage/" + computer + "/" + user,
async: false
}).responseText;
}

// thanks https://stackoverflow.com/a/37096512 !
function secondsToHms(d) {
if (Number(d) === 0){
return "none";

function get_buttons(user, computer, state = 'enabled'){

}

function add_or_remove_time(user, computer, action, seconds, rand_dom){
let form;
if(action === 'add'){
form = 'increase_time'
} else {
form = 'decrease_time'
}
$.getJSON("/" + form + "/" + computer + "/" + user + "/" + seconds, function( data ) {
$('#' + user + rand_dom + " .time_left").html(secondsToHms(data.time_left));
$('#' + user + rand_dom + " .time_spent").html(secondsToHms(data.time_spent));
});
}
d = Number(d);
var h = Math.floor(d / 3600);
var m = Math.floor(d % 3600 / 60);
var s = Math.floor(d % 3600 % 60);

var hDisplay = h > 0 ? h + (h === 1 ? " hr " : " hrs ") : "";
var mDisplay = m > 0 ? m + (m === 1 ? " min " : " mins ") : "";
var sDisplay = s > 0 ? s + (s === 1 ? " sec" : " secs") : "";
return hDisplay + mDisplay + sDisplay;
}
// thanks https://stackoverflow.com/a/37096512 !
function secondsToHms(d) {
if (Number(d) === 0){
return "none";
}
d = Number(d);
var h = Math.floor(d / 3600);
var m = Math.floor(d % 3600 / 60);
var s = Math.floor(d % 3600 % 60);

var hDisplay = h > 0 ? h + (h === 1 ? " hr " : " hrs ") : "";
var mDisplay = m > 0 ? m + (m === 1 ? " min " : " mins ") : "";
var sDisplay = s > 0 ? s + (s === 1 ? " sec" : " secs") : "";
return hDisplay + mDisplay + sDisplay;
}

</script>
<script src="{{ url_for('static', filename='bootstrap.5.1.3.min.js') }}"></script>
<script src="{{ url_for('static', filename='jquery-3.6.3.min.js') }}"></script>
</body>
</html>

Expand Down
Binary file modified timekpr-next-remote.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion timekpr-next-web.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_usage(computer, user):
return validate_request(computer, user), 500
ssh = main.get_connection(computer)
usage = main.get_usage(user, computer, ssh)
return {'result': "success", "time_left": usage['time_left'], "time_spent": usage['time_spent']}, 200
return {'result': usage['result'], "time_left": usage['time_left'], "time_spent": usage['time_spent']}, 200


@app.route("/increase_time/<computer>/<user>/<seconds>")
Expand Down

0 comments on commit c6d5e22

Please sign in to comment.