first commit
This commit is contained in:
parent
c1a55f72c5
commit
a8170c19ce
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
.env
|
2784
Cargo.lock
generated
Normal file
2784
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "services"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-files = "0.6.6"
|
||||||
|
actix-governor = "0.5.0"
|
||||||
|
actix-session = { version = "0.9.0", features = ["cookie-session"] }
|
||||||
|
actix-web = "4.8.0"
|
||||||
|
awc = { version = "3.5.0", features = ["openssl"] }
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
dotenv_config = "0.1.9"
|
||||||
|
governor = "0.6.3"
|
||||||
|
handlebars = "5.1.2"
|
||||||
|
json = "0.12.4"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
serde = { version = "1.0.204", features = ["derive"] }
|
||||||
|
serde_json = "1.0.120"
|
||||||
|
tokio = "1.38.1"
|
||||||
|
tokio-xmpp = "3.5.0"
|
11
README.md
11
README.md
@ -1,3 +1,14 @@
|
|||||||
# server-status
|
# server-status
|
||||||
|
|
||||||
Check the status of the services you offer on your server.
|
Check the status of the services you offer on your server.
|
||||||
|
|
||||||
|
* Create `.env` file containing the following variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=3000
|
||||||
|
XMPP_USER=user
|
||||||
|
XMPP_PASS=password
|
||||||
|
```
|
||||||
|
* Setup configuration in `config.json`
|
||||||
|
* run code with `cargo run`
|
||||||
|
|
||||||
|
9
src/config.json
Normal file
9
src/config.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"web": [
|
||||||
|
"lainoa.eus",
|
||||||
|
"apunteak.lainoa.eus",
|
||||||
|
"git.lainoa.eus",
|
||||||
|
"nextcloud.lainoa.eus"
|
||||||
|
],
|
||||||
|
"xmpp": "lainoa.eus"
|
||||||
|
}
|
44
src/config.rs
Normal file
44
src/config.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::{Read, Error as IoError};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub web: Vec<String>,
|
||||||
|
pub xmpp: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new() -> Result<Self, IoError> {
|
||||||
|
|
||||||
|
//let mut file = File::open("./src/config.json").unwrap().try_into().ok()?;
|
||||||
|
|
||||||
|
let mut file = match File::open(CONFIG_FILE_PATH) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(error) => {
|
||||||
|
match error.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => {
|
||||||
|
println!("Config file not found");
|
||||||
|
return Ok(Self{ //default configuration
|
||||||
|
web: ["nextcloud.com".to_string()].to_vec(),
|
||||||
|
xmpp: "conversations.im".to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buff = String::new();
|
||||||
|
file.read_to_string(&mut buff).unwrap();
|
||||||
|
//println!("{}", &buff);
|
||||||
|
let foo: Config = serde_json::from_str(&buff).unwrap();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
web: foo.web,
|
||||||
|
xmpp: foo.xmpp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG_FILE_PATH: &str = "./src/config.json";
|
246
src/main.rs
Normal file
246
src/main.rs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
mod services;
|
||||||
|
use crate::services::Service;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
use actix_files::Files;
|
||||||
|
use actix_governor::{Governor, GovernorConfigBuilder, KeyExtractor, SimpleKeyExtractionError};
|
||||||
|
use actix_web::{get, web,
|
||||||
|
http::{
|
||||||
|
header::ContentType,
|
||||||
|
StatusCode,
|
||||||
|
// Method, StatusCode,
|
||||||
|
},
|
||||||
|
//cookie::{ Key, SameSite, Cookie },
|
||||||
|
App, /*HttpRequest,*/ HttpServer, HttpResponse, Responder, HttpResponseBuilder
|
||||||
|
};
|
||||||
|
use actix_web::dev::ServiceRequest;
|
||||||
|
use governor::{NotUntil, clock::{Clock, QuantaInstant, DefaultClock}};
|
||||||
|
use awc::Client;
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use handlebars::Handlebars;
|
||||||
|
use handlebars::handlebars_helper;
|
||||||
|
use tokio_xmpp::SimpleClient as XmppClient;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CONFIG: config::Config =
|
||||||
|
Config::new().expect("to load config file");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index(/*req: HttpRequest, */ hb: web::Data<Handlebars<'_>>) -> impl Responder {
|
||||||
|
|
||||||
|
/*
|
||||||
|
if let Some(val) = req.peer_addr() {
|
||||||
|
println!("Address {:?}", val.ip());
|
||||||
|
};
|
||||||
|
println!("ip: {:?}", req.connection_info().realip_remote_addr());
|
||||||
|
*/
|
||||||
|
let mut list:Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
// add web services
|
||||||
|
let web_services = &CONFIG.web;
|
||||||
|
for web_service in web_services.into_iter() {
|
||||||
|
let mut service = "web:".to_string();
|
||||||
|
service.push_str(web_service);
|
||||||
|
list.push(service);
|
||||||
|
}
|
||||||
|
//add xmpp service
|
||||||
|
let xmpp_service = &CONFIG.xmpp;
|
||||||
|
let mut service = "xmpp:".to_string();
|
||||||
|
service.push_str(xmpp_service);
|
||||||
|
list.push(service);
|
||||||
|
|
||||||
|
// check services
|
||||||
|
let res = check(list.to_vec()).await;
|
||||||
|
|
||||||
|
let body = hb.render("index", &res).unwrap();
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("text/html")
|
||||||
|
.body(body)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check(list:Vec<String>) -> Map<String, Value>{
|
||||||
|
let mut obj = Map::new();
|
||||||
|
let mut vmap = Vec::new();
|
||||||
|
let mut map = Map::new();
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
//#[derive(Debug, strum_macros::Display)]
|
||||||
|
enum Class {
|
||||||
|
Web,
|
||||||
|
Mail,
|
||||||
|
Xmpp,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
impl Class {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Class::Web => "Web",
|
||||||
|
Class::Mail => "Mail",
|
||||||
|
Class::Xmpp => "Xmpp",
|
||||||
|
Class::Unknown => "Unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//https://stackoverflow.com/questions/65040158/can-i-create-string-enum
|
||||||
|
for service in list.into_iter(){
|
||||||
|
let class = match &service {
|
||||||
|
s if s.starts_with("web") => Class::Web,
|
||||||
|
s if s.starts_with("xmpp") => Class::Xmpp,
|
||||||
|
s if s.starts_with("mail") => Class::Mail,
|
||||||
|
_ => Class::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
match class {
|
||||||
|
Class::Web => {
|
||||||
|
let n: Vec<&str> = service.split(":").collect();
|
||||||
|
let name = n[1];
|
||||||
|
|
||||||
|
let mut url = "https://".to_string();
|
||||||
|
url.push_str(&name);
|
||||||
|
|
||||||
|
//println!("service type: {:?}", class.as_str().to_string());
|
||||||
|
let web_service = Service::new(name.to_string(), ws(url.to_string()).await, class.as_str().to_string());
|
||||||
|
|
||||||
|
obj.insert("name".to_string(), Value::String(web_service.name));
|
||||||
|
obj.insert("status".to_string(), Value::String(web_service.status));
|
||||||
|
obj.insert("class".to_string(), Value::String(web_service.class));
|
||||||
|
vmap.push(obj.clone());
|
||||||
|
},
|
||||||
|
Class::Xmpp => {
|
||||||
|
let n: Vec<&str> = service.split(":").collect();
|
||||||
|
let name = n[1];
|
||||||
|
|
||||||
|
let xmpp_service = Service::new(name.to_string(), xs(name.to_string()).await, class.as_str().to_string());
|
||||||
|
|
||||||
|
obj.insert("name".to_string(), Value::String(xmpp_service.name));
|
||||||
|
obj.insert("status".to_string(), Value::String(xmpp_service.status));
|
||||||
|
obj.insert("class".to_string(), Value::String(xmpp_service.class));
|
||||||
|
vmap.push(obj.clone());
|
||||||
|
},
|
||||||
|
Class::Mail => println!("mail service"),
|
||||||
|
Class::Unknown => println!("Unknown service")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
map.insert("data".to_string(), serde_json::json!(vmap).into());
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
//xmpp server check
|
||||||
|
async fn xs(server: String) -> StatusCode {
|
||||||
|
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
let xmpp_user = std::env::var("XMPP_USER").unwrap_or("user".to_string());
|
||||||
|
|
||||||
|
let jid = format!("{}@{}",xmpp_user,server);
|
||||||
|
let password = std::env::var("XMPP_PASS").unwrap_or("secret".to_string());
|
||||||
|
|
||||||
|
// Xmpp Client instance
|
||||||
|
let client = XmppClient::new(&jid, password.to_owned()).await;
|
||||||
|
|
||||||
|
match client {
|
||||||
|
Ok(_) => {
|
||||||
|
//println!("Client connected!");
|
||||||
|
client.expect("REASON").end().await.unwrap();
|
||||||
|
StatusCode::OK
|
||||||
|
},
|
||||||
|
Err(..) => {
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// web service check
|
||||||
|
async fn ws(url: String) -> StatusCode {
|
||||||
|
let client = Client::default();
|
||||||
|
let res = client
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
//res.unwrap().status()
|
||||||
|
match res {
|
||||||
|
Ok(status) => status.status(),
|
||||||
|
Err(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
let port = std::env::var("PORT").unwrap_or("8080".to_string());
|
||||||
|
let address = format!("127.0.0.1:{}", port);
|
||||||
|
|
||||||
|
//println!("web: {:?}", CONFIG.web);
|
||||||
|
//println!("web: {:?}", CONFIG.web[0]);
|
||||||
|
|
||||||
|
|
||||||
|
// Allow bursts with up to five requests per IP address
|
||||||
|
// and replenishes one element every two seconds
|
||||||
|
let governor_conf = GovernorConfigBuilder::default()
|
||||||
|
.per_second(4)
|
||||||
|
.burst_size(2)
|
||||||
|
.finish()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
handlebars_helper!(compare: |a: String, b: String | a == b);
|
||||||
|
let mut handlebars = Handlebars::new();
|
||||||
|
handlebars.register_helper("compare", Box::new(compare));
|
||||||
|
handlebars
|
||||||
|
.register_template_file("index", "./static/index.html")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let handlebars_ref = web::Data::new(handlebars);
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
// Enable Governor middleware
|
||||||
|
.wrap(Governor::new(&governor_conf))
|
||||||
|
// Route hello world service
|
||||||
|
//.route("/", web::get().to(index))
|
||||||
|
.app_data(handlebars_ref.clone())
|
||||||
|
.service(Files::new("/static", "static").show_files_listing())
|
||||||
|
.service(index)
|
||||||
|
})
|
||||||
|
.bind(&address)?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
//#[warn(dead_code)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
//#[allow(unused_variables)]
|
||||||
|
struct Foo;
|
||||||
|
|
||||||
|
// will return 500 error and 'Extract error' as content
|
||||||
|
impl KeyExtractor for Foo {
|
||||||
|
type Key = ();
|
||||||
|
type KeyExtractionError = SimpleKeyExtractionError<&'static str>;
|
||||||
|
|
||||||
|
fn extract(&self, _req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
|
||||||
|
Err(SimpleKeyExtractionError::new("Extract error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exceed_rate_limit_response(
|
||||||
|
&self,
|
||||||
|
negative: &NotUntil<QuantaInstant>,
|
||||||
|
mut response: HttpResponseBuilder,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let wait_time = negative
|
||||||
|
.wait_time_from(DefaultClock::default().now())
|
||||||
|
.as_secs();
|
||||||
|
response
|
||||||
|
.content_type(ContentType::plaintext())
|
||||||
|
.body(format!("Too many requests, retry in {}s", wait_time))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
27
src/services.rs
Normal file
27
src/services.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use awc::http::StatusCode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Service {
|
||||||
|
pub name: String,
|
||||||
|
pub status: String,
|
||||||
|
pub class: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service {
|
||||||
|
pub fn new(name: String, code: StatusCode, class: String) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
status: code.to_string(),
|
||||||
|
class,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub web: Vec<String>,
|
||||||
|
pub xmpp: String,
|
||||||
|
pub email: String
|
||||||
|
}
|
||||||
|
*/
|
48
static/index.html
Normal file
48
static/index.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Zerbitzuak</title>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<!--<link rel="shortcut icon" type="image/x-icon" href="/favicon">-->
|
||||||
|
<link rel="stylesheet" href="./static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Zerbitzuen egoera</h1>
|
||||||
|
<h4 class="logs">Web zerbitzuak</h4>
|
||||||
|
{{#each data}}
|
||||||
|
{{#if (compare this.class "Web")}}
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="grid-item">
|
||||||
|
{{this.name}}
|
||||||
|
</div>
|
||||||
|
<div class="grid-item">
|
||||||
|
{{#if (compare this.status "200 OK")}}
|
||||||
|
<div class="ok"></div>
|
||||||
|
{{else}}
|
||||||
|
<div class="down"></div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
<h4 class="logs">Xmpp mezularitza</h4>
|
||||||
|
{{#each data}}
|
||||||
|
{{#if (compare this.class "Xmpp")}}
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="grid-item">
|
||||||
|
{{this.name}}
|
||||||
|
</div>
|
||||||
|
<div class="grid-item">
|
||||||
|
{{#if (compare this.status "200 OK")}}
|
||||||
|
<div class="ok"></div>
|
||||||
|
{{else}}
|
||||||
|
<div class="down"></div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
334
static/styles.css
Normal file
334
static/styles.css
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin-bottom: 2.5em;
|
||||||
|
margin-top: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 span{
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link{
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
color: #1F1F1F;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
.user p{
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[name="fullNameForm"] input{
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
form, .logs {
|
||||||
|
/* border-radius: 0.2rem;
|
||||||
|
border: 1px solid #CCC;*/
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 16rem;
|
||||||
|
padding: 2rem 2.5rem 1.5rem 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: #FAFAFA;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
box-shadow: inset 0 1px 3px #DDD;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-buttons{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-buttons > a, .form-buttons > button{
|
||||||
|
flex-grow:1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
background-color: #FFF;
|
||||||
|
border-color: #51A7E8;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075) inset, 0 0 5px rgba(81, 167, 232, 0.5);
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.not-required {
|
||||||
|
background-color: #E0E0E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: #666;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 1em 0 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
border: 1px solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 2em 0 0.5em 0;
|
||||||
|
padding: 0.5em 0.7em;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
user-select: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.green {
|
||||||
|
background-color: #60B044;
|
||||||
|
background-image: linear-gradient(#8ADD6D, #60B044);
|
||||||
|
border-color: #5CA941;
|
||||||
|
}
|
||||||
|
button.green:focus,
|
||||||
|
button.green:hover {
|
||||||
|
background-color: #569E3D;
|
||||||
|
background-image: linear-gradient(#79D858, #569E3D);
|
||||||
|
border-color: #4A993E;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.red {
|
||||||
|
background-color: #E74C3C;
|
||||||
|
background-image: linear-gradient(#CF0B04, #E74C3C);
|
||||||
|
border-color: #C0392B;
|
||||||
|
}
|
||||||
|
button.red:focus,
|
||||||
|
button.red:hover{
|
||||||
|
background-color: #D13E2E;
|
||||||
|
background-image: linear-gradient(#CF0000, #D13E2E);
|
||||||
|
border-color: #C92F1E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-login-info {
|
||||||
|
background-color: #efefef;
|
||||||
|
/*background-image: linear-gradient(#fcfcfc, #f5f5f5);*/
|
||||||
|
background-image: linear-gradient(#f9e028, #ffd966);
|
||||||
|
padding-top: 1px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.date {
|
||||||
|
font-weight: 100;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.device-list li{
|
||||||
|
background-color: #efefef;
|
||||||
|
background-image: linear-gradient(#fcfcfc, #f6f6f6);
|
||||||
|
list-style: none;
|
||||||
|
font-size: 0.7em;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*alerts*/
|
||||||
|
.alerts {
|
||||||
|
margin: 2rem auto 0 auto;
|
||||||
|
max-width: 30rem;
|
||||||
|
animation-duration: 5s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
-webkit-animation-duration: 5s;
|
||||||
|
-webkit-animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
border: 1px solid;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.7em 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.error {
|
||||||
|
background-color: #E74C3C;
|
||||||
|
border-color: #C0392B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.success {
|
||||||
|
background-color: #60B044;
|
||||||
|
border-color: #5CA941;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fadeOut {
|
||||||
|
0% {opacity: 1;}
|
||||||
|
70% {opacity: 1;}
|
||||||
|
100% {opacity: 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% {opacity: 1;}
|
||||||
|
60% {opacity: 1;}
|
||||||
|
100% {opacity: 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeOut {
|
||||||
|
-webkit-animation-name: fadeOut;
|
||||||
|
animation-name: fadeOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**/
|
||||||
|
.qr-code {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: max-content;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 24rem;
|
||||||
|
padding: 0 2.5rem 0 2.5rem;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**/
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto max-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 16rem;
|
||||||
|
padding: 0 2.5rem 0 2.5rem;
|
||||||
|
align-items: first baseline;
|
||||||
|
}
|
||||||
|
.grid-item {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.grid-item a {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
.grid-item a:visited {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.grid-item h5 {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item input{
|
||||||
|
display: unset;
|
||||||
|
max-width: max-content;
|
||||||
|
}
|
||||||
|
/**/
|
||||||
|
.ok,.down {
|
||||||
|
min-width: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
.ok {
|
||||||
|
background-color: lawngreen;
|
||||||
|
}
|
||||||
|
.down {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
/**/
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
|
||||||
|
form {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
max-width: 16rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 481px) and (max-width: 760px) {
|
||||||
|
|
||||||
|
.info {
|
||||||
|
max-width: 22rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* https://remixicon.com
|
||||||
|
* https://github.com/Remix-Design/RemixIcon
|
||||||
|
* Copyright RemixIcon.com
|
||||||
|
* Released under the Apache License Version 2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "remixicon";
|
||||||
|
src: url('fonts/remixicon.eot?t=1700036445706'); /* IE9*/
|
||||||
|
src: url('fonts/remixicon.eot?t=1700036445706#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||||
|
url("fonts/remixicon.woff2?t=1700036445706") format("woff2"),
|
||||||
|
url("fonts/remixicon.woff?t=1700036445706") format("woff"),
|
||||||
|
url('fonts/remixicon.ttf?t=1700036445706') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||||
|
url('fonts/remixicon.svg?t=1700036445706#remixicon') format('svg'); /* iOS 4.1- */
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class^="ri-"], [class*="ri-"] {
|
||||||
|
font-family: 'remixicon' !important;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ri-lg { font-size: 1.3333em; line-height: 0.75em; vertical-align: -.0667em; }
|
||||||
|
.ri-xl { font-size: 1.5em; line-height: 0.6666em; vertical-align: -.075em; }
|
||||||
|
.ri-xxs { font-size: .5em; }
|
||||||
|
.ri-xs { font-size: .75em; }
|
||||||
|
.ri-sm { font-size: .875em }
|
||||||
|
.ri-1x { font-size: 1em; }
|
||||||
|
.ri-2x { font-size: 2em; }
|
||||||
|
.ri-3x { font-size: 3em; }
|
||||||
|
.ri-4x { font-size: 4em; }
|
||||||
|
.ri-5x { font-size: 5em; }
|
||||||
|
.ri-6x { font-size: 6em; }
|
||||||
|
.ri-7x { font-size: 7em; }
|
||||||
|
.ri-8x { font-size: 8em; }
|
||||||
|
.ri-9x { font-size: 9em; }
|
||||||
|
.ri-10x { font-size: 10em; }
|
||||||
|
.ri-fw { text-align: center; width: 1.25em; }
|
||||||
|
|
||||||
|
.ri-home-line:before { content: "\ee2b"; }
|
||||||
|
.ri-home-fill:before { content: "\ee26"; }
|
||||||
|
.ri-mail-line:before { content: "\eef6"; }
|
||||||
|
.ri-mail-fill:before { content: "\eef3"; }
|
||||||
|
.ri-send-plane-line:before { content: "\f0da"; }
|
||||||
|
.ri-send-plane-fill:before { content: "\f0d9"; }
|
||||||
|
.ri-chat-3-line:before { content: "\eb51"; }
|
||||||
|
.ri-chat-3-fill:before { content: "\eb50"; }
|
||||||
|
.ri-pencil-line:before { content: "\efe0"; }
|
||||||
|
.ri-pencil-fill:before { content: "\efdf"; }
|
||||||
|
.ri-file-line:before { content: "\eceb"; }
|
||||||
|
.ri-file-fill:before { content: "\ece0"; }
|
||||||
|
.ri-settings-3-line:before { content: "\f0e6"; }
|
||||||
|
.ri-settings-3-fill:before { content: "\f0e5"; }
|
||||||
|
.ri-user-line:before { content: "\f264"; }
|
||||||
|
.ri-user-fill:before { content: "\f25f"; }
|
||||||
|
.ri-account-circle-line:before { content: "\ea09"; }
|
||||||
|
.ri-account-circle-fill:before { content: "\ea08"; }
|
||||||
|
.ri-delete-bin-line:before { content: "\ec2a"; }
|
||||||
|
.ri-delete-bin-fill:before { content: "\ec29"; }
|
||||||
|
.ri-toggle-line:before { content: "\f219"; }
|
||||||
|
.ri-toggle-fill:before { content: "\f218"; }
|
||||||
|
.ri-history-line:before { content: "\ee17"; }
|
||||||
|
.ri-history-fill:before { content: "\ee16"; }}
|
Loading…
Reference in New Issue
Block a user