chore: update

This commit is contained in:
Jesús Pérez 2026-07-18 20:15:23 +01:00
parent c0281e759c
commit 642998ba35
Signed by: jesus
GPG key ID: 9F243E355E0BC939
18 changed files with 1290 additions and 292 deletions

View file

@ -4,6 +4,10 @@
[build] [build]
# Number of parallel jobs for compilation # Number of parallel jobs for compilation
jobs = 4 jobs = 4
target-dir = "/Volumes/Devel/rustelo/target"
# tokio_unstable enables tokio::runtime::LocalRuntime, which allows spawn_local
# from any task context (including tokio::spawn'd connection tasks from axum::serve).
rustflags = ["--cfg", "tokio_unstable"]
# Code generation backend # Code generation backend
# codegen-backend = "llvm" # codegen-backend = "llvm"
@ -37,7 +41,6 @@ debug = true
debug-assertions = true debug-assertions = true
overflow-checks = true overflow-checks = true
lto = false lto = false
panic = "unwind"
incremental = true incremental = true
[profile.bench] [profile.bench]
@ -48,12 +51,8 @@ debug-assertions = false
overflow-checks = false overflow-checks = false
lto = "thin" lto = "thin"
codegen-units = 1 codegen-units = 1
panic = "abort"
incremental = false incremental = false
# Resolver version
resolver = "2"
[term] [term]
# Terminal colors # Terminal colors
color = "auto" color = "auto"
@ -69,9 +68,22 @@ offline = false
# Strict version requirements for dependencies # Strict version requirements for dependencies
# force-non-semver-pre = true # force-non-semver-pre = true
[profile.ci-test]
# Pre-commit: no debug info, no incremental — shared .rlib between test + docs hooks
inherits = "test"
debug = 0
incremental = false
[profile.ci]
# CI pipeline: line-tables-only for actionable backtraces on remote failures
inherits = "test"
debug = "line-tables-only"
incremental = false
[alias] [alias]
# Custom cargo commands # Custom cargo commands
build-all = "build --all-targets" build-all = "build --all-targets"
check-all = "check --all-targets --all-features" check-all = "check --all-targets --all-features"
test-all = "test --all-features --workspace" test-all = "test --all-features --workspace"
doc-all = "doc --all-features --no-deps --open" doc-all = "doc --all-features --no-deps --open"
xtask = "run --manifest-path xtask/Cargo.toml --quiet --"

7
.config/nextest.toml Normal file
View file

@ -0,0 +1,7 @@
[profile.ci-test]
fail-fast = true
retries = 0
[profile.ci]
fail-fast = false
retries = 1

59
admin/lib/common.nu Normal file
View file

@ -0,0 +1,59 @@
# Shared helpers for NATS admin lib:
# subject builder, credential resolution, and the low-level request wrapper.
# Resolve the NATS server URL. CLI flag takes precedence over env, env over default.
export def nats-admin-url [override: string = ""] {
if ($override | is-not-empty) {
$override
} else {
$env.NATS_ADMIN_URL? | default "nats://localhost:4222"
}
}
# Resolve the NATS credentials file path.
export def nats-admin-creds [override: string = ""] {
if ($override | is-not-empty) {
$override
} else {
$env.NATS_ADMIN_CREDS? | default ""
}
}
# Build a full NATS subject from the configured namespace and a leaf name.
#
# Subject pattern: `{prefix}.{env}.{leaf}`
# Driven by NATS_ADMIN_NAMESPACE_PREFIX and NATS_ADMIN_NAMESPACE_ENV (same env
# vars used by the Rust server at startup).
#
# Examples:
# (build-admin-subject "admin.health") → "evol.website.prod.admin.health"
export def build-admin-subject [leaf: string] {
let prefix = $env.NATS_ADMIN_NAMESPACE_PREFIX? | default "rustelo"
let env_name = $env.NATS_ADMIN_NAMESPACE_ENV? | default "local"
$"($prefix).($env_name).($leaf)"
}
# Send a request-reply to a NATS subject and return the raw response string.
#
# Wraps `nats request` CLI. Credentials file is optional.
# Timeout defaults to 5 s; override via `--timeout`.
export def nats-admin-request [
subject: string # Full subject string
payload: string = "{}" # JSON payload (default empty object)
--url: string = "" # NATS server URL override
--creds: string = "" # Credentials file override
--timeout: string = "5s" # Request timeout (nats CLI format e.g. 5s, 30s)
] {
let server = nats-admin-url $url
let creds_path = nats-admin-creds $creds
let base = ["request", "--server", $server, "--timeout", $timeout, $subject, $payload]
let args = if ($creds_path | is-not-empty) {
["--creds", $creds_path] ++ $base
} else {
$base
}
^nats ...$args | str trim
}

47
admin/lib/health.nu Normal file
View file

@ -0,0 +1,47 @@
# Health check commands for the NATS admin bus.
#
# Requires a running Rustelo server with `nats-admin` feature enabled and
# `external_services.nats_admin.enabled = true`.
use ./common.nu [build-admin-subject, nats-admin-request]
# Send a health-check request and return a structured record.
#
# Response shape: { status: string, host: string, version: string }
#
# Examples:
# nats-admin health
# nats-admin health --url nats://nats-hub:4222 --creds /etc/nats/admin.creds
export def "nats-admin health" [
--url: string = "" # NATS server URL (overrides NATS_ADMIN_URL)
--creds: string = "" # Credentials file (overrides NATS_ADMIN_CREDS)
] {
let subject = build-admin-subject "admin.health"
nats-admin-request $subject --url $url --creds $creds | from json
}
# Check health and exit non-zero if the server reports anything other than "ok".
#
# Suitable for use in CI / k8s liveness probes:
# nats-admin health check && echo "healthy"
export def "nats-admin health check" [
--url: string = ""
--creds: string = ""
] {
let result = nats-admin health --url $url --creds $creds
let status = $result.status? | default "unknown"
if $status != "ok" {
error make { msg: $"Health check failed — status: ($status)" }
}
}
# Print a formatted health summary table.
export def "nats-admin health show" [
--url: string = ""
--creds: string = ""
] {
nats-admin health --url $url --creds $creds
| transpose key value
| print
}

103
admin/lib/logs.nu Normal file
View file

@ -0,0 +1,103 @@
# Log fetch commands for the NATS admin bus.
#
# Fetches recent in-process log lines captured by the Rustelo `NatsLogLayer`
# tracing subscriber. Lines are returned in chronological order.
use ./common.nu [build-admin-subject, nats-admin-request]
# Fetch recent log lines from the server's in-memory ring buffer.
#
# Returns a list of strings (one per log line) in chronological order.
#
# Examples:
# nats-admin logs # last 100 lines
# nats-admin logs --lines 50 # last 50 lines
# nats-admin logs | where $it =~ "ERROR" # filter in-shell
export def "nats-admin logs" [
--lines: int = 100 # Number of recent lines to fetch (max 2000)
--url: string = "" # NATS server URL override
--creds: string = "" # Credentials file override
] {
let subject = build-admin-subject "admin.logs"
let payload = { lines: $lines } | to json --raw
nats-admin-request $subject $payload --url $url --creds $creds --timeout "10s"
| from json
| get lines
}
# Parse log lines into a table with level, target, and message columns.
#
# Line format produced by the Rust NatsLogLayer:
# [LEVEL] target — message
#
# Examples:
# nats-admin logs table
# nats-admin logs table --lines 200 | where level == "ERROR"
export def "nats-admin logs table" [
--lines: int = 100
--url: string = ""
--creds: string = ""
] {
nats-admin logs --lines $lines --url $url --creds $creds
| each { |line|
let parts = $line | parse --regex '^\[(?P<level>\w+)\] (?P<target>[^ ]+) — (?P<message>.+)$'
if ($parts | is-not-empty) {
$parts | first
} else {
{ level: "UNKNOWN", target: "", message: $line }
}
}
}
# Show only error and warning log lines.
#
# Examples:
# nats-admin logs errors
# nats-admin logs errors --lines 500
export def "nats-admin logs errors" [
--lines: int = 500
--url: string = ""
--creds: string = ""
] {
nats-admin logs --lines $lines --url $url --creds $creds
| where { |line| ($line | str contains "[ERROR]") or ($line | str contains "[WARN]") }
}
# Poll logs repeatedly and print new lines since the last fetch.
#
# Runs until interrupted with Ctrl-C.
#
# Examples:
# nats-admin logs tail
# nats-admin logs tail --interval 3 --lines 30
export def "nats-admin logs tail" [
--interval: int = 5 # Poll interval in seconds
--lines: int = 50 # Lines per fetch (keep small for tail use)
--url: string = ""
--creds: string = ""
] {
mut last_seen: string = ""
loop {
let batch = nats-admin logs --lines $lines --url $url --creds $creds
let new_lines = if ($last_seen | is-empty) {
$batch
} else {
let pivot = $batch | enumerate | where { |it| $it.item == $last_seen } | last?
if $pivot != null {
$batch | skip ($pivot.index + 1)
} else {
$batch
}
}
if ($new_lines | is-not-empty) {
$new_lines | each { |line| print $line }
$last_seen = ($new_lines | last)
}
sleep ($interval * 1sec)
}
}

21
admin/lib/mod.nu Normal file
View file

@ -0,0 +1,21 @@
# Rustelo NATS admin lib — framework-level operator toolchain.
#
# Re-exports all admin command groups. Load from a script or REPL with:
#
# use /path/to/rustelo/admin/lib/mod.nu *
#
# or from website-impl admin scripts:
#
# use ../../rustelo/admin/lib/mod.nu *
#
# Required environment variables (set before using):
# NATS_ADMIN_URL NATS server URL (default: nats://localhost:4222)
# NATS_ADMIN_CREDS Path to nkeys credentials file (optional for dev)
# NATS_ADMIN_NAMESPACE_PREFIX Subject namespace prefix (default: rustelo)
# NATS_ADMIN_NAMESPACE_ENV Subject namespace env (default: local)
#
# All commands accept --url and --creds flags to override env vars per call.
export use ./health.nu *
export use ./logs.nu *
export use ./users.nu *

78
admin/lib/users.nu Normal file
View file

@ -0,0 +1,78 @@
# User management commands for the NATS admin bus.
#
# Sends structured requests to the `admin.users` NATS subject. The server-side
# subscriber must handle this subject with access to the auth repository.
#
# Request envelope: { "op": "list" | "get" | "disable" | "reset_sessions", ...fields }
# Response envelope: { "ok": bool, "data": any, "error": string | null }
use ./common.nu [build-admin-subject, nats-admin-request]
# Internal: send a users operation and unwrap the data payload.
def users-request [payload: record, --url: string = "", --creds: string = ""] {
let subject = build-admin-subject "admin.users"
let json_payload = $payload | to json --raw
let response = nats-admin-request $subject $json_payload --url $url --creds $creds --timeout "15s"
| from json
if not ($response.ok? | default false) {
let msg = $response.error? | default "unknown error from admin.users"
error make { msg: $"admin.users error: ($msg)" }
}
$response.data? | default {}
}
# List all registered users.
#
# Returns a table with: id, email, display_name, roles, created_at, is_active
#
# Examples:
# nats-admin users list
# nats-admin users list | where { |u| "admin" in $u.roles }
# nats-admin users list | length # total user count
export def "nats-admin users list" [
--url: string = ""
--creds: string = ""
] {
users-request { op: "list" } --url $url --creds $creds
}
# Get a single user by email address.
#
# Examples:
# nats-admin users get admin@example.com
export def "nats-admin users get" [
email: string # User email address
--url: string = ""
--creds: string = ""
] {
users-request { op: "get", email: $email } --url $url --creds $creds
}
# Disable a user account (sets is_active = false, does not delete).
#
# Examples:
# nats-admin users disable spammer@example.com
export def "nats-admin users disable" [
email: string
--url: string = ""
--creds: string = ""
] {
users-request { op: "disable", email: $email } --url $url --creds $creds
print $"User ($email) disabled."
}
# Invalidate all active sessions for a user, forcing re-login.
#
# Examples:
# nats-admin users reset-sessions user@example.com
export def "nats-admin users reset-sessions" [
email: string
--url: string = ""
--creds: string = ""
] {
users-request { op: "reset_sessions", email: $email } --url $url --creds $creds
print $"Sessions reset for ($email)."
}

Binary file not shown.

View file

@ -0,0 +1,15 @@
[package]
name = "plugin-example-theme"
version = "0.1.0"
edition = "2021"
description = "Example analytics theme plugin for Rustelo website framework"
license = "MIT"
authors = ["Rustelo Team"]
[dependencies]
rustelo_core_lib = { workspace = true }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
[dev-dependencies]
tokio = { version = "1.0", features = ["full"] }

View file

@ -0,0 +1,319 @@
# Example Analytics Theme Plugin
A complete, production-ready example plugin for the Rustelo website framework demonstrating Level 5 (Compile-Time) plugin architecture.
## 🎯 Purpose
This plugin serves as a reference implementation showing how to:
1. **Create a plugin crate** with proper structure and dependencies
2. **Implement ResourceContributor** trait for providing themes and translations
3. **Embed configuration** using `include_str!()` macros
4. **Write comprehensive tests** for plugin functionality
5. **Document thoroughly** with examples and usage instructions
6. **Follow best practices** for Rust plugin development
## 📦 What This Plugin Provides
### Themes
- **analytics-dashboard** - A professional theme optimized for analytics dashboards
- High-contrast color scheme for readability
- Professional color palette (blues, teals, ambers)
- Comprehensive spacing and typography rules
- Responsive breakpoints and animation definitions
### Translations
- **English (en)** - Complete English UI strings
- Dashboard labels and metrics
- Chart titles and legends
- Action button labels
- Status messages and tooltips
- **Spanish (es)** - Complete Spanish translations
- Full parity with English strings
- Localized terminology for analytics
## 🚀 Quick Start
### 1. Add to Your Project
In your main Rustelo project's `Cargo.toml`:
```toml
[dependencies]
plugin-example-theme = { path = "./crates/plugin-example-theme" }
```
### 2. Register the Plugin
In your server-side resources initialization (`src/resources.rs`):
```rust
use plugin_example_theme::ExampleThemeContributor;
pub fn initialize() -> Result<(), Box<dyn std::error::Error>> {
// Register your own resources
rustelo_core_lib::register_contributor(&WebsiteResourceContributor)?;
// Register the example plugin
rustelo_core_lib::register_contributor(&ExampleThemeContributor)?;
// Load all resources from configuration
rustelo_core_lib::load_resources_from_config()?;
Ok(())
}
```
### 3. Use the Plugin's Theme
In your application's CSS/styling configuration, reference the `analytics-dashboard` theme:
```css
/* Your CSS can now use the theme variables */
:root {
--color-primary: #2563EB;
--color-secondary: #14B8A6;
--color-accent: #F59E0B;
/* ... etc ... */
}
```
## 📁 Project Structure
```
plugin-example-theme/
├── Cargo.toml # Plugin manifest
├── README.md # This file
├── src/
│ ├── lib.rs # Public API and initialization
│ └── resources.rs # ResourceContributor implementation
├── config/
│ ├── themes/
│ │ └── analytics-dashboard.toml # Theme configuration
│ └── i18n/
│ ├── en.ftl # English translations
│ └── es.ftl # Spanish translations
└── examples/
└── integration_test.rs # Integration test example
```
## 🧪 Testing
### Run All Tests
```bash
cd crates/plugin-example-theme
cargo test
```
### Expected Output
```
running 10 tests
test resources::test_contributor_is_clone_and_copy ... ok
test resources::test_contributor_name ... ok
test resources::test_fluent_files_are_not_empty ... ok
test resources::test_fluent_files_are_provided ... ok
test resources::test_menus_and_footers_are_empty ... ok
test resources::test_theme_content_is_not_empty ... ok
test resources::test_theme_is_provided ... ok
test resources::test_theme_is_valid_toml ... ok
test tests::test_plugin_can_be_initialized ... ok
test tests::test_plugin_exports_public_api ... ok
test result: ok. 10 passed; 0 failed; 0 ignored
```
### Test Coverage
The plugin includes tests for:
- ✅ Contributor name and identity
- ✅ Theme availability and content
- ✅ TOML syntax validation
- ✅ Translation files (fluent) availability
- ✅ Empty menus/footers (correctly not provided)
- ✅ Copy/Clone trait implementation
- ✅ Plugin initialization
- ✅ Public API exports
## 🎨 Theme Details
### Color Palette
| Purpose | Color | Hex |
|---------|-------|-----|
| Primary | Blue | `#2563EB` |
| Secondary | Teal | `#14B8A6` |
| Accent | Amber | `#F59E0B` |
| Success | Green | `#10B981` |
| Warning | Amber | `#FBBF24` |
| Error | Red | `#EF4444` |
| Info | Blue | `#3B82F6` |
### Typography
- **Heading Font:** Inter, system-ui, sans-serif
- **Body Font:** Inter, system-ui, sans-serif
- **Monospace Font:** JetBrains Mono, Monaco, monospace
- **Scale Factor:** 1.125 (12.5% increase per step)
### Responsive Breakpoints
- Mobile: 320px
- Tablet: 768px
- Desktop: 1024px
- Wide: 1280px
- Ultra-wide: 1536px
### Spacing System
- Base unit: 8px
- Step multiplier: 1.5x
- Increments: 8px, 12px, 18px, 27px, ...
## 📝 Implementation Details
### How It Works
1. **Compile Time:** During `cargo build`, this plugin is compiled into your application
2. **Static Embedding:** Theme and i18n files are embedded via `include_str!()` macros
3. **Initialization:** When the application starts, the plugin is registered via `initialize()`
4. **Resource Loading:** The framework loads the embedded resources into memory
5. **Runtime:** The framework makes resources available to components via registries
### Why This Design?
- ✅ **No Conditional Compilation:** Framework doesn't know about plugins
- ✅ **Self-Contained:** Plugin is a complete, standalone crate
- ✅ **Type Safe:** Rust compiler validates all resource references
- ✅ **Performance:** Zero runtime overhead (fully inlined at compile time)
- ✅ **Maintainable:** Clean separation between plugin and framework
- ✅ **Scalable:** Add unlimited plugins without increasing complexity
## 🔧 Customizing This Plugin
### Modify the Theme
1. Edit `config/themes/analytics-dashboard.toml`
2. Change colors, fonts, spacing, or animations
3. Rebuild: `cargo build`
### Add Translations
1. Edit or create files in `config/i18n/`
2. Use ISO 639-1 language codes (e.g., `fr.ftl` for French)
3. Update `src/resources.rs` to include new files
4. Rebuild: `cargo build`
### Extend with More Resources
To add menus or footers:
```rust
fn menus(&self) -> HashMap<String, String> {
let mut menus = HashMap::new();
let menu_content = include_str!("../config/menus/my-menu.toml");
menus.insert("my-menu".to_string(), menu_content.to_string());
menus
}
```
## 📚 Learning Resources
### For Plugin Developers
- **PHASE3-PLUGIN-DEVELOPMENT-GUIDE.md** - Comprehensive guide to plugin development
- **PHASE1-COMPLETE.md** - Architecture foundation documentation
- **PHASE2-IMPLEMENTATION-SUMMARY.md** - Build-time code generation details
### For Framework Users
- **RUST_CODE_STYLE.md** - Rust coding standards for Rustelo
- **Framework API Docs** - ResourceContributor trait documentation
## 🎓 Architecture Level
This plugin implements **Level 5 (Compile-Time Plugins)** of the Rustelo plugin architecture:
```
Level 5: Compile-Time Plugins
├─ Trait-based registration
├─ Static code generation at build time
├─ No conditional compilation
├─ Plugin crate is self-contained
└─ Registration at application startup
```
**Future Evolution to Level 8:** The same trait interfaces can support runtime plugin loading without breaking changes.
## ✅ Quality Checklist
- ✅ Implements `ResourceContributor` trait
- ✅ Has comprehensive documentation with examples
- ✅ Includes 10+ unit tests (100% trait method coverage)
- ✅ Configuration files are valid TOML/FTL
- ✅ No hardcoded paths (uses `include_str!()`)
- ✅ No unsafe code
- ✅ No unwrap() or expect() in production code
- ✅ Cargo.toml has clear version and dependencies
- ✅ README explains purpose and usage
- ✅ Examples show typical integration
- ✅ Error handling is robust
- ✅ Performance optimized (no unnecessary allocations)
## 🤝 Contributing
To extend this example plugin:
1. Create a feature branch: `git checkout -b feature/your-feature`
2. Make changes following RUST_CODE_STYLE.md
3. Add tests for new functionality
4. Ensure all tests pass: `cargo test --all`
5. Submit pull request with clear description
## 📄 License
This example plugin is provided as part of the Rustelo framework and follows the same license terms.
## 🆘 Troubleshooting
### Plugin doesn't load
**Check:**
1. Is the plugin added to Cargo.toml?
2. Is `initialize()` called in your resources module?
3. Are there any compilation errors?
### Translations not showing
**Check:**
1. Are language codes correct (en, es, etc)?
2. Are FTL files valid Fluent syntax?
3. Is the framework loading translations?
### Theme colors not applying
**Check:**
1. Is the theme registered: `analytics-dashboard` in registry?
2. Are CSS variables using correct names?
3. Are there conflicting theme registrations?
## 📞 Support
For questions or issues with this plugin:
1. Check **PHASE3-PLUGIN-DEVELOPMENT-GUIDE.md** for comprehensive documentation
2. Review the source code comments in `src/`
3. Run tests to verify functionality
4. Check framework documentation for trait specifications
---
**Created:** 2024-11-02
**Status:** Production Ready
**Version:** 0.1.0
**Compatibility:** Rustelo Level 5+

View file

@ -0,0 +1,62 @@
# Example Theme Plugin - English Translations
# Analytics Dashboard UI strings
## Plugin Metadata
example-plugin-name = Example Analytics Theme
example-plugin-description = Professional theme for analytics dashboards with optimized data visualization
## Dashboard Headers
dashboard-title = Analytics Dashboard
dashboard-subtitle = Real-time performance metrics and insights
dashboard-refresh = Last updated { $time }
## Metrics Labels
metrics-total-users = Total Users
metrics-active-sessions = Active Sessions
metrics-page-views = Page Views
metrics-avg-session-duration = Avg Session Duration
metrics-bounce-rate = Bounce Rate
metrics-conversion-rate = Conversion Rate
## Chart Labels
chart-daily-activity = Daily Activity
chart-user-growth = User Growth
chart-traffic-sources = Traffic Sources
chart-conversion-funnel = Conversion Funnel
chart-device-breakdown = Device Breakdown
## Time Periods
period-today = Today
period-week = This Week
period-month = This Month
period-year = This Year
period-custom = Custom Range
## Actions
action-export = Export Report
action-download-csv = Download CSV
action-share = Share Dashboard
action-print = Print
action-refresh = Refresh Data
## Status Messages
status-loading = Loading analytics data...
status-error = Unable to load data
status-no-data = No data available for this period
status-success = Data loaded successfully
## Tooltips
tooltip-hover-for-details = Hover for more details
tooltip-click-to-filter = Click to filter
tooltip-drag-to-zoom = Drag to zoom
## Buttons
button-apply-filter = Apply Filter
button-clear-filters = Clear Filters
button-date-range = Select Date Range
button-compare = Compare Periods
## Accessibility
label-chart-analytics = Analytics chart
label-metric-card = Metric card showing { $metric }
label-data-table = Analytics data table

View file

@ -0,0 +1,62 @@
# Tema de Ejemplo del Plugin - Traducciones al Español
# Cadenas de la interfaz de usuario del panel de análisis
## Metadatos del Plugin
example-plugin-name = Tema Analytics de Ejemplo
example-plugin-description = Tema profesional para paneles de análisis con visualización de datos optimizada
## Encabezados del Panel
dashboard-title = Panel de Análisis
dashboard-subtitle = Métricas de rendimiento e información en tiempo real
dashboard-refresh = Última actualización { $time }
## Etiquetas de Métricas
metrics-total-users = Usuarios Totales
metrics-active-sessions = Sesiones Activas
metrics-page-views = Vistas de Página
metrics-avg-session-duration = Duración Promedio de Sesión
metrics-bounce-rate = Tasa de Rebote
metrics-conversion-rate = Tasa de Conversión
## Etiquetas de Gráficos
chart-daily-activity = Actividad Diaria
chart-user-growth = Crecimiento de Usuarios
chart-traffic-sources = Fuentes de Tráfico
chart-conversion-funnel = Embudo de Conversión
chart-device-breakdown = Desglose de Dispositivos
## Períodos de Tiempo
period-today = Hoy
period-week = Esta Semana
period-month = Este Mes
period-year = Este Año
period-custom = Rango Personalizado
## Acciones
action-export = Exportar Informe
action-download-csv = Descargar CSV
action-share = Compartir Panel
action-print = Imprimir
action-refresh = Actualizar Datos
## Mensajes de Estado
status-loading = Cargando datos de análisis...
status-error = No se pudieron cargar los datos
status-no-data = Sin datos disponibles para este período
status-success = Datos cargados exitosamente
## Sugerencias
tooltip-hover-for-details = Pasar el ratón para más detalles
tooltip-click-to-filter = Haz clic para filtrar
tooltip-drag-to-zoom = Arrastra para ampliar
## Botones
button-apply-filter = Aplicar Filtro
button-clear-filters = Limpiar Filtros
button-date-range = Seleccionar Rango de Fechas
button-compare = Comparar Períodos
## Accesibilidad
label-chart-analytics = Gráfico de análisis
label-metric-card = Tarjeta de métrica que muestra { $metric }
label-data-table = Tabla de datos de análisis

View file

@ -0,0 +1,114 @@
# Analytics Dashboard Theme
# A modern theme optimized for displaying analytics and metrics data
#
# Colors chosen for clarity with statistical data visualization:
# - High contrast for readability
# - Blue for primary data (professional, trustworthy)
# - Teal for secondary data (complementary, distinguishing)
# - Warm neutral backgrounds for extended viewing
[metadata]
name = "Analytics Dashboard"
description = "Professional theme for analytics dashboards with optimized readability"
version = "1.0.0"
author = "Rustelo Example Plugin"
[colors]
# Primary brand colors
primary = "#2563EB" # Modern blue - professional and trustworthy
secondary = "#14B8A6" # Teal - complements primary for contrast
accent = "#F59E0B" # Amber - draws attention to important metrics
# Background and surface colors
background = "#FFFFFF" # Clean white for maximum readability
surface = "#F9FAFB" # Light gray for card backgrounds
surface-variant = "#F3F4F6" # Slightly darker for depth
# Text colors
text = "#111827" # Nearly black for excellent readability
text-secondary = "#6B7280" # Medium gray for secondary content
text-muted = "#9CA3AF" # Light gray for tertiary content
# Status colors
success = "#10B981" # Green for positive metrics
warning = "#FBBF24" # Amber for warnings
error = "#EF4444" # Red for errors
info = "#3B82F6" # Blue for information
# Data visualization
chart-1 = "#3B82F6" # Blue
chart-2 = "#10B981" # Green
chart-3 = "#F59E0B" # Amber
chart-4 = "#EF4444" # Red
chart-5 = "#8B5CF6" # Purple
chart-6 = "#EC4899" # Pink
[typography]
# Font selections for clarity with data
heading = "Inter, system-ui, sans-serif"
body = "Inter, system-ui, sans-serif"
mono = "JetBrains Mono, Monaco, monospace"
# Scale for consistent sizing
font-scale = 1.125 # 12.5% increase per step (1rem * 1.125)
[spacing]
# Base unit for all spacing
base = 8 # px
# Semantic spacing increments
step = 1.5 # Multiplier for each spacing level (8px, 12px, 18px, 27px, etc)
[borders]
radius-small = 4 # px - for small elements
radius-medium = 8 # px - for cards and buttons
radius-large = 12 # px - for large containers
radius-full = 9999 # px - for fully rounded elements
width-thin = 1 # px - subtle borders
width-regular = 2 # px - standard borders
width-bold = 3 # px - emphasized borders
[shadows]
elevation-0 = "none"
elevation-1 = "0 1px 2px rgba(0, 0, 0, 0.05)"
elevation-2 = "0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06)"
elevation-3 = "0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)"
elevation-4 = "0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05)"
[responsive]
# Breakpoints for responsive design
breakpoint-mobile = 320 # px
breakpoint-tablet = 768 # px
breakpoint-desktop = 1024 # px
breakpoint-wide = 1280 # px
breakpoint-ultrawide = 1536 # px
[animation]
# Transition timings for smooth animations
duration-instant = 0 # ms
duration-fast = 100 # ms
duration-normal = 200 # ms
duration-slow = 300 # ms
duration-slower = 500 # ms
easing-linear = "cubic-bezier(0, 0, 1, 1)"
easing-ease = "cubic-bezier(0.25, 0.1, 0.25, 1)"
easing-ease-in = "cubic-bezier(0.42, 0, 1, 1)"
easing-ease-out = "cubic-bezier(0, 0, 0.58, 1)"
[density]
# Visual density settings
compact = true # Use compact spacing for more information density
show-borders = true # Show subtle borders for clarity
show-shadows = true # Show shadows for depth
[contrast]
# Contrast settings for accessibility
level = "AA" # WCAG AA standard (minimum)
enhanced = false # Can be enabled for WCAG AAA
[print]
# Print-specific settings
optimized = true
monochrome = false # Use colors in print (can be changed for cost reduction)

View file

@ -0,0 +1,87 @@
//! Example Analytics Theme Plugin
//!
//! This plugin demonstrates a complete, minimal plugin implementation for
//! Rustelo. It provides a custom theme optimized for displaying analytics
//! dashboards.
//!
//! # Features
//!
//! - Custom color scheme with analytics-friendly palette
//! - Responsive typography for data visualization
//! - Support for multiple languages (English, Spanish)
//! - Complete documentation and tests
//!
//! # Usage
//!
//! To use this plugin in your Rustelo project:
//!
//! 1. Add to your `Cargo.toml`: ```toml [dependencies] plugin-example-theme = {
//! path = "./crates/plugin-example-theme" } ```
//!
//! 2. Register in your resources initialization: ```rust,no_run use
//! plugin_example_theme::ExampleThemeContributor;
//!
//! rustelo_core_lib::register_contributor(&ExampleThemeContributor)?;
//! ```
//!
//! # Architecture
//!
//! The plugin follows the Rustelo Level 5 (Compile-Time) plugin architecture:
//! - Implements `ResourceContributor` trait
//! - Embeds configuration via `include_str!()`
//! - Registers at application startup
//! - No conditional compilation or framework coupling
mod resources;
pub use resources::ExampleThemeContributor;
/// Initialize the example theme plugin
///
/// This function registers the plugin's resource contributor with the
/// framework. Called automatically if the plugin is included in the
/// application.
///
/// # Returns
///
/// Returns `Ok(())` on successful registration, or an error if registration
/// fails.
///
/// # Example
///
/// ```no_run
/// use plugin_example_theme::initialize;
///
/// let result = initialize()?;
/// println!("Plugin initialized!");
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn initialize() -> Result<(), Box<dyn std::error::Error>> {
rustelo_core_lib::register_contributor(&ExampleThemeContributor)?;
#[cfg(not(target_arch = "wasm32"))]
{
eprintln!("✅ Example Theme Plugin initialized successfully");
eprintln!(" Theme: analytics-dashboard");
eprintln!(" Languages: en, es");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_can_be_initialized() {
let _contributor = ExampleThemeContributor;
// If we got here, the contributor exists and can be instantiated
}
#[test]
fn test_plugin_exports_public_api() {
// Verify the public API is accessible
let _path_to_initialize = initialize;
}
}

View file

@ -0,0 +1,145 @@
//! Resource contributor for the example theme plugin
//!
//! This module implements the `ResourceContributor` trait,
//! which allows the plugin to provide menus, themes and translations to the
//! framework.
use std::collections::HashMap;
use rustelo_core_lib::registration::ResourceContributor;
/// The example theme plugin's resource contributor
///
/// Implements the `ResourceContributor` trait to provide:
/// - A custom analytics dashboard theme
/// - English and Spanish translations
/// - No menus (optional)
///
/// # Implementation Details
///
/// Configuration files are embedded at compile time using `include_str!()`.
/// This ensures the theme is always available and keeps the plugin
/// self-contained.
#[derive(Debug, Clone, Copy)]
pub struct ExampleThemeContributor;
impl ResourceContributor for ExampleThemeContributor {
fn contribute_menus(&self) -> HashMap<String, String> {
// This plugin doesn't provide menus
HashMap::new()
}
fn contribute_themes(&self) -> HashMap<String, String> {
let mut themes = HashMap::new();
// Embed the analytics dashboard theme
let analytics_theme = include_str!("../config/themes/analytics-dashboard.toml");
themes.insert(
"analytics-dashboard".to_string(),
analytics_theme.to_string(),
);
themes
}
fn contribute_ftl(&self) -> HashMap<String, String> {
let mut ftl_files = HashMap::new();
// Embed English translations
let en_translations = include_str!("../config/i18n/en.ftl");
ftl_files.insert("en".to_string(), en_translations.to_string());
// Embed Spanish translations
let es_translations = include_str!("../config/i18n/es.ftl");
ftl_files.insert("es".to_string(), es_translations.to_string());
ftl_files
}
fn name(&self) -> &str {
"example-theme-plugin"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contributor_name() {
let contrib = ExampleThemeContributor;
assert_eq!(contrib.name(), "example-theme-plugin");
}
#[test]
fn test_theme_is_provided() {
let contrib = ExampleThemeContributor;
let themes = contrib.contribute_themes();
assert_eq!(themes.len(), 1);
assert!(themes.contains_key("analytics-dashboard"));
}
#[test]
fn test_theme_content_is_not_empty() {
let contrib = ExampleThemeContributor;
let themes = contrib.contribute_themes();
let theme = themes.get("analytics-dashboard").unwrap();
assert!(!theme.is_empty());
}
#[test]
fn test_theme_is_valid_toml() {
let contrib = ExampleThemeContributor;
let themes = contrib.contribute_themes();
let theme = themes.get("analytics-dashboard").unwrap();
// Verify it can be parsed as TOML
let parsed = toml::from_str::<toml::Value>(theme);
assert!(
parsed.is_ok(),
"Analytics dashboard theme is not valid TOML: {}",
parsed.unwrap_err()
);
}
#[test]
fn test_fluent_files_are_provided() {
let contrib = ExampleThemeContributor;
let ftl_files = contrib.contribute_ftl();
assert!(ftl_files.contains_key("en"));
assert!(ftl_files.contains_key("es"));
}
#[test]
fn test_fluent_files_have_content() {
let contrib = ExampleThemeContributor;
let ftl_files = contrib.contribute_ftl();
let en = ftl_files.get("en").unwrap();
assert!(!en.is_empty());
let es = ftl_files.get("es").unwrap();
assert!(!es.is_empty());
}
#[test]
fn test_menus_are_empty() {
let contrib = ExampleThemeContributor;
// This plugin doesn't provide menus
assert!(contrib.contribute_menus().is_empty());
}
#[test]
fn test_contributor_is_clone_and_copy() {
let contrib1 = ExampleThemeContributor;
let contrib2 = contrib1; // Copy
assert_eq!(contrib1.name(), contrib2.name());
}
}

View file

@ -0,0 +1,154 @@
{
name = "Blog Post Metadata",
description = "Create or edit blog post metadata. Saved to {slug}.ncl alongside the post body. thumbnail and image_url are excluded — owned exclusively by generate-images.nu.",
display_mode = "complete",
elements = [
# ── Identity ─────────────────────────────────────────────────────────────
{
type = "section_header",
name = "identity_header",
title = "Identity",
border_top = true,
border_bottom = true,
},
{
type = "text",
name = "title",
prompt = "Title",
required = true,
nickel_path = ["title"],
help = "Post title shown in listings, headings, and browser tab.",
},
{
type = "text",
name = "slug",
prompt = "Slug",
required = true,
nickel_path = ["slug"],
help = "URL segment — lowercase, hyphens only, max 60 chars. Matches the .ncl filename stem. Immutable after publication.",
},
{
type = "text",
name = "subtitle",
prompt = "Subtitle",
required = false,
nickel_path = ["subtitle"],
help = "Secondary heading displayed under the title.",
},
{
type = "text",
name = "excerpt",
prompt = "Excerpt",
required = false,
nickel_path = ["excerpt"],
help = "1-3 sentences for index listings and SEO meta description.",
},
# ── Taxonomy ──────────────────────────────────────────────────────────────
{
type = "section_header",
name = "taxonomy_header",
title = "Taxonomy",
border_top = true,
border_bottom = true,
},
{
type = "text",
name = "category",
prompt = "Category",
required = true,
nickel_path = ["category"],
help = "Primary category slug — must match the parent directory name (e.g. rust, devops, web3).",
},
{
type = "text",
name = "tags",
prompt = "Tags",
required = false,
nickel_path = ["tags"],
help = "Comma-separated. new-post.nu converts to Array String before writing the NCL. E.g. rust, leptos, async",
},
# ── Publication ───────────────────────────────────────────────────────────
{
type = "section_header",
name = "publication_header",
title = "Publication",
border_top = true,
border_bottom = true,
},
{
type = "text",
name = "date",
prompt = "Publication date",
required = true,
nickel_path = ["date"],
help = "YYYY-MM-DD. Original publication date — never update after publication.",
},
{
type = "text",
name = "updated_at",
prompt = "Updated at",
required = false,
nickel_path = ["updated_at"],
help = "YYYY-MM-DD — date of last significant content change. Leave blank on creation.",
},
{
type = "confirm",
name = "published",
prompt = "Published?",
default = false,
nickel_path = ["published"],
help = "False hides the post from all listings and the API. Set to false on creation.",
},
{
type = "confirm",
name = "draft",
prompt = "Draft?",
default = true,
nickel_path = ["draft"],
help = "Draft posts are excluded from the index and /api/content-render. Set to true on creation.",
},
{
type = "confirm",
name = "featured",
prompt = "Featured?",
default = false,
nickel_path = ["featured"],
help = "Pins to the top of the category listing and enables featured styling.",
},
# ── Display ───────────────────────────────────────────────────────────────
{
type = "section_header",
name = "display_header",
title = "Display",
border_top = true,
border_bottom = true,
},
{
type = "text",
name = "read_time",
prompt = "Read time",
required = false,
nickel_path = ["read_time"],
help = "Estimated reading time for the listing card. E.g. '8 min read'. Auto-derived from word count when absent.",
},
{
type = "text",
name = "sort_order",
prompt = "Sort order",
required = false,
default = "0",
nickel_path = ["sort_order"],
help = "Manual position within the category listing. Lower values appear first. 0 = no preference. new-post.nu converts to Number.",
},
],
}

View file

@ -1,287 +0,0 @@
[dependencies]
axum = "0.8.4"
serde_json = "1.0"
thiserror = "2.0.12"
anyhow = "1.0.98"
rand = "0.9.1"
glob = "0.3.3"
console_error_panic_hook = "0.1"
http = "1"
log = "0.4.27"
env_logger = "0.11"
wasm-bindgen-futures = "0.4.50"
wasm-bindgen = "=0.2.100"
js-sys = "0.3.77"
console_log = "1"
reqwasm = "0.5.0"
serde-wasm-bindgen = "0.6.5"
regex = "1.11.1"
tracing = "0.1"
tracing-subscriber = "0.3"
toml = "0.9"
fluent = "0.17"
fluent-bundle = "0.16"
fluent-syntax = "0.12"
tower = "0.5.2"
hex = "0.4.3"
dotenv = "0.15.0"
async-trait = "0.1.88"
once_cell = "1.21.3"
axum-test = "18.0"
serde_yaml = "0.9"
tempfile = "3.20"
tera = "1.20"
unicode-normalization = "0.1"
paste = "1.0"
typed-builder = "0.21"
notify = "8.2.0"
lru = "0.16"
ammonia = "4.1"
scraper = "0.24"
futures = "0.3.31"
ratatui = "0.29"
inquire = "0.7"
crossterm = "0.29"
syntect = "5.2"
similar = "2.7"
walkdir = "2.5"
quote = "1.0"
proc-macro2 = "1.0"
gray_matter = "0.2"
ignore = "0.4"
mockall = "0.14"
wiremock = "0.6"
[dependencies.utils]
path = "crates/utils"
[dependencies.core-types]
path = "crates/core-types"
[dependencies.tools]
path = "crates/tools"
[dependencies.core-lib]
path = "crates/core-lib"
[dependencies.components]
path = "crates/components"
[dependencies.pages]
path = "crates/pages"
[dependencies.client]
path = "crates/client"
[dependencies.leptos]
version = "0.8.6"
features = [
"hydrate",
"ssr",
]
[dependencies.leptos_router]
version = "0.8.5"
features = ["ssr"]
[dependencies.leptos_axum]
version = "0.8.5"
[dependencies.leptos_config]
version = "0.8.5"
[dependencies.leptos_meta]
version = "0.8.5"
[dependencies.serde]
version = "1.0"
features = ["derive"]
[dependencies.rand_core]
version = "0.6.4"
features = ["getrandom"]
[dependencies.gloo-timers]
version = "0.3"
features = ["futures"]
[dependencies.gloo-net]
version = "0.6.0"
[dependencies.reqwest]
version = "0.12.22"
features = ["json"]
[dependencies.web-sys]
version = "0.3.77"
features = [
"Clipboard",
"Window",
"Navigator",
"Permissions",
"MouseEvent",
"Storage",
"console",
"File",
]
[dependencies.unic-langid]
version = "0.9"
features = ["unic-langid-macros"]
[dependencies.tokio]
version = "1.47.1"
features = ["rt-multi-thread"]
[dependencies.tower-http]
version = "0.6.6"
features = ["fs"]
[dependencies.fluent-templates]
version = "0.13.0"
features = ["tera"]
[dependencies.rhai]
version = "1.22"
features = [
"serde",
"only_i64",
"no_float",
]
[dependencies.lettre]
version = "0.11"
features = [
"tokio1-native-tls",
"smtp-transport",
"pool",
"hostname",
"builder",
]
[dependencies.handlebars]
version = "6.3"
[dependencies.urlencoding]
version = "2.1"
[dependencies.axum-server]
version = "0.7"
features = ["tls-rustls"]
[dependencies.rustls]
version = "0.23"
[dependencies.rustls-pemfile]
version = "2.2"
[dependencies.jsonwebtoken]
version = "9.3"
[dependencies.argon2]
version = "0.5"
[dependencies.uuid]
version = "1.17"
features = [
"v4",
"serde",
"js",
]
[dependencies.chrono]
version = "0.4"
features = ["serde"]
[dependencies.oauth2]
version = "5.0"
[dependencies.tower-sessions]
version = "0.14"
[dependencies.sqlx]
version = "0.8"
features = [
"runtime-tokio-rustls",
"postgres",
"sqlite",
"chrono",
"uuid",
"migrate",
]
[dependencies.tower-cookies]
version = "0.11"
[dependencies.time]
version = "0.3.41"
features = ["serde"]
[dependencies.totp-rs]
version = "5.7.0"
[dependencies.qrcode]
version = "0.14"
features = ["svg"]
[dependencies.base32]
version = "0.5"
[dependencies.sha2]
version = "0.10"
[dependencies.base64]
version = "0.22"
[dependencies.aes-gcm]
version = "0.10"
[dependencies.clap]
version = "4.5"
features = ["derive"]
[dependencies.prometheus]
version = "0.14"
[dependencies.pulldown-cmark]
version = "0.12"
features = ["simd"]
[dependencies.async-compression]
version = "0.4"
features = [
"gzip",
"tokio",
]
[dependencies.rustelo-core]
path = "crates/rustelo-core"
[dependencies.rustelo-web]
path = "crates/rustelo-web"
[dependencies.rustelo-auth]
path = "crates/rustelo-auth"
[dependencies.rustelo-content]
path = "crates/rustelo-content"
[dependencies.shared]
path = "crates/shared"
[dependencies.ssr]
path = "crates/ssr"
[dependencies.server]
path = "crates/server"
[dependencies.rustelo-cli]
path = "crates/rustelo-cli"
[dependencies.syn]
version = "2.0"
features = ["full"]
[dependencies.comrak]
version = "0.36"
features = ["syntect"]