Compare commits
5 Commits
8ba0f02b98
...
06e1474396
Author | SHA1 | Date | |
---|---|---|---|
06e1474396 | |||
|
adb9c728f4 | ||
|
fc02eaea4a | ||
|
91f87302fb | ||
|
97f186dc33 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@
|
|||||||
/venv
|
/venv
|
||||||
.cargo
|
.cargo
|
||||||
/.idea
|
/.idea
|
||||||
|
web/static/index.html
|
||||||
|
web/static/assets
|
||||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "reminder-dashboard"]
|
||||||
|
path = reminder-dashboard
|
||||||
|
url = gitea@gitea.jellypro.xyz:jude/reminder-dashboard
|
879
Cargo.lock
generated
879
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder-rs"
|
name = "reminder-rs"
|
||||||
version = "1.6.50"
|
version = "1.6.48"
|
||||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0 only"
|
license = "AGPL-3.0 only"
|
||||||
@ -43,6 +43,7 @@ assets = [
|
|||||||
["conf/default.env", "etc/reminder-rs/config.env", "600"],
|
["conf/default.env", "etc/reminder-rs/config.env", "600"],
|
||||||
["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
|
["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
|
||||||
["web/static/**/*", "lib/reminder-rs/static", "644"],
|
["web/static/**/*", "lib/reminder-rs/static", "644"],
|
||||||
|
["reminder-dashboard/dist/static/**/*", "lib/reminder-rs/static", "644"],
|
||||||
["web/templates/**/*", "lib/reminder-rs/templates", "644"],
|
["web/templates/**/*", "lib/reminder-rs/templates", "644"],
|
||||||
["healthcheck", "lib/reminder-rs/healthcheck", "755"],
|
["healthcheck", "lib/reminder-rs/healthcheck", "755"],
|
||||||
["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"],
|
["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"],
|
||||||
|
1
reminder-dashboard
Submodule
1
reminder-dashboard
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 8ba7a39ce532907b845c54dd3b54bf2a4c7a04e9
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder_web"
|
name = "reminder_web"
|
||||||
version = "0.1.4"
|
version = "0.1.3"
|
||||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@ -19,3 +19,4 @@ lazy_static = "1.4.0"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
csv = "1.2"
|
csv = "1.2"
|
||||||
|
prometheus = "0.13.3"
|
||||||
|
@ -33,9 +33,11 @@ impl<'r> FromRequest<'r> for Transaction<'r> {
|
|||||||
match request.guard::<&State<Pool<Database>>>().await {
|
match request.guard::<&State<Pool<Database>>>().await {
|
||||||
Outcome::Success(pool) => match pool.begin().await {
|
Outcome::Success(pool) => match pool.begin().await {
|
||||||
Ok(transaction) => Outcome::Success(Transaction(transaction)),
|
Ok(transaction) => Outcome::Success(Transaction(transaction)),
|
||||||
Err(e) => Outcome::Error((Status::InternalServerError, TransactionError::Error(e))),
|
Err(e) => {
|
||||||
|
Outcome::Failure((Status::InternalServerError, TransactionError::Error(e)))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Outcome::Error(e) => Outcome::Error((e.0, TransactionError::Missing)),
|
Outcome::Failure(e) => Outcome::Failure((e.0, TransactionError::Missing)),
|
||||||
Outcome::Forward(f) => Outcome::Forward(f),
|
Outcome::Forward(f) => Outcome::Forward(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ mod consts;
|
|||||||
mod macros;
|
mod macros;
|
||||||
mod catchers;
|
mod catchers;
|
||||||
mod guards;
|
mod guards;
|
||||||
|
mod metrics;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
use std::{env, path::Path};
|
use std::{env, path::Path};
|
||||||
@ -25,7 +26,10 @@ use serenity::{
|
|||||||
};
|
};
|
||||||
use sqlx::{MySql, Pool};
|
use sqlx::{MySql, Pool};
|
||||||
|
|
||||||
use crate::consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES};
|
use crate::{
|
||||||
|
consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES},
|
||||||
|
metrics::{init_metrics, MetricProducer},
|
||||||
|
};
|
||||||
|
|
||||||
type Database = MySql;
|
type Database = MySql;
|
||||||
|
|
||||||
@ -64,7 +68,10 @@ pub async fn initialize(
|
|||||||
let static_path =
|
let static_path =
|
||||||
if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" };
|
if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" };
|
||||||
|
|
||||||
|
init_metrics();
|
||||||
|
|
||||||
rocket::build()
|
rocket::build()
|
||||||
|
.attach(MetricProducer)
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
.register(
|
.register(
|
||||||
"/",
|
"/",
|
||||||
@ -85,12 +92,13 @@ pub async fn initialize(
|
|||||||
.mount(
|
.mount(
|
||||||
"/",
|
"/",
|
||||||
routes![
|
routes![
|
||||||
routes::index,
|
|
||||||
routes::cookies,
|
routes::cookies,
|
||||||
|
routes::index,
|
||||||
|
routes::metrics::metrics,
|
||||||
routes::privacy,
|
routes::privacy,
|
||||||
routes::terms,
|
|
||||||
routes::return_to_same_site,
|
|
||||||
routes::report::report_error,
|
routes::report::report_error,
|
||||||
|
routes::return_to_same_site,
|
||||||
|
routes::terms,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
|
43
web/src/metrics.rs
Normal file
43
web/src/metrics.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
use prometheus::{IntCounterVec, Opts, Registry};
|
||||||
|
use rocket::{
|
||||||
|
fairing::{Fairing, Info, Kind},
|
||||||
|
Data, Request, Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref REGISTRY: Registry = Registry::new();
|
||||||
|
static ref REQUEST_COUNTER: IntCounterVec =
|
||||||
|
IntCounterVec::new(Opts::new("requests", "Requests"), &["method", "route"]).unwrap();
|
||||||
|
static ref RESPONSE_COUNTER: IntCounterVec =
|
||||||
|
IntCounterVec::new(Opts::new("responses", "Responses"), &["status", "route"]).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_metrics() {
|
||||||
|
REGISTRY.register(Box::new(REQUEST_COUNTER.clone())).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MetricProducer;
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl Fairing for MetricProducer {
|
||||||
|
fn info(&self) -> Info {
|
||||||
|
Info { name: "Metrics fairing", kind: Kind::Request }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) {
|
||||||
|
if let Some(route) = req.route() {
|
||||||
|
REQUEST_COUNTER
|
||||||
|
.with_label_values(&[req.method().as_str(), &route.uri.to_string()])
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_response<'r>(&self, req: &'r Request<'_>, resp: &mut Response<'r>) {
|
||||||
|
if let Some(route) = req.route() {
|
||||||
|
RESPONSE_COUNTER
|
||||||
|
.with_label_values(&[&resp.status().code.to_string(), &route.uri.to_string()])
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -1,10 +1,14 @@
|
|||||||
use std::collections::HashMap;
|
use std::path::Path;
|
||||||
|
|
||||||
use chrono::{naive::NaiveDateTime, Utc};
|
use chrono::{naive::NaiveDateTime, Utc};
|
||||||
use rand::{rngs::OsRng, seq::IteratorRandom};
|
use rand::{rngs::OsRng, seq::IteratorRandom};
|
||||||
use rocket::{http::CookieJar, response::Redirect, serde::json::json};
|
use rocket::{
|
||||||
use rocket_dyn_templates::Template;
|
fs::{relative, NamedFile},
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
http::CookieJar,
|
||||||
|
response::Redirect,
|
||||||
|
serde::json::json,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serenity::{
|
use serenity::{
|
||||||
client::Context,
|
client::Context,
|
||||||
http::Http,
|
http::Http,
|
||||||
@ -27,7 +31,6 @@ use crate::{
|
|||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod guild;
|
|
||||||
|
|
||||||
type Unset<T> = Option<T>;
|
type Unset<T> = Option<T>;
|
||||||
|
|
||||||
@ -51,27 +54,12 @@ fn interval_default() -> Unset<Option<u32>> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::Type)]
|
fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
||||||
#[sqlx(transparent)]
|
where
|
||||||
struct Attachment(Vec<u8>);
|
D: Deserializer<'de>,
|
||||||
|
T: Deserialize<'de>,
|
||||||
impl<'de> Deserialize<'de> for Attachment {
|
{
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Attachment, D::Error>
|
Ok(Some(Option::deserialize(deserializer)?))
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let string = String::deserialize(deserializer)?;
|
|
||||||
Ok(Attachment(base64::decode(string).map_err(de::Error::custom)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Attachment {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.collect_str(&base64::encode(&self.0))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@ -82,7 +70,7 @@ pub struct ReminderTemplate {
|
|||||||
guild_id: u32,
|
guild_id: u32,
|
||||||
#[serde(default = "template_name_default")]
|
#[serde(default = "template_name_default")]
|
||||||
name: String,
|
name: String,
|
||||||
attachment: Option<Attachment>,
|
attachment: Option<Vec<u8>>,
|
||||||
attachment_name: Option<String>,
|
attachment_name: Option<String>,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
content: String,
|
content: String,
|
||||||
@ -107,7 +95,7 @@ pub struct ReminderTemplate {
|
|||||||
pub struct ReminderTemplateCsv {
|
pub struct ReminderTemplateCsv {
|
||||||
#[serde(default = "template_name_default")]
|
#[serde(default = "template_name_default")]
|
||||||
name: String,
|
name: String,
|
||||||
attachment: Option<Attachment>,
|
attachment: Option<Vec<u8>>,
|
||||||
attachment_name: Option<String>,
|
attachment_name: Option<String>,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
content: String,
|
content: String,
|
||||||
@ -142,7 +130,8 @@ pub struct EmbedField {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Reminder {
|
pub struct Reminder {
|
||||||
attachment: Option<Attachment>,
|
#[serde(with = "base64s")]
|
||||||
|
attachment: Option<Vec<u8>>,
|
||||||
attachment_name: Option<String>,
|
attachment_name: Option<String>,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
#[serde(with = "string")]
|
#[serde(with = "string")]
|
||||||
@ -175,7 +164,8 @@ pub struct Reminder {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ReminderCsv {
|
pub struct ReminderCsv {
|
||||||
attachment: Option<Attachment>,
|
#[serde(with = "base64s")]
|
||||||
|
attachment: Option<Vec<u8>>,
|
||||||
attachment_name: Option<String>,
|
attachment_name: Option<String>,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
channel: String,
|
channel: String,
|
||||||
@ -208,7 +198,7 @@ pub struct PatchReminder {
|
|||||||
uid: String,
|
uid: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||||
attachment: Unset<Option<Attachment>>,
|
attachment: Unset<Option<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(deserialize_with = "deserialize_optional_field")]
|
#[serde(deserialize_with = "deserialize_optional_field")]
|
||||||
attachment_name: Unset<Option<String>>,
|
attachment_name: Unset<Option<String>>,
|
||||||
@ -304,14 +294,6 @@ pub fn generate_uid() -> String {
|
|||||||
.join("")
|
.join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
T: Deserialize<'de>,
|
|
||||||
{
|
|
||||||
Ok(Some(Option::deserialize(deserializer)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/serde-rs/json/issues/329#issuecomment-305608405
|
// https://github.com/serde-rs/json/issues/329#issuecomment-305608405
|
||||||
mod string {
|
mod string {
|
||||||
use std::{fmt::Display, str::FromStr};
|
use std::{fmt::Display, str::FromStr};
|
||||||
@ -336,6 +318,29 @@ mod string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod base64s {
|
||||||
|
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
if let Some(opt) = value {
|
||||||
|
serializer.collect_str(&base64::encode(opt))
|
||||||
|
} else {
|
||||||
|
serializer.serialize_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let string = Option::<String>::deserialize(deserializer)?;
|
||||||
|
Some(string.map(|b| base64::decode(b).map_err(de::Error::custom))).flatten().transpose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct DeleteReminder {
|
pub struct DeleteReminder {
|
||||||
uid: String,
|
uid: String,
|
||||||
@ -655,22 +660,26 @@ async fn create_database_channel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> {
|
||||||
if cookies.get_private("userid").is_some() {
|
if cookies.get_private("userid").is_some() {
|
||||||
let mut map = HashMap::new();
|
NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| {
|
||||||
map.insert("version", env!("CARGO_PKG_VERSION"));
|
warn!("Couldn't render dashboard: {:?}", e);
|
||||||
Ok(Template::render("dashboard", &map))
|
|
||||||
|
Redirect::to("/login/discord")
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Redirect::to("/login/discord"))
|
Err(Redirect::to("/login/discord"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<_..>")]
|
#[get("/<_..>")]
|
||||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> {
|
||||||
if cookies.get_private("userid").is_some() {
|
if cookies.get_private("userid").is_some() {
|
||||||
let mut map = HashMap::new();
|
NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| {
|
||||||
map.insert("version", env!("CARGO_PKG_VERSION"));
|
warn!("Couldn't render dashboard: {:?}", e);
|
||||||
Ok(Template::render("dashboard", &map))
|
|
||||||
|
Redirect::to("/login/discord")
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Redirect::to("/login/discord"))
|
Err(Redirect::to("/login/discord"))
|
||||||
}
|
}
|
||||||
|
@ -31,20 +31,22 @@ pub async fn discord_login(
|
|||||||
|
|
||||||
// store the pkce secret to verify the authorization later
|
// store the pkce secret to verify the authorization later
|
||||||
cookies.add_private(
|
cookies.add_private(
|
||||||
Cookie::build(("verify", pkce_verifier.secret().to_string()))
|
Cookie::build("verify", pkce_verifier.secret().to_string())
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.path("/login")
|
.path("/login")
|
||||||
.same_site(SameSite::Lax)
|
.same_site(SameSite::Lax)
|
||||||
.expires(Expiration::Session),
|
.expires(Expiration::Session)
|
||||||
|
.finish(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// store the csrf token to verify no interference
|
// store the csrf token to verify no interference
|
||||||
cookies.add_private(
|
cookies.add_private(
|
||||||
Cookie::build(("csrf", csrf_token.secret().to_string()))
|
Cookie::build("csrf", csrf_token.secret().to_string())
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.path("/login")
|
.path("/login")
|
||||||
.same_site(SameSite::Lax)
|
.same_site(SameSite::Lax)
|
||||||
.expires(Expiration::Session),
|
.expires(Expiration::Session)
|
||||||
|
.finish(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Redirect::to(auth_url.to_string())
|
Redirect::to(auth_url.to_string())
|
||||||
@ -52,9 +54,9 @@ pub async fn discord_login(
|
|||||||
|
|
||||||
#[get("/discord/logout")]
|
#[get("/discord/logout")]
|
||||||
pub async fn discord_logout(cookies: &CookieJar<'_>) -> Redirect {
|
pub async fn discord_logout(cookies: &CookieJar<'_>) -> Redirect {
|
||||||
cookies.remove_private(Cookie::from("username"));
|
cookies.remove_private(Cookie::named("username"));
|
||||||
cookies.remove_private(Cookie::from("userid"));
|
cookies.remove_private(Cookie::named("userid"));
|
||||||
cookies.remove_private(Cookie::from("access_token"));
|
cookies.remove_private(Cookie::named("access_token"));
|
||||||
|
|
||||||
Redirect::to(uri!(routes::index))
|
Redirect::to(uri!(routes::index))
|
||||||
}
|
}
|
||||||
@ -78,16 +80,17 @@ pub async fn discord_callback(
|
|||||||
.request_async(async_http_client)
|
.request_async(async_http_client)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
cookies.remove_private(Cookie::from("verify"));
|
cookies.remove_private(Cookie::named("verify"));
|
||||||
cookies.remove_private(Cookie::from("csrf"));
|
cookies.remove_private(Cookie::named("csrf"));
|
||||||
|
|
||||||
match token_result {
|
match token_result {
|
||||||
Ok(token) => {
|
Ok(token) => {
|
||||||
cookies.add_private(
|
cookies.add_private(
|
||||||
Cookie::build(("access_token", token.access_token().secret().to_string()))
|
Cookie::build("access_token", token.access_token().secret().to_string())
|
||||||
.secure(true)
|
.secure(true)
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.path("/dashboard"),
|
.path("/dashboard")
|
||||||
|
.finish(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let request_res = reqwest_client
|
let request_res = reqwest_client
|
||||||
|
18
web/src/routes/metrics.rs
Normal file
18
web/src/routes/metrics.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use prometheus;
|
||||||
|
|
||||||
|
use crate::metrics::REGISTRY;
|
||||||
|
|
||||||
|
#[get("/metrics")]
|
||||||
|
pub async fn metrics() -> String {
|
||||||
|
let encoder = prometheus::TextEncoder::new();
|
||||||
|
let res_custom = encoder.encode_to_string(®ISTRY.gather());
|
||||||
|
|
||||||
|
match res_custom {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error encoding metrics: {:?}", e);
|
||||||
|
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
pub mod metrics;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -218,17 +218,16 @@ div.inset-content {
|
|||||||
margin-right: 10%;
|
margin-right: 10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.flash-message {
|
div.flash-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.flash-message {
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
margin: 16px !important;
|
margin: 16px !important;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
bottom: 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.flash-message.is-active {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@ -633,6 +632,10 @@ li.highlight {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-row-edit > button {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.button-row .button-row-reminder {
|
.button-row .button-row-reminder {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
Loading…
Reference in New Issue
Block a user