Compare commits
4 Commits
feat/CAI-4
...
8b16eba1ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b16eba1ad | ||
|
|
58420b4547 | ||
|
|
d473f7570b | ||
|
|
46b2ee5dfa |
@@ -96,76 +96,19 @@ jobs:
|
||||
run: cargo test --features web --no-default-features
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 3: Build Docker image and push to registry
|
||||
# Only on main and release/* branches
|
||||
# Stage 3: Deploy (only after tests pass, only on main)
|
||||
# ---------------------------------------------------------------------------
|
||||
build-and-push:
|
||||
name: Build & Push Image
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: docker
|
||||
needs: [test]
|
||||
if: >-
|
||||
github.event_name == 'push' &&
|
||||
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/'))
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git init
|
||||
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||
git fetch --depth=1 origin "${GITHUB_SHA}"
|
||||
git checkout FETCH_HEAD
|
||||
|
||||
- name: Determine image tag
|
||||
id: tag
|
||||
run: |
|
||||
BRANCH="${GITHUB_REF#refs/heads/}"
|
||||
# Replace / with - for valid Docker tags (e.g. release/1.0 -> release-1.0)
|
||||
BRANCH_SAFE=$(echo "$BRANCH" | tr '/' '-')
|
||||
SHA=$(echo "$GITHUB_SHA" | head -c 8)
|
||||
echo "tag=${BRANCH_SAFE}-${SHA}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to container registry
|
||||
run: >-
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}"
|
||||
| docker login https://registry.meghsakha.com
|
||||
-u "${{ secrets.REGISTRY_USERNAME }}"
|
||||
--password-stdin
|
||||
|
||||
- name: Build Docker image
|
||||
run: >-
|
||||
docker build
|
||||
-t registry.meghsakha.com/certifai/dashboard:${{ steps.tag.outputs.tag }}
|
||||
-t registry.meghsakha.com/certifai/dashboard:latest
|
||||
.
|
||||
|
||||
- name: Push Docker image
|
||||
run: |
|
||||
docker push registry.meghsakha.com/certifai/dashboard:${{ steps.tag.outputs.tag }}
|
||||
docker push registry.meghsakha.com/certifai/dashboard:latest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 3b: Generate changelog from conventional commits
|
||||
# Only on main and release/* branches
|
||||
# ---------------------------------------------------------------------------
|
||||
changelog:
|
||||
name: Changelog
|
||||
runs-on: docker
|
||||
needs: [test]
|
||||
if: >-
|
||||
github.event_name == 'push' &&
|
||||
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/'))
|
||||
if: github.ref == 'refs/heads/main'
|
||||
container:
|
||||
image: rust:1.89-bookworm
|
||||
image: alpine:latest
|
||||
steps:
|
||||
- name: Checkout (full history)
|
||||
- name: Trigger Coolify deploy
|
||||
run: |
|
||||
git clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" .
|
||||
git checkout "${GITHUB_SHA}"
|
||||
- name: Install git-cliff
|
||||
run: cargo install git-cliff --locked
|
||||
- name: Generate changelog
|
||||
run: git cliff --output CHANGELOG.md
|
||||
- name: Upload changelog artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: changelog
|
||||
path: CHANGELOG.md
|
||||
apk add --no-cache curl
|
||||
curl -sf "${{ secrets.COOLIFY_WEBHOOK }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
|
||||
|
||||
|
||||
@@ -237,10 +237,6 @@ The SaaS application dashboard is the landing page for the company admin to view
|
||||
|
||||
This project is written in dioxus with fullstack and router features. MongoDB is used as a database for maintaining user state. Keycloak is used as identity provider for user management.
|
||||
|
||||
## Features management
|
||||
|
||||
All features are detailed and described under the features folder in clear markdown instructions which are valid for both human and AI code developers.
|
||||
|
||||
## Code structure
|
||||
The following folder structure is maintained for separation of concerns:
|
||||
- src/components/*.rs : All components that are required to be rendered are placed here. These are frontend only, reusable components that are specific for the application.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# CERTifAI
|
||||
|
||||
[](https://gitea.meghsakha.com/sharang/certifai/actions?workflow=ci.yml)
|
||||
[](https://www.rust-lang.org/)
|
||||
[](https://dioxuslabs.com/)
|
||||
[](LICENSE)
|
||||
[](https://gdpr.eu/)
|
||||
|
||||
This project is a SaaS application dashboard for administation of self-hosted private GenAI (generative AI) toolbox for companies and individuals. The purpose of the dashboard is to manage LLMs, Agents, MCP Servers and other GenAI related features.
|
||||
|
||||
The purpose of `CERTifAI`is to provide self-hosted or GDPR-Conform GenAI infrastructure to companies who do not wish to subscribe to non-EU cloud providers to protect their intellectual property from being used as training data.
|
||||
@@ -19,9 +25,6 @@ The SaaS application dashboard is the landing page for the company admin to view
|
||||
|
||||
This project is written in dioxus with fullstack and router features. MongoDB is used as a database for maintaining user state. Keycloak is used as identity provider for user management.
|
||||
|
||||
## Features management
|
||||
|
||||
All features are detailed and described under the features folder in clear markdown instructions which are valid for both human and AI code developers.
|
||||
|
||||
## Code structure
|
||||
The following folder structure is maintained for separation of concerns:
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# CAI-1
|
||||
|
||||
This feature creates a new login/registration page for the GenAI admin dashboard. The user management is provided by Keycloak, which also serves the login/registration flow. The dioxus app should detect if a user is already logged-in or not, and if not, redirect the user to the keycloak landing page and after successful login, capture the user's access token in a state and save a session state.
|
||||
|
||||
Steps to follow:
|
||||
- Create a docker-compose file for hosting a local keycloak and create a realm for testing and a client for Oauth.
|
||||
- Setup the environment variables using .env. Fill the environment with keycloak URL, realm, client ID and secret.
|
||||
- Create a user state in Dioxus which manages the session and the access token. Add other user identifying information like email address to the state.
|
||||
- Modify dioxus to check the state and load the correct URL based on the state.
|
||||
@@ -1,3 +0,0 @@
|
||||
# CERTifAI 2
|
||||
|
||||
This feature defines the types for database as well as the API between the dashboard backend and frontend.
|
||||
@@ -46,12 +46,16 @@ fn LandingNav() -> Element {
|
||||
}
|
||||
div { class: "landing-nav-actions",
|
||||
Link {
|
||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
||||
to: Route::Login {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-ghost btn-sm",
|
||||
"Log In"
|
||||
}
|
||||
Link {
|
||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
||||
to: Route::Login {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-primary btn-sm",
|
||||
"Get Started"
|
||||
}
|
||||
@@ -67,9 +71,7 @@ fn HeroSection() -> Element {
|
||||
rsx! {
|
||||
section { class: "hero-section",
|
||||
div { class: "hero-content",
|
||||
div { class: "hero-badge badge badge-outline",
|
||||
"Privacy-First GenAI Infrastructure"
|
||||
}
|
||||
div { class: "hero-badge badge badge-outline", "Privacy-First GenAI Infrastructure" }
|
||||
h1 { class: "hero-title",
|
||||
"Your AI. Your Data."
|
||||
br {}
|
||||
@@ -82,16 +84,14 @@ fn HeroSection() -> Element {
|
||||
}
|
||||
div { class: "hero-actions",
|
||||
Link {
|
||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
||||
to: Route::Login {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-primary btn-lg",
|
||||
"Get Started"
|
||||
Icon { icon: BsArrowRight, width: 18, height: 18 }
|
||||
}
|
||||
a {
|
||||
href: "#features",
|
||||
class: "btn btn-outline btn-lg",
|
||||
"Learn More"
|
||||
}
|
||||
a { href: "#features", class: "btn btn-outline btn-lg", "Learn More" }
|
||||
}
|
||||
}
|
||||
div { class: "hero-graphic",
|
||||
@@ -105,27 +105,44 @@ fn HeroSection() -> Element {
|
||||
defs {
|
||||
linearGradient {
|
||||
id: "grad1",
|
||||
x1: "0%", y1: "0%",
|
||||
x2: "100%", y2: "100%",
|
||||
x1: "0%",
|
||||
y1: "0%",
|
||||
x2: "100%",
|
||||
y2: "100%",
|
||||
stop { offset: "0%", stop_color: "#91a4d2" }
|
||||
stop { offset: "100%", stop_color: "#6d85c6" }
|
||||
}
|
||||
linearGradient {
|
||||
id: "grad2",
|
||||
x1: "0%", y1: "100%",
|
||||
x2: "100%", y2: "0%",
|
||||
x1: "0%",
|
||||
y1: "100%",
|
||||
x2: "100%",
|
||||
y2: "0%",
|
||||
stop { offset: "0%", stop_color: "#f97066" }
|
||||
stop { offset: "100%", stop_color: "#f9a066" }
|
||||
}
|
||||
radialGradient {
|
||||
id: "glow",
|
||||
cx: "50%", cy: "50%", r: "50%",
|
||||
stop { offset: "0%", stop_color: "rgba(145,164,210,0.3)" }
|
||||
stop { offset: "100%", stop_color: "rgba(145,164,210,0)" }
|
||||
cx: "50%",
|
||||
cy: "50%",
|
||||
r: "50%",
|
||||
stop {
|
||||
offset: "0%",
|
||||
stop_color: "rgba(145,164,210,0.3)",
|
||||
}
|
||||
stop {
|
||||
offset: "100%",
|
||||
stop_color: "rgba(145,164,210,0)",
|
||||
}
|
||||
}
|
||||
}
|
||||
// Background glow
|
||||
circle { cx: "200", cy: "200", r: "180", fill: "url(#glow)" }
|
||||
circle {
|
||||
cx: "200",
|
||||
cy: "200",
|
||||
r: "180",
|
||||
fill: "url(#glow)",
|
||||
}
|
||||
// Shield outline
|
||||
path {
|
||||
d: "M200 40 L340 110 L340 230 C340 300 270 360 200 380 \
|
||||
@@ -145,36 +162,98 @@ fn HeroSection() -> Element {
|
||||
opacity: "0.8",
|
||||
}
|
||||
// Network nodes
|
||||
circle { cx: "200", cy: "180", r: "8", fill: "url(#grad1)" }
|
||||
circle { cx: "150", cy: "230", r: "6", fill: "url(#grad2)" }
|
||||
circle { cx: "250", cy: "230", r: "6", fill: "url(#grad2)" }
|
||||
circle { cx: "200", cy: "280", r: "6", fill: "url(#grad1)" }
|
||||
circle { cx: "130", cy: "170", r: "4", fill: "#91a4d2", opacity: "0.6" }
|
||||
circle { cx: "270", cy: "170", r: "4", fill: "#91a4d2", opacity: "0.6" }
|
||||
circle {
|
||||
cx: "200",
|
||||
cy: "180",
|
||||
r: "8",
|
||||
fill: "url(#grad1)",
|
||||
}
|
||||
circle {
|
||||
cx: "150",
|
||||
cy: "230",
|
||||
r: "6",
|
||||
fill: "url(#grad2)",
|
||||
}
|
||||
circle {
|
||||
cx: "250",
|
||||
cy: "230",
|
||||
r: "6",
|
||||
fill: "url(#grad2)",
|
||||
}
|
||||
circle {
|
||||
cx: "200",
|
||||
cy: "280",
|
||||
r: "6",
|
||||
fill: "url(#grad1)",
|
||||
}
|
||||
circle {
|
||||
cx: "130",
|
||||
cy: "170",
|
||||
r: "4",
|
||||
fill: "#91a4d2",
|
||||
opacity: "0.6",
|
||||
}
|
||||
circle {
|
||||
cx: "270",
|
||||
cy: "170",
|
||||
r: "4",
|
||||
fill: "#91a4d2",
|
||||
opacity: "0.6",
|
||||
}
|
||||
// Network connections
|
||||
line {
|
||||
x1: "200", y1: "180", x2: "150", y2: "230",
|
||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.4",
|
||||
x1: "200",
|
||||
y1: "180",
|
||||
x2: "150",
|
||||
y2: "230",
|
||||
stroke: "#91a4d2",
|
||||
stroke_width: "1",
|
||||
opacity: "0.4",
|
||||
}
|
||||
line {
|
||||
x1: "200", y1: "180", x2: "250", y2: "230",
|
||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.4",
|
||||
x1: "200",
|
||||
y1: "180",
|
||||
x2: "250",
|
||||
y2: "230",
|
||||
stroke: "#91a4d2",
|
||||
stroke_width: "1",
|
||||
opacity: "0.4",
|
||||
}
|
||||
line {
|
||||
x1: "150", y1: "230", x2: "200", y2: "280",
|
||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.4",
|
||||
x1: "150",
|
||||
y1: "230",
|
||||
x2: "200",
|
||||
y2: "280",
|
||||
stroke: "#91a4d2",
|
||||
stroke_width: "1",
|
||||
opacity: "0.4",
|
||||
}
|
||||
line {
|
||||
x1: "250", y1: "230", x2: "200", y2: "280",
|
||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.4",
|
||||
x1: "250",
|
||||
y1: "230",
|
||||
x2: "200",
|
||||
y2: "280",
|
||||
stroke: "#91a4d2",
|
||||
stroke_width: "1",
|
||||
opacity: "0.4",
|
||||
}
|
||||
line {
|
||||
x1: "200", y1: "180", x2: "130", y2: "170",
|
||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.3",
|
||||
x1: "200",
|
||||
y1: "180",
|
||||
x2: "130",
|
||||
y2: "170",
|
||||
stroke: "#91a4d2",
|
||||
stroke_width: "1",
|
||||
opacity: "0.3",
|
||||
}
|
||||
line {
|
||||
x1: "200", y1: "180", x2: "270", y2: "170",
|
||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.3",
|
||||
x1: "200",
|
||||
y1: "180",
|
||||
x2: "270",
|
||||
y2: "170",
|
||||
stroke: "#91a4d2",
|
||||
stroke_width: "1",
|
||||
opacity: "0.3",
|
||||
}
|
||||
// Checkmark inside shield center
|
||||
path {
|
||||
@@ -236,37 +315,49 @@ fn FeaturesGrid() -> Element {
|
||||
}
|
||||
div { class: "features-grid",
|
||||
FeatureCard {
|
||||
icon: rsx! { Icon { icon: BsServer, width: 28, height: 28 } },
|
||||
icon: rsx! {
|
||||
Icon { icon: BsServer, width: 28, height: 28 }
|
||||
},
|
||||
title: "Self-Hosted Infrastructure",
|
||||
description: "Deploy on your own hardware or private cloud. \
|
||||
Full control over your AI stack with no external dependencies.",
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! { Icon { icon: BsShieldCheck, width: 28, height: 28 } },
|
||||
icon: rsx! {
|
||||
Icon { icon: BsShieldCheck, width: 28, height: 28 }
|
||||
},
|
||||
title: "GDPR Compliant",
|
||||
description: "EU data residency guaranteed. Your data never \
|
||||
leaves your infrastructure or gets shared with third parties.",
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! { Icon { icon: FaCubes, width: 28, height: 28 } },
|
||||
icon: rsx! {
|
||||
Icon { icon: FaCubes, width: 28, height: 28 }
|
||||
},
|
||||
title: "LLM Management",
|
||||
description: "Deploy, monitor, and manage multiple language \
|
||||
models. Switch between models with zero downtime.",
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! { Icon { icon: BsRobot, width: 28, height: 28 } },
|
||||
icon: rsx! {
|
||||
Icon { icon: BsRobot, width: 28, height: 28 }
|
||||
},
|
||||
title: "Agent Builder",
|
||||
description: "Create custom AI agents with integrated Langchain \
|
||||
and Langfuse for full observability and control.",
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! { Icon { icon: BsGlobe2, width: 28, height: 28 } },
|
||||
icon: rsx! {
|
||||
Icon { icon: BsGlobe2, width: 28, height: 28 }
|
||||
},
|
||||
title: "MCP Server Management",
|
||||
description: "Manage Model Context Protocol servers to extend \
|
||||
your AI capabilities with external tool integrations.",
|
||||
}
|
||||
FeatureCard {
|
||||
icon: rsx! { Icon { icon: BsKey, width: 28, height: 28 } },
|
||||
icon: rsx! {
|
||||
Icon { icon: BsKey, width: 28, height: 28 }
|
||||
},
|
||||
title: "API Key Management",
|
||||
description: "Generate API keys, track usage per seat, and \
|
||||
set fine-grained permissions for every integration.",
|
||||
@@ -300,9 +391,7 @@ fn HowItWorks() -> Element {
|
||||
rsx! {
|
||||
section { id: "how-it-works", class: "how-it-works-section",
|
||||
h2 { class: "section-title", "Up and Running in Minutes" }
|
||||
p { class: "section-subtitle",
|
||||
"Three steps to sovereign AI infrastructure."
|
||||
}
|
||||
p { class: "section-subtitle", "Three steps to sovereign AI infrastructure." }
|
||||
div { class: "steps-grid",
|
||||
StepCard {
|
||||
number: "01",
|
||||
@@ -353,21 +442,23 @@ fn StepCard(number: &'static str, title: &'static str, description: &'static str
|
||||
fn CtaBanner() -> Element {
|
||||
rsx! {
|
||||
section { class: "cta-banner",
|
||||
h2 { class: "cta-title",
|
||||
"Ready to take control of your AI infrastructure?"
|
||||
}
|
||||
h2 { class: "cta-title", "Ready to take control of your AI infrastructure?" }
|
||||
p { class: "cta-subtitle",
|
||||
"Start deploying sovereign GenAI today. No credit card required."
|
||||
}
|
||||
div { class: "cta-actions",
|
||||
Link {
|
||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
||||
to: Route::Login {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-primary btn-lg",
|
||||
"Get Started Free"
|
||||
Icon { icon: BsArrowRight, width: 18, height: 18 }
|
||||
}
|
||||
Link {
|
||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
||||
to: Route::Login {
|
||||
redirect_url: "/dashboard".into(),
|
||||
},
|
||||
class: "btn btn-outline btn-lg",
|
||||
"Log In"
|
||||
}
|
||||
@@ -389,9 +480,7 @@ fn LandingFooter() -> Element {
|
||||
}
|
||||
span { "CERTifAI" }
|
||||
}
|
||||
p { class: "footer-tagline",
|
||||
"Sovereign GenAI infrastructure for enterprises."
|
||||
}
|
||||
p { class: "footer-tagline", "Sovereign GenAI infrastructure for enterprises." }
|
||||
}
|
||||
div { class: "footer-links-group",
|
||||
h4 { class: "footer-links-heading", "Product" }
|
||||
|
||||
@@ -22,9 +22,7 @@ pub fn PrivacyPage() -> Element {
|
||||
}
|
||||
main { class: "legal-content",
|
||||
h1 { "Privacy Policy" }
|
||||
p { class: "legal-updated",
|
||||
"Last updated: February 2026"
|
||||
}
|
||||
p { class: "legal-updated", "Last updated: February 2026" }
|
||||
|
||||
h2 { "1. Introduction" }
|
||||
p {
|
||||
@@ -90,9 +88,7 @@ pub fn PrivacyPage() -> Element {
|
||||
li { "Request erasure of your data" }
|
||||
li { "Restrict or object to processing" }
|
||||
li { "Data portability" }
|
||||
li {
|
||||
"Lodge a complaint with a supervisory authority"
|
||||
}
|
||||
li { "Lodge a complaint with a supervisory authority" }
|
||||
}
|
||||
|
||||
h2 { "7. Contact" }
|
||||
|
||||
Reference in New Issue
Block a user