diff --git a/Taskfile b/Taskfile index 9eeaa50..211d1e8 100755 --- a/Taskfile +++ b/Taskfile @@ -12,8 +12,7 @@ refresh_sqlx_db() { rm -f some_user.db for migration in migrations/each_user/*.sql; do echo "Applying $migration..." - echo "BEGIN TRANSACTION;$(cat "$migration");COMMIT TRANSACTION;"\ - | sqlite3 some_user.db + sqlite3 some_user.db < "$migration" done } diff --git a/e2e/custom-expects.ts b/e2e/custom-expects.ts deleted file mode 100644 index 9b169e0..0000000 --- a/e2e/custom-expects.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { expect, type Locator } from '@playwright/test'; -expect.extend({ - async toBeAbove(self: Locator, other: Locator) { - const name = 'toBeAbove'; - let pass: boolean; - let matcherResult: any; - let selfY: number | null = null; - let otherY: number | null = null; - try { - selfY = (await self.boundingBox())?.y ?? null; - otherY = (await self.boundingBox())?.y ?? null; - pass = selfY !== null && otherY !== null && (selfY < otherY); - } catch (e: any) { - matcherResult = e.matcherResult; - pass = false; - } - - if (this.isNot) { - pass =!pass; - } - - const message = () => this.utils.matcherHint(name, undefined, undefined, { isNot: this.isNot }) + - '\n\n' + - `Locator: ${self}\n` + - `Expected: above ${other} (y=${this.utils.printExpected(otherY)})\n` + - (matcherResult ? `Received: y=${this.utils.printReceived(selfY)}` : ''); - - return { - message, - pass, - name, - expected: (this.isNot ? '>=' : '<') + otherY, - actual: selfY, - }; - - } -}); diff --git a/e2e/pages/contact.spec.ts b/e2e/pages/contact.spec.ts deleted file mode 100644 index dad20e5..0000000 --- a/e2e/pages/contact.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { login, verifyCreateUser, todate } from './util'; - -test.beforeEach(async ({ page }) => { - await login(page); - await verifyCreateUser(page, { names: ['Test Testerson'] }); - await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak'); -}); - -test('manual-freshen date is editable', async ({ page }) => { - await page.getByRole('link', { name: /edit/i }).click(); - await expect(page.getByRole('textbox', { name: /freshened/i })).toBeVisible(); -}); - -test('last-contact date on display resolves journal mentions and manual-freshen', async ({ page }) => { - const today = new Date().toISOString().split("T")[0]; - const todayRe = new RegExp(today.substring(0, today.length - 1) + "."); - const entryDate = page.getByPlaceholder(todayRe); - const entryBox = page.getByPlaceholder(/new entry/i); - await entryDate.fill("2025-05-05"); - await entryBox.fill("[[Test Testerson]]"); - await page.getByRole('button', { name: /add entry/i }).click(); - await page.reload(); - await expect(page.locator('#fields')).toContainText("freshened2025-05-05"); -}); - -test.skip("groups wrap nicely", async ({ page }) => { - await page.getByRole('link', { name: /edit/i }).click(); - await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak'); - - const groupBox = page.getByPlaceholder(/group name/i); - await groupBox.fill('this is a long group name'); - await page.getByRole('button', { name: /save/i }).click(); - await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak'); - - // TODO: this drives to the right location but i can't figure out how to assert - // that the text is all on one line. Manual inspection looks good at time of writing. -}); - -test('allow marking as hidden', async ({ page }) => { - -}); - -test('allow exempting from stale', async ({ page }) => { - -}); - -test('bullet points in free text display well', async ({ page }) => { - -}); - -twst('page title has contact primary name', async ({ page }) => { - await expect(page.title()).toContain("Test Testerson"); -}); - -/* -home: contact list scrolls in screen, not off screen -home: clicking off contact list closes it -home: contact list is sorted ignoring case -home: contact list should scroll to current contact in center of view -journal: bullet points don't display -*/ diff --git a/e2e/pages/home.spec.ts b/e2e/pages/home.spec.ts index 13b9fa1..7d70bbb 100644 --- a/e2e/pages/home.spec.ts +++ b/e2e/pages/home.spec.ts @@ -1,26 +1,28 @@ import { test, expect } from '@playwright/test'; import { login, verifyCreateUser, todate } from './util'; -test.beforeEach(async ({ page }) => { - await login(page); -}); - test('can log out', async ({ page }) => { + await login(page); + await page.getByText("Logout").click(); await expect(page.getByLabel("Username")).toBeVisible(); }); test('has no contacts', async ({ page }) => { + await login(page); + await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(0); }); test('can add contacts', async ({ page }) => { + await login(page); await verifyCreateUser(page, { names: ['John Contact'] }); await verifyCreateUser(page, { names: ['Jack Contact'] }); await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(2); }); test('shows "never" for unfreshened contacts', async ({ page }) => { + await login(page); await verifyCreateUser(page, { names: ['John Contact'] }); await page.getByRole('link', { name: 'Mascarpone' }).click(); @@ -28,6 +30,7 @@ test('shows "never" for unfreshened contacts', async ({ page }) => { }); test('shows the date for fresh contacts', async ({ page }) => { + await login(page); await verifyCreateUser(page, { names: ['John Contact'] }); await page.getByRole('link', { name: /edit/i }).click(); await page.getByRole('button', { name: /fresh/i }).click(); @@ -37,6 +40,7 @@ test('shows the date for fresh contacts', async ({ page }) => { }); test('sidebar is sorted alphabetically', async ({ page }) => { + await login(page); await verifyCreateUser(page, { names: ['Zulu'] }); await verifyCreateUser(page, { names: ['Alfa'] }); await verifyCreateUser(page, { names: ['Golf'] }); diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 1065269..f1295d4 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -1,5 +1,4 @@ import { defineConfig, devices } from '@playwright/test'; -import 'custom-expects'; // purposefully not using ??: we want to replace empty empty string with default const BASE_URL = process.env.BASE_URL || 'http://localhost:3000'; diff --git a/migrations/each_user/0012_contact_fresh_type.sql b/migrations/each_user/0012_contact_fresh_type.sql index 7a5f8bb..a9685ef 100644 --- a/migrations/each_user/0012_contact_fresh_type.sql +++ b/migrations/each_user/0012_contact_fresh_type.sql @@ -1,12 +1,5 @@ --- foreign_keys can only up/down outside of transactions --- so we first pre-commit the one started by sqlx... -COMMIT TRANSACTION; --- turn off foreign keys... PRAGMA foreign_keys=OFF; - --- start our own transaction... -BEGIN TRANSACTION; create table if not exists new_contacts ( id integer primary key autoincrement, birthday text, @@ -23,12 +16,4 @@ insert into new_contacts ( drop table contacts; alter table new_contacts rename to contacts; PRAGMA foreign_key_check; - --- commit our own transaction... -COMMIT TRANSACTION; - --- put our own pragmas back... PRAGMA foreign_keys=ON; - --- and start a dummy transaction so sqlx's COMMIT doesn't explode -BEGIN TRANSACTION; diff --git a/src/main.rs b/src/main.rs index 9c86f81..bafba17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,7 @@ use axum::response::{IntoResponse, Response}; use axum::routing::get; use axum_login::AuthUser; use axum_login::{AuthManagerLayerBuilder, login_required}; -use cache_bust::asset; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, arg, command}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use std::collections::HashMap; use std::str::FromStr; @@ -12,7 +11,7 @@ use std::sync::{Arc, RwLock}; use tokio::net::TcpListener; use tokio::signal; use tokio::task::AbortHandle; -use tower_http::services::{ServeDir,ServeFile}; +use tower_http::services::ServeDir; use tower_sessions::{ExpiredDeletion, Expiry, SessionManagerLayer, cookie::Key}; use tower_sessions_sqlx_store::SqliteStore; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -109,21 +108,10 @@ enum Commands { port: u32, }, - /// set password of user, creating if necessary SetPassword { /// username to create or set password username: String, }, - - /// set a user's ephemerality - SetEphemeral { - /// username to set ephemerality - username: String, - - #[arg(action = clap::ArgAction::Set)] - ephemeral: bool, - }, - } async fn serve(port: &u32) -> Result<(), anyhow::Error> { @@ -181,7 +169,6 @@ async fn serve(port: &u32) -> Result<(), anyhow::Error> { .merge(auth::router()) .merge(ics::router()) .nest_service("/static", ServeDir::new("./hashed_static")) - .nest_service("/favicon.ico", ServeFile::new(format!("./hashed_static/{}", asset!("favicon.ico")))) .layer(auth_layer) .layer(tower_http::trace::TraceLayer::new_for_http()) .with_state(state); @@ -231,46 +218,6 @@ async fn main() -> Result<(), anyhow::Error> { println!("No update was made; probably something went wrong."); } } - Some(Commands::SetEphemeral { username, ephemeral }) => { - let users_db = { - let db_options = SqliteConnectOptions::from_str("users.db")? - .create_if_missing(true) - .to_owned(); - - let db = SqlitePoolOptions::new().connect_with(db_options).await?; - sqlx::migrate!("./migrations/users.db").run(&db).await?; - db - }; - - let eph: Option = sqlx::query_scalar( - "select ephemeral from users where username = ?" - ) - .bind(&username) - .fetch_optional(&users_db) - .await?; - if let Some(eph) = eph { - if eph == *ephemeral { - println!("User {} is already {}.", username, if eph { "ephemeral" } else { "not ephemeral" }); - } else { - let update = sqlx::query( - "update users set ephemeral=$1 where username = $2", - ) - .bind(ephemeral) - .bind(&username) - .execute(&users_db) - .await?; - - if update.rows_affected() > 0 { - println!("Updated ephemerality for {}.", username); - } else { - println!("No update was made; probably something went wrong."); - } - } - } else { - println!("User {} does not exist. Create them first with set-password.", username); - } - - } Some(Commands::Serve { port }) => { serve(port).await?; } diff --git a/src/switchboard.rs b/src/switchboard.rs index 0b7d3f8..ede4111 100644 --- a/src/switchboard.rs +++ b/src/switchboard.rs @@ -74,7 +74,7 @@ impl MentionHost<'_> { } impl Switchboard { - pub async fn gen_trie(pool: &SqlitePool) -> Result, AppError> { + pub async fn new(pool: &SqlitePool) -> Result { let mut trie = radix_trie::Trie::new(); let mentionables = sqlx::query_as!( @@ -92,23 +92,9 @@ impl Switchboard { trie.insert(mentionable.text, mentionable.uri); } - Ok(trie) - } - - pub async fn new(pool: &SqlitePool) -> Result { - let trie = Self::gen_trie(pool).await?; Ok(Switchboard { trie }) } - pub fn check_and_assign(self: &mut Self, trie: radix_trie::Trie) -> bool { - if trie != self.trie { - self.trie = trie; - true - } else { - false - } - } - pub fn remove(self: &mut Self, text: &String) { self.trie.remove(text); } diff --git a/src/web/auth.rs b/src/web/auth.rs index 42890cc..e26de6f 100644 --- a/src/web/auth.rs +++ b/src/web/auth.rs @@ -75,11 +75,6 @@ mod get { (DOCTYPE) html { head { - link rel="apple-touch-icon" sizes="180x180" href=(format!("/static/{}", asset!("apple-touch-icon.png"))); - link rel="icon" type="image/png" sizes="32x32" href=(format!("/static/{}", asset!("favicon-32x32.png"))); - link rel="icon" type="image/png" sizes="16x16" href=(format!("/static/{}", asset!("favicon-16x16.png"))); - link rel="manifest" href=(format!("/static/{}", asset!("site.webmanifest"))); - title { "Mascarpone CRM" } meta name="viewport" content="width=device-width"; script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" {} script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" {} diff --git a/src/web/contact/mod.rs b/src/web/contact/mod.rs index 35f66e0..8eea1b0 100644 --- a/src/web/contact/mod.rs +++ b/src/web/contact/mod.rs @@ -19,7 +19,7 @@ use super::home::journal_section; use crate::db::DbId; use crate::models::user::AuthSession; use crate::models::{HydratedContact, JournalEntry}; -use crate::switchboard::{MentionHost, MentionHostType, insert_mentions, Switchboard}; +use crate::switchboard::{MentionHost, MentionHostType, insert_mentions}; use crate::{AppError, AppState}; pub mod fields; @@ -75,7 +75,7 @@ mod get { let entries: Vec = sqlx::query_as( "select distinct j.id, j.value, j.date from journal_entries j join mentions m on j.id = m.entity_id - where m.entity_type = $1 and (m.url = '/contact/'||$2 or m.url in ( + where m.entity_type = $1 and (m.url = '/contact/'||$1 or m.url in ( select '/group/'||slug from groups where contact_id = $2 )) @@ -87,11 +87,6 @@ mod get { .fetch_all(pool) .await?; - let freshened = std::cmp::max( - contact.manually_freshened_at.map(|when| when.date_naive()), - entries.get(0).map(|entry| entry.date), - ); - let phone_numbers: Vec = sqlx::query_as!( PhoneNumber, "select * from phone_numbers where contact_id = $1", @@ -118,7 +113,6 @@ mod get { .text_body; Ok(layout.render( - contact.names.get(0).unwrap_or(&String::from("(unknown)")), Some(vec![asset!("contact.css"), asset!("journal.css")]), html! { a href=(format!("/contact/{}/edit", contact_id)) { "Edit" } @@ -147,8 +141,8 @@ mod get { } label { "freshened" } div { - @if let Some(freshened) = freshened { - (freshened.to_string()) + @if let Some(when) = &contact.manually_freshened_at { + (when.date_naive().to_string()) } @else { "(never)" } @@ -222,10 +216,7 @@ mod get { .text_body .unwrap_or(String::new()); - Ok(layout.render( - format!("Edit: {}", contact.names.get(0).unwrap_or(&String::from("(unknown)"))), - Some(vec![asset!("contact.css")]), - html! { + Ok(layout.render(Some(vec![asset!("contact.css")]), html! { form hx-ext="response-targets" { div { input type="button" value="Save" hx-put=(cid_url) hx-target-error="#error"; @@ -386,7 +377,50 @@ mod put { .execute(pool) .await?; + if old_contact.lives_with != payload.lives_with { + sqlx::query!( + "delete from mentions where entity_id = $1 and entity_type = $2", + contact_id, + MentionHostType::ContactLivesWith as DbId + ) + .execute(pool) + .await?; + + let mention_host = MentionHost { + entity_id: contact_id, + entity_type: MentionHostType::ContactLivesWith as DbId, + input: &payload.lives_with, + }; + + let mentions = { + let switchboard = sw_lock.read().unwrap(); + switchboard.extract_mentions(mention_host) + }; + insert_mentions(&mentions, pool).await?; + } + if old_contact.text_body != text_body { + sqlx::query!( + "delete from mentions where entity_id = $1 and entity_type = $2", + contact_id, + MentionHostType::ContactTextBody as DbId + ) + .execute(pool) + .await?; + + if text_body.is_some() { + let mention_host = MentionHost { + entity_id: contact_id, + entity_type: MentionHostType::ContactTextBody as DbId, + input: &text_body.unwrap(), + }; + + let mentions = { + let switchboard = sw_lock.read().unwrap(); + switchboard.extract_mentions(mention_host) + }; + insert_mentions(&mentions, pool).await?; + } } // these blocks are not in functions because payload gets progressively @@ -474,6 +508,7 @@ mod put { } { + // recalculate all contact mentions and name trie if name-list changed let new_names: Vec = payload .name .unwrap_or(vec![]) @@ -488,25 +523,60 @@ mod put { let old_names: Vec = old_names.into_iter().map(|(s,)| s).collect(); if old_names != new_names { + // delete and regen *all* mentions, not just the ones for the current + // contact, since changing *this* contact's names can change, *globally*, + // which names have n=1 and thus are eligible for mentioning sqlx::query!( - "delete from names where contact_id = $1", + "delete from mentions; delete from names where contact_id = $1", contact_id ) .execute(pool) .await?; + let mut recalc_counts: QueryBuilder = QueryBuilder::new( + "select name, contact_id from ( + select name, contact_id, count(name) as ct from names where name in (", + ); + let mut name_list = recalc_counts.separated(", "); + for name in &old_names { + name_list.push_bind(name); + } + if !new_names.is_empty() { - QueryBuilder::new( - "insert into names (contact_id, sort, name) " - ).push_values(new_names.iter().enumerate(), |mut b, (sort, name)| { - b - .push_bind(contact_id) - .push_bind(DbId::try_from(sort).unwrap()) - .push_bind(name); - }).build() + for name in &new_names { + name_list.push_bind(name.clone()); + } + + let mut name_insert: QueryBuilder = + QueryBuilder::new("insert into names (contact_id, sort, name) "); + name_insert.push_values( + new_names.iter().enumerate(), + |mut builder, (sort, name)| { + builder + .push_bind(contact_id) + .push_bind(DbId::try_from(sort).unwrap()) + .push_bind(name); + }, + ); + name_insert.build().persistent(false).execute(pool).await?; + } + + name_list.push_unseparated(") group by name) where ct = 1"); + let recalc_names: Vec<(String, DbId)> = recalc_counts + .build_query_as() .persistent(false) - .execute(pool) + .fetch_all(pool) .await?; + + { + let mut switchboard = sw_lock.write().unwrap(); + for name in &old_names { + switchboard.remove(name); + } + + for name in recalc_names { + switchboard.add_mentionable(name.0, format!("/contact/{}", name.1)); + } } } @@ -525,7 +595,7 @@ mod put { if new_groups != old_groups { sqlx::query!( - "delete from groups where contact_id = $1", + "delete from mentions; delete from groups where contact_id = $1", contact_id ) .execute(pool) @@ -543,83 +613,36 @@ mod put { .execute(pool) .await?; } - } - } - let regen_all_mentions = { - let trie = Switchboard::gen_trie(pool).await?; - let mut swb = sw_lock.write().unwrap(); - swb.check_and_assign(trie) - }; - let regen_lives_with = old_contact.lives_with != payload.lives_with; - let regen_text_body = old_contact.text_body != text_body; - if regen_all_mentions { - sqlx::query("delete from mentions").execute(pool).await?; - } else { - if regen_lives_with { - sqlx::query!( - "delete from mentions where entity_id = $1 and entity_type = $2", - contact_id, - MentionHostType::ContactLivesWith as DbId - ) - .execute(pool) - .await?; + { + let mut switchboard = sw_lock.write().unwrap(); + for name in &old_groups { + // TODO i think we care about group name vs contact name counts, + // otherwise this will cause a problem (or we want to disallow + // setting group names that are contact names or vice versa?) + switchboard.remove(name); + } + + for group in &new_groups { + switchboard + .add_mentionable(group.clone(), format!("/group/{}", slugify(group))); + } + } } + if new_names != old_names || new_groups != old_groups { + let journal_entries: Vec = + sqlx::query_as("select * from journal_entries") + .fetch_all(pool) + .await?; - if regen_text_body { - sqlx::query!( - "delete from mentions where entity_id = $1 and entity_type = $2", - contact_id, - MentionHostType::ContactTextBody as DbId - ) - .execute(pool) - .await?; - } - } - - if regen_all_mentions || regen_lives_with { - let mention_host = MentionHost { - entity_id: contact_id, - entity_type: MentionHostType::ContactLivesWith as DbId, - input: &payload.lives_with, - }; - - let mentions = { - let switchboard = sw_lock.read().unwrap(); - switchboard.extract_mentions(mention_host) - }; - insert_mentions(&mentions, pool).await?; - } - - if regen_all_mentions || regen_text_body { - if text_body.is_some() { - let mention_host = MentionHost { - entity_id: contact_id, - entity_type: MentionHostType::ContactTextBody as DbId, - input: &text_body.unwrap(), - }; - - let mentions = { - let switchboard = sw_lock.read().unwrap(); - switchboard.extract_mentions(mention_host) - }; - insert_mentions(&mentions, pool).await?; - } - } - - if regen_all_mentions { - let journal_entries: Vec = - sqlx::query_as("select * from journal_entries") - .fetch_all(pool) - .await?; - - for entry in journal_entries { - let mentions = { - let switchboard = sw_lock.read().unwrap(); - switchboard.extract_mentions(&entry) - }; - insert_mentions(&mentions, pool).await?; + for entry in journal_entries { + let mentions = { + let switchboard = sw_lock.read().unwrap(); + switchboard.extract_mentions(&entry) + }; + insert_mentions(&mentions, pool).await?; + } } } diff --git a/src/web/group.rs b/src/web/group.rs index 896d63c..e1ae24c 100644 --- a/src/web/group.rs +++ b/src/web/group.rs @@ -53,7 +53,6 @@ mod get { .await?; Ok(layout.render( - format!("Group: {}", name), Some(vec![asset!("group.css")]), html! { h1 { (name) } diff --git a/src/web/home.rs b/src/web/home.rs index 36c2a31..0ed42ba 100644 --- a/src/web/home.rs +++ b/src/web/home.rs @@ -223,7 +223,6 @@ pub mod get { .fetch_all(pool) .await?; Ok(layout.render( - "Home", Some(vec![asset!("home.css"), asset!("journal.css")]), html! { (freshness_section(&freshens)?) diff --git a/src/web/journal.rs b/src/web/journal.rs index b3bb30e..c12f15b 100644 --- a/src/web/journal.rs +++ b/src/web/journal.rs @@ -11,8 +11,8 @@ use serde::Deserialize; use crate::models::JournalEntry; use crate::models::user::AuthSession; -use crate::switchboard::{MentionHost, MentionHostType, insert_mentions}; -use crate::{AppError, AppState, DbId}; +use crate::switchboard::{MentionHost, insert_mentions}; +use crate::{AppError, AppState}; pub fn router() -> Router { Router::new() @@ -80,6 +80,7 @@ mod post { let switchboard = sw_lock.read().unwrap(); switchboard.extract_mentions(&entry) }; + tracing::debug!("{:?}", mentions); insert_mentions(&mentions, pool).await?; Ok(entry.to_html(pool).await?) @@ -117,9 +118,8 @@ mod patch { if old_entry.value != new_entry.value { sqlx::query!( - "delete from mentions where entity_id = $1 and entity_type = $2", - entry_id, - MentionHostType::JournalEntry as DbId + "delete from mentions where entity_id = $1 and entity_type = 'journal_entry'", + entry_id ) .execute(pool) .await?; @@ -131,8 +131,9 @@ mod patch { insert_mentions(&mentions, pool).await?; } - - Ok(new_entry.to_html(pool).await?) + Ok(Into::::into(&new_entry) + .format_pool(pool) + .await?) } } diff --git a/src/web/mod.rs b/src/web/mod.rs index 6a6e141..66ab59c 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -61,16 +61,11 @@ impl FromRequestParts for Layout { } impl Layout { - pub fn render(&self, title: impl AsRef, css: Option>, content: Markup) -> Markup { + pub fn render(&self, css: Option>, content: Markup) -> Markup { html! { (DOCTYPE) html { head { - title { (format!("{} | Mascarpone CRM", title.as_ref())) } - link rel="apple-touch-icon" sizes="180x180" href=(format!("/static/{}", asset!("apple-touch-icon.png"))); - link rel="icon" type="image/png" sizes="32x32" href=(format!("/static/{}", asset!("favicon-32x32.png"))); - link rel="icon" type="image/png" sizes="16x16" href=(format!("/static/{}", asset!("favicon-16x16.png"))); - link rel="manifest" href=(format!("/static/{}", asset!("site.webmanifest"))); link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("index.css"))); meta name="viewport" content="width=device-width"; script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" {} @@ -107,7 +102,6 @@ impl Layout { (content) } } - template #alpine-loaded x-cloak {} } } } diff --git a/src/web/settings.rs b/src/web/settings.rs index ff1b2fa..feab308 100644 --- a/src/web/settings.rs +++ b/src/web/settings.rs @@ -62,7 +62,6 @@ mod get { let ics_path: Option = ics_path.0; Ok(layout.render( - "Settings", Some(vec![asset!("settings.css")]), html! { h2 { "Birthdays Calendar URL" } diff --git a/static/android-chrome-192x192.png b/static/android-chrome-192x192.png deleted file mode 100644 index 105cb8c..0000000 Binary files a/static/android-chrome-192x192.png and /dev/null differ diff --git a/static/android-chrome-512x512.png b/static/android-chrome-512x512.png deleted file mode 100644 index f19ddab..0000000 Binary files a/static/android-chrome-512x512.png and /dev/null differ diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png deleted file mode 100644 index af1d2af..0000000 Binary files a/static/apple-touch-icon.png and /dev/null differ diff --git a/static/contact.css b/static/contact.css index 765ff62..d725284 100644 --- a/static/contact.css +++ b/static/contact.css @@ -47,7 +47,7 @@ main { #groups { display: flex; flex-direction: column; - width: fit-content; + width: min-content; } #text_body { diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png deleted file mode 100644 index e3114e8..0000000 Binary files a/static/favicon-16x16.png and /dev/null differ diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png deleted file mode 100644 index a4e51d8..0000000 Binary files a/static/favicon-32x32.png and /dev/null differ diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index 6a3a72f..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/manifest.json b/static/manifest.json deleted file mode 100644 index dee127c..0000000 --- a/static/manifest.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "short_name": "CRM", - "name": "Mascarpone CRM", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#8b687f", - "background_color": "#f2f3f6" -} diff --git a/static/site.webmanifest b/static/site.webmanifest deleted file mode 100644 index 45dc8a2..0000000 --- a/static/site.webmanifest +++ /dev/null @@ -1 +0,0 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file