Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ jobs:
mv backend/src/tests/daemon_config-next.json backend/src/tests/daemon_config.json
mv ui/static/services-next.json ui/static/services.json
mv docs/SERVICES-NEXT.md docs/SERVICES.md
mv ui/static/billing-plans-next.json ui/static/billing-plans.json
echo "✅ Updated test fixtures"

- name: Commit and push fixture updates
Expand All @@ -94,7 +95,7 @@ jobs:
git add backend/Cargo.toml backend/Cargo.lock ui/package.json ui/package-lock.json

# Stage fixture updates
git add backend/src/tests/netvisor.sql backend/src/tests/daemon_config.json ui/static/services.json docs/SERVICES.md
git add backend/src/tests/netvisor.sql backend/src/tests/daemon_config.json ui/static/services.json docs/SERVICES.md ui/static/billing-plans.json

if git diff --staged --quiet; then
echo "No fixture changes to commit"
Expand Down
22 changes: 22 additions & 0 deletions backend/migrations/20251129180942_nfs-consolidate.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Migration: Convert NasDevice service definitions to NFS
-- The NasDevice and NFSServer definitions are duplicates; consolidating to NFSServer

-- Step 1: Update services table
UPDATE services
SET service_definition = 'NFS'
WHERE service_definition = 'Nas Device';

-- Step 2: Update services embedded in topologies
UPDATE topologies
SET services = (
SELECT jsonb_agg(
CASE
WHEN service->>'service_definition' = 'Nas Device' THEN
jsonb_set(service, '{service_definition}', '"NFS"')
ELSE
service
END
)
FROM jsonb_array_elements(services) AS service
)
WHERE services @> '[{"service_definition": "Nas Device"}]';
43 changes: 2 additions & 41 deletions backend/src/bin/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use netvisor::server::{
auth::AuthenticatedEntity, logging::request_logging_middleware,
rate_limit::rate_limit_middleware,
},
billing::types::base::{BillingPlan, BillingRate, PlanConfig},
billing::plans::get_all_plans,
config::{AppState, ServerCli, ServerConfig},
organizations::r#impl::base::{Organization, OrganizationBase},
shared::{
Expand Down Expand Up @@ -207,46 +207,7 @@ async fn main() -> anyhow::Result<()> {
let all_users = user_service.get_all(EntityFilter::unfiltered()).await?;

if let Some(billing_service) = billing_service {
billing_service
.initialize_products(vec![
BillingPlan::Starter(PlanConfig {
base_cents: 1499,
rate: BillingRate::Month,
trial_days: 0,
seat_cents: None,
network_cents: None,
included_seats: Some(1),
included_networks: Some(1),
}),
BillingPlan::Pro(PlanConfig {
base_cents: 2499,
rate: BillingRate::Month,
trial_days: 7,
seat_cents: None,
network_cents: None,
included_seats: Some(1),
included_networks: Some(3),
}),
BillingPlan::Team(PlanConfig {
base_cents: 7999,
rate: BillingRate::Month,
trial_days: 7,
seat_cents: Some(1000),
network_cents: Some(800),
included_seats: Some(5),
included_networks: Some(5),
}),
BillingPlan::Business(PlanConfig {
base_cents: 14999,
rate: BillingRate::Month,
trial_days: 14,
seat_cents: Some(800),
network_cents: Some(500),
included_seats: Some(10),
included_networks: Some(25),
}),
])
.await?;
billing_service.initialize_products(get_all_plans()).await?;
}

// First load - populate user and org
Expand Down
1 change: 1 addition & 0 deletions backend/src/server/billing/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod handlers;
pub mod plans;
pub mod service;
pub mod subscriber;
pub mod types;
71 changes: 71 additions & 0 deletions backend/src/server/billing/plans.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use super::types::base::{BillingPlan, BillingRate, PlanConfig};

/// Returns the canonical list of billing plans for NetVisor.
/// This is the single source of truth for plan definitions.
fn get_default_plans() -> Vec<BillingPlan> {
vec![
BillingPlan::Starter(PlanConfig {
base_cents: 1499,
rate: BillingRate::Month,
trial_days: 0,
seat_cents: None,
network_cents: None,
included_seats: Some(1),
included_networks: Some(1),
}),
BillingPlan::Pro(PlanConfig {
base_cents: 2499,
rate: BillingRate::Month,
trial_days: 7,
seat_cents: None,
network_cents: None,
included_seats: Some(1),
included_networks: Some(3),
}),
BillingPlan::Team(PlanConfig {
base_cents: 7999,
rate: BillingRate::Month,
trial_days: 7,
seat_cents: Some(1000),
network_cents: Some(800),
included_seats: Some(5),
included_networks: Some(5),
}),
BillingPlan::Business(PlanConfig {
base_cents: 14999,
rate: BillingRate::Month,
trial_days: 14,
seat_cents: Some(800),
network_cents: Some(500),
included_seats: Some(10),
included_networks: Some(25),
}),
]
}

fn get_enterprise_plan() -> BillingPlan {
BillingPlan::Enterprise(PlanConfig {
base_cents: 0,
rate: BillingRate::Month,
trial_days: 14,
seat_cents: None,
network_cents: None,
included_seats: None,
included_networks: None,
})
}

/// Returns both monthly and yearly versions of all plans.
/// Yearly plans get a 20% discount.
pub fn get_all_plans() -> Vec<BillingPlan> {
let monthly_plans = get_default_plans();
let mut all_plans = monthly_plans.clone();
all_plans.push(get_enterprise_plan());

// Add yearly versions with 20% discount
for plan in monthly_plans {
all_plans.push(plan.to_yearly(0.20));
}

all_plans
}
12 changes: 3 additions & 9 deletions backend/src/server/billing/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,8 @@ impl BillingService {
pub async fn initialize_products(&self, plans: Vec<BillingPlan>) -> Result<(), Error> {
let mut created_plans = Vec::new();

let all_plans: Vec<BillingPlan> = plans
.clone()
.iter()
.map(|p| p.to_yearly(0.20))
.chain(plans)
.collect();

tracing::info!(
plan_count = all_plans.len(),
plan_count = plans.len(),
"Initializing Stripe products and prices"
);

Expand Down Expand Up @@ -157,7 +150,7 @@ impl BillingService {
}
};

for plan in all_plans {
for plan in plans {
// Check if product exists, create if not
let product_id = plan.stripe_product_id();
let product = match RetrieveProduct::new(product_id.clone())
Expand Down Expand Up @@ -731,6 +724,7 @@ impl BillingService {
}
BillingPlan::Team { .. } => {}
BillingPlan::Business { .. } => {}
BillingPlan::Enterprise { .. } => {}
}

organization.base.plan_status = Some(sub.status.to_string());
Expand Down
39 changes: 36 additions & 3 deletions backend/src/server/billing/types/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub enum BillingPlan {
Pro(PlanConfig),
Team(PlanConfig),
Business(PlanConfig),
Enterprise(PlanConfig),
}

impl PartialEq for BillingPlan {
Expand Down Expand Up @@ -128,6 +129,7 @@ impl BillingPlan {
"pro" => Some(Self::Pro(plan_config)),
"team" => Some(Self::Team(plan_config)),
"business" => Some(Self::Business(plan_config)),
"enterprise" => Some(Self::Enterprise(plan_config)),
_ => None,
}
}
Expand All @@ -139,6 +141,7 @@ impl BillingPlan {
BillingPlan::Pro(plan_config) => *plan_config,
BillingPlan::Team(plan_config) => *plan_config,
BillingPlan::Business(plan_config) => *plan_config,
BillingPlan::Enterprise(plan_config) => *plan_config,
}
}

Expand All @@ -149,11 +152,15 @@ impl BillingPlan {
BillingPlan::Pro(plan_config) => *plan_config = config,
BillingPlan::Team(plan_config) => *plan_config = config,
BillingPlan::Business(plan_config) => *plan_config = config,
BillingPlan::Enterprise(plan_config) => *plan_config = config,
}
}

pub fn is_commercial(&self) -> bool {
matches!(self, BillingPlan::Team(_) | BillingPlan::Business(_))
matches!(
self,
BillingPlan::Team(_) | BillingPlan::Business(_) | BillingPlan::Enterprise(_)
)
}

pub fn stripe_product_id(&self) -> String {
Expand Down Expand Up @@ -238,6 +245,15 @@ impl BillingPlan {
audit_logs: true,
remove_powered_by: true,
},
BillingPlan::Enterprise { .. } => BillingPlanFeatures {
share_views: true,
onboarding_call: true,
dedicated_support_channel: true,
commercial_license: true,
api_access: true,
audit_logs: true,
remove_powered_by: true,
},
}
}
}
Expand Down Expand Up @@ -302,7 +318,8 @@ impl EntityMetadataProvider for BillingPlan {
BillingPlan::Starter { .. } => "ThumbsUp",
BillingPlan::Pro { .. } => "Zap",
BillingPlan::Team { .. } => "Users",
BillingPlan::Business { .. } => "Building",
BillingPlan::Business { .. } => "Briefcase",
BillingPlan::Enterprise { .. } => "Building",
}
}

Expand All @@ -312,7 +329,8 @@ impl EntityMetadataProvider for BillingPlan {
BillingPlan::Starter { .. } => "blue",
BillingPlan::Pro { .. } => "yellow",
BillingPlan::Team { .. } => "orange",
BillingPlan::Business { .. } => "gray",
BillingPlan::Business { .. } => "brown",
BillingPlan::Enterprise { .. } => "gray",
}
}
}
Expand All @@ -325,6 +343,7 @@ impl TypeMetadataProvider for BillingPlan {
BillingPlan::Pro { .. } => "Pro",
BillingPlan::Team { .. } => "Team",
BillingPlan::Business { .. } => "Business",
BillingPlan::Enterprise { .. } => "Enterprise",
}
}

Expand All @@ -341,11 +360,25 @@ impl TypeMetadataProvider for BillingPlan {
BillingPlan::Business { .. } => {
"Manage multi-site and multi-customer documentation with advanced features"
}
BillingPlan::Enterprise { .. } => {
"Deploy NetVisor with enterprise-grade features and functionality"
}
}
}

fn metadata(&self) -> serde_json::Value {
let config = self.config();

serde_json::json!({
// Pricing information
"base_cents": config.base_cents,
"rate": config.rate,
"trial_days": config.trial_days,
"seat_cents": config.seat_cents,
"network_cents": config.network_cents,
"included_seats": config.included_seats,
"included_networks": config.included_networks,
// Feature flags and metadata
"features": self.features(),
"is_commercial": self.is_commercial()
})
Expand Down
3 changes: 3 additions & 0 deletions backend/src/server/services/definitions/jira_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ impl ServiceDefinition for JiraServer {
Some(200..300),
)
}
fn logo_url(&self) -> &'static str {
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jira.svg"
}
}

inventory::submit!(ServiceDefinitionFactory::new(create_service::<JiraServer>));
1 change: 0 additions & 1 deletion backend/src/server/services/definitions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ pub mod ceph;
pub mod file_server;
pub mod filezilla_server;
pub mod minio;
pub mod nas_device;
pub mod next_cloud;
pub mod nfs_server;
pub mod open_media_vault;
Expand Down
30 changes: 0 additions & 30 deletions backend/src/server/services/definitions/nas_device.rs

This file was deleted.

2 changes: 1 addition & 1 deletion backend/src/server/services/definitions/netvisor_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl ServiceDefinition for NetvisorDaemon {
}

fn logo_url(&self) -> &'static str {
"/logos/netvisor-logo.png"
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/netvisor.png"
}
}

Expand Down
2 changes: 1 addition & 1 deletion backend/src/server/services/definitions/netvisor_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl ServiceDefinition for NetvisorServer {
}

fn logo_url(&self) -> &'static str {
"/logos/netvisor-logo.png"
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/netvisor.png"
}
}

Expand Down
Loading