Compare commits
34 Commits
4c98dd93ad
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ad77f7ef6 | |||
| 3c46cb7c5b | |||
| 6c5c85ec4c | |||
| d7de80e7e0 | |||
| 9d88936b4a | |||
| 83fbb16b6b | |||
| fd5a187258 | |||
| 2b7ba3c96b | |||
| c1a00d34ab | |||
| 0afc51f576 | |||
| 61fd356846 | |||
| 360c9edcef | |||
| 42d6910ae7 | |||
| 58c929dd38 | |||
| 478d5f3c17 | |||
| bb35206fff | |||
| 0787525134 | |||
| f6ae86617c | |||
| e483bf73e7 | |||
| b12767e0fa | |||
| 588e18bbd2 | |||
| aede9a796e | |||
| 22577fe3fb | |||
| ec7ab8aadc | |||
| bd6d8d78fc | |||
| f8112f897e | |||
| 40fe69ed65 | |||
| 724e6a8ecd | |||
| ee8797c142 | |||
| 40a7d9018c | |||
| fb1cb8e834 | |||
| 493cd71d17 | |||
| 277a65c6c7 | |||
| 7778a94cf5 |
@@ -0,0 +1,112 @@
|
||||
name: Production Deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- refactored
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.nciphered.com
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build and Push Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
git.nciphered.com/shaamilahmed/htmx:latest
|
||||
git.nciphered.com/shaamilahmed/htmx:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy-droplet:
|
||||
name: Deploy to DigitalOcean Droplet
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ secrets.DEPLOY_TARGET == 'droplet' }}
|
||||
steps:
|
||||
- name: Executing remote SSH commands to deploy
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.DROPLET_HOST }}
|
||||
username: ${{ secrets.DROPLET_USER }}
|
||||
key: ${{ secrets.DROPLET_SSH_KEY }}
|
||||
script: |
|
||||
# Authenticate with Gitea registry on Droplet
|
||||
docker login -u "${{ github.actor }}" -p "${{ secrets.REGISTRY_TOKEN }}" git.nciphered.com
|
||||
|
||||
# Pull latest image
|
||||
docker pull git.nciphered.com/shaamilahmed/htmx:latest
|
||||
|
||||
# Stop existing container
|
||||
docker rm -f ${{ secrets.APP_CONTAINER_NAME }} || true
|
||||
|
||||
# Run container with user-defined docker flags (e.g., --network, -p)
|
||||
docker run -d \
|
||||
--name ${{ secrets.APP_CONTAINER_NAME }} \
|
||||
${{ secrets.DOCKER_RUN_FLAGS }} \
|
||||
-e DATABASE_URL="${{ secrets.DATABASE_URL_DROPLET }}" \
|
||||
-e DATABASE_NAME="${{ secrets.DATABASE_NAME }}" \
|
||||
-e JWT_SECRET="${{ secrets.JWT_SECRET }}" \
|
||||
-e HOST="0.0.0.0" \
|
||||
-e PORT="${{ secrets.APP_PORT }}" \
|
||||
--restart unless-stopped \
|
||||
git.nciphered.com/shaamilahmed/htmx:latest
|
||||
|
||||
|
||||
deploy-cloudrun:
|
||||
name: Deploy to Google Cloud Run
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ secrets.DEPLOY_TARGET == 'cloudrun' }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Authenticate with Google Cloud
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_SA_KEY }}
|
||||
|
||||
- name: Set up Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@v2
|
||||
|
||||
- name: Configure Docker Authentication
|
||||
run: |
|
||||
gcloud auth configure-docker asia-southeast1-docker.pkg.dev --quiet
|
||||
|
||||
- name: Tag and Push Image to Artifact Registry
|
||||
run: |
|
||||
# Build/Tag for Google Artifact Registry
|
||||
docker tag git.nciphered.com/shaamilahmed/htmx:latest asia-southeast1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/stick/app:latest
|
||||
docker push asia-southeast1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/stick/app:latest
|
||||
|
||||
- name: Deploy to Google Cloud Run
|
||||
uses: google-github-actions/deploy-cloudrun@v2
|
||||
with:
|
||||
service: stick-app
|
||||
image: asia-southeast1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/stick/app:latest
|
||||
region: asia-southeast1
|
||||
env_vars: |
|
||||
DATABASE_URL=${{ secrets.DATABASE_URL_CLOUDRUN }}
|
||||
DATABASE_NAME=${{ secrets.DATABASE_NAME }}
|
||||
JWT_SECRET=${{ secrets.JWT_SECRET }}
|
||||
HOST=0.0.0.0
|
||||
PORT=3007
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
target/
|
||||
node_modules/
|
||||
.antigravitycli/
|
||||
static/tailwind.css
|
||||
|
||||
@@ -200,3 +200,35 @@ pub async fn delete_task_handler(
|
||||
Ok(Redirect::to("/tasks"))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment to a Cloud Host (DigitalOcean Droplet)
|
||||
|
||||
For production deployments (such as to a DigitalOcean Droplet), the application is fully containerized and configured via standard environment variables.
|
||||
|
||||
The application is completely decoupled from the underlying hosting, networking, and database infrastructure. You are responsible for provisioning the database and supplying the connection configuration.
|
||||
|
||||
### 1. Build the Application Container
|
||||
Build the application Docker image:
|
||||
```bash
|
||||
docker build -t stick-app .
|
||||
```
|
||||
|
||||
### 2. Deploy the Application Container
|
||||
Run the container on your target Docker network, providing the connection details to your pre-existing MongoDB database container through environment variables:
|
||||
```bash
|
||||
docker run -d \
|
||||
--name stick-app \
|
||||
--network your-docker-network \
|
||||
-p 80:3007 \
|
||||
-e DATABASE_URL="mongodb://your-mongodb-host:27017" \
|
||||
-e DATABASE_NAME="stick_db" \
|
||||
-e JWT_SECRET="your_secure_production_jwt_signing_key_at_least_32_chars_long" \
|
||||
-e HOST="0.0.0.0" \
|
||||
-e PORT="3007" \
|
||||
--restart unless-stopped \
|
||||
stick-app
|
||||
```
|
||||
*Note: Adjust the port mapping (`-p`), container name, network name, and `DATABASE_URL` environment variable as necessary to integrate with your custom proxy or container infrastructure.*
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use axum::{
|
||||
extract::{Query, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
http::StatusCode,
|
||||
Form,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use crate::common::errors::AppError;
|
||||
@@ -73,3 +74,72 @@ pub async fn get_audit_logs(
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PurgeRequest {
|
||||
pub retention_days: i64,
|
||||
}
|
||||
|
||||
pub async fn purge_audit_logs(
|
||||
user: AuthenticatedUser,
|
||||
State(audit_repo): State<MongoAuditRepository>,
|
||||
logger: crate::audit::AuditLogger,
|
||||
Form(input): Form<PurgeRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
if !user.is_admin {
|
||||
return Err(AppError::Forbidden("Only administrators can purge audit logs".to_string()));
|
||||
}
|
||||
|
||||
if input.retention_days < 1 {
|
||||
return Err(AppError::BadRequest("Retention window must be at least 1 day".to_string()));
|
||||
}
|
||||
|
||||
let threshold = chrono::Utc::now() - chrono::Duration::days(input.retention_days);
|
||||
|
||||
// 1. Fetch logs that are going to be purged
|
||||
let logs_to_purge = audit_repo.find_older_than(threshold).await?;
|
||||
|
||||
if logs_to_purge.is_empty() {
|
||||
return Err(AppError::BadRequest("No audit logs found older than the retention period".to_string()));
|
||||
}
|
||||
|
||||
// 2. Serialize logs to JSON
|
||||
let json_data = serde_json::to_vec_pretty(&logs_to_purge)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to serialize logs: {}", e)))?;
|
||||
|
||||
// 3. Delete from the database
|
||||
let deleted_count = audit_repo.delete_older_than(threshold).await?;
|
||||
|
||||
// 4. Log the purge action itself (which stays because it was created now, retention is >= 1 day)
|
||||
logger.log(
|
||||
"Purge",
|
||||
"System",
|
||||
None,
|
||||
Some(format!(
|
||||
"Purged {} logs older than {} days (Threshold: {})",
|
||||
deleted_count, input.retention_days, threshold.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
)),
|
||||
Some(serde_json::json!({
|
||||
"retention_days": input.retention_days,
|
||||
"deleted_count": deleted_count,
|
||||
"threshold": threshold,
|
||||
})),
|
||||
).await;
|
||||
|
||||
// 5. Construct the attachment response to force download
|
||||
let filename = format!(
|
||||
"audit_logs_purge_{}.json",
|
||||
chrono::Utc::now().format("%Y%m%d_%H%M%S")
|
||||
);
|
||||
|
||||
let response = Response::builder()
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
axum::http::header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{}\"", filename),
|
||||
)
|
||||
.body(axum::body::Body::from(json_data))
|
||||
.map_err(|e| AppError::Internal(format!("Failed to construct response: {}", e)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
+4
-1
@@ -2,7 +2,7 @@ pub mod models;
|
||||
pub mod repository;
|
||||
pub mod handlers;
|
||||
|
||||
use axum::{routing::get, Router, extract::FromRef};
|
||||
use axum::{routing::{get, post}, Router, extract::FromRef};
|
||||
use crate::common::config::Config;
|
||||
use crate::audit::repository::MongoAuditRepository;
|
||||
use crate::auth::extractors::AuthenticatedUser;
|
||||
@@ -14,13 +14,16 @@ pub fn router<S>() -> Router<S>
|
||||
where
|
||||
Config: axum::extract::FromRef<S>,
|
||||
MongoAuditRepository: axum::extract::FromRef<S>,
|
||||
mongodb::Database: axum::extract::FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
Router::new()
|
||||
.route("/auth/audit", get(handlers::get_audit_logs))
|
||||
.route("/auth/audit/purge", post(handlers::purge_audit_logs))
|
||||
}
|
||||
|
||||
/// Lower-level helper function to write audit log entries directly to the database.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn log_action(
|
||||
db: &mongodb::Database,
|
||||
user_opt: Option<&AuthenticatedUser>,
|
||||
|
||||
+37
-12
@@ -57,39 +57,35 @@ impl MongoAuditRepository {
|
||||
|
||||
if let Some(ent_id_str) = entity_id {
|
||||
let trimmed = ent_id_str.trim();
|
||||
if !trimmed.is_empty() {
|
||||
if let Ok(oid) = ObjectId::parse_str(trimmed) {
|
||||
filter.insert("entity_id", oid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Date filters range
|
||||
let mut date_query = doc! {};
|
||||
|
||||
if let Some(start) = start_date {
|
||||
let trimmed = start.trim();
|
||||
if !trimmed.is_empty() {
|
||||
if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(trimmed, "%Y-%m-%d") {
|
||||
if let Some(naive_datetime) = naive_date.and_hms_opt(0, 0, 0) {
|
||||
if let Some(naive_datetime) = chrono::NaiveDate::parse_from_str(trimmed, "%Y-%m-%d")
|
||||
.ok()
|
||||
.and_then(|d| d.and_hms_opt(0, 0, 0))
|
||||
{
|
||||
let dt = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(naive_datetime, chrono::Utc);
|
||||
date_query.insert("$gte", mongodb::bson::DateTime::from_chrono(dt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(end) = end_date {
|
||||
let trimmed = end.trim();
|
||||
if !trimmed.is_empty() {
|
||||
if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(trimmed, "%Y-%m-%d") {
|
||||
if let Some(naive_datetime) = naive_date.and_hms_opt(23, 59, 59) {
|
||||
if let Some(naive_datetime) = chrono::NaiveDate::parse_from_str(trimmed, "%Y-%m-%d")
|
||||
.ok()
|
||||
.and_then(|d| d.and_hms_opt(23, 59, 59))
|
||||
{
|
||||
let dt = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(naive_datetime, chrono::Utc);
|
||||
date_query.insert("$lte", mongodb::bson::DateTime::from_chrono(dt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !date_query.is_empty() {
|
||||
filter.insert("timestamp", date_query);
|
||||
@@ -104,4 +100,33 @@ impl MongoAuditRepository {
|
||||
let logs = cursor.try_collect::<Vec<AuditLog>>().await?;
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
pub async fn find_older_than(
|
||||
&self,
|
||||
threshold: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<Vec<AuditLog>, AppError> {
|
||||
use futures::TryStreamExt;
|
||||
let collection = self.db.collection::<AuditLog>("audit_logs");
|
||||
let filter = doc! {
|
||||
"timestamp": { "$lt": mongodb::bson::DateTime::from_chrono(threshold) }
|
||||
};
|
||||
let find_options = mongodb::options::FindOptions::builder()
|
||||
.sort(doc! { "timestamp": 1 })
|
||||
.build();
|
||||
let cursor = collection.find(filter).with_options(find_options).await?;
|
||||
let logs = cursor.try_collect::<Vec<AuditLog>>().await?;
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
pub async fn delete_older_than(
|
||||
&self,
|
||||
threshold: chrono::DateTime<chrono::Utc>,
|
||||
) -> Result<u64, AppError> {
|
||||
let collection = self.db.collection::<AuditLog>("audit_logs");
|
||||
let filter = doc! {
|
||||
"timestamp": { "$lt": mongodb::bson::DateTime::from_chrono(threshold) }
|
||||
};
|
||||
let result = collection.delete_many(filter).await?;
|
||||
Ok(result.deleted_count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,13 @@ pub struct LoggingTemplate {
|
||||
pub authenticated: bool,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "docs/tables.html")]
|
||||
pub struct TablesTemplate {
|
||||
pub username: String,
|
||||
pub authenticated: bool,
|
||||
}
|
||||
|
||||
// Define individual handlers
|
||||
pub async fn index_handler(
|
||||
user_opt: Option<AuthenticatedUser>,
|
||||
@@ -221,3 +228,10 @@ pub async fn logging_handler(
|
||||
let (authenticated, username) = get_session_info(user_opt);
|
||||
Ok(HtmlTemplate(LoggingTemplate { username, authenticated }))
|
||||
}
|
||||
|
||||
pub async fn tables_handler(
|
||||
user_opt: Option<AuthenticatedUser>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let (authenticated, username) = get_session_info(user_opt);
|
||||
Ok(HtmlTemplate(TablesTemplate { username, authenticated }))
|
||||
}
|
||||
|
||||
+2
-1
@@ -5,7 +5,7 @@ use self::handlers::{
|
||||
index_handler, buttons_handler, inputs_handler, date_time_handler,
|
||||
combobox_handler, toggles_handler, modals_handler, sheets_handler,
|
||||
tabs_accordion_handler, visuals_handler, scrollbars_handler, feedback_handler,
|
||||
logging_handler,
|
||||
logging_handler, tables_handler,
|
||||
};
|
||||
|
||||
pub fn router<S>() -> Router<S>
|
||||
@@ -29,4 +29,5 @@ where
|
||||
.route("/docs/scrollbars", get(scrollbars_handler))
|
||||
.route("/docs/feedback", get(feedback_handler))
|
||||
.route("/docs/logging", get(logging_handler))
|
||||
.route("/docs/tables", get(tables_handler))
|
||||
}
|
||||
|
||||
+10
-6
@@ -76,12 +76,12 @@ html, body {
|
||||
/* Custom Scrollbars */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--border)) transparent;
|
||||
scrollbar-color: rgba(100, 116, 139, 0.5) transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
@@ -90,18 +90,22 @@ html, body {
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
background: rgba(100, 116, 139, 0.5);
|
||||
border-radius: 9999px;
|
||||
border: 1px solid transparent;
|
||||
background-clip: padding-box;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--muted-foreground));
|
||||
background: rgba(148, 163, 184, 0.8);
|
||||
}
|
||||
|
||||
.overflow-y-overlay {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
/* Custom Date/Time Inputs */
|
||||
|
||||
@@ -5,6 +5,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (!modal) return;
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
const content = modal.querySelector('.modal-content');
|
||||
if (content) {
|
||||
@@ -24,6 +25,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}, 300); // Wait for transition animation
|
||||
};
|
||||
|
||||
@@ -626,17 +628,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// --- SIDEBAR NAVIGATION ACTIVE HIGHLIGHTING ---
|
||||
const currentPath = window.location.pathname;
|
||||
const sidebar = document.getElementById('wiki-sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.querySelectorAll('a[data-wiki-path]').forEach(link => {
|
||||
document.querySelectorAll('a[data-wiki-path]').forEach(link => {
|
||||
const path = link.getAttribute('data-wiki-path');
|
||||
if (currentPath === path) {
|
||||
link.className = 'flex items-center px-3 py-2 text-xs font-semibold rounded-lg bg-indigo-600/20 text-indigo-400 border border-indigo-500/10 transition';
|
||||
} else {
|
||||
link.className = 'flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-accent hover:text-accent-foreground transition';
|
||||
link.className = 'flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- CUSTOM SELECT DROPDOWN LOGIC ---
|
||||
document.addEventListener('click', (event) => {
|
||||
|
||||
-2799
File diff suppressed because it is too large
Load Diff
+48
-13
@@ -13,6 +13,9 @@
|
||||
<p class="text-slate-400 text-sm mt-1">Review, search, and audit system activities and state changes</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button data-modal-target="purge-modal" class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all px-4 py-2.5 bg-rose-950/40 border border-rose-900/30 hover:bg-rose-950/60 text-rose-400 shadow-md shadow-rose-950/10">
|
||||
Purge Logs
|
||||
</button>
|
||||
<a href="/auth/users" class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all px-4 py-2.5 bg-slate-900 border border-slate-800 hover:bg-slate-800 text-slate-300">
|
||||
Manage Users
|
||||
</a>
|
||||
@@ -104,17 +107,16 @@
|
||||
</div>
|
||||
|
||||
<!-- Logs Table -->
|
||||
<div class="bg-[#1e293b]/40 backdrop-blur-xl border border-slate-900 rounded-3xl shadow-2xl overflow-hidden mb-8">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-800 text-left text-sm text-slate-300">
|
||||
<thead>
|
||||
<tr class="text-xs font-bold text-slate-400 uppercase tracking-wider bg-slate-900/30">
|
||||
<th class="px-6 py-4">Timestamp</th>
|
||||
<th class="px-6 py-4">User</th>
|
||||
<th class="px-6 py-4">Event</th>
|
||||
<th class="px-6 py-4">Target Entity</th>
|
||||
<th class="px-6 py-4">IP / Details</th>
|
||||
<th class="px-6 py-4 text-right">Replay Payload</th>
|
||||
{{ ui::table_container_open(id="audit-table", max_height="68vh") }}
|
||||
<table class="min-w-full divide-y divide-slate-800 text-left text-sm text-slate-300 relative">
|
||||
<thead class="shadow-[0_1px_0_0_rgba(255,255,255,0.05)]">
|
||||
<tr class="text-xs font-bold text-slate-400 uppercase tracking-wider">
|
||||
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031] whitespace-nowrap">Timestamp</th>
|
||||
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031] whitespace-nowrap">User</th>
|
||||
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031] whitespace-nowrap">Event</th>
|
||||
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031] whitespace-nowrap">Target Entity</th>
|
||||
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031] whitespace-nowrap">IP / Details</th>
|
||||
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031] text-right whitespace-nowrap">Replay Payload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
@@ -212,12 +214,45 @@
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ ui::table_container_close(id="audit-table") }}
|
||||
|
||||
<!-- Footer Navigation -->
|
||||
<div class="mt-6 text-center text-sm text-slate-400">
|
||||
<a href="/auth/password" class="font-medium text-sky-400 hover:underline">← Back to Account Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Purge Modal Dialog -->
|
||||
<div id="purge-modal" class="modal-dialog fixed inset-0 z-50 items-center justify-center hidden" role="dialog" aria-modal="true">
|
||||
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||
<div class="modal-content relative z-10 w-full max-w-md scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl">
|
||||
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-rose-500/10 border border-rose-500/20 text-rose-400 mb-3">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-sm font-bold text-slate-100 text-center">Purge Audit Logs</h3>
|
||||
<p class="text-xs text-slate-400 mt-2 text-center leading-relaxed">
|
||||
Specify the number of days of logs to retain. This operation will permanently delete all logs older than this window. You will be forced to download a JSON archival dump of the purged logs before deletion.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="/auth/audit/purge" class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label for="retention_days" class="block text-[11px] font-semibold text-slate-400 mb-1.5 text-left">Retention Period (Days)</label>
|
||||
<input type="number" id="retention_days" name="retention_days" min="1" value="30" required
|
||||
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-650 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50">
|
||||
<span class="text-[10px] text-slate-500 mt-1 block text-left">Minimum 1 day of logs must be retained.</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button type="button" class="modal-close flex-1 py-2.5 rounded-xl bg-secondary border border-border hover:bg-slate-800 transition text-xs font-semibold text-slate-200">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="flex-1 py-2.5 rounded-xl bg-rose-650 hover:bg-rose-600 text-white shadow-lg shadow-rose-600/10 transition text-xs font-bold" onclick="setTimeout(() => { window.closeModal(document.getElementById('purge-modal')); window.location.reload(); }, 1500)">
|
||||
Archived Purge
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
{% if let Some(err) = error %}
|
||||
<div class="mb-6 p-4 rounded-xl bg-rose-500/10 border border-rose-500/20 text-rose-400 text-sm flex items-start gap-2.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 flex-shrink-0 mt-0.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 shrink-0 mt-0.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
|
||||
</svg>
|
||||
<span>{{ err }}</span>
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
{% if let Some(msg) = success %}
|
||||
<div class="mb-6 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm flex items-start gap-2.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 flex-shrink-0 mt-0.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 shrink-0 mt-0.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 0 1-1.043 3.296 3.745 3.745 0 0 1-3.296 1.043A3.745 3.745 0 0 1 12 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 0 1-3.296-1.043 3.745 3.745 0 0 1-1.043-3.296A3.745 3.745 0 0 1 3 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 0 1 1.043-3.296 3.746 3.746 0 0 1 3.296-1.043A3.746 3.746 0 0 1 12 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 0 1 3.296 1.043 3.746 3.746 0 0 1 1.043 3.296A3.745 3.745 0 0 1 21 12Z" />
|
||||
</svg>
|
||||
<span>{{ msg }}</span>
|
||||
|
||||
@@ -79,23 +79,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-5 rounded-2xl bg-indigo-500/5 border border-indigo-500/10 flex items-start gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400 flex-shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.109A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-slate-200">Developer Roster</h4>
|
||||
<p class="text-xs text-slate-400 mt-1 leading-relaxed">Manage team developer profiles, update their technical skills, or clear their metadata.</p>
|
||||
<div class="mt-4">
|
||||
<a href="/developers" class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all px-4 py-2.5 bg-secondary border border-border hover:bg-secondary/80 text-slate-200">
|
||||
Manage Developers
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,15 +26,14 @@
|
||||
</div>
|
||||
|
||||
<!-- User List -->
|
||||
<div class="bg-[#1e293b]/40 backdrop-blur-xl border border-slate-900 rounded-3xl p-6 shadow-xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-800 text-left text-sm text-slate-300">
|
||||
<thead>
|
||||
{{ ui::table_container_open(id="users-table", max_height="auto") }}
|
||||
<table class="min-w-full divide-y divide-slate-800 text-left text-sm text-slate-300 relative">
|
||||
<thead class="shadow-[0_1px_0_0_rgba(255,255,255,0.05)]">
|
||||
<tr class="text-xs font-bold text-slate-400 uppercase tracking-wider">
|
||||
<th scope="col" class="px-6 py-4">Username</th>
|
||||
<th scope="col" class="px-6 py-4">Role</th>
|
||||
<th scope="col" class="px-6 py-4">Created At</th>
|
||||
<th scope="col" class="px-6 py-4 text-right">Actions</th>
|
||||
<th scope="col" class="sticky top-0 z-10 px-6 py-4 bg-[#162031] whitespace-nowrap">Username</th>
|
||||
<th scope="col" class="sticky top-0 z-10 px-6 py-4 bg-[#162031] whitespace-nowrap">Role</th>
|
||||
<th scope="col" class="sticky top-0 z-10 px-6 py-4 bg-[#162031] whitespace-nowrap">Created At</th>
|
||||
<th scope="col" class="sticky top-0 z-10 px-6 py-4 bg-[#162031] text-right whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
@@ -97,8 +96,7 @@
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ ui::table_container_close(id="users-table") }}
|
||||
|
||||
<div class="mt-6 text-center text-sm text-slate-400">
|
||||
<a href="/auth/password" class="font-medium text-sky-400 hover:underline">← Back to Account Settings</a>
|
||||
|
||||
+59
-1
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<nav class="flex items-center space-x-4">
|
||||
<nav class="hidden md:flex items-center space-x-4">
|
||||
<a href="/docs" class="text-sm font-medium text-muted-foreground hover:text-white transition py-2 px-3 rounded-lg hover:bg-secondary">
|
||||
Documentation
|
||||
</a>
|
||||
@@ -74,6 +74,15 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<!-- Hamburger Menu Button (Mobile) -->
|
||||
<div class="flex items-center md:hidden">
|
||||
<button data-sheet-target="mobile-nav-sheet" class="inline-flex items-center justify-center p-2 rounded-xl text-muted-foreground hover:text-white hover:bg-secondary transition focus:outline-none" aria-label="Open main menu">
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -90,5 +99,54 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Mobile Navigation Drawer (Sheet) -->
|
||||
<div id="mobile-nav-sheet" class="sheet-dialog fixed inset-0 z-50 hidden" role="dialog" aria-modal="true">
|
||||
<!-- Backdrop -->
|
||||
<div class="sheet-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||
<!-- Content Container (sliding in from the right) -->
|
||||
<div class="sheet-content fixed inset-y-0 right-0 z-10 w-full max-w-xs translate-x-full transition-transform duration-300 border-l border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between pb-6 border-b border-border">
|
||||
<span class="text-lg font-bold text-slate-100">Menu</span>
|
||||
<button class="sheet-close p-2 rounded-xl text-muted-foreground hover:text-white hover:bg-secondary transition">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Navigation Links -->
|
||||
<nav class="flex flex-col gap-2 mt-6 grow">
|
||||
<a href="/docs" class="flex items-center px-4 py-3 text-sm font-semibold text-muted-foreground hover:text-white rounded-xl hover:bg-secondary transition sheet-close">
|
||||
Documentation
|
||||
</a>
|
||||
{% if authenticated %}
|
||||
<a href="/tasks" class="flex items-center px-4 py-3 text-sm font-semibold text-muted-foreground hover:text-white rounded-xl hover:bg-secondary transition sheet-close">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/developers" class="flex items-center px-4 py-3 text-sm font-semibold text-muted-foreground hover:text-white rounded-xl hover:bg-secondary transition sheet-close">
|
||||
Developers
|
||||
</a>
|
||||
<div class="h-px bg-border my-2"></div>
|
||||
<a href="/auth/password" class="flex items-center justify-between px-4 py-3 text-sm font-semibold text-sky-400 hover:text-sky-300 rounded-xl hover:bg-secondary/40 border border-sky-500/10 transition sheet-close">
|
||||
<span>Logged in as: {{ username }}</span>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</a>
|
||||
<form action="/auth/logout" method="post" class="mt-auto">
|
||||
<button type="submit" class="flex w-full items-center justify-center px-4 py-3 text-sm font-semibold text-rose-400 hover:text-rose-300 rounded-xl hover:bg-rose-950/20 transition sheet-close">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="/auth/login" class="flex items-center px-4 py-3 text-sm font-semibold text-muted-foreground hover:text-white rounded-xl hover:bg-secondary transition sheet-close">
|
||||
Log In
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro modal(id, title, content, close_label="Close") %}
|
||||
<div id="{{ id }}" class="modal-dialog fixed inset-0 z-50 flex items-center justify-center hidden" role="dialog" aria-modal="true">
|
||||
<div id="{{ id }}" class="modal-dialog fixed inset-0 z-50 items-center justify-center hidden" role="dialog" aria-modal="true">
|
||||
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||
<div class="modal-content relative z-10 w-full max-w-sm scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl text-center">
|
||||
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 mb-3">
|
||||
@@ -80,7 +80,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro modal_open(id, title) %}
|
||||
<div id="{{ id }}" class="modal-dialog fixed inset-0 z-50 flex items-center justify-center hidden" role="dialog" aria-modal="true">
|
||||
<div id="{{ id }}" class="modal-dialog fixed inset-0 z-50 items-center justify-center hidden" role="dialog" aria-modal="true">
|
||||
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||
<div class="modal-content relative z-10 w-full max-w-sm scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl text-center">
|
||||
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 mb-3">
|
||||
@@ -234,7 +234,7 @@
|
||||
{% endif %}
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="{{ name }}" class="sr-only peer" {% if checked %}checked{% endif %}>
|
||||
<div class="w-9 h-5 bg-secondary rounded-full border border-border peer-checked:bg-indigo-600 peer-checked:border-indigo-500 transition-all duration-300 relative after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-slate-400 after:rounded-full after:h-[14px] after:w-[14px] after:transition-all peer-checked:after:translate-x-4 peer-checked:after:bg-white"></div>
|
||||
<div class="w-9 h-5 bg-secondary rounded-full border border-border peer-checked:bg-indigo-600 peer-checked:border-indigo-500 transition-all duration-300 relative after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:bg-slate-400 after:rounded-full after:h-3.5 after:w-3.5 after:transition-all peer-checked:after:translate-x-4 peer-checked:after:bg-white"></div>
|
||||
</label>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -279,7 +279,7 @@
|
||||
</span>
|
||||
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="datepicker-popover absolute left-0 z-20 mt-2 w-[270px] p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
|
||||
<div class="datepicker-popover absolute left-0 z-20 mt-2 w-67.5 p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
|
||||
<div class="flex items-center justify-between mb-3.5">
|
||||
<button type="button" class="datepicker-prev p-1.5 rounded-lg hover:bg-secondary text-muted-foreground/90 hover:text-white transition">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
@@ -315,7 +315,7 @@
|
||||
</span>
|
||||
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="timepicker-popover absolute right-0 sm:left-0 z-20 mt-2 w-[230px] p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
|
||||
<div class="timepicker-popover absolute right-0 sm:left-0 z-20 mt-2 w-57.5 p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
|
||||
<div class="flex gap-2 justify-center items-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Hr</span>
|
||||
@@ -360,7 +360,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tabs_content_open() %}
|
||||
<div class="p-4 bg-card/50 rounded-xl border border-border text-xs text-muted-foreground min-h-[5rem]">
|
||||
<div class="p-4 bg-card/50 rounded-xl border border-border text-xs text-muted-foreground min-h-20">
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tab_pane_open(group, id, is_active=true) %}
|
||||
@@ -388,3 +388,14 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro table_container_open(id, max_height="68vh") %}
|
||||
<!-- Table Container -->
|
||||
<div class="backdrop-blur-xl border border-slate-900 rounded-3xl shadow-2xl mb-8 overflow-hidden" style="background: linear-gradient(to bottom, #162031 49px, rgba(30, 41, 59, 0.4) 49px);">
|
||||
<div id="{{ id }}-container" class="overflow-x-auto overflow-y-auto scrollbar-thin rounded-[22px]" style="max-height: {{ max_height }};">
|
||||
{% endmacro %}
|
||||
|
||||
{% macro table_container_close(id) %}
|
||||
</div>
|
||||
</div>{% endmacro %}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<div class="flex items-center gap-2 shrink-0 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<a href="/developers/{{ dev.id.unwrap().to_hex() }}/edit" class="p-2 rounded-lg bg-sky-500/10 hover:bg-sky-500/20 border border-sky-500/20 text-sky-400 transition" title="Edit Developer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.83 20.013a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||||
|
||||
@@ -125,6 +125,20 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-bold text-indigo-400 block mb-1">📱 Mobile-First Responsive Layout</span>
|
||||
<p class="text-slate-400 leading-normal">
|
||||
The navigation elements use responsive design classes to ensure compatibility across screen sizes:
|
||||
</p>
|
||||
<ul class="list-disc pl-5 mt-1.5 space-y-1.5 text-slate-400 leading-normal">
|
||||
<li>
|
||||
<strong class="text-slate-200">Responsive App Navbar:</strong> The master header navigation switches from a horizontal desktop layout (<code>hidden md:flex</code>) to a mobile hamburger menu button (<code>flex md:hidden</code>). The button targets a sliding navigation drawer (<code>#mobile-nav-sheet</code>) that transitions in from the right.
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-slate-200">Collapsible Wiki Directory:</strong> On documentation views, the large guides sidebar is hidden on mobile devices (<code>hidden lg:block</code>) and replaced with an inline trigger bar (<code>Explore Guides</code>). Clicking this trigger transitions the guides directory drawer (<code>#wiki-sidebar-sheet</code>) from the left.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -232,6 +232,24 @@ where
|
||||
The `payload` field is crucial. By storing the JSON serialization of the model BEFORE or AFTER the action, system administrators can reconstruct state history. For deletions, always record the serial snapshot of the deleted model in the payload so it is never permanently lost to audit inquiries.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="font-bold text-indigo-400 block mb-1">🗑️ Compliance & Archival Purging</span>
|
||||
<p class="text-slate-400 leading-normal">
|
||||
To maintain storage hygiene, administrators can purge logs older than <code>x</code> days (minimum 1 day). To prevent accidental loss of audit history, the system enforces a strict <strong>archival download requirement</strong>:
|
||||
</p>
|
||||
<ul class="list-disc pl-5 mt-1.5 space-y-1.5 text-slate-400 leading-normal">
|
||||
<li>
|
||||
The purge action queries the records target for deletion and serializes them to JSON format in memory.
|
||||
</li>
|
||||
<li>
|
||||
The database records are deleted, and a new audit log entry recording the purge event details is created to preserve the audit trail chain.
|
||||
</li>
|
||||
<li>
|
||||
The server immediately streams the JSON data as a downloadable attachment, forcing the browser to save the backup locally.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<aside class="lg:w-64 shrink-0">
|
||||
<!-- Mobile Wiki Sidebar Trigger (Visible on small screens only) -->
|
||||
<div class="lg:hidden w-full mb-6 bg-card/30 backdrop-blur border border-border p-4 rounded-3xl flex items-center justify-between shadow-sm">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs font-bold text-slate-100 uppercase tracking-wider">Documentation</span>
|
||||
<span class="text-[10px] text-muted-foreground">Select a guide to read</span>
|
||||
</div>
|
||||
<button data-sheet-target="wiki-sidebar-sheet" class="inline-flex items-center justify-center gap-1.5 px-4 py-2.5 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white shadow-md shadow-indigo-600/10 transition">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h7" />
|
||||
</svg>
|
||||
Explore Guides
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation Sidebar (Hidden on mobile) -->
|
||||
<aside class="hidden lg:block lg:w-64 shrink-0">
|
||||
<div class="sticky top-24 space-y-1.5 p-4 rounded-3xl border border-border bg-card/50 backdrop-blur-xl" id="wiki-sidebar">
|
||||
<span class="px-3 text-[10px] font-bold text-slate-500 uppercase tracking-wider block mb-2">Wiki Navigation</span>
|
||||
<a href="/docs" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs">Introduction</a>
|
||||
@@ -23,9 +38,57 @@
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Layout & Navigation</span>
|
||||
<a href="/docs/tabs-accordion" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/tabs-accordion">Tabs & Accordions</a>
|
||||
<a href="/docs/scrollbars" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/scrollbars">Custom Scrollbars</a>
|
||||
<a href="/docs/tables" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/tables">Tables & Scroll Sync</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Visuals & Feedback</span>
|
||||
<a href="/docs/visuals" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/visuals">Avatars & Badges</a>
|
||||
<a href="/docs/feedback" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/feedback">Toasts & Alerts</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Mobile Navigation Sidebar (Sheet - sliding in from left) -->
|
||||
<div id="wiki-sidebar-sheet" class="sheet-dialog fixed inset-0 z-50 hidden" role="dialog" aria-modal="true">
|
||||
<!-- Backdrop -->
|
||||
<div class="sheet-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
|
||||
<!-- Content Container -->
|
||||
<div class="sheet-content fixed inset-y-0 left-0 z-10 w-full max-w-xs -translate-x-full transition-transform duration-300 border-r border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl flex flex-col overflow-y-auto">
|
||||
<div class="flex items-center justify-between pb-4 border-b border-border mb-4">
|
||||
<span class="text-sm font-bold text-slate-100 uppercase tracking-wider">Wiki Navigation</span>
|
||||
<button class="sheet-close p-2 rounded-xl text-muted-foreground hover:text-white hover:bg-secondary transition">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Mobile Sidebar Links -->
|
||||
<nav class="flex flex-col gap-1.5">
|
||||
<a href="/docs" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs">Introduction</a>
|
||||
<div class="h-px bg-secondary my-1"></div>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-650 uppercase tracking-wider block mt-2 mb-1">Architecture</span>
|
||||
<a href="/docs/logging" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/logging">Audit Logging</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-650 uppercase tracking-wider block mt-2 mb-1">Actions</span>
|
||||
<a href="/docs/buttons" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/buttons">Buttons</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-650 uppercase tracking-wider block mt-2 mb-1">Forms & Inputs</span>
|
||||
<a href="/docs/inputs" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/inputs">Form Fields & Select</a>
|
||||
<a href="/docs/date-time" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/date-time">Date & Time Pickers</a>
|
||||
<a href="/docs/combobox" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/combobox">Autocomplete (Combobox)</a>
|
||||
<a href="/docs/toggles" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/toggles">Switches & Checkboxes</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-650 uppercase tracking-wider block mt-2 mb-1">Overlays</span>
|
||||
<a href="/docs/modals" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/modals">Dialog Modals</a>
|
||||
<a href="/docs/sheets" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/sheets">Slide-over Drawers</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-650 uppercase tracking-wider block mt-2 mb-1">Layout & Navigation</span>
|
||||
<a href="/docs/tabs-accordion" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/tabs-accordion">Tabs & Accordions</a>
|
||||
<a href="/docs/scrollbars" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/scrollbars">Custom Scrollbars</a>
|
||||
<a href="/docs/tables" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/tables">Tables & Scroll Sync</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-650 uppercase tracking-wider block mt-2 mb-1">Visuals & Feedback</span>
|
||||
<a href="/docs/visuals" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/visuals">Avatars & Badges</a>
|
||||
<a href="/docs/feedback" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition sheet-close" data-wiki-path="/docs/feedback">Toasts & Alerts</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Tables - Design System Wiki{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="pb-6 border-b border-border">
|
||||
<span class="text-xs font-semibold text-indigo-400">Layout & Navigation</span>
|
||||
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Tables</h1>
|
||||
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
|
||||
A standardized container macro for table styling and scrollbar-fitting layout. It resolves common styling issues like corner clipping and header bleed-through.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Section Usage -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-bold text-slate-200">Container Showcase</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
The macro standardizes the background styling, shadows, rounded borders, vertical height bounds, and sticky headers.
|
||||
</p>
|
||||
|
||||
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
|
||||
<!-- Tab Headers -->
|
||||
<div class="flex border-b border-border/60 pb-1.5">
|
||||
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400 cursor-pointer" onclick="toggleWikiTabs(this, 'table-sandbox')">Interactive Demo</button>
|
||||
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-slate-250 cursor-pointer" onclick="toggleWikiTabs(this, 'table-code')">Askama Usage</button>
|
||||
</div>
|
||||
|
||||
<!-- Demo Viewport -->
|
||||
<div id="table-sandbox" class="wiki-pane">
|
||||
{{ ui::table_container_open(id="docs-demo-table", max_height="250px") }}
|
||||
<table class="min-w-full divide-y divide-slate-800 text-left text-xs text-slate-300 relative">
|
||||
<thead class="shadow-[0_1px_0_0_rgba(255,255,255,0.05)]">
|
||||
<tr class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
||||
<th class="sticky top-0 z-10 px-5 py-3 bg-[#162031] whitespace-nowrap">ID Code</th>
|
||||
<th class="sticky top-0 z-10 px-5 py-3 bg-[#162031] whitespace-nowrap">Full Name</th>
|
||||
<th class="sticky top-0 z-10 px-5 py-3 bg-[#162031] whitespace-nowrap">Position Title</th>
|
||||
<th class="sticky top-0 z-10 px-5 py-3 bg-[#162031] whitespace-nowrap">Base Office</th>
|
||||
<th class="sticky top-0 z-10 px-5 py-3 bg-[#162031] whitespace-nowrap">Current Shift Status</th>
|
||||
<th class="sticky top-0 z-10 px-5 py-3 bg-[#162031] text-right whitespace-nowrap">Actions & Config</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800 bg-[#151f30]/20">
|
||||
<tr class="hover:bg-[#1e293b]/20 transition">
|
||||
<td class="px-5 py-3 font-mono text-sky-400">#001_a9f</td>
|
||||
<td class="px-5 py-3 font-semibold text-slate-200">Arthur Dent</td>
|
||||
<td class="px-5 py-3">Sandwich Maker</td>
|
||||
<td class="px-5 py-3">Space Station 4</td>
|
||||
<td class="px-5 py-3"><span class="px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-[9px] font-bold">ACTIVE</span></td>
|
||||
<td class="px-5 py-3 text-right"><button class="text-sky-400 hover:underline font-semibold cursor-pointer">View Detail</button></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-[#1e293b]/20 transition">
|
||||
<td class="px-5 py-3 font-mono text-sky-400">#002_c3d</td>
|
||||
<td class="px-5 py-3 font-semibold text-slate-200">Tricia McMillan</td>
|
||||
<td class="px-5 py-3">Reporter / Anchor</td>
|
||||
<td class="px-5 py-3">Sector 12 Office</td>
|
||||
<td class="px-5 py-3"><span class="px-2 py-0.5 rounded-full bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 text-[9px] font-bold">ON DUTY</span></td>
|
||||
<td class="px-5 py-3 text-right"><button class="text-sky-400 hover:underline font-semibold cursor-pointer">View Detail</button></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-[#1e293b]/20 transition">
|
||||
<td class="px-5 py-3 font-mono text-sky-400">#003_e5f</td>
|
||||
<td class="px-5 py-3 font-semibold text-slate-200">Ford Prefect</td>
|
||||
<td class="px-5 py-3">Researcher</td>
|
||||
<td class="px-5 py-3">Betelgeuse V</td>
|
||||
<td class="px-5 py-3"><span class="px-2 py-0.5 rounded-full bg-amber-500/10 text-amber-400 border border-amber-500/20 text-[9px] font-bold">TRAVELING</span></td>
|
||||
<td class="px-5 py-3 text-right"><button class="text-sky-400 hover:underline font-semibold cursor-pointer">View Detail</button></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-[#1e293b]/20 transition">
|
||||
<td class="px-5 py-3 font-mono text-sky-400">#004_g7h</td>
|
||||
<td class="px-5 py-3 font-semibold text-slate-200">Zaphod Beeblebrox</td>
|
||||
<td class="px-5 py-3">Ex-President</td>
|
||||
<td class="px-5 py-3">Heart of Gold</td>
|
||||
<td class="px-5 py-3"><span class="px-2 py-0.5 rounded-full bg-rose-500/10 text-rose-400 border border-rose-500/20 text-[9px] font-bold">SUSPENDED</span></td>
|
||||
<td class="px-5 py-3 text-right"><button class="text-sky-400 hover:underline font-semibold cursor-pointer">View Detail</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ ui::table_container_close(id="docs-demo-table") }}
|
||||
</div>
|
||||
|
||||
<!-- Code Snippet Area -->
|
||||
<div id="table-code" class="wiki-pane hidden space-y-4">
|
||||
<div class="relative group">
|
||||
<button class="absolute top-2 right-2 p-1.5 rounded-lg border border-border bg-popover/80 backdrop-blur text-[10px] font-semibold text-slate-400 hover:text-white hover:bg-secondary opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-1.5 cursor-pointer" onclick="copyCodeSnippet(this)">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Markup
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>{{ "{%" }} import "components/macros.html" as ui {{ "%}" }}
|
||||
|
||||
{{ ui::table_container_open(id="my-custom-table", max_height="68vh") }}
|
||||
<table class="min-w-full divide-y divide-slate-800 text-left text-sm text-slate-300 relative">
|
||||
<thead class="shadow-[0_1px_0_0_rgba(255,255,255,0.05)]">
|
||||
<tr class="text-xs font-bold text-slate-400 uppercase tracking-wider">
|
||||
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031]">Column 1</th>
|
||||
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031]">Column 2</th>
|
||||
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031] text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800">
|
||||
<tr class="hover:bg-[#1e293b]/20 transition">
|
||||
<td class="px-6 py-4">Cell Data A1</td>
|
||||
<td class="px-6 py-4">Cell Data A2</td>
|
||||
<td class="px-6 py-4 text-right">Edit</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ ui::table_container_close(id="my-custom-table") }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Technical Specification Section -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-bold text-slate-200">How It Works</h2>
|
||||
<div class="border border-border rounded-3xl p-6 bg-secondary/10 space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-xs font-bold text-indigo-400 uppercase tracking-wider">Stylized Container Wrapper</h4>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
Tables are automatically styled with rounded-3xl corners and background styling. The table container is scrollable, displaying a native horizontal or vertical scrollbar as needed.
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-xs font-bold text-indigo-400 uppercase tracking-wider">Rounded Corner Clipping Fix</h4>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
To prevent scrollbars from clipping at the corners of high-radius panels (like Shadcn's <code>rounded-3xl</code> panels), the border and shadow styling are kept on the outer wrapper, while the overflow scroll boundary is restricted to an inner container with slightly smaller radius adjustments (<code>rounded-[22px]</code>).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleWikiTabs(btn, paneId) {
|
||||
const card = btn.closest('.border-border');
|
||||
if (!card) return;
|
||||
card.querySelectorAll('button').forEach(b => {
|
||||
b.classList.remove('border-sky-500', 'text-sky-400');
|
||||
b.classList.add('border-transparent', 'text-muted-foreground', 'hover:text-slate-250');
|
||||
});
|
||||
btn.classList.remove('border-transparent', 'text-muted-foreground', 'hover:text-slate-250');
|
||||
btn.classList.add('border-sky-500', 'text-sky-400');
|
||||
card.querySelectorAll('.wiki-pane').forEach(p => p.classList.add('hidden'));
|
||||
const target = card.querySelector('#' + paneId);
|
||||
if (target) target.classList.remove('hidden');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="bg-[#1e293b]/30 hover:bg-[#1e293b]/40 border border-slate-900 rounded-2xl p-5 flex items-start gap-4 transition duration-300 {% if item.task.is_completed %} opacity-60 {% endif %} relative overflow-hidden group">
|
||||
|
||||
<!-- Checkmark Indicator -->
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
<div class="shrink-0 mt-0.5">
|
||||
{% if item.task.is_completed %}
|
||||
<div class="w-5 h-5 rounded-full bg-emerald-500/20 border border-emerald-500/50 flex items-center justify-center text-emerald-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5">
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<div class="flex items-center gap-2 shrink-0 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
{% if !item.task.is_completed %}
|
||||
<form action="/tasks/{{ item.task.id.unwrap().to_hex() }}/complete" method="post" class="inline">
|
||||
<button type="submit" class="p-2 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 text-emerald-400 transition" title="Mark Completed">
|
||||
|
||||
Reference in New Issue
Block a user