mirror of
https://github.com/perstarkse/minne.git
synced 2026-07-05 20:41:41 +02:00
feat: release build bundles assets
This commit is contained in:
@@ -4,7 +4,7 @@ use super::types::{analytics::Analytics, system_settings::SystemSettings, Stored
|
||||
use axum_session::{SessionConfig, SessionError, SessionStore};
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use futures::Stream;
|
||||
use std::ops::Deref;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
use surrealdb::{
|
||||
engine::any::{connect, Any},
|
||||
opt::auth::Root,
|
||||
@@ -15,6 +15,9 @@ use surrealdb::{
|
||||
pub struct SurrealDbClient {
|
||||
pub client: Surreal<Any>,
|
||||
}
|
||||
pub trait ProvidesDb {
|
||||
fn db(&self) -> &Arc<SurrealDbClient>;
|
||||
}
|
||||
|
||||
impl SurrealDbClient {
|
||||
/// # Initialize a new datbase client
|
||||
|
||||
@@ -4,6 +4,10 @@ pub use minijinja_contrib;
|
||||
pub use minijinja_embed;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait ProvidesTemplateEngine {
|
||||
fn template_engine(&self) -> &Arc<TemplateEngine>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum TemplateEngine {
|
||||
// Use AutoReload for debug builds (debug_assertions is true)
|
||||
|
||||
@@ -30,6 +30,8 @@ plotly = "0.12.1"
|
||||
surrealdb = "2.0.4"
|
||||
tower-http = { version = "0.6.2", features = ["fs"] }
|
||||
chrono-tz = "0.10.1"
|
||||
tower-serve-static = "0.1.1"
|
||||
include_dir = "0.7.4"
|
||||
|
||||
common = { path = "../common" }
|
||||
composite-retrieval = { path = "../composite-retrieval" }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
(function(){var g;htmx.defineExtension("sse",{init:function(e){g=e;if(htmx.createEventSource==undefined){htmx.createEventSource=t}},getSelectors:function(){return["[sse-connect]","[data-sse-connect]","[sse-swap]","[data-sse-swap]"]},onEvent:function(e,t){var r=t.target||t.detail.elt;switch(e){case"htmx:beforeCleanupElement":var n=g.getInternalData(r);var s=n.sseEventSource;if(s){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"nodeReplaced"});n.sseEventSource.close()}return;case"htmx:afterProcessNode":i(r)}}});function t(e){return new EventSource(e,{withCredentials:true})}function a(n){if(g.getAttributeValue(n,"sse-swap")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var t=g.getAttributeValue(n,"sse-swap");var r=t.split(",");for(var i=0;i<r.length;i++){const u=r[i].trim();const c=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(u,c);return}if(!g.triggerEvent(n,"htmx:sseBeforeMessage",e)){return}f(n,e.data);g.triggerEvent(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=c;a.addEventListener(u,c)}}if(g.getAttributeValue(n,"hx-trigger")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var o=g.getTriggerSpecs(n);o.forEach(function(t){if(t.trigger.slice(0,4)!=="sse:"){return}var r=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(t.trigger.slice(4),r)}htmx.trigger(n,t.trigger,e);htmx.trigger(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=r;a.addEventListener(t.trigger.slice(4),r)})}}function i(e,t){if(e==null){return null}if(g.getAttributeValue(e,"sse-connect")){var r=g.getAttributeValue(e,"sse-connect");if(r==null){return}n(e,r,t)}a(e)}function n(r,e,n){var s=htmx.createEventSource(e);s.onerror=function(e){g.triggerErrorEvent(r,"htmx:sseError",{error:e,source:s});if(l(r)){return}if(s.readyState===EventSource.CLOSED){n=n||0;n=Math.max(Math.min(n*2,128),1);var t=n*500;window.setTimeout(function(){i(r,n)},t)}};s.onopen=function(e){g.triggerEvent(r,"htmx:sseOpen",{source:s});if(n&&n>0){const t=r.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]");for(let e=0;e<t.length;e++){a(t[e])}n=0}};g.getInternalData(r).sseEventSource=s;var t=g.getAttributeValue(r,"sse-close");if(t){s.addEventListener(t,function(){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"message"});s.close()})}}function l(e){if(!g.bodyContains(e)){var t=g.getInternalData(e).sseEventSource;if(t!=undefined){g.triggerEvent(e,"htmx:sseClose",{source:t,type:"nodeMissing"});t.close();return true}}return false}function f(t,r){g.withExtensions(t,function(e){r=e.transformResponse(r,null,t)});var e=g.getSwapSpecification(t);var n=g.getTarget(t);g.swap(n,r,e)}function v(e){return g.getInternalData(e).sseEventSource!=null}})();
|
||||
+1
@@ -0,0 +1 @@
|
||||
(function(){var g;htmx.defineExtension("sse",{init:function(e){g=e;if(htmx.createEventSource==undefined){htmx.createEventSource=t}},getSelectors:function(){return["[sse-connect]","[data-sse-connect]","[sse-swap]","[data-sse-swap]"]},onEvent:function(e,t){var r=t.target||t.detail.elt;switch(e){case"htmx:beforeCleanupElement":var n=g.getInternalData(r);var s=n.sseEventSource;if(s){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"nodeReplaced"});n.sseEventSource.close()}return;case"htmx:afterProcessNode":i(r)}}});function t(e){return new EventSource(e,{withCredentials:true})}function a(n){if(g.getAttributeValue(n,"sse-swap")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var t=g.getAttributeValue(n,"sse-swap");var r=t.split(",");for(var i=0;i<r.length;i++){const u=r[i].trim();const c=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(u,c);return}if(!g.triggerEvent(n,"htmx:sseBeforeMessage",e)){return}f(n,e.data);g.triggerEvent(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=c;a.addEventListener(u,c)}}if(g.getAttributeValue(n,"hx-trigger")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var o=g.getTriggerSpecs(n);o.forEach(function(t){if(t.trigger.slice(0,4)!=="sse:"){return}var r=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(t.trigger.slice(4),r)}htmx.trigger(n,t.trigger,e);htmx.trigger(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=r;a.addEventListener(t.trigger.slice(4),r)})}}function i(e,t){if(e==null){return null}if(g.getAttributeValue(e,"sse-connect")){var r=g.getAttributeValue(e,"sse-connect");if(r==null){return}n(e,r,t)}a(e)}function n(r,e,n){var s=htmx.createEventSource(e);s.onerror=function(e){g.triggerErrorEvent(r,"htmx:sseError",{error:e,source:s});if(l(r)){return}if(s.readyState===EventSource.CLOSED){n=n||0;n=Math.max(Math.min(n*2,128),1);var t=n*500;window.setTimeout(function(){i(r,n)},t)}};s.onopen=function(e){g.triggerEvent(r,"htmx:sseOpen",{source:s});if(n&&n>0){const t=r.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]");for(let e=0;e<t.length;e++){a(t[e])}n=0}};g.getInternalData(r).sseEventSource=s;var t=g.getAttributeValue(r,"sse-close");if(t){s.addEventListener(t,function(){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"message"});s.close()})}}function l(e){if(!g.bodyContains(e)){var t=g.getInternalData(e).sseEventSource;if(t!=undefined){g.triggerEvent(e,"htmx:sseClose",{source:t,type:"nodeMissing"});t.close();return true}}return false}function f(t,r){g.withExtensions(t,function(e){r=e.transformResponse(r,null,t)});var e=g.getSwapSpecification(t);var n=g.getTarget(t);g.swap(n,r,e)}function v(e){return g.getInternalData(e).sseEventSource!=null}})();
|
||||
+1
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 790 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
@@ -0,0 +1,61 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@plugin "daisyui" {
|
||||
exclude: rootscrollbargutter;
|
||||
}
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@config '../tailwind.config.js';
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
|
||||
@view-transition {
|
||||
navigation: auto;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply font-satoshi;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
/* satoshi.css */
|
||||
@font-face {
|
||||
font-family: 'Satoshi';
|
||||
src: url('fonts/Satoshi-Variable.woff2') format('woff2'),
|
||||
url('fonts/Satoshi-Variable.woff') format('woff'),
|
||||
url('fonts/Satoshi-Variable.ttf') format('truetype');
|
||||
font-weight: 300 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Satoshi';
|
||||
src: url('fonts/Satoshi-VariableItalic.woff2') format('woff2'),
|
||||
url('fonts/Satoshi-VariableItalic.woff') format('woff'),
|
||||
url('fonts/Satoshi-VariableItalic.ttf') format('truetype');
|
||||
font-weight: 300 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "minne",
|
||||
"short_name": "minne",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
const initializeTheme = () => {
|
||||
const themeToggle = document.querySelector('.theme-controller');
|
||||
if (!themeToggle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Initialize theme from local storage or system preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const initialTheme = savedTheme ? savedTheme : (prefersDark ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-theme', initialTheme);
|
||||
themeToggle.checked = initialTheme === 'dark';
|
||||
|
||||
// Update theme and local storage on toggle
|
||||
themeToggle.addEventListener('change', () => {
|
||||
const theme = themeToggle.checked ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Run the initialization after the DOM is fully loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeTheme();
|
||||
});
|
||||
|
||||
// Reinitialize theme toggle after HTMX swaps
|
||||
document.addEventListener('htmx:afterSwap', initializeTheme);
|
||||
document.addEventListener('htmx:afterSettle', initializeTheme);
|
||||
@@ -1,48 +1,43 @@
|
||||
use axum_session::SessionStore;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use common::create_template_engine;
|
||||
use common::storage::db::SurrealDbClient;
|
||||
use common::utils::config::AppConfig;
|
||||
use common::utils::template_engine::TemplateEngine;
|
||||
use common::utils::template_engine::{ProvidesTemplateEngine, TemplateEngine};
|
||||
use common::{create_template_engine, storage::db::ProvidesDb};
|
||||
use std::sync::Arc;
|
||||
use surrealdb::engine::any::Any;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{OpenAIClientType, SessionStoreType};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HtmlState {
|
||||
pub db: Arc<SurrealDbClient>,
|
||||
pub openai_client: Arc<async_openai::Client<async_openai::config::OpenAIConfig>>,
|
||||
pub openai_client: Arc<OpenAIClientType>,
|
||||
pub templates: Arc<TemplateEngine>,
|
||||
pub session_store: Arc<SessionStore<SessionSurrealPool<Any>>>,
|
||||
pub session_store: Arc<SessionStoreType>,
|
||||
}
|
||||
|
||||
impl HtmlState {
|
||||
pub async fn new(config: &AppConfig) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
pub fn new_with_resources(
|
||||
db: Arc<SurrealDbClient>,
|
||||
openai_client: Arc<OpenAIClientType>,
|
||||
session_store: Arc<SessionStoreType>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let template_engine = create_template_engine!("templates");
|
||||
debug!("Template engine created for html_router.");
|
||||
|
||||
let surreal_db_client = Arc::new(
|
||||
SurrealDbClient::new(
|
||||
&config.surrealdb_address,
|
||||
&config.surrealdb_username,
|
||||
&config.surrealdb_password,
|
||||
&config.surrealdb_namespace,
|
||||
&config.surrealdb_database,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
surreal_db_client.ensure_initialized().await?;
|
||||
|
||||
let openai_client = Arc::new(async_openai::Client::new());
|
||||
|
||||
let session_store = Arc::new(surreal_db_client.create_session_store().await?);
|
||||
|
||||
let app_state = HtmlState {
|
||||
db: surreal_db_client.clone(),
|
||||
templates: Arc::new(template_engine),
|
||||
openai_client: openai_client.clone(),
|
||||
Ok(Self {
|
||||
db,
|
||||
openai_client,
|
||||
session_store,
|
||||
};
|
||||
|
||||
Ok(app_state)
|
||||
templates: Arc::new(template_engine),
|
||||
})
|
||||
}
|
||||
}
|
||||
impl ProvidesDb for HtmlState {
|
||||
fn db(&self) -> &Arc<SurrealDbClient> {
|
||||
&self.db
|
||||
}
|
||||
}
|
||||
impl ProvidesTemplateEngine for HtmlState {
|
||||
fn template_engine(&self) -> &Arc<TemplateEngine> {
|
||||
&self.templates
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ pub mod router_factory;
|
||||
pub mod routes;
|
||||
|
||||
use axum::{extract::FromRef, Router};
|
||||
use axum_session::Session;
|
||||
use axum_session::{Session, SessionStore};
|
||||
use axum_session_auth::AuthSession;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use common::storage::types::user::User;
|
||||
@@ -14,6 +14,8 @@ use surrealdb::{engine::any::Any, Surreal};
|
||||
|
||||
pub type AuthSessionType = AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>;
|
||||
pub type SessionType = Session<SessionSurrealPool<Any>>;
|
||||
pub type SessionStoreType = SessionStore<SessionSurrealPool<Any>>;
|
||||
pub type OpenAIClientType = async_openai::Client<async_openai::config::OpenAIConfig>;
|
||||
|
||||
/// Html routes
|
||||
pub fn html_routes<S>(app_state: &HtmlState) -> Router<S>
|
||||
|
||||
@@ -3,31 +3,28 @@ use axum::{
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use surrealdb::engine::any::Any;
|
||||
|
||||
use common::storage::types::analytics::Analytics;
|
||||
use common::storage::{db::ProvidesDb, types::analytics::Analytics};
|
||||
|
||||
use crate::html_state::HtmlState;
|
||||
use crate::SessionType;
|
||||
|
||||
pub async fn analytics_middleware(
|
||||
State(state): State<HtmlState>,
|
||||
session: axum_session::Session<SessionSurrealPool<Any>>,
|
||||
/// Middleware to count unique visitors and page loads
|
||||
pub async fn analytics_middleware<S>(
|
||||
State(state): State<S>,
|
||||
session: SessionType,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// Get the path from the request
|
||||
) -> Response
|
||||
where
|
||||
S: ProvidesDb + Clone + Send + Sync + 'static,
|
||||
{
|
||||
let path = request.uri().path();
|
||||
|
||||
// Only count if it's a main page request (not assets or other resources)
|
||||
if !path.starts_with("/assets") && !path.starts_with("/_next") && !path.contains('.') {
|
||||
if !path.starts_with("/assets") && !path.contains('.') {
|
||||
if !session.get::<bool>("counted_visitor").unwrap_or(false) {
|
||||
let _ = Analytics::increment_visitors(&state.db).await;
|
||||
let _ = Analytics::increment_visitors(state.db()).await;
|
||||
session.set("counted_visitor", true);
|
||||
}
|
||||
|
||||
let _ = Analytics::increment_page_loads(&state.db).await;
|
||||
let _ = Analytics::increment_page_loads(state.db()).await;
|
||||
}
|
||||
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::html_state::HtmlState;
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Response},
|
||||
Extension,
|
||||
};
|
||||
use common::error::AppError;
|
||||
use common::{error::AppError, utils::template_engine::ProvidesTemplateEngine};
|
||||
use minijinja::{context, Value};
|
||||
use serde::Serialize;
|
||||
use tracing::error;
|
||||
@@ -106,58 +105,45 @@ impl IntoResponse for TemplateResponse {
|
||||
}
|
||||
}
|
||||
|
||||
struct TemplateStateWrapper {
|
||||
state: HtmlState,
|
||||
template_response: TemplateResponse,
|
||||
}
|
||||
pub async fn with_template_response<S>(State(state): State<S>, response: Response) -> Response
|
||||
where
|
||||
S: ProvidesTemplateEngine + Clone + Send + Sync + 'static,
|
||||
{
|
||||
if let Some(template_response) = response.extensions().get::<TemplateResponse>().cloned() {
|
||||
let template_engine = state.template_engine();
|
||||
|
||||
impl IntoResponse for TemplateStateWrapper {
|
||||
fn into_response(self) -> Response {
|
||||
let template_engine = &self.state.templates;
|
||||
|
||||
match &self.template_response.template_kind {
|
||||
match &template_response.template_kind {
|
||||
TemplateKind::Full(name) => {
|
||||
match template_engine.render(name, &self.template_response.context) {
|
||||
match template_engine.render(name, &template_response.context) {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(e) => {
|
||||
error!("Failed to render template: {:?}", e);
|
||||
error!("Failed to render template '{}': {:?}", name, e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, fallback_error()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
TemplateKind::Partial(template, block) => {
|
||||
match template_engine.render_block(template, block, &self.template_response.context)
|
||||
{
|
||||
match template_engine.render_block(template, block, &template_response.context) {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(e) => {
|
||||
error!("Failed to render block: {:?}", e);
|
||||
error!("Failed to render block '{}/{}': {:?}", template, block, e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, fallback_error()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
TemplateKind::Error(status) => {
|
||||
match template_engine.render("errors/error.html", &self.template_response.context) {
|
||||
match template_engine.render("errors/error.html", &template_response.context) {
|
||||
Ok(html) => (*status, Html(html)).into_response(),
|
||||
Err(_) => (*status, fallback_error()).into_response(),
|
||||
Err(e) => {
|
||||
error!("Failed to render error template: {:?}", e);
|
||||
(*status, fallback_error()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
TemplateKind::Redirect(path) => {
|
||||
(StatusCode::OK, [(axum_htmx::HX_REDIRECT, path.clone())], "").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn with_template_response(
|
||||
State(state): State<HtmlState>,
|
||||
response: Response,
|
||||
) -> Response {
|
||||
if let Some(template_response) = response.extensions().get::<TemplateResponse>().cloned() {
|
||||
TemplateStateWrapper {
|
||||
state,
|
||||
template_response,
|
||||
}
|
||||
.into_response()
|
||||
} else {
|
||||
response
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use axum_session_auth::{AuthConfig, AuthSessionLayer};
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use common::storage::types::user::User;
|
||||
use surrealdb::{engine::any::Any, Surreal};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::{
|
||||
html_state::HtmlState,
|
||||
@@ -18,6 +17,27 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! create_asset_service {
|
||||
// Takes the relative path to the asset directory
|
||||
($relative_path:expr) => {{
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let crate_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let assets_path = crate_dir.join($relative_path);
|
||||
tracing::debug!("Assets: Serving from filesystem: {:?}", assets_path);
|
||||
tower_http::services::ServeDir::new(assets_path)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
tracing::debug!("Assets: Serving embedded directory");
|
||||
static ASSETS_DIR: include_dir::Dir<'static> =
|
||||
include_dir::include_dir!("$CARGO_MANIFEST_DIR/assets");
|
||||
tower_serve_static::ServeDir::new(&ASSETS_DIR)
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub struct RouterFactory<S> {
|
||||
app_state: HtmlState,
|
||||
public_routers: Vec<Router<S>>,
|
||||
@@ -108,27 +128,35 @@ where
|
||||
}
|
||||
|
||||
// Add public assets to public router
|
||||
if let Some(assets) = self.public_assets_config {
|
||||
public_router =
|
||||
public_router.nest_service(&assets.path, ServeDir::new(assets.directory));
|
||||
if let Some(assets_config) = self.public_assets_config {
|
||||
// Call the macro using the stored relative directory path
|
||||
let asset_service = create_asset_service!(&assets_config.directory);
|
||||
// Nest the resulting service under the stored URL path
|
||||
public_router = public_router.nest_service(&assets_config.path, asset_service);
|
||||
}
|
||||
|
||||
// Start with an empty protected router
|
||||
let mut protected_router = Router::new();
|
||||
|
||||
// Merge all protected routers
|
||||
// Check if there are any protected routers
|
||||
let has_protected_routes =
|
||||
!self.protected_routers.is_empty() || !self.nested_protected_routes.is_empty();
|
||||
|
||||
// Merge root-level protected routers
|
||||
for router in self.protected_routers {
|
||||
protected_router = protected_router.merge(router);
|
||||
}
|
||||
|
||||
// Add nested protected routes
|
||||
// Nest protected routers
|
||||
for (path, router) in self.nested_protected_routes {
|
||||
protected_router = protected_router.nest(&path, router);
|
||||
}
|
||||
|
||||
// Apply auth middleware to all protected routes
|
||||
let protected_router =
|
||||
protected_router.route_layer(from_fn_with_state(self.app_state.clone(), require_auth));
|
||||
// Apply auth middleware
|
||||
if has_protected_routes {
|
||||
protected_router = protected_router
|
||||
.route_layer(from_fn_with_state(self.app_state.clone(), require_auth));
|
||||
}
|
||||
|
||||
// Combine public and protected routes
|
||||
let mut router = Router::new().merge(public_router).merge(protected_router);
|
||||
@@ -142,11 +170,11 @@ where
|
||||
router
|
||||
.layer(from_fn_with_state(
|
||||
self.app_state.clone(),
|
||||
analytics_middleware,
|
||||
analytics_middleware::<HtmlState>,
|
||||
))
|
||||
.layer(map_response_with_state(
|
||||
self.app_state.clone(),
|
||||
with_template_response,
|
||||
with_template_response::<HtmlState>,
|
||||
))
|
||||
.layer(
|
||||
AuthSessionLayer::<User, String, SessionSurrealPool<Any>, Surreal<Any>>::new(Some(
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends "body_base.html" %}
|
||||
{% block main %}
|
||||
<main class="flex justify-center grow mt-2 sm:mt-4 pb-10">
|
||||
<div class="container">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-4">
|
||||
<!-- Documentation Menu -->
|
||||
<aside class="bg-base-200 rounded-lg p-4">
|
||||
{% include "documentation/menu.html" %}
|
||||
</aside>
|
||||
<!-- Main Content -->
|
||||
|
||||
<article
|
||||
class="prose prose-sm md:prose-base prose-h1:mb-2 prose-h2:my-2 prose-p:my-2 prose-ul:my-2 prose-pre:my-2 flex mx-auto justify-center flex-col">
|
||||
{% block article %}
|
||||
{% endblock %}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -1,58 +0,0 @@
|
||||
{% extends 'documentation/base.html' %}
|
||||
{% block article %}
|
||||
<h1>Get Started with Minne</h1>
|
||||
<p>Minne offers two installation options to suit your needs:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Hosted Version:</strong> Enjoy a hassle‐free experience by signing up for the ready‐to‐use service.
|
||||
Simply navigate to <code>/signup</code> to create an account.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Self-Hosted:</strong> Gain full control by running Minne on your own infrastructure. Visit
|
||||
<a href="https://github.com/perstarkse/minne">GitHub</a> to download the latest release. After extracting the
|
||||
release, open the <code>config.yaml</code> file and set the following configurations:
|
||||
</li>
|
||||
</ol>
|
||||
<pre class="overflow-x-auto text-sm">
|
||||
<code class="break-words whitespace-pre-wrap">
|
||||
OPENAI_API_KEY: your_api_key
|
||||
DB_ADDRESS: your_db_address
|
||||
DB_USER: your_db_user
|
||||
DB_PASSWORD: your_db_password
|
||||
</code>
|
||||
</pre>
|
||||
<p>The database settings relate to a running instance of SurrealDB. You can opt for their cloud solution or run your
|
||||
own instance.</p>
|
||||
|
||||
<p>Once your configuration is complete, start both the server and the worker. They can be hosted on separate
|
||||
machines, with different resource requirements:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Server:</strong> Lightweight. A minimum of 1 core and 256MB of RAM is recommended.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Worker:</strong> Handles content parsing and creation of database entities. It's recommended to allocate at
|
||||
least two cores and 1024 MB RAM. It will run on less but might run into constraints depending on the content being
|
||||
parsed.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>After launching the services, navigate to <code><your_url>:3000/signup</code> to register. The first
|
||||
account created will automatically receive admin permissions, allowing you to later disable further registrations
|
||||
via the <code>/admin</code> page if desired.</p>
|
||||
|
||||
<p>From the homepage (<code>/</code>), you can:</p>
|
||||
|
||||
<ul>
|
||||
<li>Submit content, including files, videos, and URLs for ingestion.</li>
|
||||
<li>Monitor job statuses and manage your existing content.</li>
|
||||
<li>Search your content or start a chat conversation for assistance.</li>
|
||||
</ul>
|
||||
|
||||
<p>Visit the <code>/knowledge</code> page to view your content organized by different sections. This page also
|
||||
provides a visual demonstration of the graph database structure, enhancing your understanding of content
|
||||
relationships.</p>
|
||||
|
||||
<p>This streamlined setup ensures intuitive onboarding while offering robust customization options. Whether you are
|
||||
a novice or an advanced user, Minne is designed to deliver a smooth experience and reliable performance.</p>
|
||||
{% endblock %}
|
||||
@@ -1,26 +0,0 @@
|
||||
{% extends "documentation/base.html" %}
|
||||
{% block article %}
|
||||
<h1 class="text-3xl mb-2">Documentation</h1>
|
||||
<p>
|
||||
Personalised Knowledge Management (PKM) is a system designed to help individuals organise, store, and retrieve
|
||||
information effectively. It empowers users to create a personalised workflow for managing knowledge, enabling
|
||||
better decision-making and productivity.
|
||||
</p>
|
||||
<p>
|
||||
This documentation will guide you through the core concepts, tools, and best practices for building and
|
||||
maintaining your own PKM system.
|
||||
</p>
|
||||
<div class="card bg-base-200 rounded-lg shadow-md">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title not-prose">Getting Started</h3>
|
||||
<p>
|
||||
To begin, explore the sections in the navigation menu. Each section provides detailed insights into
|
||||
different
|
||||
aspects of PKM, from foundational principles to advanced techniques.
|
||||
</p>
|
||||
<div class="card-actions">
|
||||
<a href="/documentation/quick-start" class="btn btn-primary text-primary-content" hx-boost="true">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,28 +0,0 @@
|
||||
<ul class="menu bg-base-200 rounded-box w-full ">
|
||||
<li><a hx-boost="true" class="{{'menu-active' if current_path=='/index' }}" href="/documentation">Start</a></li>
|
||||
<li><a hx-boost="true" class="{{'menu-active' if current_path=='/get-started' }}"
|
||||
href="/documentation/get-started">Get Started</a></li>
|
||||
<li><a hx-boost="true" class="{{'menu-active' if current_path=='/mobile-friendly' }}"
|
||||
href="/documentation/mobile-friendly">Mobile friendly</a></li>
|
||||
<li><a hx-boost="true" class="{{'menu-active' if current_path=='/privacy-policy' }}"
|
||||
href="/documentation/privacy-policy">Privacy Policy</a></li>
|
||||
<li>
|
||||
<details open>
|
||||
<summary>Core Concepts</summary>
|
||||
<ul>
|
||||
<li><a hx-boost="true" href="/documentation/submenu1">What is PKM?</a></li>
|
||||
<li><a hx-boost="true" href="/documentation/submenu2">Benefits of PKM</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary>Tools & Techniques</summary>
|
||||
<ul>
|
||||
<li><a hx-boost="true" href="/documentation/tools">Tools for PKM</a></li>
|
||||
<li><a hx-boost="true" href="/documentation/techniques">Effective Techniques</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li><a hx-boost="true" href="/documentation/faq">FAQ</a></li>
|
||||
</ul>
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends 'documentation/base.html' %}
|
||||
{% block article %}
|
||||
<h1>Mobile Friendly Ingression: How to Submit Content from iOS to Minne</h1>
|
||||
<p>Minne is built with simplicity in mind. Whether you wish to save a file, capture a thought, or share a page,
|
||||
submitting content is effortless. Our server provides API access that enables users to perform actions using a
|
||||
personalized API key.</p>
|
||||
|
||||
<p>An iOS shortcut has been developed to streamline the process of sending content. To begin, navigate to
|
||||
<code>/account</code> and generate an API key. Once created, you will see an option to download the iOS shortcut.
|
||||
</p>
|
||||
|
||||
<p>After downloading the shortcut, update the "Get response from URL" authentication headers with your API key. If
|
||||
you are self-hosting, ensure the URL is adjusted accordingly.</p>
|
||||
|
||||
<p>The shortcut integrates seamlessly with iOS. When you "share with Minne," you will be prompted to provide
|
||||
instructions to the AI and to either choose an existing category or create a new one for your submission.</p>
|
||||
|
||||
<p>While an Android solution is in the works, for now you can add the web app to your home screen as a Progressive
|
||||
Web App (PWA) for a similar mobile-friendly experience.</p>
|
||||
{% endblock %}
|
||||
@@ -1,27 +0,0 @@
|
||||
{% extends 'documentation/base.html' %}
|
||||
{% block article %}
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>We value your privacy and are committed to protecting your personal information. This policy
|
||||
outlines how we handle your data and ensures transparency in our practices.</p>
|
||||
|
||||
<h2>Data Collection</h2>
|
||||
<p>We only collect data that is necessary for the functionality of our services. Any data you upload to
|
||||
our site remains your property and will not be shared with third parties unless required by law.</p>
|
||||
|
||||
<h2>Cookies</h2>
|
||||
<p>We do not use cookies for tracking or analytics. The cookies we employ are strictly for session
|
||||
management, ensuring a smooth and secure user experience.</p>
|
||||
|
||||
<h2>No Unnecessary Data Extraction</h2>
|
||||
<p>We believe that unnecessary data extraction is unethical and a poor practice. We only collect the
|
||||
minimum amount of data required to provide our services effectively, ensuring your privacy is respected at all
|
||||
times.</p>
|
||||
|
||||
<h2>Your Rights</h2>
|
||||
<p>You have the right to access, modify, or delete your data at any time. If you have any concerns
|
||||
about how your data is handled, please contact us.</p>
|
||||
|
||||
<h2>Changes to This Policy</h2>
|
||||
<p>We may update this privacy policy from time to time. Any changes will be posted on this page, and we
|
||||
encourage you to review it periodically.</p>
|
||||
{% endblock %}
|
||||
+20
-2
@@ -21,8 +21,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Get config
|
||||
let config = get_config()?;
|
||||
|
||||
// Set up server components
|
||||
let html_state = HtmlState::new(&config).await?;
|
||||
// Set up router states
|
||||
let db = Arc::new(
|
||||
SurrealDbClient::new(
|
||||
&config.surrealdb_address,
|
||||
&config.surrealdb_username,
|
||||
&config.surrealdb_password,
|
||||
&config.surrealdb_namespace,
|
||||
&config.surrealdb_database,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
// Ensure db is initialized
|
||||
db.ensure_initialized().await?;
|
||||
|
||||
let session_store = Arc::new(db.create_session_store().await?);
|
||||
let openai_client = Arc::new(async_openai::Client::new());
|
||||
|
||||
let html_state = HtmlState::new_with_resources(db, openai_client, session_store)?;
|
||||
|
||||
let api_state = ApiState {
|
||||
db: html_state.db.clone(),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use api_router::{api_routes_v1, api_state::ApiState};
|
||||
use axum::{extract::FromRef, Router};
|
||||
use common::utils::config::get_config;
|
||||
use common::{storage::db::SurrealDbClient, utils::config::get_config};
|
||||
use html_router::{html_routes, html_state::HtmlState};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
@@ -18,7 +20,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = get_config()?;
|
||||
|
||||
// Set up router states
|
||||
let html_state = HtmlState::new(&config).await?;
|
||||
let db = Arc::new(
|
||||
SurrealDbClient::new(
|
||||
&config.surrealdb_address,
|
||||
&config.surrealdb_username,
|
||||
&config.surrealdb_password,
|
||||
&config.surrealdb_namespace,
|
||||
&config.surrealdb_database,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
// Ensure db is initialized
|
||||
db.ensure_initialized().await?;
|
||||
|
||||
let session_store = Arc::new(db.create_session_store().await?);
|
||||
let openai_client = Arc::new(async_openai::Client::new());
|
||||
|
||||
let html_state = HtmlState::new_with_resources(db, openai_client, session_store)?;
|
||||
|
||||
let api_state = ApiState {
|
||||
db: html_state.db.clone(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user