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
|
run: cargo test --features web --no-default-features
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Stage 3: Build Docker image and push to registry
|
# Stage 3: Deploy (only after tests pass, only on main)
|
||||||
# Only on main and release/* branches
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
build-and-push:
|
deploy:
|
||||||
name: Build & Push Image
|
name: Deploy
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
needs: [test]
|
needs: [test]
|
||||||
if: >-
|
if: github.ref == 'refs/heads/main'
|
||||||
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/'))
|
|
||||||
container:
|
container:
|
||||||
image: rust:1.89-bookworm
|
image: alpine:latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout (full history)
|
- name: Trigger Coolify deploy
|
||||||
run: |
|
run: |
|
||||||
git clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" .
|
apk add --no-cache curl
|
||||||
git checkout "${GITHUB_SHA}"
|
curl -sf "${{ secrets.COOLIFY_WEBHOOK }}" \
|
||||||
- name: Install git-cliff
|
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
|
||||||
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
|
|
||||||
|
|||||||
@@ -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.
|
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
|
## Code structure
|
||||||
The following folder structure is maintained for separation of concerns:
|
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.
|
- 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
|
# 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.
|
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.
|
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.
|
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
|
## Code structure
|
||||||
The following folder structure is maintained for separation of concerns:
|
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",
|
div { class: "landing-nav-actions",
|
||||||
Link {
|
Link {
|
||||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
to: Route::Login {
|
||||||
|
redirect_url: "/dashboard".into(),
|
||||||
|
},
|
||||||
class: "btn btn-ghost btn-sm",
|
class: "btn btn-ghost btn-sm",
|
||||||
"Log In"
|
"Log In"
|
||||||
}
|
}
|
||||||
Link {
|
Link {
|
||||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
to: Route::Login {
|
||||||
|
redirect_url: "/dashboard".into(),
|
||||||
|
},
|
||||||
class: "btn btn-primary btn-sm",
|
class: "btn btn-primary btn-sm",
|
||||||
"Get Started"
|
"Get Started"
|
||||||
}
|
}
|
||||||
@@ -67,9 +71,7 @@ fn HeroSection() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
section { class: "hero-section",
|
section { class: "hero-section",
|
||||||
div { class: "hero-content",
|
div { class: "hero-content",
|
||||||
div { class: "hero-badge badge badge-outline",
|
div { class: "hero-badge badge badge-outline", "Privacy-First GenAI Infrastructure" }
|
||||||
"Privacy-First GenAI Infrastructure"
|
|
||||||
}
|
|
||||||
h1 { class: "hero-title",
|
h1 { class: "hero-title",
|
||||||
"Your AI. Your Data."
|
"Your AI. Your Data."
|
||||||
br {}
|
br {}
|
||||||
@@ -82,16 +84,14 @@ fn HeroSection() -> Element {
|
|||||||
}
|
}
|
||||||
div { class: "hero-actions",
|
div { class: "hero-actions",
|
||||||
Link {
|
Link {
|
||||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
to: Route::Login {
|
||||||
|
redirect_url: "/dashboard".into(),
|
||||||
|
},
|
||||||
class: "btn btn-primary btn-lg",
|
class: "btn btn-primary btn-lg",
|
||||||
"Get Started"
|
"Get Started"
|
||||||
Icon { icon: BsArrowRight, width: 18, height: 18 }
|
Icon { icon: BsArrowRight, width: 18, height: 18 }
|
||||||
}
|
}
|
||||||
a {
|
a { href: "#features", class: "btn btn-outline btn-lg", "Learn More" }
|
||||||
href: "#features",
|
|
||||||
class: "btn btn-outline btn-lg",
|
|
||||||
"Learn More"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div { class: "hero-graphic",
|
div { class: "hero-graphic",
|
||||||
@@ -105,27 +105,44 @@ fn HeroSection() -> Element {
|
|||||||
defs {
|
defs {
|
||||||
linearGradient {
|
linearGradient {
|
||||||
id: "grad1",
|
id: "grad1",
|
||||||
x1: "0%", y1: "0%",
|
x1: "0%",
|
||||||
x2: "100%", y2: "100%",
|
y1: "0%",
|
||||||
|
x2: "100%",
|
||||||
|
y2: "100%",
|
||||||
stop { offset: "0%", stop_color: "#91a4d2" }
|
stop { offset: "0%", stop_color: "#91a4d2" }
|
||||||
stop { offset: "100%", stop_color: "#6d85c6" }
|
stop { offset: "100%", stop_color: "#6d85c6" }
|
||||||
}
|
}
|
||||||
linearGradient {
|
linearGradient {
|
||||||
id: "grad2",
|
id: "grad2",
|
||||||
x1: "0%", y1: "100%",
|
x1: "0%",
|
||||||
x2: "100%", y2: "0%",
|
y1: "100%",
|
||||||
|
x2: "100%",
|
||||||
|
y2: "0%",
|
||||||
stop { offset: "0%", stop_color: "#f97066" }
|
stop { offset: "0%", stop_color: "#f97066" }
|
||||||
stop { offset: "100%", stop_color: "#f9a066" }
|
stop { offset: "100%", stop_color: "#f9a066" }
|
||||||
}
|
}
|
||||||
radialGradient {
|
radialGradient {
|
||||||
id: "glow",
|
id: "glow",
|
||||||
cx: "50%", cy: "50%", r: "50%",
|
cx: "50%",
|
||||||
stop { offset: "0%", stop_color: "rgba(145,164,210,0.3)" }
|
cy: "50%",
|
||||||
stop { offset: "100%", stop_color: "rgba(145,164,210,0)" }
|
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
|
// Background glow
|
||||||
circle { cx: "200", cy: "200", r: "180", fill: "url(#glow)" }
|
circle {
|
||||||
|
cx: "200",
|
||||||
|
cy: "200",
|
||||||
|
r: "180",
|
||||||
|
fill: "url(#glow)",
|
||||||
|
}
|
||||||
// Shield outline
|
// Shield outline
|
||||||
path {
|
path {
|
||||||
d: "M200 40 L340 110 L340 230 C340 300 270 360 200 380 \
|
d: "M200 40 L340 110 L340 230 C340 300 270 360 200 380 \
|
||||||
@@ -145,36 +162,98 @@ fn HeroSection() -> Element {
|
|||||||
opacity: "0.8",
|
opacity: "0.8",
|
||||||
}
|
}
|
||||||
// Network nodes
|
// Network nodes
|
||||||
circle { cx: "200", cy: "180", r: "8", fill: "url(#grad1)" }
|
circle {
|
||||||
circle { cx: "150", cy: "230", r: "6", fill: "url(#grad2)" }
|
cx: "200",
|
||||||
circle { cx: "250", cy: "230", r: "6", fill: "url(#grad2)" }
|
cy: "180",
|
||||||
circle { cx: "200", cy: "280", r: "6", fill: "url(#grad1)" }
|
r: "8",
|
||||||
circle { cx: "130", cy: "170", r: "4", fill: "#91a4d2", opacity: "0.6" }
|
fill: "url(#grad1)",
|
||||||
circle { cx: "270", cy: "170", r: "4", fill: "#91a4d2", opacity: "0.6" }
|
}
|
||||||
|
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
|
// Network connections
|
||||||
line {
|
line {
|
||||||
x1: "200", y1: "180", x2: "150", y2: "230",
|
x1: "200",
|
||||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.4",
|
y1: "180",
|
||||||
|
x2: "150",
|
||||||
|
y2: "230",
|
||||||
|
stroke: "#91a4d2",
|
||||||
|
stroke_width: "1",
|
||||||
|
opacity: "0.4",
|
||||||
}
|
}
|
||||||
line {
|
line {
|
||||||
x1: "200", y1: "180", x2: "250", y2: "230",
|
x1: "200",
|
||||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.4",
|
y1: "180",
|
||||||
|
x2: "250",
|
||||||
|
y2: "230",
|
||||||
|
stroke: "#91a4d2",
|
||||||
|
stroke_width: "1",
|
||||||
|
opacity: "0.4",
|
||||||
}
|
}
|
||||||
line {
|
line {
|
||||||
x1: "150", y1: "230", x2: "200", y2: "280",
|
x1: "150",
|
||||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.4",
|
y1: "230",
|
||||||
|
x2: "200",
|
||||||
|
y2: "280",
|
||||||
|
stroke: "#91a4d2",
|
||||||
|
stroke_width: "1",
|
||||||
|
opacity: "0.4",
|
||||||
}
|
}
|
||||||
line {
|
line {
|
||||||
x1: "250", y1: "230", x2: "200", y2: "280",
|
x1: "250",
|
||||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.4",
|
y1: "230",
|
||||||
|
x2: "200",
|
||||||
|
y2: "280",
|
||||||
|
stroke: "#91a4d2",
|
||||||
|
stroke_width: "1",
|
||||||
|
opacity: "0.4",
|
||||||
}
|
}
|
||||||
line {
|
line {
|
||||||
x1: "200", y1: "180", x2: "130", y2: "170",
|
x1: "200",
|
||||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.3",
|
y1: "180",
|
||||||
|
x2: "130",
|
||||||
|
y2: "170",
|
||||||
|
stroke: "#91a4d2",
|
||||||
|
stroke_width: "1",
|
||||||
|
opacity: "0.3",
|
||||||
}
|
}
|
||||||
line {
|
line {
|
||||||
x1: "200", y1: "180", x2: "270", y2: "170",
|
x1: "200",
|
||||||
stroke: "#91a4d2", stroke_width: "1", opacity: "0.3",
|
y1: "180",
|
||||||
|
x2: "270",
|
||||||
|
y2: "170",
|
||||||
|
stroke: "#91a4d2",
|
||||||
|
stroke_width: "1",
|
||||||
|
opacity: "0.3",
|
||||||
}
|
}
|
||||||
// Checkmark inside shield center
|
// Checkmark inside shield center
|
||||||
path {
|
path {
|
||||||
@@ -236,37 +315,49 @@ fn FeaturesGrid() -> Element {
|
|||||||
}
|
}
|
||||||
div { class: "features-grid",
|
div { class: "features-grid",
|
||||||
FeatureCard {
|
FeatureCard {
|
||||||
icon: rsx! { Icon { icon: BsServer, width: 28, height: 28 } },
|
icon: rsx! {
|
||||||
|
Icon { icon: BsServer, width: 28, height: 28 }
|
||||||
|
},
|
||||||
title: "Self-Hosted Infrastructure",
|
title: "Self-Hosted Infrastructure",
|
||||||
description: "Deploy on your own hardware or private cloud. \
|
description: "Deploy on your own hardware or private cloud. \
|
||||||
Full control over your AI stack with no external dependencies.",
|
Full control over your AI stack with no external dependencies.",
|
||||||
}
|
}
|
||||||
FeatureCard {
|
FeatureCard {
|
||||||
icon: rsx! { Icon { icon: BsShieldCheck, width: 28, height: 28 } },
|
icon: rsx! {
|
||||||
|
Icon { icon: BsShieldCheck, width: 28, height: 28 }
|
||||||
|
},
|
||||||
title: "GDPR Compliant",
|
title: "GDPR Compliant",
|
||||||
description: "EU data residency guaranteed. Your data never \
|
description: "EU data residency guaranteed. Your data never \
|
||||||
leaves your infrastructure or gets shared with third parties.",
|
leaves your infrastructure or gets shared with third parties.",
|
||||||
}
|
}
|
||||||
FeatureCard {
|
FeatureCard {
|
||||||
icon: rsx! { Icon { icon: FaCubes, width: 28, height: 28 } },
|
icon: rsx! {
|
||||||
|
Icon { icon: FaCubes, width: 28, height: 28 }
|
||||||
|
},
|
||||||
title: "LLM Management",
|
title: "LLM Management",
|
||||||
description: "Deploy, monitor, and manage multiple language \
|
description: "Deploy, monitor, and manage multiple language \
|
||||||
models. Switch between models with zero downtime.",
|
models. Switch between models with zero downtime.",
|
||||||
}
|
}
|
||||||
FeatureCard {
|
FeatureCard {
|
||||||
icon: rsx! { Icon { icon: BsRobot, width: 28, height: 28 } },
|
icon: rsx! {
|
||||||
|
Icon { icon: BsRobot, width: 28, height: 28 }
|
||||||
|
},
|
||||||
title: "Agent Builder",
|
title: "Agent Builder",
|
||||||
description: "Create custom AI agents with integrated Langchain \
|
description: "Create custom AI agents with integrated Langchain \
|
||||||
and Langfuse for full observability and control.",
|
and Langfuse for full observability and control.",
|
||||||
}
|
}
|
||||||
FeatureCard {
|
FeatureCard {
|
||||||
icon: rsx! { Icon { icon: BsGlobe2, width: 28, height: 28 } },
|
icon: rsx! {
|
||||||
|
Icon { icon: BsGlobe2, width: 28, height: 28 }
|
||||||
|
},
|
||||||
title: "MCP Server Management",
|
title: "MCP Server Management",
|
||||||
description: "Manage Model Context Protocol servers to extend \
|
description: "Manage Model Context Protocol servers to extend \
|
||||||
your AI capabilities with external tool integrations.",
|
your AI capabilities with external tool integrations.",
|
||||||
}
|
}
|
||||||
FeatureCard {
|
FeatureCard {
|
||||||
icon: rsx! { Icon { icon: BsKey, width: 28, height: 28 } },
|
icon: rsx! {
|
||||||
|
Icon { icon: BsKey, width: 28, height: 28 }
|
||||||
|
},
|
||||||
title: "API Key Management",
|
title: "API Key Management",
|
||||||
description: "Generate API keys, track usage per seat, and \
|
description: "Generate API keys, track usage per seat, and \
|
||||||
set fine-grained permissions for every integration.",
|
set fine-grained permissions for every integration.",
|
||||||
@@ -300,9 +391,7 @@ fn HowItWorks() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
section { id: "how-it-works", class: "how-it-works-section",
|
section { id: "how-it-works", class: "how-it-works-section",
|
||||||
h2 { class: "section-title", "Up and Running in Minutes" }
|
h2 { class: "section-title", "Up and Running in Minutes" }
|
||||||
p { class: "section-subtitle",
|
p { class: "section-subtitle", "Three steps to sovereign AI infrastructure." }
|
||||||
"Three steps to sovereign AI infrastructure."
|
|
||||||
}
|
|
||||||
div { class: "steps-grid",
|
div { class: "steps-grid",
|
||||||
StepCard {
|
StepCard {
|
||||||
number: "01",
|
number: "01",
|
||||||
@@ -353,21 +442,23 @@ fn StepCard(number: &'static str, title: &'static str, description: &'static str
|
|||||||
fn CtaBanner() -> Element {
|
fn CtaBanner() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
section { class: "cta-banner",
|
section { class: "cta-banner",
|
||||||
h2 { class: "cta-title",
|
h2 { class: "cta-title", "Ready to take control of your AI infrastructure?" }
|
||||||
"Ready to take control of your AI infrastructure?"
|
|
||||||
}
|
|
||||||
p { class: "cta-subtitle",
|
p { class: "cta-subtitle",
|
||||||
"Start deploying sovereign GenAI today. No credit card required."
|
"Start deploying sovereign GenAI today. No credit card required."
|
||||||
}
|
}
|
||||||
div { class: "cta-actions",
|
div { class: "cta-actions",
|
||||||
Link {
|
Link {
|
||||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
to: Route::Login {
|
||||||
|
redirect_url: "/dashboard".into(),
|
||||||
|
},
|
||||||
class: "btn btn-primary btn-lg",
|
class: "btn btn-primary btn-lg",
|
||||||
"Get Started Free"
|
"Get Started Free"
|
||||||
Icon { icon: BsArrowRight, width: 18, height: 18 }
|
Icon { icon: BsArrowRight, width: 18, height: 18 }
|
||||||
}
|
}
|
||||||
Link {
|
Link {
|
||||||
to: Route::Login { redirect_url: "/dashboard".into() },
|
to: Route::Login {
|
||||||
|
redirect_url: "/dashboard".into(),
|
||||||
|
},
|
||||||
class: "btn btn-outline btn-lg",
|
class: "btn btn-outline btn-lg",
|
||||||
"Log In"
|
"Log In"
|
||||||
}
|
}
|
||||||
@@ -389,9 +480,7 @@ fn LandingFooter() -> Element {
|
|||||||
}
|
}
|
||||||
span { "CERTifAI" }
|
span { "CERTifAI" }
|
||||||
}
|
}
|
||||||
p { class: "footer-tagline",
|
p { class: "footer-tagline", "Sovereign GenAI infrastructure for enterprises." }
|
||||||
"Sovereign GenAI infrastructure for enterprises."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
div { class: "footer-links-group",
|
div { class: "footer-links-group",
|
||||||
h4 { class: "footer-links-heading", "Product" }
|
h4 { class: "footer-links-heading", "Product" }
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ pub fn PrivacyPage() -> Element {
|
|||||||
}
|
}
|
||||||
main { class: "legal-content",
|
main { class: "legal-content",
|
||||||
h1 { "Privacy Policy" }
|
h1 { "Privacy Policy" }
|
||||||
p { class: "legal-updated",
|
p { class: "legal-updated", "Last updated: February 2026" }
|
||||||
"Last updated: February 2026"
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 { "1. Introduction" }
|
h2 { "1. Introduction" }
|
||||||
p {
|
p {
|
||||||
@@ -90,9 +88,7 @@ pub fn PrivacyPage() -> Element {
|
|||||||
li { "Request erasure of your data" }
|
li { "Request erasure of your data" }
|
||||||
li { "Restrict or object to processing" }
|
li { "Restrict or object to processing" }
|
||||||
li { "Data portability" }
|
li { "Data portability" }
|
||||||
li {
|
li { "Lodge a complaint with a supervisory authority" }
|
||||||
"Lodge a complaint with a supervisory authority"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 { "7. Contact" }
|
h2 { "7. Contact" }
|
||||||
|
|||||||
Reference in New Issue
Block a user