From 70f4a195bcad7724ec189ecfc1454c20fa20a342 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 28 Dec 2024 14:05:03 -0800 Subject: [PATCH] Use materialized view for summaries --- ...98451dda0b82e5f44906f4bb6b26ac3bab81.json} | 62 +++-- ...c744ee666517800ad9b9ce35570abdb993dec.json | 12 + web/migrations/20241228203720_fix-indexes.sql | 7 + .../20241228210331_summary-mview.sql | 49 ++++ ...mates.rs => refresh_materialized_views.rs} | 9 +- web/src/crons/refresh_travel_states.rs | 10 +- web/src/db.rs | 15 +- web/src/main.rs | 2 +- web/src/models.rs | 74 +++++- web/src/routes/api/base.rs | 99 ++------ web/static/assets/main.js | 228 +++++++++++++----- web/static/assets/style.css | 147 ++++++++--- web/static/index.html | 35 ++- 13 files changed, 539 insertions(+), 210 deletions(-) rename web/.sqlx/{query-63b607113f772c95d6c93345d10a69e494d99591613e6d0ffb4fc29080dc1af2.json => query-86129b0c5c0e6989a2b9b67a6fc498451dda0b82e5f44906f4bb6b26ac3bab81.json} (54%) create mode 100644 web/.sqlx/query-8f3ff1b848842b6c90ab9c4487bc744ee666517800ad9b9ce35570abdb993dec.json create mode 100644 web/migrations/20241228203720_fix-indexes.sql create mode 100644 web/migrations/20241228210331_summary-mview.sql rename web/src/crons/{refresh_queue_estimates.rs => refresh_materialized_views.rs} (68%) diff --git a/web/.sqlx/query-63b607113f772c95d6c93345d10a69e494d99591613e6d0ffb4fc29080dc1af2.json b/web/.sqlx/query-86129b0c5c0e6989a2b9b67a6fc498451dda0b82e5f44906f4bb6b26ac3bab81.json similarity index 54% rename from web/.sqlx/query-63b607113f772c95d6c93345d10a69e494d99591613e6d0ffb4fc29080dc1af2.json rename to web/.sqlx/query-86129b0c5c0e6989a2b9b67a6fc498451dda0b82e5f44906f4bb6b26ac3bab81.json index 7e48a42..396e308 100644 --- a/web/.sqlx/query-63b607113f772c95d6c93345d10a69e494d99591613e6d0ffb4fc29080dc1af2.json +++ b/web/.sqlx/query-86129b0c5c0e6989a2b9b67a6fc498451dda0b82e5f44906f4bb6b26ac3bab81.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM worlds WHERE hidden = FALSE", + "query": "SELECT * FROM world_summary", "describe": { "columns": [ { @@ -35,34 +35,64 @@ }, { "ordinal": 6, - "name": "is_cloud", - "type_info": "Bool" + "name": "region_name", + "type_info": "Varchar" }, { "ordinal": 7, - "name": "region_name", - "type_info": "Varchar" + "name": "status", + "type_info": "Int2" }, { "ordinal": 8, - "name": "hidden", + "name": "category", + "type_info": "Int2" + }, + { + "ordinal": 9, + "name": "can_create", "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "prohibit", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "time", + "type_info": "Timestamp" + }, + { + "ordinal": 12, + "name": "size", + "type_info": "Int4" + }, + { + "ordinal": 13, + "name": "duration", + "type_info": "Float8" } ], "parameters": { "Left": [] }, "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true ] }, - "hash": "63b607113f772c95d6c93345d10a69e494d99591613e6d0ffb4fc29080dc1af2" + "hash": "86129b0c5c0e6989a2b9b67a6fc498451dda0b82e5f44906f4bb6b26ac3bab81" } diff --git a/web/.sqlx/query-8f3ff1b848842b6c90ab9c4487bc744ee666517800ad9b9ce35570abdb993dec.json b/web/.sqlx/query-8f3ff1b848842b6c90ab9c4487bc744ee666517800ad9b9ce35570abdb993dec.json new file mode 100644 index 0000000..99ae433 --- /dev/null +++ b/web/.sqlx/query-8f3ff1b848842b6c90ab9c4487bc744ee666517800ad9b9ce35570abdb993dec.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "REFRESH MATERIALIZED VIEW CONCURRENTLY world_summary", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "8f3ff1b848842b6c90ab9c4487bc744ee666517800ad9b9ce35570abdb993dec" +} diff --git a/web/migrations/20241228203720_fix-indexes.sql b/web/migrations/20241228203720_fix-indexes.sql new file mode 100644 index 0000000..cbc5a99 --- /dev/null +++ b/web/migrations/20241228203720_fix-indexes.sql @@ -0,0 +1,7 @@ +DROP INDEX travel_states_time_idx; +ALTER TABLE travel_states DROP CONSTRAINT travel_states_pkey; +CREATE UNIQUE INDEX travel_states_pkey ON travel_states(world_id, time DESC); + +DROP INDEX world_statuses_time_idx; +ALTER TABLE world_statuses DROP CONSTRAINT world_statuses_pkey; +CREATE UNIQUE INDEX world_statuses_pkey ON world_statuses(world_id, time DESC); \ No newline at end of file diff --git a/web/migrations/20241228210331_summary-mview.sql b/web/migrations/20241228210331_summary-mview.sql new file mode 100644 index 0000000..9e18cf9 --- /dev/null +++ b/web/migrations/20241228210331_summary-mview.sql @@ -0,0 +1,49 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS world_summary AS + SELECT + w.world_id, + w.world_name, + w.datacenter_id, + w.datacenter_name, + w.region_id, + w.region_abbreviation, + w.region_name, + ws.status, + ws.category, + ws.can_create, + ts.prohibit, + qe.time, + qe.size, + qe.duration + FROM + worlds w + INNER JOIN ( + SELECT + DISTINCT ON (world_id) world_id, + prohibit + FROM + travel_states + ORDER BY + world_id, + time DESC + ) ts ON w.world_id = ts.world_id + INNER JOIN ( + SELECT + DISTINCT ON (world_id) world_id, + status, + category, + can_create + FROM + world_statuses + ORDER BY + world_id, + time DESC + ) ws ON w.world_id = ws.world_id + INNER JOIN ( + SELECT + * + FROM + queue_estimates + ) qe ON w.world_id = qe.world_id + WHERE + w.hidden = FALSE; + \ No newline at end of file diff --git a/web/src/crons/refresh_queue_estimates.rs b/web/src/crons/refresh_materialized_views.rs similarity index 68% rename from web/src/crons/refresh_queue_estimates.rs rename to web/src/crons/refresh_materialized_views.rs index af324d0..f5c0e3c 100644 --- a/web/src/crons/refresh_queue_estimates.rs +++ b/web/src/crons/refresh_materialized_views.rs @@ -8,24 +8,25 @@ use crate::{await_cancellable, db}; use super::CronJob; -pub struct RefreshQueueEstimates { +pub struct RefreshMaterializedViews { pool: PgPool, } -impl RefreshQueueEstimates { +impl RefreshMaterializedViews { pub fn new(pool: PgPool) -> Self { Self { pool } } } #[async_trait] -impl CronJob for RefreshQueueEstimates { - const NAME: &'static str = "refresh_queue_estimates"; +impl CronJob for RefreshMaterializedViews { + const NAME: &'static str = "refresh_materialized_views"; const PERIOD: Duration = Duration::from_secs(60); async fn run(&self, stop_signal: CancellationToken) -> anyhow::Result<()> { let pool = &self.pool; await_cancellable!(db::refresh_queue_estimates(pool), stop_signal); + await_cancellable!(db::refresh_world_summaries(pool), stop_signal); Ok(()) } } diff --git a/web/src/crons/refresh_travel_states.rs b/web/src/crons/refresh_travel_states.rs index 4ef3796..748e5fa 100644 --- a/web/src/crons/refresh_travel_states.rs +++ b/web/src/crons/refresh_travel_states.rs @@ -129,15 +129,7 @@ impl CronJob for RefreshTravelStates { } } - if let Some(t) = travel_time { - if t != data.average_elapsed_time { - log::error!("Travel time changed"); - log::error!("Home {}: {}", data.home_world_id, data.average_elapsed_time); - log::error!("Old: {}", t); - } - } else { - travel_time = Some(data.average_elapsed_time); - } + travel_time = Some(data.average_elapsed_time); } if travel_map.is_empty() || travel_time.is_none() { diff --git a/web/src/db.rs b/web/src/db.rs index e442bd5..e024905 100644 --- a/web/src/db.rs +++ b/web/src/db.rs @@ -1,8 +1,8 @@ use crate::{ db_wrappers::{DatabaseU16, DatabaseU64}, models::{ - Connection, DbQueueEstimate, DbTravelState, DbWorldInfo, DbWorldStatus, QueueEstimate, - QueueSize, Recap, + Connection, DbQueueEstimate, DbTravelState, DbWorldStatus, DbWorldSummaryInfo, + QueueEstimate, QueueSize, Recap, WorldSummaryInfo, }, }; use sqlx::{postgres::PgQueryResult, Error, PgPool, QueryBuilder}; @@ -483,8 +483,15 @@ pub async fn get_world_statuses_by_world_id( .await } -pub async fn get_world_info(pool: &PgPool) -> Result, Error> { - sqlx::query_as!(DbWorldInfo, r#"SELECT * FROM worlds WHERE hidden = FALSE"#) +pub async fn refresh_world_summaries(pool: &PgPool) -> Result { + sqlx::query!(r#"REFRESH MATERIALIZED VIEW CONCURRENTLY world_summary"#) + .execute(pool) + .await +} + +pub async fn get_world_summaries(pool: &PgPool) -> Result, Error> { + sqlx::query_as!(DbWorldSummaryInfo, r#"SELECT * FROM world_summary"#) .fetch_all(pool) .await + .map(|summary| summary.into_iter().map(WorldSummaryInfo::from).collect()) } diff --git a/web/src/main.rs b/web/src/main.rs index 896e923..5bd4e19 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -72,7 +72,7 @@ async fn main() -> Result<(), ServerError> { .expect("Error creating reqwest client"); let refresh_queue_estimates_token = - crons::create_cron_job(crons::RefreshQueueEstimates::new(db_pool.clone())); + crons::create_cron_job(crons::RefreshMaterializedViews::new(db_pool.clone())); let refresh_travel_states_token = crons::create_cron_job( crons::RefreshTravelStates::new(config.stasis.clone(), db_pool.clone()) diff --git a/web/src/models.rs b/web/src/models.rs index 8f1f925..3d40f46 100644 --- a/web/src/models.rs +++ b/web/src/models.rs @@ -228,7 +228,73 @@ pub struct DbWorldInfo { pub hidden: bool, } -#[derive(Serialize)] +#[derive(Debug, sqlx::FromRow)] +pub struct DbWorldSummaryInfo { + pub world_id: Option, + pub world_name: Option, + pub datacenter_id: Option, + pub datacenter_name: Option, + pub region_id: Option, + pub region_name: Option, + pub region_abbreviation: Option, + + pub status: Option, + pub category: Option, + pub can_create: Option, + + pub prohibit: Option, + + pub time: Option, + pub size: Option, + pub duration: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorldSummaryInfo { + pub world_id: u16, + pub world_name: String, + pub datacenter_id: u16, + pub datacenter_name: String, + pub region_id: u16, + pub region_name: String, + pub region_abbreviation: String, + + pub status: i16, + pub category: i16, + pub can_create: bool, + + pub travel_prohibit: bool, + + pub queue_time: DatabaseDateTime, + pub queue_size: i32, + pub queue_duration: f64, +} + +impl From for WorldSummaryInfo { + fn from(db: DbWorldSummaryInfo) -> Self { + Self { + world_id: db.world_id.unwrap_or_default() as u16, + world_name: db.world_name.unwrap_or_default(), + datacenter_id: db.datacenter_id.unwrap_or_default() as u16, + datacenter_name: db.datacenter_name.unwrap_or_default(), + region_id: db.region_id.unwrap_or_default() as u16, + region_name: db.region_name.unwrap_or_default(), + region_abbreviation: db.region_abbreviation.unwrap_or_default(), + + status: db.status.unwrap_or_default(), + category: db.category.unwrap_or_default(), + can_create: db.can_create.unwrap_or_default(), + + travel_prohibit: db.prohibit.unwrap_or_default(), + + queue_time: DatabaseDateTime::from(db.time.unwrap_or(time::PrimitiveDateTime::MIN)), + queue_size: db.size.unwrap_or_default(), + queue_duration: db.duration.unwrap_or_default(), + } + } +} + +#[derive(Serialize, Deserialize)] pub struct Summary { pub average_travel_time: i32, pub worlds: Vec, @@ -236,7 +302,7 @@ pub struct Summary { pub regions: Vec, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct WorldSummary { pub id: u16, pub name: String, @@ -252,7 +318,7 @@ pub struct WorldSummary { pub queue_last_update: DatabaseDateTime, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct DatacenterSummary { pub id: u16, pub name: String, @@ -263,7 +329,7 @@ pub struct DatacenterSummary { // pub open_ports: f32, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct RegionSummary { pub id: u16, pub name: String, diff --git a/web/src/routes/api/base.rs b/web/src/routes/api/base.rs index ac5a0f1..95b5b48 100644 --- a/web/src/routes/api/base.rs +++ b/web/src/routes/api/base.rs @@ -1,16 +1,14 @@ use crate::{ db, - db_wrappers::DatabaseDateTime, middleware::{auth::BasicAuthentication, version::UserAgentVersion}, models::{ - DatacenterSummary, DbWorldInfo, DbWorldStatus, QueueEstimate, QueueSize, Recap, - RegionSummary, Summary, WorldQueryFilter, WorldSummary, + DatacenterSummary, QueueSize, Recap, RegionSummary, Summary, WorldQueryFilter, + WorldSummary, WorldSummaryInfo, }, }; use actix_web::{ dev::HttpServiceFactory, error::ErrorInternalServerError, get, route, web, HttpResponse, Result, }; -use anyhow::anyhow; use konst::{ option, primitive::{parse_i64, parse_u32}, @@ -195,104 +193,55 @@ async fn get_world_statuses( #[get("/summary/")] async fn get_summary(pool: web::Data) -> Result { - let world_info = db::get_world_info(&pool); - let travel_states = db::get_travel_states(&pool); + let world_summaries = db::get_world_summaries(&pool); let travel_time = db::get_travel_time(&pool); - let queue_estimates = db::get_queue_estimates(&pool); - let world_statuses = db::get_world_statuses(&pool); - match tokio::join!( - world_info, - travel_states, - travel_time, - queue_estimates, - world_statuses - ) { - ( - Ok(world_info), - Ok(travel_states), - Ok(travel_time), - Ok(queue_estimates), - Ok(world_statuses), - ) => Ok(HttpResponse::Ok().json( - construct_summary( - world_info, - travel_states, - travel_time, - queue_estimates, - world_statuses, - ) - .map_err(ErrorInternalServerError)?, + match tokio::join!(world_summaries, travel_time) { + (Ok(world_summaries), Ok(travel_time)) => Ok(HttpResponse::Ok().json( + construct_summary(world_summaries, travel_time).map_err(ErrorInternalServerError)?, )), - (Err(e), _, _, _, _) - | (_, Err(e), _, _, _) - | (_, _, Err(e), _, _) - | (_, _, _, Err(e), _) - | (_, _, _, _, Err(e)) => Err(ErrorInternalServerError(e)), + (Err(e), _) | (_, Err(e)) => Err(ErrorInternalServerError(e)), } } fn construct_summary( - world_info: Vec, - travel_states: HashMap, + world_summaries: Vec, travel_time: i32, - queues: Vec, - statuses: Vec, ) -> anyhow::Result { let mut regions = HashMap::new(); let mut datacenters = HashMap::new(); let mut worlds = HashMap::new(); - for world in &world_info { + for world in &world_summaries { regions - .entry(world.region_id.0) + .entry(world.region_id) .or_insert_with(|| RegionSummary { - id: world.region_id.0, + id: world.region_id, name: world.region_name.clone(), abbreviation: world.region_abbreviation.clone(), }); datacenters - .entry(world.datacenter_id.0) + .entry(world.datacenter_id) .or_insert_with(|| DatacenterSummary { - id: world.datacenter_id.0, + id: world.datacenter_id, name: world.datacenter_name.clone(), - region_id: world.region_id.0, + region_id: world.region_id, }); - let world_id = world.world_id.0; - let travel_info = travel_states - .get(&world_id) - .ok_or_else(|| anyhow!("No travel info {world_id}"))?; - let queue_info = queues - .iter() - .find(|q| q.world_id == world_id) - .cloned() - .unwrap_or_else(|| QueueEstimate { - world_id, - last_update: DatabaseDateTime(time::OffsetDateTime::now_utc()), - last_size: 0, - last_duration: 0.0, - }); - //.ok_or_else(|| anyhow!("No queue info {world_id}"))?; - let status_info = statuses - .iter() - .find(|s| s.world_id.0 == world_id) - .ok_or_else(|| anyhow!("No status info {world_id}"))?; - worlds - .entry(world.world_id.0) + .entry(world.world_id) .or_insert_with(|| WorldSummary { - id: world.world_id.0, + id: world.world_id, name: world.world_name.clone(), - datacenter_id: world.datacenter_id.0, + datacenter_id: world.datacenter_id, - travel_prohibited: *travel_info, - world_status: status_info.status, - world_category: status_info.category, - world_character_creation_enabled: status_info.can_create, + travel_prohibited: world.travel_prohibit, + world_status: world.status, + world_category: world.category, + world_character_creation_enabled: world.can_create, - queue_size: queue_info.last_size, - queue_duration: queue_info.last_duration, - queue_last_update: queue_info.last_update, + queue_size: world.queue_size, + queue_duration: world.queue_duration, + queue_last_update: world.queue_time, }); } Ok(Summary { diff --git a/web/static/assets/main.js b/web/static/assets/main.js index e69c4b4..e4bbb77 100644 --- a/web/static/assets/main.js +++ b/web/static/assets/main.js @@ -1,5 +1,6 @@ const main = document.querySelector('main'); -const region_group = document.querySelector('.region-hyperlinks'); +const region_hyperlinks = document.querySelector('.region-hyperlinks'); +const region_dropdown = document.querySelector('.region-dropdown'); const global_table = document.querySelector('#table-global'); const nav = document.querySelector('nav'); const theme_toggles = document.querySelectorAll('.theme-toggle'); @@ -12,7 +13,7 @@ function updateAnchorOffset(e) { } addEventListener("resize", updateAnchorOffset); -addEventListener("DOMContentLoaded", updateAnchorOffset); +updateAnchorOffset(); function switchTheme(e) { toggleTheme(); @@ -39,8 +40,8 @@ function format_relative_past(time) { } function format_future_duration(diff) { - if (diff < 1000) { - return 'now'; + if (diff < 500) { + return 'soon'; } return 'in ' + humanizeDuration(diff, { largest: 2, round: true }); } @@ -50,12 +51,13 @@ function update_global_row(row, text) { data.textContent = text; } -function get_region_section(region_id) { - let ret = region_group.querySelector('#region-' + region_id); +function get_region_section_a(region_id) { + let ret = region_hyperlinks.querySelector('#region-a-' + region_id); if (ret === null) { ret = create_hierarchy({ "tag": "li", - "id": "region-" + region_id, + "id": "region-a-" + region_id, + "class_name": "hide-mobile", "children": [ { "tag": "a", @@ -65,9 +67,30 @@ function get_region_section(region_id) { } ] }); - region_group.appendChild(ret); + region_hyperlinks.appendChild(ret); } - return ret; + return ret.querySelector('a'); +} +function get_region_section_b(region_id) { + let ret = region_dropdown.querySelector('#region-b-' + region_id); + if (ret === null) { + ret = create_hierarchy({ + "tag": "li", + "id": "region-b-" + region_id, + "children": [ + { + "tag": "a", + "attributes": { "href": "#" }, + "content": "Region" + } + ] + }); + ret.children[0].addEventListener('click', function (e) { + region_dropdown.parentElement.removeAttribute('open'); + }); + region_dropdown.appendChild(ret); + } + return ret.querySelector('a'); } function get_dc_section(datacenter_id) { @@ -104,78 +127,87 @@ function get_dc_section(datacenter_id) { function get_world_row(datacenter_id, world_id) { let dc_section = get_dc_section(datacenter_id); - let ret = dc_section.querySelector('.worlds-container>#world-' + world_id); + let ret = dc_section.querySelector('#world-' + world_id); if (ret === null) { ret = create_hierarchy({ - "tag": "article", - "id": "world-" + world_id, + "tag": "div", + "class_name": "shadow-container", "children": [ { - "tag": "header", - "class_name": "world-header", + "tag": "article", + "id": "world-" + world_id, "children": [ { - "tag": "h4", - "class_name": "world-name" - }, - { - "tag": "h4", - "class_name": "world-icons", + "tag": "header", + "class_name": "world-header", "children": [ { - "tag": "span", - "children": [ - { - "tag": "span", - "class_name": "status" - } - ] + "tag": "h4", + "class_name": "world-name" }, { - "tag": "span", + "tag": "h4", + "class_name": "world-icons", "children": [ { "tag": "span", - "class_name": "status" - } - ] - }, - { - "tag": "span", - "children": [ + "attributes": { "data-placement": "shifted" }, + "children": [ + { + "tag": "span", + "class_name": "status", + } + ] + }, + { + "tag": "span", + "attributes": { "data-placement": "shifted-long" }, + "children": [ + { + "tag": "span", + "class_name": "status", + } + ] + }, { "tag": "span", - "class_name": "status" + "attributes": { "data-placement": "shifted-long" }, + "children": [ + { + "tag": "span", + "class_name": "status", + } + ] } ] } ] - } - ] - }, - { - "tag": "div", - "class_name": "world-body", - "attributes": { "data-placement": "bottom" }, - "children": [ - { - "tag": "div", - "children": [ - { - "tag": "h6", - "content": "Queue Time" - }, - { "tag": "div", "class_name": "queue-time" } - ] }, { "tag": "div", + "class_name": "world-body", + "attributes": { "data-placement": "bottom" }, "children": [ { - "tag": "h6", - "content": "Queue Size" + "tag": "div", + "children": [ + { + "tag": "h6", + "content": "Queue Time" + }, + { "tag": "div", "class_name": "queue-time" } + ] }, - { "tag": "div", "class_name": "queue-size" } + { + "tag": "div", + "children": [ + { + "tag": "h6", + "content": "Queue Size" + }, + { "tag": "div", "class_name": "queue-size" } + ] + } ] } ] @@ -218,6 +250,72 @@ function create_hierarchy(data) { return ret; } +function toggleModal(e) { + e.preventDefault(); + const modal = document.getElementById(e.currentTarget.dataset.target); + if (!modal) return; + modal && (modal.open ? closeModal(modal) : openModal(modal)); +}; + +const modalIsOpenClass = "modal-is-open"; +const modalOpeningClass = "modal-is-opening"; +const modalClosingClass = "modal-is-closing"; +const scrollbarWidthCssVar = "--pico-scrollbar-width"; +const modalAnimDur = 400; +const modalAnimDur2 = 400; +let visibleModal = null; +function openModal(modal) { + const html = document.documentElement; + const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; + if (scrollbarWidth) { + html.style.setProperty(scrollbarWidthCssVar, `${scrollbarWidth}px`); + } + html.classList.add(modalIsOpenClass); + html.classList.add(modalOpeningClass); + setTimeout(() => { + visibleModal = modal; + }, modalAnimDur); + setTimeout(() => { + html.classList.remove(modalOpeningClass); + }, modalAnimDur2); + modal.showModal(); +}; + +function closeModal(modal) { + visibleModal = null; + const html = document.documentElement; + html.classList.add(modalClosingClass); + setTimeout(() => { + html.classList.remove(modalClosingClass, modalIsOpenClass); + html.style.removeProperty(scrollbarWidthCssVar); + modal.close(); + }, modalAnimDur2); +}; + +document.addEventListener("click", (event) => { + if (visibleModal === null) return; + const modalContent = visibleModal.querySelector("article"); + const isClickInside = modalContent.contains(event.target); + !isClickInside && closeModal(visibleModal); +}); + +document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && visibleModal) { + closeModal(visibleModal); + } +}); + +// Get scrollbar width +const getScrollbarWidth = () => { + const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; + return scrollbarWidth; +}; + +// Is scrollbar visible +const isScrollbarVisible = () => { + return document.body.scrollHeight > screen.height; +}; + const status_lookup = { 1: ['Online', 'status-online'], 2: ['Offline', 'status-offline'], @@ -268,9 +366,10 @@ function update_dc_data(data, regions) { } function update_region_data(data, datacenters) { - let section = get_region_section(data.id); - section.querySelector('a').textContent = data.name; - section.querySelector('a').setAttribute('href', '#dc-' + datacenters.find(dc => dc.region_id === data.id).id); + for (let section of [get_region_section_a(data.id), get_region_section_b(data.id)]) { + section.textContent = data.name; + section.setAttribute('href', '#dc-' + datacenters.find(dc => dc.region_id === data.id).id); + } } function update_global_data(data) { @@ -311,9 +410,12 @@ function update_from_summary(summary) { update_region_data(region, summary.datacenters); } - let hash = document.getElementById(window.location.hash.slice(1)); - if (hash !== null) { - hash.scrollIntoView(); + if (!scrolled_to_hash) { + scrolled_to_hash = true; + let hash = document.getElementById(window.location.hash.slice(1)); + if (hash !== null) { + hash.scrollIntoView(); + } } } @@ -360,6 +462,8 @@ function queue_url_update(url) { timer_group.classList.remove('timer-reloading'); timer_group.classList.add('timer-waiting'); } + +let scrolled_to_hash = false; try { let summary = localStorage.getItem('summary'); if (summary !== null) { diff --git a/web/static/assets/style.css b/web/static/assets/style.css index 744f94f..ec8be8c 100644 --- a/web/static/assets/style.css +++ b/web/static/assets/style.css @@ -1,3 +1,65 @@ +@media (min-width: 1024px) { + .logo+* { + display: inline !important; + } + + .hide-desktop { + display: none; + } + + .hide-mobile { + display: inline-block !important; + } +} + +.hide-mobile { + display: none; +} + +.world-icons [data-tooltip][data-placement=shifted] { + --pico-tooltip-shift: -50%; +} + +.world-icons [data-tooltip][data-placement=shifted-long] { + --pico-tooltip-shift: calc(-100% + 1.5rem); +} + +@media (hover: hover) and (pointer:fine) { + + .world-icons [data-tooltip]:focus::before, + .world-icons [data-tooltip]:hover::before { + transform: translate(var(--pico-tooltip-shift), 0); + } +} + +.world-icons [data-tooltip]::before { + transform: var(--pico-tooltip-slide-to); + --pico-tooltip-slide-to: translate(var(--pico-tooltip-shift), -.25rem); +} + +#table-global td { + text-align: right; +} + +details.dropdown summary { + display: grid; + grid-template-columns: 1fr auto; + transform: translateY(.125rem); +} + +details.dropdown summary::before { + width: auto !important; +} + +details.dropdown summary+ul { + left: initial; + right: 0; +} + +dialog { + animation-duration: 400ms; +} + span:has(> nav) { position: sticky; top: 0; @@ -8,6 +70,10 @@ span:has(> nav) { border-bottom: var(--pico-border-width) solid var(--pico-muted-border-color); } +hgroup { + text-align: center; +} + span:has(> .status), .world-body { border-bottom: none !important; @@ -54,21 +120,18 @@ span:has(> .status)::before { } .worlds-container { - display: flex; - flex-flow: row wrap; - justify-content: space-between; - gap: 0; + display: grid; + column-gap: var(--pico-block-spacing-horizontal); + row-gap: var(--pico-block-spacing-vertical); + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); white-space: nowrap; } .worlds-container>article { - flex-grow: 1; - flex-basis: 30%; + margin: 0; display: flex; flex-direction: column; - - margin: var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal); } .world-header, @@ -85,7 +148,6 @@ span:has(> .status)::before { --pico-typography-spacing-vertical: 0; margin-bottom: calc(var(--pico-block-spacing-vertical) * 0.5); - /* flex-wrap: wrap; */ align-items: center; justify-content: space-between; } @@ -104,14 +166,7 @@ span:has(> .status)::before { text-align: right; } -/* .world-name { - flex-grow: 1; - flex-shrink: 1; - text-align: center; -} */ - .world-icons { - /* flex-grow: 1; */ font-size: inherit; display: flex; @@ -130,15 +185,6 @@ span:has(> .status)::before { scroll-margin-top: calc(var(--link-offset) + 1rem); } -.defs-only { - position: fixed; - left: -9999px; - top: -9999px; - z-index: -1; - width: 0; - height: 0; -} - li:has(.logo-container) { padding: 0; } @@ -161,18 +207,18 @@ li:has(.logo-container) { .logo { display: inline; height: 3rem; - width: 3rem; + min-width: 3rem; border-radius: 30%; } .logo+* { - display: inline; + display: none; + /* display: inline; */ margin: 0; } nav { text-align: center; - justify-content: space-between; } nav>* { @@ -181,6 +227,12 @@ nav>* { .region-hyperlinks { justify-content: center; + white-space: nowrap; +} + +.region-hyperlinks li:has(details.dropdown) { + padding-top: 0 !important; + padding-bottom: 0 !important; } nav>:last-child { @@ -200,6 +252,15 @@ nav>:last-child { transform: translateY(-.125rem); } +nav details.dropdown>summary { + background: none !important; + border: inherit !important; +} + +nav details.dropdown>summary::after { + transform: translateY(.0625rem); +} + @keyframes waiting { 0% { stroke-dashoffset: calc(9px*2*pi); @@ -257,4 +318,34 @@ g.timer-reloading circle { stroke-dasharray: 75, 100; stroke-dashoffset: -5; animation: reloading-dash 1.5s ease-in-out infinite; +} + +.shadow-container { + --pico-card-box-shadow: 0 transparent; + --pico-shadow-card-radius: 0.25rem; + --pico-shadow-card-color: rgb(var(--pico-box-shadow-color)); + position: relative; +} + +.shadow-container::before { + z-index: -1; + content: ""; + position: absolute; + opacity: .25; + border-radius: var(--pico-border-radius); + inset: calc(var(--pico-shadow-card-radius) * -1); + filter: blur(var(--pico-shadow-card-radius)); + background: var(--pico-shadow-card-color); +} + +.shadow-container article { + margin-bottom: 0; +} + +[data-theme=light] { + --pico-box-shadow-color: 129, 145, 181; +} + +[data-theme=dark] { + --pico-box-shadow-color: 7, 9, 12; } \ No newline at end of file diff --git a/web/static/index.html b/web/static/index.html index 738f14f..b81aaec 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -7,13 +7,11 @@ Waitingway - + - - - + @@ -29,14 +27,23 @@

Waitingway