12 Commits

Author SHA1 Message Date
feea01a47e Release 0.12.2 2022-01-20 00:53:32 +03:00
fcacd9f15c user friednly is required table 2022-01-19 15:09:45 +03:00
b2256784dd move header buttons into one row 2022-01-19 13:42:43 +03:00
fee87ddbc8 more typed actors 2022-01-19 12:19:55 +03:00
dc882b74bf move modals to form validation 2022-01-19 03:04:59 +03:00
7a6cd84ce3 Release 0.12.1 2022-01-17 22:35:50 +03:00
33b750123d disable covreport coz it breaks the dist 2022-01-17 22:34:26 +03:00
d049238dcf Release 0.12.0 2022-01-17 22:28:13 +03:00
5d72852420 add status endpoint 2022-01-17 22:26:48 +03:00
78a00e2cab sbt improvelemnts 2022-01-17 12:17:39 +03:00
786c3d7d48 Release 0.11.1 2022-01-17 05:21:11 +03:00
8a1d99b319 change sorting order 2022-01-17 05:19:56 +03:00
29 changed files with 338 additions and 233 deletions

1
.gitignore vendored
View File

@ -75,6 +75,7 @@ lib_managed/
src_managed/
project/boot/
project/plugins/project/
.bsp/
# Scala-IDE specific
.scala_dependencies

View File

@ -1,3 +1,5 @@
organization := "me.arcanis"
name := "ffxivbis"
scalaVersion := "2.13.6"
@ -5,12 +7,3 @@ scalaVersion := "2.13.6"
scalacOptions ++= Seq("-deprecation", "-feature")
enablePlugins(JavaAppPackaging)
assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
case "application.conf" => MergeStrategy.concat
case "module-info.class" => MergeStrategy.first
case x =>
val oldStrategy = (assemblyMergeStrategy in assembly).value
oldStrategy(x)
}

View File

@ -1 +1 @@
sbt.version = 1.3.3
sbt.version = 1.5.8

View File

@ -1,4 +1,4 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")
addDependencyTreePlugin

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
</head>
<body>
@ -69,7 +69,7 @@
data-single-select="true"
data-sortable="true"
data-sort-name="nick"
data-sort-order="desc"
data-sort-order="asc"
data-sort-reset="true"
data-toolbar="#toolbar">
<thead class="table-primary">
@ -86,23 +86,24 @@
<div id="update-bis-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<form class="modal-content">
<div class="modal-header">
<form class="modal-content" action="javascript:" onsubmit="updateBis()">
<div class="modal-header form-group row">
<div class="btn-group" role="group" aria-label="Update bis">
<input id="add-piece-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="add-piece-btn">add piece</label>
<input id="update-bis-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off">
<label class="btn btn-outline-primary" for="update-bis-btn">update bis</label>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="player">player</label>
<div class="col-sm-8">
<select id="player" name="player" class="form-control" title="player"></select>
<select id="player" name="player" class="form-control" title="player" required></select>
</div>
</div>
<div id="piece-row" class="form-group row">
@ -120,14 +121,14 @@
<div id="bis-link-row" class="form-group row" style="display: none">
<label class="col-sm-4 col-form-label" for="bis-link">link</label>
<div class="col-sm-8">
<input id="bis-link" name="link" class="form-control" placeholder="link to bis" onkeyup="disableSubmitBisButton()">
<input id="bis-link" name="link" class="form-control" placeholder="link to bis">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button id="submit-add-bis-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPiece()" disabled>add</button>
<button id="submit-update-bis-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="setBis()" style="display: none" disabled>set</button>
<button id="submit-add-bis-btn" type="submit" class="btn btn-primary">add</button>
<button id="submit-set-bis-btn" type="submit" class="btn btn-primary" style="display: none">set</button>
</div>
</div>
</form>
@ -171,7 +172,7 @@
const updateButton = $("#update-btn");
const submitAddBisButton = $("#submit-add-bis-btn");
const submitUpdateBisButton = $("#submit-update-bis-btn");
const submitSetBisButton = $("#submit-set-bis-btn");
const updateBisDialog = $("#update-bis-dialog");
const addPieceButton = $("#add-piece-btn");
@ -208,12 +209,8 @@
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function disableSubmitBisButton() {
const nonEmpty = (playerInput.val() !== null); // well lol
submitUpdateBisButton.attr("disabled", !(nonEmpty && linkInput.val()));
submitAddBisButton.attr("disabled", !(nonEmpty));
updateBisDialog.modal("hide");
return true; // action expects boolean result
}
function hideControls() {
@ -222,20 +219,24 @@
}
function hideLinkPart() {
disableSubmitBisButton();
bisLinkRow.hide();
submitUpdateBisButton.hide();
linkInput.prop("required", false);
submitSetBisButton.hide();
pieceRow.show();
pieceTypeRow.show();
pieceInput.prop("required", true);
pieceTypeInput.prop("required", true);
submitAddBisButton.show();
}
function hidePiecePart() {
disableSubmitBisButton();
bisLinkRow.show();
submitUpdateBisButton.show();
linkInput.prop("required", true);
submitSetBisButton.show();
pieceRow.hide();
pieceTypeRow.hide();
pieceInput.prop("required", false);
pieceTypeInput.prop("required", false);
submitAddBisButton.hide();
}
@ -269,7 +270,6 @@
return option;
});
playerInput.empty().append(options);
disableSubmitBisButton();
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
@ -327,6 +327,18 @@
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
updateBisDialog.modal("hide");
return true; // action expects boolean result
}
function updateBis() {
if (updateBisButton.is(":checked")) {
return setBis();
}
if (addPieceButton.is(":checked")) {
return addPiece();
}
return false; // should not happen
}
$(function () {
@ -344,6 +356,7 @@
table.bootstrapTable({});
reload();
reset();
});
</script>

View File

@ -6,11 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
</head>
<body>

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
</head>
<body>
@ -88,35 +88,35 @@
<div id="add-loot-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form class="modal-content" action="javascript:" onsubmit="addLoot()">
<div class="modal-header">
<h4 class="modal-title">add looted piece</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<form class="modal-body">
<div class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="player">player</label>
<div class="col-sm-8">
<select id="player" name="player" class="form-control" title="player"></select>
<select id="player" name="player" class="form-control" title="player" required></select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="piece">piece</label>
<div class="col-sm-8">
<select id="piece" name="piece" class="form-control" title="piece"></select>
<select id="piece" name="piece" class="form-control" title="piece" required></select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="piece-type">piece type</label>
<div class="col-sm-8">
<select id="piece-type" name="pieceType" class="form-control" title="pieceType"></select>
<select id="piece-type" name="pieceType" class="form-control" title="pieceType" required></select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="job">job</label>
<div class="col-sm-8">
<select id="job" name="job" class="form-control" title="job"></select>
<select id="job" name="job" class="form-control" title="job" required></select>
</div>
</div>
<div class="form-group row">
@ -141,14 +141,14 @@
</tr>
</thead>
</table>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button>
<button id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addLoot()" disabled>add</button>
<button type="submit" class="btn btn-primary">add</button>
</div>
</div>
</form>
</div>
</div>
@ -189,7 +189,6 @@
const addButton = $("#add-btn");
const removeButton = $("#remove-btn");
const submitLootButton = $("#submit-btn");
const addLootDialog = $("#add-loot-dialog");
const freeLootInput = $("#free-loot");
@ -221,6 +220,8 @@
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
addLootDialog.modal("hide");
return true; // action expects boolean result
}
function hideControls() {
@ -260,7 +261,6 @@
return option;
});
playerInput.empty().append(options);
submitLootButton.attr("disabled", options.length === 0);
},
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
@ -310,7 +310,7 @@
return {
nick: stat.nick,
job: stat.job,
isRequired: stat.isRequired,
isRequired: stat.isRequired ? "yes" : "no",
lootCount: stat.lootCount,
lootCountBiS: stat.lootCountBiS,
lootCountTotal: stat.lootCountTotal,

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
</head>
<body>
@ -65,7 +65,7 @@
data-single-select="true"
data-sortable="true"
data-sort-name="nick"
data-sort-order="desc"
data-sort-order="asc"
data-sort-reset="true"
data-toolbar="#toolbar">
<thead class="table-primary">
@ -84,23 +84,23 @@
<div id="add-player-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form class="modal-content" action="javascript:" onsubmit="addPlayer()">
<div class="modal-header">
<h4 class="modal-title">add new player</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<form class="modal-body">
<div class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="nick">player name</label>
<div class="col-sm-8">
<input id="nick" name="nick" class="form-control" placeholder="nick" onkeyup="disableAddPlayerForm()">
<input id="nick" name="nick" class="form-control" placeholder="nick" required>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="job">player job</label>
<div class="col-sm-8">
<select id="job" name="job" class="form-control" title="job"></select>
<select id="job" name="job" class="form-control" title="job" required></select>
</div>
</div>
<div class="form-group row">
@ -115,13 +115,13 @@
<input id="priority" name="priority" type="number" class="form-control" value="0">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button id="submit-player-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPlayer()" disabled>add</button>
<button type="submit" class="btn btn-primary">add</button>
</div>
</div>
</form>
</div>
</div>
@ -162,7 +162,6 @@
const removeButton = $("#remove-btn");
const addPlayerDialog = $("#add-player-dialog");
const submitPlayerButton = $("#submit-player-btn");
const jobInput = $("#job");
const linkInput = $("#link");
@ -187,6 +186,8 @@
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
addPlayerDialog.modal("hide");
return true; // action expects boolean result
}
function bisLinkFormatter(link, row) {
@ -197,10 +198,6 @@
}
}
function disableAddPlayerForm() {
submitPlayerButton.attr("disabled", !nickInput.val());
}
function hideControls() {
addButton.attr("hidden", isReadOnly);
removeButton.attr("hidden", isReadOnly);

View File

@ -6,9 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" type="text/css">
<link href="/static/favicon.ico" rel="shortcut icon">
<link rel="shortcut icon" href="/static/favicon.ico">
</head>
<body>

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon">
<link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/static/styles.css" type="text/css">
</head>
<body>
@ -69,7 +69,7 @@
data-single-select="true"
data-sortable="true"
data-sort-name="username"
data-sort-order="desc"
data-sort-order="asc"
data-sort-reset="true"
data-toolbar="#toolbar">
<thead class="table-primary">
@ -84,38 +84,38 @@
<div id="add-user-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form class="modal-content" action="javascript:" onsubmit="addUser()">
<div class="modal-header">
<h4 class="modal-title">add new user</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<form class="modal-body">
<div class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="username">login</label>
<div class="col-sm-8">
<input id="username" name="username" class="form-control" placeholder="username" onkeyup="disableAddUserForm()">
<input id="username" name="username" class="form-control" placeholder="username" required>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="password">password</label>
<div class="col-sm-8">
<input id="password" name="password" type="password" class="form-control" placeholder="password" onkeyup="disableAddUserForm()">
<input id="password" name="password" type="password" class="form-control" placeholder="password" required>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label" for="permission">permission</label>
<div class="col-sm-8">
<select id="permission" name="permission" class="form-control" title="permission"></select>
<select id="permission" name="permission" class="form-control" title="permission" required></select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addUser()" disabled>add</button>
<button type="submit" class="btn btn-primary">add</button>
</div>
</div>
</form>
</div>
</div>
@ -156,7 +156,6 @@
const removeButton = $("#remove-btn");
const addUserDialog = $("#add-user-dialog");
const submitUserButton = $("#submit-btn");
const usernameInput = $("#username");
const passwordInput = $("#password");
@ -176,10 +175,8 @@
success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
});
}
function disableAddUserForm() {
submitUserButton.attr("disabled", !(usernameInput.val() && passwordInput.val()));
addUserDialog.modal("hide");
return true; // action expects boolean result
}
function hideControls() {

View File

@ -11,6 +11,6 @@
<appender-ref ref="http" />
</logger>
<logger name="org.flywaydb.core.internal" level="INFO" />
<logger name="com.zaxxer.hikari.pool.HikariPool" level="INFO" />
<logger name="com.zaxxer.hikari.pool" level="INFO" />
</configuration>

View File

@ -57,14 +57,14 @@ me.arcanis.ffxivbis {
# ttl of cached logins
cache-timeout = 1m
}
}
default-dispatcher {
type = Dispatcher
executor = "thread-pool-executor"
thread-pool-executor {
fixed-pool-size = 16
}
throughput = 1
}
default-dispatcher {
type = Dispatcher
executor = "thread-pool-executor"
thread-pool-executor {
fixed-pool-size = 16
}
throughput = 1
}
}

View File

@ -37,7 +37,7 @@ function loadTypes(url, selector) {
}
function setupFormClear(dialog, reset) {
dialog.on("shown.bs.modal", function () {
dialog.on("hide.bs.modal", function () {
$(this).find("form").trigger("reset");
$(this).find("table").bootstrapTable("removeAll");
if (reset) {

View File

@ -13,8 +13,8 @@ import akka.http.scaladsl.server.Directive0
import akka.http.scaladsl.server.Directives.{extractClientIP, extractRequestContext, mapResponse, optionalHeaderValueByType}
import com.typesafe.scalalogging.Logger
import java.time.{Instant, ZoneId}
import java.time.format.DateTimeFormatter
import java.time.{Instant, ZoneId}
import java.util.Locale
trait HttpLog {
@ -68,7 +68,7 @@ object HttpLog {
val httpLogDatetimeFormatter: DateTimeFormatter =
DateTimeFormatter
.ofPattern("dd/MMM/uuuu:HH:mm:ss xx ")
.ofPattern("dd/MMM/uuuu:HH:mm:ss xx")
.withLocale(Locale.UK)
.withZone(ZoneId.systemDefault())
}

View File

@ -25,7 +25,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
private val config = system.settings.config
implicit val scheduler: Scheduler = system.scheduler
implicit val timeout: Timeout = config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
implicit val timeout: Timeout = config.getTimeout("me.arcanis.ffxivbis.settings.request-timeout")
private val auth = AuthorizationProvider(config, storage, timeout, scheduler)

View File

@ -22,6 +22,7 @@ class Swagger(config: Config) extends SwaggerHttpService {
classOf[api.v1.LootEndpoint],
classOf[api.v1.PartyEndpoint],
classOf[api.v1.PlayerEndpoint],
classOf[api.v1.StatusEndpoint],
classOf[api.v1.TypesEndpoint],
classOf[api.v1.UserEndpoint]
)
@ -35,7 +36,7 @@ class Swagger(config: Config) extends SwaggerHttpService {
override val host: String =
if (config.hasPath("me.arcanis.ffxivbis.web.hostname")) config.getString("me.arcanis.ffxivbis.web.hostname")
else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getString("me.arcanis.ffxivbis.web.port")}"
else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getInt("me.arcanis.ffxivbis.web.port")}"
private val basicAuth = new SecurityScheme()
.description("basic http auth")

View File

@ -32,14 +32,15 @@ class RootApiV1Endpoint(
private val lootEndpoint = new LootEndpoint(storage, auth)
private val partyEndpoint = new PartyEndpoint(storage, provider, auth)
private val playerEndpoint = new PlayerEndpoint(storage, provider, auth)
private val statusEndpoint = new StatusEndpoint
private val typesEndpoint = new TypesEndpoint(config)
private val userEndpoint = new UserEndpoint(storage, auth)
def route: Route =
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~
playerEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~ playerEndpoint.route ~
statusEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2019-2022 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v1
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.{Content, Schema}
import io.swagger.v3.oas.annotations.responses.ApiResponse
import jakarta.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._
@Path("/api/v1")
class StatusEndpoint extends JsonSupport {
def route: Route = getServerStatus
@GET
@Path("status")
@Produces(value = Array("application/json"))
@Operation(
summary = "server status",
description = "Returns the server status descriptor",
responses = Array(
new ApiResponse(
responseCode = "200",
description = "Service status descriptor",
content = Array(new Content(schema = new Schema(implementation = classOf[StatusModel])))
),
new ApiResponse(
responseCode = "500",
description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
),
),
tags = Array("status"),
)
def getServerStatus: Route =
path("status") {
get {
complete {
StatusModel(
version = Option(getClass.getPackage.getImplementationVersion),
)
}
}
}
}

View File

@ -52,5 +52,6 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkModel] = jsonFormat2(PlayerBiSLinkModel.apply)
implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersModel] =
jsonFormat9(PlayerIdWithCountersModel.apply)
implicit val statusFormat: RootJsonFormat[StatusModel] = jsonFormat1(StatusModel.apply)
implicit val userFormat: RootJsonFormat[UserModel] = jsonFormat4(UserModel.apply)
}

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) 2019-2022 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema
case class StatusModel(@Schema(description = "server version") version: Option[String])

View File

@ -24,14 +24,14 @@ class PartyService(context: ActorContext[Message], storage: ActorRef[DatabaseMes
with StrictLogging {
import me.arcanis.ffxivbis.utils.Implicits._
private val cacheTimeout: FiniteDuration =
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.cache-timeout")
private val cacheTimeout =
context.system.settings.config.getFiniteDuration("me.arcanis.ffxivbis.settings.cache-timeout")
implicit private val executionContext: ExecutionContext = {
val selector = DispatcherSelector.fromConfig("me.arcanis.ffxivbis.default-dispatcher")
context.system.dispatchers.lookup(selector)
}
implicit private val timeout: Timeout =
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
context.system.settings.config.getTimeout("me.arcanis.ffxivbis.settings.request-timeout")
implicit private val scheduler: Scheduler = context.system.scheduler
override def onMessage(msg: Message): Behavior[Message] = handle(Map.empty)(msg)

View File

@ -8,30 +8,32 @@
*/
package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.service.database.Database
trait DatabaseBiSHandler { this: Database =>
def bisHandler: DatabaseMessage.Handler = {
case AddPieceToBis(playerId, piece, client) =>
run(profile.insertPieceBiS(playerId, piece))(_ => client ! ())
Behaviors.same
def bisHandler(msg: BisDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddPieceToBis(playerId, piece, client) =>
run(profile.insertPieceBiS(playerId, piece))(_ => client ! ())
Behaviors.same
case GetBiS(partyId, maybePlayerId, client) =>
run {
getParty(partyId, withBiS = true, withLoot = false)
.map(filterParty(_, maybePlayerId))
}(client ! _)
Behaviors.same
case GetBiS(partyId, maybePlayerId, client) =>
run {
getParty(partyId, withBiS = true, withLoot = false)
.map(filterParty(_, maybePlayerId))
}(client ! _)
Behaviors.same
case RemovePieceFromBiS(playerId, piece, client) =>
run(profile.deletePieceBiS(playerId, piece))(_ => client ! ())
Behaviors.same
case RemovePieceFromBiS(playerId, piece, client) =>
run(profile.deletePieceBiS(playerId, piece))(_ => client ! ())
Behaviors.same
case RemovePiecesFromBiS(playerId, client) =>
run(profile.deletePiecesBiS(playerId))(_ => client ! ())
Behaviors.same
}
case RemovePiecesFromBiS(playerId, client) =>
run(profile.deletePiecesBiS(playerId))(_ => client ! ())
Behaviors.same
}
}

View File

@ -11,7 +11,7 @@ package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
import akka.actor.typed.{Behavior, DispatcherSelector}
import com.typesafe.config.Config
import me.arcanis.ffxivbis.messages.DatabaseMessage
import me.arcanis.ffxivbis.messages.{BisDatabaseMessage, DatabaseMessage, LootDatabaseMessage, PartyDatabaseMessage, UserDatabaseMessage}
import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.storage.DatabaseProfile
@ -32,8 +32,12 @@ class DatabaseImpl(context: ActorContext[DatabaseMessage])
override val config: Config = context.system.settings.config
override val profile: DatabaseProfile = new DatabaseProfile(executionContext, config)
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] = handle(msg)
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case msg: BisDatabaseMessage => bisHandler(msg)
case msg: LootDatabaseMessage => lootHandler(msg)
case msg: PartyDatabaseMessage => partyHandler(msg)
case msg: UserDatabaseMessage => userHandler(msg)
}
private def handle: DatabaseMessage.Handler =
bisHandler.orElse(lootHandler).orElse(partyHandler).orElse(userHandler)
}

View File

@ -8,6 +8,7 @@
*/
package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.Loot
@ -17,28 +18,29 @@ import java.time.Instant
trait DatabaseLootHandler { this: Database =>
def lootHandler: DatabaseMessage.Handler = {
case AddPieceTo(playerId, piece, isFreeLoot, client) =>
val loot = Loot(-1, piece, Instant.now, isFreeLoot)
run(profile.insertPiece(playerId, loot))(_ => client ! ())
Behaviors.same
def lootHandler(msg: LootDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddPieceTo(playerId, piece, isFreeLoot, client) =>
val loot = Loot(-1, piece, Instant.now, isFreeLoot)
run(profile.insertPiece(playerId, loot))(_ => client ! ())
Behaviors.same
case GetLoot(partyId, maybePlayerId, client) =>
run {
getParty(partyId, withBiS = false, withLoot = true)
.map(filterParty(_, maybePlayerId))
}(client ! _)
Behaviors.same
case GetLoot(partyId, maybePlayerId, client) =>
run {
getParty(partyId, withBiS = false, withLoot = true)
.map(filterParty(_, maybePlayerId))
}(client ! _)
Behaviors.same
case RemovePieceFrom(playerId, piece, isFreeLoot, client) =>
run(profile.deletePiece(playerId, piece, isFreeLoot))(_ => client ! ())
Behaviors.same
case RemovePieceFrom(playerId, piece, isFreeLoot, client) =>
run(profile.deletePiece(playerId, piece, isFreeLoot))(_ => client ! ())
Behaviors.same
case SuggestLoot(partyId, piece, client) =>
run {
getParty(partyId, withBiS = true, withLoot = true)
.map(_.suggestLoot(piece))
}(client ! _)
Behaviors.same
}
case SuggestLoot(partyId, piece, client) =>
run {
getParty(partyId, withBiS = true, withLoot = true)
.map(_.suggestLoot(piece))
}(client ! _)
Behaviors.same
}
}

View File

@ -8,6 +8,7 @@
*/
package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{BiS, Player}
@ -17,50 +18,51 @@ import scala.concurrent.Future
trait DatabasePartyHandler { this: Database =>
def partyHandler: DatabaseMessage.Handler = {
case AddPlayer(player, client) =>
run(profile.insertPlayer(player))(_ => client ! ())
Behaviors.same
def partyHandler(msg: PartyDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddPlayer(player, client) =>
run(profile.insertPlayer(player))(_ => client ! ())
Behaviors.same
case GetParty(partyId, client) =>
run(getParty(partyId, withBiS = true, withLoot = true))(client ! _)
Behaviors.same
case GetParty(partyId, client) =>
run(getParty(partyId, withBiS = true, withLoot = true))(client ! _)
Behaviors.same
case GetPartyDescription(partyId, client) =>
run(profile.getPartyDescription(partyId))(client ! _)
Behaviors.same
case GetPartyDescription(partyId, client) =>
run(profile.getPartyDescription(partyId))(client ! _)
Behaviors.same
case GetPlayer(playerId, client) =>
run {
profile
.getPlayerFull(playerId)
.flatMap { maybePlayerData =>
Future.traverse(maybePlayerData.toSeq) { playerData =>
for {
bis <- profile.getPiecesBiS(playerId)
loot <- profile.getPieces(playerId)
} yield Player(
playerData.id,
playerId.partyId,
playerId.job,
playerId.nick,
BiS(bis.map(_.piece)),
loot,
playerData.link,
playerData.priority
)
case GetPlayer(playerId, client) =>
run {
profile
.getPlayerFull(playerId)
.flatMap { maybePlayerData =>
Future.traverse(maybePlayerData.toSeq) { playerData =>
for {
bis <- profile.getPiecesBiS(playerId)
loot <- profile.getPieces(playerId)
} yield Player(
playerData.id,
playerId.partyId,
playerId.job,
playerId.nick,
BiS(bis.map(_.piece)),
loot,
playerData.link,
playerData.priority
)
}
}
}
.map(_.headOption)
}(client ! _)
Behaviors.same
.map(_.headOption)
}(client ! _)
Behaviors.same
case RemovePlayer(playerId, client) =>
run(profile.deletePlayer(playerId))(_ => client ! ())
Behaviors.same
case RemovePlayer(playerId, client) =>
run(profile.deletePlayer(playerId))(_ => client ! ())
Behaviors.same
case UpdateParty(description, client) =>
run(profile.insertPartyDescription(description))(_ => client ! ())
Behaviors.same
}
case UpdateParty(description, client) =>
run(profile.insertPartyDescription(description))(_ => client ! ())
Behaviors.same
}
}

View File

@ -8,32 +8,34 @@
*/
package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.service.database.Database
trait DatabaseUserHandler { this: Database =>
def userHandler: DatabaseMessage.Handler = {
case AddUser(user, isHashedPassword, client) =>
val toInsert = if (isHashedPassword) user else user.withHashedPassword
run(profile.insertUser(toInsert))(_ => client ! ())
Behaviors.same
def userHandler(msg: UserDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddUser(user, isHashedPassword, client) =>
val toInsert = if (isHashedPassword) user else user.withHashedPassword
run(profile.insertUser(toInsert))(_ => client ! ())
Behaviors.same
case DeleteUser(partyId, username, client) =>
run(profile.deleteUser(partyId, username))(_ => client ! ())
Behaviors.same
case DeleteUser(partyId, username, client) =>
run(profile.deleteUser(partyId, username))(_ => client ! ())
Behaviors.same
case Exists(partyId, client) =>
run(profile.exists(partyId))(client ! _)
Behaviors.same
case Exists(partyId, client) =>
run(profile.exists(partyId))(client ! _)
Behaviors.same
case GetUser(partyId, username, client) =>
run(profile.getUser(partyId, username))(client ! _)
Behaviors.same
case GetUser(partyId, username, client) =>
run(profile.getUser(partyId, username))(client ! _)
Behaviors.same
case GetUsers(partyId, client) =>
run(profile.getUsers(partyId))(client ! _)
Behaviors.same
}
case GetUsers(partyId, client) =>
run(profile.getUsers(partyId))(client ! _)
Behaviors.same
}
}

View File

@ -11,7 +11,6 @@ package me.arcanis.ffxivbis.utils
import akka.util.Timeout
import com.typesafe.config.Config
import java.time.Duration
import java.util.concurrent.TimeUnit
import scala.concurrent.duration.FiniteDuration
import scala.language.implicitConversions
@ -19,20 +18,14 @@ import scala.util.Try
object Implicits {
implicit def getBooleanFromOptionString(maybeYes: Option[String]): Boolean = maybeYes.map(_.toLowerCase) match {
case Some("yes" | "on") => true
case _ => false
}
implicit def getFiniteDuration(duration: Duration): FiniteDuration =
FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS)
implicit def getTimeout(duration: Duration): Timeout =
FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS)
implicit class ConfigExtension(config: Config) {
def getFiniteDuration(path: String): FiniteDuration =
FiniteDuration(config.getDuration(path, TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS)
def getOptString(path: String): Option[String] =
Try(config.getString(path)).toOption.filter(_.nonEmpty)
def getTimeout(path: String): Timeout = getFiniteDuration(path)
}
}

View File

@ -0,0 +1,29 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.testkit.ScalatestRouteTest
import com.typesafe.config.Config
import me.arcanis.ffxivbis.Settings
import me.arcanis.ffxivbis.http.api.v1.json._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import scala.language.postfixOps
class StatusEndpointTest extends AnyWordSpecLike
with Matchers with ScalatestRouteTest with JsonSupport {
override val testConfig: Config = Settings.withRandomDatabase
private val route = new StatusEndpoint().route
"api v1 status endpoint" must {
"return server status" in {
Get("/status") ~> route ~> check {
status shouldEqual StatusCodes.OK
}
}
}
}

View File

@ -1 +1 @@
version := "0.11.0"
version := "0.12.2"