17 Commits
0.10 ... 0.11.2

Author SHA1 Message Date
84da2eb489 Update 9206bf87bb8e_add_portainer_stack_fields_to_instances.py 2025-06-24 14:42:46 +02:00
a9a61c98f5 update payment plan settings 2025-06-24 14:35:34 +02:00
4678022c7b Update c94c2b2b9f2e_add_version_tracking_fields_to_instances.py 2025-06-24 14:16:15 +02:00
ca2d2e6587 transmit info 2025-06-24 14:11:16 +02:00
912f97490c oayment plan visuals in the instances page 2025-06-24 13:54:43 +02:00
d7f5809771 saving payment data 2025-06-24 12:08:13 +02:00
782be6bd38 restore instance linking and launching 2025-06-24 11:50:07 +02:00
996adb4bce pricing 2025-06-24 11:25:10 +02:00
6412d9f01a support 2025-06-24 09:52:09 +02:00
875e20304b support articles 2025-06-24 09:43:31 +02:00
fed00ff2a0 public pages content 2025-06-24 09:32:50 +02:00
10560a01fb company info placeholders 2025-06-23 22:42:56 +02:00
56e7f1be53 Better public pages style 2025-06-23 22:35:12 +02:00
f5168c27bf Update instances.html 2025-06-23 19:06:08 +02:00
4cf9cca116 version display on instances page 2025-06-23 15:46:29 +02:00
af375a2b5c version v3 2025-06-23 15:17:17 +02:00
23a55e025c Update launch_progress.js 2025-06-23 15:05:07 +02:00
47 changed files with 8878 additions and 505 deletions

246
PRICING_CONFIGURATION.md Normal file
View File

@@ -0,0 +1,246 @@
# Pricing Configuration Feature
This document describes the new configurable pricing feature that allows MASTER instances to manage pricing plans through the admin interface.
## Overview
The pricing configuration feature allows administrators on MASTER instances to:
- Create, edit, and delete pricing plans
- Configure plan features, prices, and settings
- Set resource quotas for rooms, conversations, storage, and users
- Mark plans as "Most Popular" or "Custom"
- Control plan visibility and ordering
- Update pricing without code changes
## Features
### Pricing Plan Management
- **Plan Name**: Display name for the pricing plan
- **Description**: Optional description shown below the plan name
- **Pricing**: Monthly and annual prices (annual prices are typically 20% lower)
- **Features**: Dynamic list of features with checkmarks
- **Button Configuration**: Customizable button text and URL
- **Plan Types**: Regular plans with prices or custom plans
- **Popular Plans**: Mark one plan as "Most Popular" with special styling
- **Active/Inactive**: Toggle plan visibility
- **Ordering**: Control the display order of plans
### Resource Quotas
- **Room Quota**: Maximum number of rooms allowed (0 = unlimited)
- **Conversation Quota**: Maximum number of conversations allowed (0 = unlimited)
- **Storage Quota**: Maximum storage in gigabytes (0 = unlimited)
- **Manager Quota**: Maximum number of manager users allowed (0 = unlimited)
- **Admin Quota**: Maximum number of admin users allowed (0 = unlimited)
### Admin Interface
- **Pricing Tab**: New tab in admin settings (MASTER instances only)
- **Add/Edit Modals**: User-friendly forms for plan management
- **Real-time Updates**: Changes are reflected immediately
- **Feature Management**: Add/remove features dynamically
- **Quota Configuration**: Set resource limits for each plan
- **Status Toggles**: Quick switches for plan properties
## Setup Instructions
### 1. Database Migration
Run the migrations to create the pricing_plans table and add quota fields:
```bash
# Apply the migrations
alembic upgrade head
```
### 2. Initialize Default Plans (Optional)
Run the initialization script to create default pricing plans:
```bash
# Set MASTER environment variable
export MASTER=true
# Run the initialization script
python init_pricing_plans.py
```
This will create four default plans with quotas:
- **Starter**: 5 rooms, 10 conversations, 10GB storage, 10 managers, 1 admin
- **Professional**: 25 rooms, 50 conversations, 100GB storage, 50 managers, 3 admins
- **Enterprise**: 100 rooms, 200 conversations, 500GB storage, 200 managers, 10 admins
- **Custom**: Unlimited everything
### 3. Access Admin Interface
1. Log in as an admin user on a MASTER instance
2. Go to Settings
3. Click on the "Pricing" tab
4. Configure your pricing plans
## Usage
### Creating a New Plan
1. Click "Add New Plan" in the pricing tab
2. Fill in the plan details:
- **Name**: Plan display name
- **Description**: Optional description
- **Monthly Price**: Price per month
- **Annual Price**: Price per month when billed annually
- **Quotas**: Set resource limits (0 = unlimited)
- **Features**: Add features using the "Add Feature" button
- **Button Text**: Text for the call-to-action button
- **Button URL**: URL the button should link to
- **Options**: Check "Most Popular", "Custom Plan", or "Active" as needed
3. Click "Create Plan"
### Editing a Plan
1. Click the "Edit" button on any plan card
2. Modify the plan details in the modal
3. Click "Update Plan"
### Managing Plan Status
- **Active/Inactive**: Use the toggle switch in the plan header
- **Most Popular**: Check the "Most Popular" checkbox (only one plan can be popular)
- **Custom Plan**: Check "Custom Plan" for plans without fixed pricing
### Deleting a Plan
1. Click the "Delete" button on a plan card
2. Confirm the deletion in the modal
## Technical Details
### Database Schema
The `pricing_plans` table includes:
- `id`: Primary key
- `name`: Plan name (required)
- `description`: Optional description
- `monthly_price`: Monthly price (float)
- `annual_price`: Annual price (float)
- `features`: JSON array of feature strings
- `button_text`: Button display text
- `button_url`: Button link URL
- `is_popular`: Boolean for "Most Popular" styling
- `is_custom`: Boolean for custom plans
- `is_active`: Boolean for plan visibility
- `order_index`: Integer for display ordering
- `room_quota`: Maximum rooms (0 = unlimited)
- `conversation_quota`: Maximum conversations (0 = unlimited)
- `storage_quota_gb`: Maximum storage in GB (0 = unlimited)
- `manager_quota`: Maximum managers (0 = unlimited)
- `admin_quota`: Maximum admins (0 = unlimited)
- `created_by`: Foreign key to user who created the plan
- `created_at`/`updated_at`: Timestamps
### API Endpoints
- `POST /api/admin/pricing-plans` - Create new plan
- `GET /api/admin/pricing-plans/<id>` - Get plan details
- `PUT /api/admin/pricing-plans/<id>` - Update plan
- `DELETE /api/admin/pricing-plans/<id>` - Delete plan
- `PATCH /api/admin/pricing-plans/<id>/status` - Update plan status
### Template Integration
The pricing page automatically uses configured plans:
- Falls back to hardcoded plans if no plans are configured
- Supports dynamic feature lists
- Handles custom plans without pricing
- Shows/hides billing toggle based on plan types
- Displays quota information in plan cards
### Quota Enforcement
The PricingPlan model includes utility methods for quota checking:
- `check_quota(quota_type, current_count)`: Returns True if quota allows the operation
- `get_quota_remaining(quota_type, current_count)`: Returns remaining quota
- `format_quota_display(quota_type)`: Formats quota for display
- `get_storage_quota_bytes()`: Converts GB to bytes for storage calculations
Example usage in your application:
```python
# Check if user can create a new room
plan = PricingPlan.query.get(user_plan_id)
current_rooms = Room.query.filter_by(created_by=user.id).count()
if plan.check_quota('room_quota', current_rooms):
# Allow room creation
pass
else:
# Show upgrade message
pass
```
## Security
- Only admin users can access pricing configuration
- Only MASTER instances can configure pricing
- All API endpoints require authentication and admin privileges
- CSRF protection is enabled for all forms
## Customization
### Styling
The pricing plans use the existing CSS variables:
- `--primary-color`: Main brand color
- `--secondary-color`: Secondary brand color
- `--shadow-color`: Card shadows
### Button URLs
Configure button URLs to point to:
- Contact forms
- Payment processors
- Sales pages
- Custom landing pages
### Features
Features can include:
- Storage limits
- User limits
- Feature availability
- Support levels
- Integration options
### Quota Integration
To integrate quotas into your application:
1. **User Plan Assignment**: Associate users with pricing plans
2. **Quota Checking**: Use the `check_quota()` method before operations
3. **Upgrade Prompts**: Show upgrade messages when quotas are exceeded
4. **Usage Tracking**: Track current usage for quota calculations
## Troubleshooting
### Common Issues
1. **Pricing tab not visible**
- Ensure you're on a MASTER instance (`MASTER=true`)
- Ensure you're logged in as an admin user
2. **Plans not showing on pricing page**
- Check that plans are marked as "Active"
- Verify the database migration was applied
- Check for JavaScript errors in browser console
3. **Features not saving**
- Ensure at least one feature is provided
- Check that feature text is not empty
4. **Quota fields not working**
- Verify the quota migration was applied
- Check that quota values are integers
- Ensure quota fields are included in form submissions
5. **API errors**
- Verify CSRF token is included in requests
- Check that all required fields are provided
- Ensure proper JSON formatting for features
### Debugging
- Check browser console for JavaScript errors
- Review server logs for API errors
- Verify database connectivity
- Test with default plans first
## Future Enhancements
Potential future improvements:
- Plan categories/tiers
- Regional pricing
- Currency support
- Promotional pricing
- Plan comparison features
- Analytics and usage tracking
- Automatic quota enforcement middleware
- Usage dashboard for quota monitoring

Binary file not shown.

Binary file not shown.

170
init_pricing_plans.py Normal file
View File

@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""
Script to initialize default pricing plans in the database.
This should be run on a MASTER instance only.
"""
import os
import sys
from app import app, db
from models import PricingPlan, User
def init_pricing_plans():
"""Initialize default pricing plans"""
# Check if this is a MASTER instance
if os.environ.get('MASTER', 'false').lower() != 'true':
print("Error: This script should only be run on a MASTER instance.")
print("Set MASTER=true environment variable to run this script.")
sys.exit(1)
with app.app_context():
# Check if pricing plans already exist
existing_plans = PricingPlan.query.count()
if existing_plans > 0:
print(f"Found {existing_plans} existing pricing plans. Skipping initialization.")
return
# Get the first admin user
admin_user = User.query.filter_by(is_admin=True).first()
if not admin_user:
print("Error: No admin user found. Please create an admin user first.")
sys.exit(1)
# Default pricing plans
default_plans = [
{
'name': 'Starter',
'description': 'Perfect for small teams getting started',
'monthly_price': 29.0,
'annual_price': 23.0,
'features': [
'Up to 5 rooms',
'Up to 10 conversations',
'10GB storage',
'Up to 10 managers',
'Email support'
],
'button_text': 'Get Started',
'button_url': '#',
'is_popular': False,
'is_custom': False,
'is_active': True,
'order_index': 1,
'room_quota': 5,
'conversation_quota': 10,
'storage_quota_gb': 10,
'manager_quota': 10,
'admin_quota': 1
},
{
'name': 'Professional',
'description': 'Ideal for growing businesses',
'monthly_price': 99.0,
'annual_price': 79.0,
'features': [
'Up to 25 rooms',
'Up to 50 conversations',
'100GB storage',
'Up to 50 managers',
'Priority support'
],
'button_text': 'Get Started',
'button_url': '#',
'is_popular': True,
'is_custom': False,
'is_active': True,
'order_index': 2,
'room_quota': 25,
'conversation_quota': 50,
'storage_quota_gb': 100,
'manager_quota': 50,
'admin_quota': 3
},
{
'name': 'Enterprise',
'description': 'For large organizations with advanced needs',
'monthly_price': 299.0,
'annual_price': 239.0,
'features': [
'Up to 100 rooms',
'Up to 200 conversations',
'500GB storage',
'Up to 200 managers',
'24/7 dedicated support'
],
'button_text': 'Get Started',
'button_url': '#',
'is_popular': False,
'is_custom': False,
'is_active': True,
'order_index': 3,
'room_quota': 100,
'conversation_quota': 200,
'storage_quota_gb': 500,
'manager_quota': 200,
'admin_quota': 10
},
{
'name': 'Custom',
'description': 'Tailored solutions for enterprise customers',
'monthly_price': 0.0,
'annual_price': 0.0,
'features': [
'Unlimited rooms',
'Unlimited conversations',
'Unlimited storage',
'Unlimited users',
'Custom integrations',
'Dedicated account manager'
],
'button_text': 'Contact Sales',
'button_url': '#',
'is_popular': False,
'is_custom': True,
'is_active': True,
'order_index': 4,
'room_quota': 0,
'conversation_quota': 0,
'storage_quota_gb': 0,
'manager_quota': 0,
'admin_quota': 0
}
]
# Create pricing plans
for plan_data in default_plans:
plan = PricingPlan(
name=plan_data['name'],
description=plan_data['description'],
monthly_price=plan_data['monthly_price'],
annual_price=plan_data['annual_price'],
features=plan_data['features'],
button_text=plan_data['button_text'],
button_url=plan_data['button_url'],
is_popular=plan_data['is_popular'],
is_custom=plan_data['is_custom'],
is_active=plan_data['is_active'],
order_index=plan_data['order_index'],
room_quota=plan_data['room_quota'],
conversation_quota=plan_data['conversation_quota'],
storage_quota_gb=plan_data['storage_quota_gb'],
manager_quota=plan_data['manager_quota'],
admin_quota=plan_data['admin_quota'],
created_by=admin_user.id
)
db.session.add(plan)
try:
db.session.commit()
print("Successfully created default pricing plans:")
for plan_data in default_plans:
print(f" - {plan_data['name']}: €{plan_data['monthly_price']}/month")
print("\nYou can now configure these plans in the admin settings.")
except Exception as e:
db.session.rollback()
print(f"Error creating pricing plans: {e}")
sys.exit(1)
if __name__ == '__main__':
init_pricing_plans()

View File

@@ -0,0 +1,46 @@
"""add portainer stack fields to instances
Revision ID: 9206bf87bb8e
Revises: add_quota_fields
Create Date: 2025-06-24 14:02:17.375785
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = '9206bf87bb8e'
down_revision = 'add_quota_fields'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
# Check if columns already exist
result = conn.execute(text("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'instances'
AND column_name IN ('portainer_stack_id', 'portainer_stack_name')
"""))
existing_columns = [row[0] for row in result.fetchall()]
# Add portainer stack columns if they don't exist
with op.batch_alter_table('instances', schema=None) as batch_op:
if 'portainer_stack_id' not in existing_columns:
batch_op.add_column(sa.Column('portainer_stack_id', sa.String(length=100), nullable=True))
if 'portainer_stack_name' not in existing_columns:
batch_op.add_column(sa.Column('portainer_stack_name', sa.String(length=100), nullable=True))
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('instances', schema=None) as batch_op:
batch_op.drop_column('portainer_stack_name')
batch_op.drop_column('portainer_stack_id')
# ### end Alembic commands ###

View File

@@ -0,0 +1,56 @@
"""add help articles table
Revision ID: add_help_articles_table
Revises: c94c2b2b9f2e
Create Date: 2024-12-19 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = 'add_help_articles_table'
down_revision = 'c94c2b2b9f2e'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
result = conn.execute(text("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'help_articles'
);
"""))
exists = result.scalar()
if not exists:
op.create_table('help_articles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('category', sa.String(length=50), nullable=False),
sa.Column('body', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('is_published', sa.Boolean(), nullable=True, server_default='true'),
sa.Column('order_index', sa.Integer(), nullable=True, server_default='0'),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# Create indexes for better performance
op.create_index('idx_help_articles_category', 'help_articles', ['category'])
op.create_index('idx_help_articles_published', 'help_articles', ['is_published'])
op.create_index('idx_help_articles_order', 'help_articles', ['order_index'])
op.create_index('idx_help_articles_created_at', 'help_articles', ['created_at'])
def downgrade():
op.drop_index('idx_help_articles_category', table_name='help_articles')
op.drop_index('idx_help_articles_published', table_name='help_articles')
op.drop_index('idx_help_articles_order', table_name='help_articles')
op.drop_index('idx_help_articles_created_at', table_name='help_articles')
op.drop_table('help_articles')

View File

@@ -0,0 +1,62 @@
"""add pricing plans table
Revision ID: add_pricing_plans_table
Revises: add_help_articles_table
Create Date: 2024-12-19 11:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = 'add_pricing_plans_table'
down_revision = 'add_help_articles_table'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
result = conn.execute(text("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'pricing_plans'
);
"""))
exists = result.scalar()
if not exists:
op.create_table('pricing_plans',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('monthly_price', sa.Float(), nullable=False),
sa.Column('annual_price', sa.Float(), nullable=False),
sa.Column('features', sa.JSON(), nullable=False),
sa.Column('is_popular', sa.Boolean(), nullable=True, server_default='false'),
sa.Column('is_custom', sa.Boolean(), nullable=True, server_default='false'),
sa.Column('button_text', sa.String(length=50), nullable=True, server_default="'Get Started'"),
sa.Column('button_url', sa.String(length=200), nullable=True, server_default="'#'"),
sa.Column('order_index', sa.Integer(), nullable=True, server_default='0'),
sa.Column('is_active', sa.Boolean(), nullable=True, server_default='true'),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# Create indexes for better performance
op.create_index('idx_pricing_plans_active', 'pricing_plans', ['is_active'])
op.create_index('idx_pricing_plans_order', 'pricing_plans', ['order_index'])
op.create_index('idx_pricing_plans_popular', 'pricing_plans', ['is_popular'])
op.create_index('idx_pricing_plans_created_at', 'pricing_plans', ['created_at'])
def downgrade():
op.drop_index('idx_pricing_plans_active', table_name='pricing_plans')
op.drop_index('idx_pricing_plans_order', table_name='pricing_plans')
op.drop_index('idx_pricing_plans_popular', table_name='pricing_plans')
op.drop_index('idx_pricing_plans_created_at', table_name='pricing_plans')
op.drop_table('pricing_plans')

View File

@@ -0,0 +1,55 @@
"""add quota fields to pricing plans
Revision ID: add_quota_fields
Revises: add_pricing_plans_table
Create Date: 2024-12-19 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = 'add_quota_fields'
down_revision = 'add_pricing_plans_table'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
# Check if columns already exist
result = conn.execute(text("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'pricing_plans'
AND column_name IN ('room_quota', 'conversation_quota', 'storage_quota_gb', 'manager_quota', 'admin_quota')
"""))
existing_columns = [row[0] for row in result.fetchall()]
# Add quota columns if they don't exist
if 'room_quota' not in existing_columns:
op.add_column('pricing_plans', sa.Column('room_quota', sa.Integer(), nullable=True, server_default='0'))
if 'conversation_quota' not in existing_columns:
op.add_column('pricing_plans', sa.Column('conversation_quota', sa.Integer(), nullable=True, server_default='0'))
if 'storage_quota_gb' not in existing_columns:
op.add_column('pricing_plans', sa.Column('storage_quota_gb', sa.Integer(), nullable=True, server_default='0'))
if 'manager_quota' not in existing_columns:
op.add_column('pricing_plans', sa.Column('manager_quota', sa.Integer(), nullable=True, server_default='0'))
if 'admin_quota' not in existing_columns:
op.add_column('pricing_plans', sa.Column('admin_quota', sa.Integer(), nullable=True, server_default='0'))
def downgrade():
# Remove quota columns
op.drop_column('pricing_plans', 'admin_quota')
op.drop_column('pricing_plans', 'manager_quota')
op.drop_column('pricing_plans', 'storage_quota_gb')
op.drop_column('pricing_plans', 'conversation_quota')
op.drop_column('pricing_plans', 'room_quota')

View File

@@ -20,11 +20,21 @@ def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('email_template')
op.drop_table('notification')
# Check if columns already exist before adding them
connection = op.get_bind()
inspector = sa.inspect(connection)
existing_columns = [col['name'] for col in inspector.get_columns('instances')]
with op.batch_alter_table('instances', schema=None) as batch_op:
batch_op.add_column(sa.Column('deployed_version', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('deployed_branch', sa.String(length=100), nullable=True))
batch_op.add_column(sa.Column('latest_version', sa.String(length=50), nullable=True))
batch_op.add_column(sa.Column('version_checked_at', sa.DateTime(), nullable=True))
if 'deployed_version' not in existing_columns:
batch_op.add_column(sa.Column('deployed_version', sa.String(length=50), nullable=True))
if 'deployed_branch' not in existing_columns:
batch_op.add_column(sa.Column('deployed_branch', sa.String(length=100), nullable=True))
if 'latest_version' not in existing_columns:
batch_op.add_column(sa.Column('latest_version', sa.String(length=50), nullable=True))
if 'version_checked_at' not in existing_columns:
batch_op.add_column(sa.Column('version_checked_at', sa.DateTime(), nullable=True))
with op.batch_alter_table('room_file', schema=None) as batch_op:
batch_op.drop_constraint(batch_op.f('fk_room_file_deleted_by_user'), type_='foreignkey')
@@ -37,11 +47,20 @@ def downgrade():
with op.batch_alter_table('room_file', schema=None) as batch_op:
batch_op.create_foreign_key(batch_op.f('fk_room_file_deleted_by_user'), 'user', ['deleted_by'], ['id'])
# Check if columns exist before dropping them
connection = op.get_bind()
inspector = sa.inspect(connection)
existing_columns = [col['name'] for col in inspector.get_columns('instances')]
with op.batch_alter_table('instances', schema=None) as batch_op:
batch_op.drop_column('version_checked_at')
batch_op.drop_column('latest_version')
batch_op.drop_column('deployed_branch')
batch_op.drop_column('deployed_version')
if 'version_checked_at' in existing_columns:
batch_op.drop_column('version_checked_at')
if 'latest_version' in existing_columns:
batch_op.drop_column('latest_version')
if 'deployed_branch' in existing_columns:
batch_op.drop_column('deployed_branch')
if 'deployed_version' in existing_columns:
batch_op.drop_column('deployed_version')
op.create_table('notification',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),

142
models.py
View File

@@ -528,6 +528,9 @@ class Instance(db.Model):
status = db.Column(db.String(20), nullable=False, default='inactive')
status_details = db.Column(db.Text, nullable=True)
connection_token = db.Column(db.String(64), unique=True, nullable=True)
# Portainer integration fields
portainer_stack_id = db.Column(db.String(100), nullable=True) # Portainer stack ID
portainer_stack_name = db.Column(db.String(100), nullable=True) # Portainer stack name
# Version tracking fields
deployed_version = db.Column(db.String(50), nullable=True) # Current deployed version (commit hash or tag)
deployed_branch = db.Column(db.String(100), nullable=True) # Branch that was deployed
@@ -537,4 +540,141 @@ class Instance(db.Model):
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP'), onupdate=db.text('CURRENT_TIMESTAMP'))
def __repr__(self):
return f'<Instance {self.name}>'
return f'<Instance {self.name}>'
class HelpArticle(db.Model):
__tablename__ = 'help_articles'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
category = db.Column(db.String(50), nullable=False) # getting-started, user-management, file-management, communication, security, administration
body = db.Column(db.Text, nullable=False) # Rich text content
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
is_published = db.Column(db.Boolean, default=True)
order_index = db.Column(db.Integer, default=0) # For ordering articles within categories
# Relationships
creator = db.relationship('User', backref=db.backref('created_help_articles', cascade='all, delete-orphan'), foreign_keys=[created_by])
def __repr__(self):
return f'<HelpArticle {self.title} ({self.category})>'
@classmethod
def get_categories(cls):
"""Get all available categories with their display names"""
return {
'getting-started': 'Getting Started',
'user-management': 'User Management',
'file-management': 'File Management',
'communication': 'Communication',
'security': 'Security & Privacy',
'administration': 'Administration'
}
@classmethod
def get_articles_by_category(cls, category, published_only=True):
"""Get articles for a specific category"""
query = cls.query.filter_by(category=category)
if published_only:
query = query.filter_by(is_published=True)
return query.order_by(cls.order_index.asc(), cls.created_at.desc()).all()
@classmethod
def get_all_published(cls):
"""Get all published articles grouped by category"""
articles = cls.query.filter_by(is_published=True).order_by(cls.order_index.asc(), cls.created_at.desc()).all()
grouped = {}
for article in articles:
if article.category not in grouped:
grouped[article.category] = []
grouped[article.category].append(article)
return grouped
class PricingPlan(db.Model):
__tablename__ = 'pricing_plans'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
monthly_price = db.Column(db.Float, nullable=False)
annual_price = db.Column(db.Float, nullable=False)
features = db.Column(db.JSON, nullable=False) # List of feature strings
is_popular = db.Column(db.Boolean, default=False)
is_custom = db.Column(db.Boolean, default=False)
button_text = db.Column(db.String(50), default='Get Started')
button_url = db.Column(db.String(200), default='#')
order_index = db.Column(db.Integer, default=0)
is_active = db.Column(db.Boolean, default=True)
# Quota fields
room_quota = db.Column(db.Integer, default=0) # 0 = unlimited
conversation_quota = db.Column(db.Integer, default=0) # 0 = unlimited
storage_quota_gb = db.Column(db.Integer, default=0) # 0 = unlimited, stored in GB
manager_quota = db.Column(db.Integer, default=0) # 0 = unlimited
admin_quota = db.Column(db.Integer, default=0) # 0 = unlimited
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
# Relationships
creator = db.relationship('User', backref=db.backref('created_pricing_plans', cascade='all, delete-orphan'), foreign_keys=[created_by])
def __repr__(self):
return f'<PricingPlan {self.name}>'
@classmethod
def get_active_plans(cls):
"""Get all active pricing plans ordered by order_index"""
return cls.query.filter_by(is_active=True).order_by(cls.order_index).all()
@classmethod
def get_popular_plan(cls):
"""Get the plan marked as most popular"""
return cls.query.filter_by(is_active=True, is_popular=True).first()
def get_storage_quota_bytes(self):
"""Get storage quota in bytes"""
if self.storage_quota_gb == 0:
return 0 # Unlimited
return self.storage_quota_gb * 1024 * 1024 * 1024 # Convert GB to bytes
def format_quota_display(self, quota_type):
"""Format quota for display"""
if quota_type == 'room_quota':
return 'Unlimited' if self.room_quota == 0 else f'{self.room_quota} rooms'
elif quota_type == 'conversation_quota':
return 'Unlimited' if self.conversation_quota == 0 else f'{self.conversation_quota} conversations'
elif quota_type == 'storage_quota_gb':
return 'Unlimited' if self.storage_quota_gb == 0 else f'{self.storage_quota_gb}GB'
elif quota_type == 'manager_quota':
return 'Unlimited' if self.manager_quota == 0 else f'{self.manager_quota} managers'
elif quota_type == 'admin_quota':
return 'Unlimited' if self.admin_quota == 0 else f'{self.admin_quota} admins'
return 'Unknown'
def check_quota(self, quota_type, current_count):
"""Check if a quota would be exceeded"""
if quota_type == 'room_quota':
return self.room_quota == 0 or current_count < self.room_quota
elif quota_type == 'conversation_quota':
return self.conversation_quota == 0 or current_count < self.conversation_quota
elif quota_type == 'storage_quota_gb':
return self.storage_quota_gb == 0 or current_count < self.storage_quota_gb
elif quota_type == 'manager_quota':
return self.manager_quota == 0 or current_count < self.manager_quota
elif quota_type == 'admin_quota':
return self.admin_quota == 0 or current_count < self.admin_quota
return True
def get_quota_remaining(self, quota_type, current_count):
"""Get remaining quota"""
if quota_type == 'room_quota':
return float('inf') if self.room_quota == 0 else max(0, self.room_quota - current_count)
elif quota_type == 'conversation_quota':
return float('inf') if self.conversation_quota == 0 else max(0, self.conversation_quota - current_count)
elif quota_type == 'storage_quota_gb':
return float('inf') if self.storage_quota_gb == 0 else max(0, self.storage_quota_gb - current_count)
elif quota_type == 'manager_quota':
return float('inf') if self.manager_quota == 0 else max(0, self.manager_quota - current_count)
elif quota_type == 'admin_quota':
return float('inf') if self.admin_quota == 0 else max(0, self.admin_quota - current_count)
return 0

View File

@@ -1,8 +1,9 @@
from flask import Blueprint, jsonify
from flask import Blueprint, jsonify, request
from flask_login import login_required, current_user
from models import db, Room, RoomFile, User, DocuPulseSettings
from models import db, Room, RoomFile, User, DocuPulseSettings, HelpArticle
import os
from datetime import datetime
import json
admin = Blueprint('admin', __name__)
@@ -253,4 +254,475 @@ def get_usage_stats():
stats = DocuPulseSettings.get_usage_stats()
return jsonify(stats)
except Exception as e:
return jsonify({'error': str(e)}), 500
return jsonify({'error': str(e)}), 500
@admin.route('/api/admin/help-articles/<int:article_id>', methods=['PUT'])
@login_required
def update_help_article(article_id):
"""Update a help article"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
article = HelpArticle.query.get_or_404(article_id)
title = request.form.get('title')
category = request.form.get('category')
body = request.form.get('body')
order_index = int(request.form.get('order_index', 0))
is_published = request.form.get('is_published') == 'true'
if not title or not category or not body:
return jsonify({'error': 'Title, category, and body are required'}), 400
# Validate category
valid_categories = HelpArticle.get_categories().keys()
if category not in valid_categories:
return jsonify({'error': 'Invalid category'}), 400
article.title = title
article.category = category
article.body = body
article.order_index = order_index
article.is_published = is_published
article.updated_at = datetime.utcnow()
db.session.commit()
# Log the event
log_event(
event_type='help_article_update',
details={
'article_id': article.id,
'title': article.title,
'category': article.category,
'updated_by': f"{current_user.username} {current_user.last_name}"
},
user_id=current_user.id
)
db.session.commit()
return jsonify({'success': True, 'message': 'Article updated successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@admin.route('/api/admin/help-articles/<int:article_id>', methods=['DELETE'])
@login_required
def delete_help_article(article_id):
"""Delete a help article"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
article = HelpArticle.query.get_or_404(article_id)
# Log the event before deletion
log_event(
event_type='help_article_delete',
details={
'article_id': article.id,
'title': article.title,
'category': article.category,
'deleted_by': f"{current_user.username} {current_user.last_name}"
},
user_id=current_user.id
)
db.session.delete(article)
db.session.commit()
return jsonify({'success': True, 'message': 'Article deleted successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
# Help Articles API endpoints
@admin.route('/api/admin/help-articles', methods=['GET'])
@login_required
def get_help_articles():
"""Get all help articles"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
articles = HelpArticle.query.order_by(HelpArticle.category.asc(), HelpArticle.order_index.asc(), HelpArticle.created_at.desc()).all()
articles_data = []
for article in articles:
articles_data.append({
'id': article.id,
'title': article.title,
'category': article.category,
'body': article.body,
'created_at': article.created_at.isoformat() if article.created_at else None,
'updated_at': article.updated_at.isoformat() if article.updated_at else None,
'created_by': article.created_by,
'is_published': article.is_published,
'order_index': article.order_index
})
return jsonify({'articles': articles_data})
@admin.route('/api/admin/help-articles', methods=['POST'])
@login_required
def create_help_article():
"""Create a new help article"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
title = request.form.get('title')
category = request.form.get('category')
body = request.form.get('body')
order_index = int(request.form.get('order_index', 0))
is_published = request.form.get('is_published') == 'true'
if not title or not category or not body:
return jsonify({'error': 'Title, category, and body are required'}), 400
# Validate category
valid_categories = HelpArticle.get_categories().keys()
if category not in valid_categories:
return jsonify({'error': 'Invalid category'}), 400
article = HelpArticle(
title=title,
category=category,
body=body,
order_index=order_index,
is_published=is_published,
created_by=current_user.id
)
db.session.add(article)
db.session.commit()
# Log the event
log_event(
event_type='help_article_create',
details={
'article_id': article.id,
'title': article.title,
'category': article.category,
'created_by': f"{current_user.username} {current_user.last_name}"
},
user_id=current_user.id
)
db.session.commit()
return jsonify({'success': True, 'message': 'Article created successfully', 'article_id': article.id})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@admin.route('/api/admin/help-articles/<int:article_id>', methods=['GET'])
@login_required
def get_help_article(article_id):
"""Get a specific help article"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
article = HelpArticle.query.get_or_404(article_id)
article_data = {
'id': article.id,
'title': article.title,
'category': article.category,
'body': article.body,
'created_at': article.created_at.isoformat() if article.created_at else None,
'updated_at': article.updated_at.isoformat() if article.updated_at else None,
'created_by': article.created_by,
'is_published': article.is_published,
'order_index': article.order_index
}
return jsonify({'article': article_data})
@admin.route('/api/admin/pricing-plans', methods=['POST'])
@login_required
def create_pricing_plan():
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
# Check if this is a MASTER instance
is_master = os.environ.get('MASTER', 'false').lower() == 'true'
if not is_master:
return jsonify({'error': 'Pricing configuration is only available on MASTER instances'}), 403
try:
from models import PricingPlan
# Get form data
name = request.form.get('name')
description = request.form.get('description')
monthly_price = float(request.form.get('monthly_price'))
annual_price = float(request.form.get('annual_price'))
features = json.loads(request.form.get('features', '[]'))
button_text = request.form.get('button_text', 'Get Started')
button_url = request.form.get('button_url', '#')
is_popular = request.form.get('is_popular') == 'true'
is_custom = request.form.get('is_custom') == 'true'
is_active = request.form.get('is_active') == 'true'
# Get quota fields
room_quota = int(request.form.get('room_quota', 0))
conversation_quota = int(request.form.get('conversation_quota', 0))
storage_quota_gb = int(request.form.get('storage_quota_gb', 0))
manager_quota = int(request.form.get('manager_quota', 0))
admin_quota = int(request.form.get('admin_quota', 0))
# Validate required fields
if not name or not features:
return jsonify({'error': 'Name and features are required'}), 400
# Get the highest order index
max_order = db.session.query(db.func.max(PricingPlan.order_index)).scalar() or 0
# Create new pricing plan
plan = PricingPlan(
name=name,
description=description,
monthly_price=monthly_price,
annual_price=annual_price,
features=features,
button_text=button_text,
button_url=button_url,
is_popular=is_popular,
is_custom=is_custom,
is_active=is_active,
room_quota=room_quota,
conversation_quota=conversation_quota,
storage_quota_gb=storage_quota_gb,
manager_quota=manager_quota,
admin_quota=admin_quota,
order_index=max_order + 1,
created_by=current_user.id
)
db.session.add(plan)
db.session.commit()
return jsonify({'success': True, 'message': 'Pricing plan created successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@admin.route('/api/admin/pricing-plans/<int:plan_id>', methods=['GET'])
@login_required
def get_pricing_plan(plan_id):
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
from models import PricingPlan
plan = PricingPlan.query.get(plan_id)
if not plan:
return jsonify({'error': 'Pricing plan not found'}), 404
return jsonify({
'success': True,
'plan': {
'id': plan.id,
'name': plan.name,
'description': plan.description,
'monthly_price': plan.monthly_price,
'annual_price': plan.annual_price,
'features': plan.features,
'button_text': plan.button_text,
'button_url': plan.button_url,
'is_popular': plan.is_popular,
'is_custom': plan.is_custom,
'is_active': plan.is_active,
'order_index': plan.order_index,
'room_quota': plan.room_quota,
'conversation_quota': plan.conversation_quota,
'storage_quota_gb': plan.storage_quota_gb,
'manager_quota': plan.manager_quota,
'admin_quota': plan.admin_quota
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@admin.route('/api/admin/pricing-plans/<int:plan_id>', methods=['PUT'])
@login_required
def update_pricing_plan(plan_id):
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
# Check if this is a MASTER instance
is_master = os.environ.get('MASTER', 'false').lower() == 'true'
if not is_master:
return jsonify({'error': 'Pricing configuration is only available on MASTER instances'}), 403
try:
from models import PricingPlan
plan = PricingPlan.query.get(plan_id)
if not plan:
return jsonify({'error': 'Pricing plan not found'}), 404
# Get form data
name = request.form.get('name')
description = request.form.get('description')
monthly_price = float(request.form.get('monthly_price'))
annual_price = float(request.form.get('annual_price'))
features = json.loads(request.form.get('features', '[]'))
button_text = request.form.get('button_text', 'Get Started')
button_url = request.form.get('button_url', '#')
is_popular = request.form.get('is_popular') == 'true'
is_custom = request.form.get('is_custom') == 'true'
is_active = request.form.get('is_active') == 'true'
# Get quota fields
room_quota = int(request.form.get('room_quota', 0))
conversation_quota = int(request.form.get('conversation_quota', 0))
storage_quota_gb = int(request.form.get('storage_quota_gb', 0))
manager_quota = int(request.form.get('manager_quota', 0))
admin_quota = int(request.form.get('admin_quota', 0))
# Validate required fields
if not name or not features:
return jsonify({'error': 'Name and features are required'}), 400
# Update plan
plan.name = name
plan.description = description
plan.monthly_price = monthly_price
plan.annual_price = annual_price
plan.features = features
plan.button_text = button_text
plan.button_url = button_url
plan.is_popular = is_popular
plan.is_custom = is_custom
plan.is_active = is_active
plan.room_quota = room_quota
plan.conversation_quota = conversation_quota
plan.storage_quota_gb = storage_quota_gb
plan.manager_quota = manager_quota
plan.admin_quota = admin_quota
db.session.commit()
return jsonify({'success': True, 'message': 'Pricing plan updated successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@admin.route('/api/admin/pricing-plans/<int:plan_id>', methods=['DELETE'])
@login_required
def delete_pricing_plan(plan_id):
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
# Check if this is a MASTER instance
is_master = os.environ.get('MASTER', 'false').lower() == 'true'
if not is_master:
return jsonify({'error': 'Pricing configuration is only available on MASTER instances'}), 403
try:
from models import PricingPlan
plan = PricingPlan.query.get(plan_id)
if not plan:
return jsonify({'error': 'Pricing plan not found'}), 404
db.session.delete(plan)
db.session.commit()
return jsonify({'success': True, 'message': 'Pricing plan deleted successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@admin.route('/api/admin/pricing-plans/<int:plan_id>/status', methods=['PATCH'])
@login_required
def update_pricing_plan_status(plan_id):
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
# Check if this is a MASTER instance
is_master = os.environ.get('MASTER', 'false').lower() == 'true'
if not is_master:
return jsonify({'error': 'Pricing configuration is only available on MASTER instances'}), 403
try:
from models import PricingPlan
plan = PricingPlan.query.get(plan_id)
if not plan:
return jsonify({'error': 'Pricing plan not found'}), 404
data = request.get_json()
field = data.get('field')
value = data.get('value')
if field not in ['is_active', 'is_popular', 'is_custom']:
return jsonify({'error': 'Invalid field'}), 400
setattr(plan, field, value)
db.session.commit()
return jsonify({'success': True, 'message': 'Plan status updated successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@admin.route('/api/admin/pricing-plans', methods=['GET'])
@login_required
def get_pricing_plans():
"""Get all active pricing plans for instance launch"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
from models import PricingPlan
# Get all active pricing plans ordered by order_index
plans = PricingPlan.query.filter_by(is_active=True).order_by(PricingPlan.order_index).all()
plans_data = []
for plan in plans:
plans_data.append({
'id': plan.id,
'name': plan.name,
'description': plan.description,
'monthly_price': plan.monthly_price,
'annual_price': plan.annual_price,
'features': plan.features,
'button_text': plan.button_text,
'button_url': plan.button_url,
'is_popular': plan.is_popular,
'is_custom': plan.is_custom,
'is_active': plan.is_active,
'order_index': plan.order_index,
'room_quota': plan.room_quota,
'conversation_quota': plan.conversation_quota,
'storage_quota_gb': plan.storage_quota_gb,
'manager_quota': plan.manager_quota,
'admin_quota': plan.admin_quota,
'format_quota_display': {
'room_quota': plan.format_quota_display('room_quota'),
'conversation_quota': plan.format_quota_display('conversation_quota'),
'storage_quota_gb': plan.format_quota_display('storage_quota_gb'),
'manager_quota': plan.format_quota_display('manager_quota'),
'admin_quota': plan.format_quota_display('admin_quota')
}
})
return jsonify({
'success': True,
'plans': plans_data
})
except Exception as e:
return jsonify({'error': str(e)}), 500

View File

@@ -2,9 +2,11 @@ from flask import Blueprint, jsonify, request, current_app, make_response, flash
from functools import wraps
from models import (
KeyValueSettings, User, Room, Conversation, RoomFile,
SiteSettings, DocuPulseSettings, Event, Mail, ManagementAPIKey, PasswordSetupToken, PasswordResetToken
SiteSettings, DocuPulseSettings, Event, Mail, ManagementAPIKey, PasswordSetupToken, PasswordResetToken,
HelpArticle
)
from extensions import db, csrf
from utils import log_event
from datetime import datetime, timedelta
import os
import jwt
@@ -563,4 +565,34 @@ def generate_password_reset_token(current_user, user_id):
'reset_url': reset_url,
'expires_at': reset_token.expires_at.isoformat(),
'user_email': user.email
})
})
# Version Information
@admin_api.route('/version-info', methods=['GET'])
@csrf.exempt
@token_required
def get_version_info(current_user):
"""Get version information from environment variables"""
try:
version_info = {
'app_version': os.environ.get('APP_VERSION', 'unknown'),
'git_commit': os.environ.get('GIT_COMMIT', 'unknown'),
'git_branch': os.environ.get('GIT_BRANCH', 'unknown'),
'deployed_at': os.environ.get('DEPLOYED_AT', 'unknown'),
'ismaster': os.environ.get('ISMASTER', 'false'),
'port': os.environ.get('PORT', 'unknown'),
'pricing_tier_name': os.environ.get('PRICING_TIER_NAME', 'unknown')
}
return jsonify(version_info)
except Exception as e:
current_app.logger.error(f"Error getting version info: {str(e)}")
return jsonify({
'error': str(e),
'app_version': 'unknown',
'git_commit': 'unknown',
'git_branch': 'unknown',
'deployed_at': 'unknown',
'pricing_tier_name': 'unknown'
}), 500

View File

@@ -1090,13 +1090,9 @@ def save_instance():
if existing_instance:
# Update existing instance
existing_instance.port = data['port']
existing_instance.domains = data['domains']
existing_instance.stack_id = data['stack_id']
existing_instance.stack_name = data['stack_name']
existing_instance.portainer_stack_id = data['stack_id']
existing_instance.portainer_stack_name = data['stack_name']
existing_instance.status = data['status']
existing_instance.repository = data['repository']
existing_instance.branch = data['branch']
existing_instance.deployed_version = data.get('deployed_version', 'unknown')
existing_instance.deployed_branch = data.get('deployed_branch', data['branch'])
existing_instance.version_checked_at = datetime.utcnow()
@@ -1107,13 +1103,9 @@ def save_instance():
'message': 'Instance data updated successfully',
'data': {
'name': existing_instance.name,
'port': existing_instance.port,
'domains': existing_instance.domains,
'stack_id': existing_instance.stack_id,
'stack_name': existing_instance.stack_name,
'portainer_stack_id': existing_instance.portainer_stack_id,
'portainer_stack_name': existing_instance.portainer_stack_name,
'status': existing_instance.status,
'repository': existing_instance.repository,
'branch': existing_instance.branch,
'deployed_version': existing_instance.deployed_version,
'deployed_branch': existing_instance.deployed_branch
}
@@ -1126,14 +1118,11 @@ def save_instance():
rooms_count=0,
conversations_count=0,
data_size=0.0,
payment_plan='Basic',
payment_plan=data.get('payment_plan', 'Basic'),
main_url=f"https://{data['domains'][0]}" if data['domains'] else f"http://localhost:{data['port']}",
status=data['status'],
port=data['port'],
stack_id=data['stack_id'],
stack_name=data['stack_name'],
repository=data['repository'],
branch=data['branch'],
portainer_stack_id=data['stack_id'],
portainer_stack_name=data['stack_name'],
deployed_version=data.get('deployed_version', 'unknown'),
deployed_branch=data.get('deployed_branch', data['branch'])
)
@@ -1145,13 +1134,9 @@ def save_instance():
'message': 'Instance data saved successfully',
'data': {
'name': instance.name,
'port': instance.port,
'domains': instance.domains,
'stack_id': instance.stack_id,
'stack_name': instance.stack_name,
'portainer_stack_id': instance.portainer_stack_id,
'portainer_stack_name': instance.portainer_stack_name,
'status': instance.status,
'repository': instance.repository,
'branch': instance.branch,
'deployed_version': instance.deployed_version,
'deployed_branch': instance.deployed_branch
}

View File

@@ -625,6 +625,188 @@ def init_routes(main_bp):
'is_valid': is_valid
})
@main_bp.route('/instances/<int:instance_id>/version-info')
@login_required
@require_password_change
def instance_version_info(instance_id):
if not os.environ.get('MASTER', 'false').lower() == 'true':
return jsonify({'error': 'Unauthorized'}), 403
instance = Instance.query.get_or_404(instance_id)
# Check if instance has a connection token
if not instance.connection_token:
return jsonify({
'error': 'Instance not authenticated',
'deployed_version': instance.deployed_version,
'deployed_branch': instance.deployed_branch
})
try:
# Get JWT token using the connection token
jwt_response = requests.post(
f"{instance.main_url.rstrip('/')}/api/admin/management-token",
headers={
'X-API-Key': instance.connection_token,
'Accept': 'application/json'
},
timeout=5
)
if jwt_response.status_code != 200:
return jsonify({
'error': 'Failed to authenticate with instance',
'deployed_version': instance.deployed_version,
'deployed_branch': instance.deployed_branch
})
jwt_data = jwt_response.json()
jwt_token = jwt_data.get('token')
if not jwt_token:
return jsonify({
'error': 'No JWT token received',
'deployed_version': instance.deployed_version,
'deployed_branch': instance.deployed_branch
})
# Fetch version information from the instance
response = requests.get(
f"{instance.main_url.rstrip('/')}/api/admin/version-info",
headers={
'Authorization': f'Bearer {jwt_token}',
'Accept': 'application/json'
},
timeout=5
)
if response.status_code == 200:
version_data = response.json()
# Update the instance with the fetched version information
instance.deployed_version = version_data.get('app_version', instance.deployed_version)
instance.deployed_branch = version_data.get('git_branch', instance.deployed_branch)
instance.version_checked_at = datetime.utcnow()
db.session.commit()
return jsonify({
'success': True,
'deployed_version': instance.deployed_version,
'deployed_branch': instance.deployed_branch,
'git_commit': version_data.get('git_commit'),
'deployed_at': version_data.get('deployed_at'),
'version_checked_at': instance.version_checked_at.isoformat() if instance.version_checked_at else None
})
else:
return jsonify({
'error': f'Failed to fetch version info: {response.status_code}',
'deployed_version': instance.deployed_version,
'deployed_branch': instance.deployed_branch
})
except Exception as e:
current_app.logger.error(f"Error fetching version info: {str(e)}")
return jsonify({
'error': f'Error fetching version info: {str(e)}',
'deployed_version': instance.deployed_version,
'deployed_branch': instance.deployed_branch
})
@main_bp.route('/api/latest-version')
@login_required
@require_password_change
def get_latest_version():
if not os.environ.get('MASTER', 'false').lower() == 'true':
return jsonify({'error': 'Unauthorized'}), 403
try:
# Get Git settings
git_settings = KeyValueSettings.get_value('git_settings')
if not git_settings:
return jsonify({
'error': 'Git settings not configured',
'latest_version': 'unknown',
'latest_commit': 'unknown',
'last_checked': None
})
latest_tag = None
latest_commit = None
if git_settings['provider'] == 'gitea':
headers = {
'Accept': 'application/json',
'Authorization': f'token {git_settings["token"]}'
}
# Get the latest tag
tags_response = requests.get(
f'{git_settings["url"]}/api/v1/repos/{git_settings["repo"]}/tags',
headers=headers,
timeout=10
)
if tags_response.status_code == 200:
tags_data = tags_response.json()
if tags_data:
# Sort tags by commit date (newest first) and get the latest
sorted_tags = sorted(tags_data, key=lambda x: x.get('commit', {}).get('created', ''), reverse=True)
if sorted_tags:
latest_tag = sorted_tags[0].get('name')
latest_commit = sorted_tags[0].get('commit', {}).get('id')
else:
# Try token as query parameter if header auth fails
tags_response = requests.get(
f'{git_settings["url"]}/api/v1/repos/{git_settings["repo"]}/tags?token={git_settings["token"]}',
headers={'Accept': 'application/json'},
timeout=10
)
if tags_response.status_code == 200:
tags_data = tags_response.json()
if tags_data:
sorted_tags = sorted(tags_data, key=lambda x: x.get('commit', {}).get('created', ''), reverse=True)
if sorted_tags:
latest_tag = sorted_tags[0].get('name')
latest_commit = sorted_tags[0].get('commit', {}).get('id')
elif git_settings['provider'] == 'gitlab':
headers = {
'PRIVATE-TOKEN': git_settings['token'],
'Accept': 'application/json'
}
# Get the latest tag
tags_response = requests.get(
f'{git_settings["url"]}/api/v4/projects/{git_settings["repo"].replace("/", "%2F")}/repository/tags',
headers=headers,
params={'order_by': 'version', 'sort': 'desc', 'per_page': 1},
timeout=10
)
if tags_response.status_code == 200:
tags_data = tags_response.json()
if tags_data:
latest_tag = tags_data[0].get('name')
latest_commit = tags_data[0].get('commit', {}).get('id')
return jsonify({
'success': True,
'latest_version': latest_tag or 'unknown',
'latest_commit': latest_commit or 'unknown',
'repository': git_settings.get('repo', 'unknown'),
'provider': git_settings.get('provider', 'unknown'),
'last_checked': datetime.utcnow().isoformat()
})
except Exception as e:
current_app.logger.error(f"Error fetching latest version: {str(e)}")
return jsonify({
'error': f'Error fetching latest version: {str(e)}',
'latest_version': 'unknown',
'latest_commit': 'unknown',
'last_checked': datetime.utcnow().isoformat()
}), 500
UPLOAD_FOLDER = '/app/uploads/profile_pics'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
@@ -940,7 +1122,7 @@ def init_routes(main_bp):
active_tab = request.args.get('tab', 'colors')
# Validate tab parameter
valid_tabs = ['colors', 'general', 'email_templates', 'mails', 'security', 'events', 'debugging', 'smtp', 'connections']
valid_tabs = ['colors', 'general', 'email_templates', 'mails', 'security', 'events', 'debugging', 'smtp', 'connections', 'pricing']
if active_tab not in valid_tabs:
active_tab = 'colors'
@@ -998,6 +1180,12 @@ def init_routes(main_bp):
current_page = mails.page
users = User.query.order_by(User.username).all()
# Get pricing plans for the pricing tab (only for MASTER instances)
pricing_plans = []
if active_tab == 'pricing':
from models import PricingPlan
pricing_plans = PricingPlan.query.order_by(PricingPlan.order_index).all()
if request.method == 'GET':
company_form.company_name.data = site_settings.company_name
company_form.company_website.data = site_settings.company_website
@@ -1028,6 +1216,7 @@ def init_routes(main_bp):
nginx_settings=nginx_settings,
git_settings=git_settings,
cloudflare_settings=cloudflare_settings,
pricing_plans=pricing_plans,
csrf_token=generate_csrf())
@main_bp.route('/settings/update-smtp', methods=['POST'])
@@ -1975,12 +2164,19 @@ def init_routes(main_bp):
@login_required
@require_password_change
def development_wiki():
if not os.environ.get('MASTER', 'false').lower() == 'true':
flash('This page is only available in master instances.', 'error')
return redirect(url_for('main.dashboard'))
return render_template('wiki/base.html')
@main_bp.route('/support-articles')
@login_required
@require_password_change
def support_articles():
# Check if this is a master instance
is_master = os.environ.get('MASTER', 'false').lower() == 'true'
if not is_master:
flash('This page is only available on the master instance.', 'error')
return redirect(url_for('main.dashboard'))
return render_template('admin/support_articles.html')
@main_bp.route('/api/version')
def api_version():
# Get version information from environment variables

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, render_template, redirect, url_for
from models import SiteSettings
from flask import Blueprint, render_template, redirect, url_for, request
from models import SiteSettings, HelpArticle, PricingPlan
import os
def init_public_routes(public_bp):
@@ -8,6 +8,11 @@ def init_public_routes(public_bp):
site_settings = SiteSettings.query.first()
return dict(site_settings=site_settings)
@public_bp.context_processor
def inject_pricing_plans():
"""Make PricingPlan model available in templates"""
return dict(PricingPlan=PricingPlan)
@public_bp.route('/features')
def features():
"""Features page"""
@@ -23,11 +28,6 @@ def init_public_routes(public_bp):
"""About page"""
return render_template('public/about.html')
@public_bp.route('/blog')
def blog():
"""Blog page"""
return render_template('public/blog.html')
@public_bp.route('/careers')
def careers():
"""Careers page"""
@@ -43,6 +43,30 @@ def init_public_routes(public_bp):
"""Help Center page"""
return render_template('public/help.html')
@public_bp.route('/help/articles')
def help_articles():
"""Display help articles by category"""
category = request.args.get('category', '')
# Get all published articles grouped by category
all_articles = HelpArticle.get_all_published()
categories = HelpArticle.get_categories()
# If a specific category is requested, filter to that category
if category and category in categories:
articles = HelpArticle.get_articles_by_category(category)
category_name = categories[category]
else:
articles = []
category_name = None
return render_template('public/help_articles.html',
articles=articles,
all_articles=all_articles,
categories=categories,
current_category=category,
category_name=category_name)
@public_bp.route('/contact')
def contact():
"""Contact page"""
@@ -68,12 +92,7 @@ def init_public_routes(public_bp):
"""Terms of Service page"""
return render_template('public/terms.html')
@public_bp.route('/gdpr')
def gdpr():
"""GDPR page"""
return render_template('public/gdpr.html')
@public_bp.route('/compliance')
def compliance():
"""Compliance page"""
return render_template('public/compliance.html')
@public_bp.route('/cookies')
def cookies():
"""Cookie Policy page"""
return render_template('public/cookies.html')

View File

@@ -593,6 +593,9 @@ async function startLaunch(data) {
// Save instance data
await updateStep(10, 'Saving Instance Data', 'Storing instance information...');
try {
// Get the launch data from sessionStorage to access pricing tier info
const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData') || '{}');
const instanceData = {
name: data.instanceName,
port: data.port,
@@ -603,7 +606,8 @@ async function startLaunch(data) {
repository: data.repository,
branch: data.branch,
deployed_version: dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown',
deployed_branch: data.branch
deployed_branch: data.branch,
payment_plan: launchData.pricingTier?.name || 'Basic' // Use the selected pricing tier name
};
console.log('Saving instance data:', instanceData);
const saveResult = await saveInstanceData(instanceData);
@@ -2014,7 +2018,9 @@ async function downloadDockerCompose(repo, branch) {
const result = await response.json();
return {
success: true,
content: result.content
content: result.content,
commit_hash: result.commit_hash,
latest_tag: result.latest_tag
};
} catch (error) {
console.error('Error downloading docker-compose.yml:', error);
@@ -2044,28 +2050,45 @@ async function saveInstanceData(instanceData) {
if (existingInstance) {
console.log('Instance already exists:', instanceData.port);
// Update existing instance with new data
const updateResponse = await fetch('/api/admin/save-instance', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
name: instanceData.port,
port: instanceData.port,
domains: instanceData.domains,
stack_id: instanceData.stack_id || '',
stack_name: instanceData.stack_name,
status: instanceData.status,
repository: instanceData.repository,
branch: instanceData.branch,
deployed_version: instanceData.deployed_version,
deployed_branch: instanceData.deployed_branch,
payment_plan: instanceData.payment_plan || 'Basic'
})
});
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
console.error('Error updating instance:', errorText);
throw new Error(`Failed to update instance data: ${updateResponse.status} ${updateResponse.statusText}`);
}
const updateResult = await updateResponse.json();
console.log('Instance updated:', updateResult);
return {
success: true,
data: {
name: instanceData.port,
company: 'loading...',
rooms_count: 0,
conversations_count: 0,
data_size: 0.0,
payment_plan: 'Basic',
main_url: `https://${instanceData.domains[0]}`,
status: 'inactive',
port: instanceData.port,
stack_id: instanceData.stack_id || '', // Use empty string if null
stack_name: instanceData.stack_name,
repository: instanceData.repository,
branch: instanceData.branch
}
data: updateResult.data
};
}
// If instance doesn't exist, create it
const response = await fetch('/instances/add', {
const response = await fetch('/api/admin/save-instance', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -2073,18 +2096,16 @@ async function saveInstanceData(instanceData) {
},
body: JSON.stringify({
name: instanceData.port,
company: 'loading...',
rooms_count: 0,
conversations_count: 0,
data_size: 0.0,
payment_plan: 'Basic',
main_url: `https://${instanceData.domains[0]}`,
status: 'inactive',
port: instanceData.port,
stack_id: instanceData.stack_id || '', // Use empty string if null
domains: instanceData.domains,
stack_id: instanceData.stack_id || '',
stack_name: instanceData.stack_name,
status: instanceData.status,
repository: instanceData.repository,
branch: instanceData.branch
branch: instanceData.branch,
deployed_version: instanceData.deployed_version,
deployed_branch: instanceData.deployed_branch,
payment_plan: instanceData.payment_plan || 'Basic'
})
});
@@ -2597,6 +2618,32 @@ async function checkStackExists(stackName) {
// Add new function to deploy stack
async function deployStack(dockerComposeContent, stackName, port) {
try {
// Get the launch data from sessionStorage to access pricing tier info
const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData'));
// Fetch the pricing tier details to get the actual quota values
let pricingTierDetails = null;
if (launchData?.pricingTier?.id) {
try {
const pricingResponse = await fetch(`/api/admin/pricing-plans/${launchData.pricingTier.id}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
}
});
if (pricingResponse.ok) {
const pricingData = await pricingResponse.json();
if (pricingData.success) {
pricingTierDetails = pricingData.plan;
}
}
} catch (error) {
console.warn('Failed to fetch pricing tier details:', error);
}
}
// First, attempt to deploy the stack
const response = await fetch('/api/admin/deploy-stack', {
method: 'POST',
@@ -2631,6 +2678,35 @@ async function deployStack(dockerComposeContent, stackName, port) {
{
name: 'DEPLOYED_AT',
value: new Date().toISOString()
},
// Pricing tier environment variables with actual quota values
{
name: 'PRICING_TIER_ID',
value: launchData?.pricingTier?.id?.toString() || '0'
},
{
name: 'PRICING_TIER_NAME',
value: launchData?.pricingTier?.name || 'Unknown'
},
{
name: 'ROOM_QUOTA',
value: pricingTierDetails?.room_quota?.toString() || '0'
},
{
name: 'CONVERSATION_QUOTA',
value: pricingTierDetails?.conversation_quota?.toString() || '0'
},
{
name: 'STORAGE_QUOTA_GB',
value: pricingTierDetails?.storage_quota_gb?.toString() || '0'
},
{
name: 'MANAGER_QUOTA',
value: pricingTierDetails?.manager_quota?.toString() || '0'
},
{
name: 'ADMIN_QUOTA',
value: pricingTierDetails?.admin_quota?.toString() || '0'
}
]
})

View File

@@ -0,0 +1,310 @@
document.addEventListener('DOMContentLoaded', function() {
// Feature management for add form
const addFeatureBtn = document.getElementById('addFeatureBtn');
const featuresContainer = document.getElementById('featuresContainer');
if (addFeatureBtn) {
addFeatureBtn.addEventListener('click', function() {
addFeatureField(featuresContainer);
});
}
// Feature management for edit form
const editAddFeatureBtn = document.getElementById('editAddFeatureBtn');
const editFeaturesContainer = document.getElementById('editFeaturesContainer');
if (editAddFeatureBtn) {
editAddFeatureBtn.addEventListener('click', function() {
addFeatureField(editFeaturesContainer);
});
}
// Remove feature buttons
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-feature-btn')) {
e.target.closest('.input-group').remove();
}
});
// Add Pricing Plan Form
const addPricingPlanForm = document.getElementById('addPricingPlanForm');
if (addPricingPlanForm) {
addPricingPlanForm.addEventListener('submit', function(e) {
e.preventDefault();
submitPricingPlanForm(this, 'POST', '/api/admin/pricing-plans');
});
}
// Edit Pricing Plan Form
const editPricingPlanForm = document.getElementById('editPricingPlanForm');
if (editPricingPlanForm) {
editPricingPlanForm.addEventListener('submit', function(e) {
e.preventDefault();
const planId = document.getElementById('editPlanId').value;
submitPricingPlanForm(this, 'PUT', `/api/admin/pricing-plans/${planId}`);
});
}
// Edit Plan Buttons
document.addEventListener('click', function(e) {
if (e.target.classList.contains('edit-plan-btn')) {
const planId = e.target.getAttribute('data-plan-id');
loadPlanForEdit(planId);
}
});
// Delete Plan Buttons
document.addEventListener('click', function(e) {
if (e.target.classList.contains('delete-plan-btn')) {
const planId = e.target.getAttribute('data-plan-id');
const planName = e.target.closest('.card').querySelector('.card-header h5').textContent;
showDeleteConfirmation(planId, planName);
}
});
// Confirm Delete Button
const confirmDeleteBtn = document.getElementById('confirmDeletePlan');
if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', function() {
const planId = this.getAttribute('data-plan-id');
deletePricingPlan(planId);
});
}
// Toggle switches
document.addEventListener('change', function(e) {
if (e.target.classList.contains('plan-status-toggle')) {
const planId = e.target.getAttribute('data-plan-id');
const isActive = e.target.checked;
updatePlanStatus(planId, 'is_active', isActive);
}
if (e.target.classList.contains('plan-popular-toggle')) {
const planId = e.target.getAttribute('data-plan-id');
const isPopular = e.target.checked;
updatePlanStatus(planId, 'is_popular', isPopular);
}
if (e.target.classList.contains('plan-custom-toggle')) {
const planId = e.target.getAttribute('data-plan-id');
const isCustom = e.target.checked;
updatePlanStatus(planId, 'is_custom', isCustom);
}
});
});
function addFeatureField(container) {
const featureGroup = document.createElement('div');
featureGroup.className = 'input-group mb-2';
featureGroup.innerHTML = `
<input type="text" class="form-control feature-input" name="features[]" required>
<button type="button" class="btn btn-outline-danger remove-feature-btn">
<i class="fas fa-times"></i>
</button>
`;
container.appendChild(featureGroup);
}
function submitPricingPlanForm(form, method, url) {
const formData = new FormData(form);
// Convert features array
const features = [];
form.querySelectorAll('input[name="features[]"]').forEach(input => {
if (input.value.trim()) {
features.push(input.value.trim());
}
});
// Remove features from formData and add as JSON
formData.delete('features[]');
formData.append('features', JSON.stringify(features));
// Convert checkboxes to boolean values
const checkboxes = ['is_popular', 'is_custom', 'is_active'];
checkboxes.forEach(field => {
formData.set(field, formData.get(field) === 'on');
});
fetch(url, {
method: method,
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Pricing plan saved successfully!', 'success');
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showNotification(data.error || 'Error saving pricing plan', 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Error saving pricing plan', 'error');
});
}
function loadPlanForEdit(planId) {
fetch(`/api/admin/pricing-plans/${planId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const plan = data.plan;
// Populate form fields
document.getElementById('editPlanId').value = plan.id;
document.getElementById('editPlanName').value = plan.name;
document.getElementById('editPlanDescription').value = plan.description || '';
document.getElementById('editMonthlyPrice').value = plan.monthly_price;
document.getElementById('editAnnualPrice').value = plan.annual_price;
document.getElementById('editButtonText').value = plan.button_text;
document.getElementById('editButtonUrl').value = plan.button_url;
document.getElementById('editIsPopular').checked = plan.is_popular;
document.getElementById('editIsCustom').checked = plan.is_custom;
document.getElementById('editIsActive').checked = plan.is_active;
// Populate quota fields
document.getElementById('editRoomQuota').value = plan.room_quota || 0;
document.getElementById('editConversationQuota').value = plan.conversation_quota || 0;
document.getElementById('editStorageQuota').value = plan.storage_quota_gb || 0;
document.getElementById('editManagerQuota').value = plan.manager_quota || 0;
document.getElementById('editAdminQuota').value = plan.admin_quota || 0;
// Populate features
const editFeaturesContainer = document.getElementById('editFeaturesContainer');
editFeaturesContainer.innerHTML = '';
plan.features.forEach(feature => {
const featureGroup = document.createElement('div');
featureGroup.className = 'input-group mb-2';
featureGroup.innerHTML = `
<input type="text" class="form-control feature-input" name="features[]" value="${feature}" required>
<button type="button" class="btn btn-outline-danger remove-feature-btn">
<i class="fas fa-times"></i>
</button>
`;
editFeaturesContainer.appendChild(featureGroup);
});
// Add one empty feature field if no features
if (plan.features.length === 0) {
addFeatureField(editFeaturesContainer);
}
} else {
showNotification(data.error || 'Error loading plan', 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Error loading plan', 'error');
});
}
function showDeleteConfirmation(planId, planName) {
document.getElementById('deletePlanName').textContent = planName;
document.getElementById('confirmDeletePlan').setAttribute('data-plan-id', planId);
const deleteModal = new bootstrap.Modal(document.getElementById('deletePricingPlanModal'));
deleteModal.show();
}
function deletePricingPlan(planId) {
fetch(`/api/admin/pricing-plans/${planId}`, {
method: 'DELETE',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Pricing plan deleted successfully!', 'success');
// Close modal
const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deletePricingPlanModal'));
deleteModal.hide();
// Remove from DOM
const planElement = document.querySelector(`[data-plan-id="${planId}"]`);
if (planElement) {
planElement.remove();
}
// Check if no plans left
const remainingPlans = document.querySelectorAll('[data-plan-id]');
if (remainingPlans.length === 0) {
window.location.reload();
}
} else {
showNotification(data.error || 'Error deleting plan', 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Error deleting plan', 'error');
});
}
function updatePlanStatus(planId, field, value) {
fetch(`/api/admin/pricing-plans/${planId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value
},
body: JSON.stringify({
field: field,
value: value
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Plan status updated successfully!', 'success');
} else {
showNotification(data.error || 'Error updating plan status', 'error');
// Revert the toggle
const toggle = document.querySelector(`[data-plan-id="${planId}"].${field.replace('is_', 'plan-')}-toggle`);
if (toggle) {
toggle.checked = !value;
}
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Error updating plan status', 'error');
// Revert the toggle
const toggle = document.querySelector(`[data-plan-id="${planId}"].${field.replace('is_', 'plan-')}-toggle`);
if (toggle) {
toggle.checked = !value;
}
});
}
function showNotification(message, type) {
// Create notification element
const notification = document.createElement('div');
notification.className = `alert alert-${type === 'success' ? 'success' : 'danger'} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}

View File

@@ -0,0 +1,489 @@
{% extends "common/base.html" %}
{% block title %}Support Articles - DocuPulse{% endblock %}
{% block extra_css %}
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.css" rel="stylesheet">
<style>
.article-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.article-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.category-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.status-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.note-editor {
border: 1px solid var(--border-color);
border-radius: 0.375rem;
}
.note-editor.note-frame {
border-color: var(--border-color);
}
.note-editor .note-editing-area {
background-color: var(--white);
}
.note-editor .note-toolbar {
background-color: var(--bg-color);
border-bottom: 1px solid var(--border-color);
border-radius: 0.375rem 0.375rem 0 0;
}
.note-editor .note-btn {
border: 1px solid var(--border-color);
background-color: var(--white);
color: var(--text-dark);
}
.note-editor .note-btn:hover {
background-color: var(--primary-color);
color: var(--white);
border-color: var(--primary-color);
}
.note-editor .note-btn.active {
background-color: var(--primary-color);
color: var(--white);
border-color: var(--primary-color);
}
.note-editor .note-editing-area .note-editable {
color: var(--text-dark);
font-family: inherit;
line-height: 1.6;
padding: 15px;
}
.note-editor .note-status-output {
background-color: var(--bg-color);
border-top: 1px solid var(--border-color);
color: var(--text-muted);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0" style="color: var(--primary-color);">
<i class="fas fa-life-ring me-2"></i>Support Articles
</h1>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createArticleModal">
<i class="fas fa-plus me-2"></i>Create New Article
</button>
</div>
<!-- Articles List -->
<div class="row" id="articlesList">
<!-- Articles will be loaded here via AJAX -->
</div>
</div>
</div>
</div>
<!-- Create Article Modal -->
<div class="modal fade" id="createArticleModal" tabindex="-1" aria-labelledby="createArticleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createArticleModalLabel">Create New Help Article</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="createArticleForm">
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="articleTitle" class="form-label">Title</label>
<input type="text" class="form-control" id="articleTitle" name="title" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="articleCategory" class="form-label">Category</label>
<select class="form-select" id="articleCategory" name="category" required>
<option value="">Select Category</option>
<option value="getting-started">Getting Started</option>
<option value="user-management">User Management</option>
<option value="file-management">File Management</option>
<option value="communication">Communication</option>
<option value="security">Security & Privacy</option>
<option value="administration">Administration</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label for="articleBody" class="form-label">Content</label>
<textarea class="form-control" id="articleBody" name="body" rows="15" required></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="orderIndex" class="form-label">Order Index</label>
<input type="number" class="form-control" id="orderIndex" name="order_index" value="0" min="0">
<div class="form-text">Lower numbers appear first in the category</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isPublished" name="is_published" checked>
<label class="form-check-label" for="isPublished">
Publish immediately
</label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Create Article</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Article Modal -->
<div class="modal fade" id="editArticleModal" tabindex="-1" aria-labelledby="editArticleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editArticleModalLabel">Edit Help Article</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editArticleForm">
<input type="hidden" id="editArticleId" name="id">
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="editArticleTitle" class="form-label">Title</label>
<input type="text" class="form-control" id="editArticleTitle" name="title" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="editArticleCategory" class="form-label">Category</label>
<select class="form-select" id="editArticleCategory" name="category" required>
<option value="">Select Category</option>
<option value="getting-started">Getting Started</option>
<option value="user-management">User Management</option>
<option value="file-management">File Management</option>
<option value="communication">Communication</option>
<option value="security">Security & Privacy</option>
<option value="administration">Administration</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label for="editArticleBody" class="form-label">Content</label>
<textarea class="form-control" id="editArticleBody" name="body" rows="15" required></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editOrderIndex" class="form-label">Order Index</label>
<input type="number" class="form-control" id="editOrderIndex" name="order_index" value="0" min="0">
<div class="form-text">Lower numbers appear first in the category</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="editIsPublished" name="is_published">
<label class="form-check-label" for="editIsPublished">
Published
</label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update Article</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteArticleModal" tabindex="-1" aria-labelledby="deleteArticleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteArticleModalLabel">Delete Article</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this article? This action cannot be undone.</p>
<p class="text-muted" id="deleteArticleTitle"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDelete">Delete Article</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Summernote for rich text editing
$('#articleBody, #editArticleBody').summernote({
height: 400,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'italic', 'strikethrough', 'clear']],
['fontname', ['fontname']],
['fontsize', ['fontsize']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['height', ['height']],
['table', ['table']],
['insert', ['link', 'picture', 'video', 'hr']],
['view', ['fullscreen', 'codeview', 'help']],
['undo', ['undo', 'redo']]
],
styleTags: [
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
],
fontNames: ['Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Helvetica', 'Impact', 'Tahoma', 'Times New Roman', 'Verdana'],
fontSizes: ['8', '9', '10', '11', '12', '14', '16', '18', '24', '36'],
callbacks: {
onInit: function() {
// Ensure proper styling when modal opens
$('.note-editor').css('border-color', 'var(--border-color)');
$('.note-editing-area').css('background-color', 'var(--white)');
}
}
});
// Load articles on page load
loadArticles();
// Handle create article form submission
document.getElementById('createArticleForm').addEventListener('submit', function(e) {
e.preventDefault();
createArticle();
});
// Handle edit article form submission
document.getElementById('editArticleForm').addEventListener('submit', function(e) {
e.preventDefault();
updateArticle();
});
// Handle delete confirmation
document.getElementById('confirmDelete').addEventListener('click', function() {
deleteArticle();
});
});
function loadArticles() {
fetch('/api/admin/help-articles')
.then(response => response.json())
.then(data => {
const articlesList = document.getElementById('articlesList');
articlesList.innerHTML = '';
if (data.articles.length === 0) {
articlesList.innerHTML = `
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body text-center py-5">
<i class="fas fa-file-alt text-muted" style="font-size: 3rem; opacity: 0.5;"></i>
<h4 class="text-muted mt-3">No Articles Yet</h4>
<p class="text-muted">Create your first help article to get started.</p>
</div>
</div>
</div>
`;
return;
}
const categoryNames = {
'getting-started': 'Getting Started',
'user-management': 'User Management',
'file-management': 'File Management',
'communication': 'Communication',
'security': 'Security & Privacy',
'administration': 'Administration'
};
data.articles.forEach(article => {
const card = document.createElement('div');
card.className = 'col-lg-6 col-xl-4 mb-4';
card.innerHTML = `
<div class="card article-card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge category-badge" style="background-color: var(--primary-color);">
${categoryNames[article.category]}
</span>
<span class="badge status-badge ${article.is_published ? 'bg-success' : 'bg-warning'}">
${article.is_published ? 'Published' : 'Draft'}
</span>
</div>
<h5 class="card-title mb-2">${article.title}</h5>
<p class="card-text text-muted small mb-3">
${article.body.substring(0, 100)}${article.body.length > 100 ? '...' : ''}
</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
Order: ${article.order_index} | Created: ${new Date(article.created_at).toLocaleDateString()}
</small>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="editArticle(${article.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger" onclick="confirmDelete(${article.id}, '${article.title}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
`;
articlesList.appendChild(card);
});
})
.catch(error => {
console.error('Error loading articles:', error);
showAlert('Error loading articles: ' + error.message, 'danger');
});
}
function createArticle() {
const form = document.getElementById('createArticleForm');
const formData = new FormData(form);
formData.set('body', $('#articleBody').summernote('code'));
formData.set('is_published', document.getElementById('isPublished').checked);
fetch('/api/admin/help-articles', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('createArticleModal'));
modal.hide();
form.reset();
$('#articleBody').summernote('code', '');
loadArticles();
showAlert('Article created successfully!', 'success');
} else {
throw new Error(data.error || 'Unknown error');
}
})
.catch(error => {
console.error('Error creating article:', error);
showAlert('Error creating article: ' + error.message, 'danger');
});
}
function editArticle(articleId) {
fetch(`/api/admin/help-articles/${articleId}`)
.then(response => response.json())
.then(data => {
document.getElementById('editArticleId').value = data.article.id;
document.getElementById('editArticleTitle').value = data.article.title;
document.getElementById('editArticleCategory').value = data.article.category;
$('#editArticleBody').summernote('code', data.article.body);
document.getElementById('editOrderIndex').value = data.article.order_index;
document.getElementById('editIsPublished').checked = data.article.is_published;
const modal = new bootstrap.Modal(document.getElementById('editArticleModal'));
modal.show();
})
.catch(error => {
console.error('Error loading article:', error);
showAlert('Error loading article: ' + error.message, 'danger');
});
}
function updateArticle() {
const form = document.getElementById('editArticleForm');
const formData = new FormData(form);
formData.set('body', $('#editArticleBody').summernote('code'));
formData.set('is_published', document.getElementById('editIsPublished').checked);
fetch(`/api/admin/help-articles/${document.getElementById('editArticleId').value}`, {
method: 'PUT',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('editArticleModal'));
modal.hide();
loadArticles();
showAlert('Article updated successfully!', 'success');
} else {
throw new Error(data.error || 'Unknown error');
}
})
.catch(error => {
console.error('Error updating article:', error);
showAlert('Error updating article: ' + error.message, 'danger');
});
}
function confirmDelete(articleId, title) {
document.getElementById('deleteArticleId').value = articleId;
document.getElementById('deleteArticleTitle').textContent = title;
const modal = new bootstrap.Modal(document.getElementById('deleteArticleModal'));
modal.show();
}
function deleteArticle() {
const articleId = document.getElementById('deleteArticleId').value;
fetch(`/api/admin/help-articles/${articleId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteArticleModal'));
modal.hide();
loadArticles();
showAlert('Article deleted successfully!', 'success');
} else {
throw new Error(data.error || 'Unknown error');
}
})
.catch(error => {
console.error('Error deleting article:', error);
showAlert('Error deleting article: ' + error.message, 'danger');
});
}
function showAlert(message, type) {
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-triangle'} me-2"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
const container = document.querySelector('.container-fluid');
container.insertAdjacentHTML('afterbegin', alertHtml);
}
</script>
{% endblock %}

View File

@@ -100,6 +100,11 @@
<i class="fas fa-book"></i> Development Wiki
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.support_articles' %}active{% endif %}" href="{{ url_for('main.support_articles') }}">
<i class="fas fa-life-ring"></i> Support Articles
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'contacts.contacts_list' %}active{% endif %}" href="{{ url_for('contacts.contacts_list') }}">

View File

@@ -0,0 +1,138 @@
<!-- Animated Numbers Component -->
<div class="stats-section">
<div class="container">
<div class="row text-center">
{% for stat in stats %}
<div class="col-md-{{ 12 // stats|length }}">
<div class="stat-item">
<span class="stat-number" data-value="{{ stat.value }}" data-suffix="{{ stat.suffix }}">{{ stat.display }}</span>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<style>
.stats-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 80px 0;
position: relative;
overflow: hidden;
}
.stats-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.stat-item {
text-align: center;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.stat-number {
font-size: 3rem;
font-weight: 700;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
width: 100%;
}
.stat-label {
font-size: 1.1rem;
opacity: 0.9;
text-align: center;
}
</style>
<script>
// Function to animate number counting
function animateNumber(element, endValue, suffix = '', duration = 2000) {
const start = performance.now();
const startValue = 0;
const difference = endValue - startValue;
function updateNumber(currentTime) {
const elapsed = currentTime - start;
const progress = Math.min(elapsed / duration, 1);
// Easing function for smooth animation
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentValue = startValue + (difference * easeOutQuart);
// Format the number based on the suffix
let displayValue;
if (suffix === '%') {
displayValue = currentValue.toFixed(1) + suffix;
} else if (suffix === '-bit') {
displayValue = Math.round(currentValue) + suffix;
} else if (suffix === '/7') {
displayValue = Math.round(currentValue) + suffix;
} else if (suffix === '+') {
displayValue = Math.round(currentValue) + suffix;
} else if (suffix === 'K+') {
displayValue = Math.round(currentValue) + suffix;
} else {
displayValue = Math.round(currentValue) + (suffix || '');
}
element.textContent = displayValue;
if (progress < 1) {
requestAnimationFrame(updateNumber);
} else {
// Ensure the final value is correct
element.textContent = element.getAttribute('data-value') + (suffix || '');
}
}
requestAnimationFrame(updateNumber);
}
// Initialize animated numbers when component is loaded
document.addEventListener('DOMContentLoaded', function() {
const statsObserver = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const statNumbers = entry.target.querySelectorAll('.stat-number');
statNumbers.forEach((stat, index) => {
setTimeout(() => {
const value = parseFloat(stat.getAttribute('data-value'));
const suffix = stat.getAttribute('data-suffix') || '';
if (!isNaN(value)) {
animateNumber(stat, value, suffix, 2000);
}
}, index * 300); // Stagger the animations
});
// Only trigger once
statsObserver.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
// Observe the stats section
const statsSection = document.querySelector('.stats-section');
if (statsSection) {
statsObserver.observe(statsSection);
}
});
</script>

View File

@@ -1,9 +1,9 @@
<!-- CTA Buttons Component -->
<div class="d-flex justify-content-center gap-3 flex-wrap">
<a href="{{ primary_url }}" class="btn btn-lg px-5 py-3" style="background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); border: none; color: white; border-radius: 25px; font-weight: 600;">
<a href="{{ primary_url }}" class="btn btn-primary btn-lg px-5 py-3">
<i class="{{ primary_icon }} me-2"></i>{{ primary_text }}
</a>
<a href="{{ secondary_url }}" class="btn btn-lg px-5 py-3" style="border: 2px solid var(--primary-color); color: var(--primary-color); background: transparent; border-radius: 25px; font-weight: 600;">
<a href="{{ secondary_url }}" class="btn btn-outline-primary btn-lg px-5 py-3">
<i class="{{ secondary_icon }} me-2"></i>{{ secondary_text }}
</a>
</div>

View File

@@ -0,0 +1,111 @@
<!-- Reusable Explainer Video Modal Component -->
<div class="modal fade" id="explainerVideoModal" tabindex="-1" aria-labelledby="explainerVideoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="explainerVideoModalLabel">
<i class="fas fa-play-circle me-2"></i>{{ modal_title|default('DocuPulse Overview') }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="ratio ratio-16x9">
<div class="bg-dark d-flex align-items-center justify-content-center" style="border-radius: 8px;">
<div class="text-center text-white">
<i class="fas fa-video fa-3x mb-3 opacity-50"></i>
<h5>{{ video_title|default('Explainer Video') }}</h5>
<p class="text-muted">{{ video_placeholder|default('Video placeholder - Replace with actual explainer video') }}</p>
<button class="btn btn-primary btn-lg px-4 py-3">
<i class="fas fa-play me-2"></i>Play Video
</button>
</div>
</div>
</div>
<div class="mt-4">
<h6>{{ learning_title|default('What you\'ll learn:') }}</h6>
<ul class="list-unstyled">
{% for point in learning_points|default([
'How DocuPulse streamlines document management',
'Room-based collaboration features',
'Security and permission controls',
'Real-time messaging and notifications'
]) %}
<li><i class="fas fa-check text-success me-2"></i>{{ point }}</li>
{% endfor %}
</ul>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-lg px-4 py-3" data-bs-dismiss="modal">Close</button>
<a href="{{ cta_url|default(url_for('public.pricing')) }}" class="btn btn-primary btn-lg px-4 py-3">
<i class="fas fa-rocket me-2"></i>{{ cta_text|default('Get Started Now') }}
</a>
</div>
</div>
</div>
</div>
<style>
/* Unified button styling for modal */
.modal .btn-close {
background: none;
border: none;
width: auto;
height: auto;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
opacity: 0.6;
padding: 8px;
border-radius: 4px;
}
.modal .btn-close:hover {
background: rgba(108, 117, 125, 0.1);
transform: translateY(-1px);
opacity: 1;
}
.modal .btn-close::before {
content: '×';
color: var(--text-dark);
font-size: 20px;
font-weight: bold;
line-height: 1;
}
.modal .btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.modal .btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.modal .btn-secondary {
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
color: white;
}
.modal .btn-secondary:hover {
background: linear-gradient(135deg, #5a6268 0%, #495057 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(108, 117, 125, 0.3);
color: white;
}
.modal .btn-lg {
padding: 15px 40px;
font-size: 1.1rem;
}
</style>

View File

@@ -17,7 +17,6 @@
<h6 class="mb-3" style="color: var(--white);">Company</h6>
<ul class="list-unstyled">
<li><a href="#" style="color: var(--border-light); text-decoration: none;">About</a></li>
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Blog</a></li>
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Careers</a></li>
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Press</a></li>
</ul>
@@ -27,7 +26,6 @@
<ul class="list-unstyled">
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Help Center</a></li>
<li><a href="#contact" style="color: var(--border-light); text-decoration: none;">Contact</a></li>
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Status</a></li>
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Security</a></li>
</ul>
</div>
@@ -36,8 +34,7 @@
<ul class="list-unstyled">
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Privacy</a></li>
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Terms</a></li>
<li><a href="#" style="color: var(--border-light); text-decoration: none;">GDPR</a></li>
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Compliance</a></li>
<li><a href="#" style="color: var(--border-light); text-decoration: none;">Cookies</a></li>
</ul>
</div>
</div>

View File

@@ -24,7 +24,6 @@
</a>
<ul class="dropdown-menu" style="background-color: var(--white); border: 1px solid var(--border-color); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);">
<li><a class="dropdown-item" href="{{ url_for('public.about') }}" style="color: var(--text-dark);">About</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.blog') }}" style="color: var(--text-dark);">Blog</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.careers') }}" style="color: var(--text-dark);">Careers</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.press') }}" style="color: var(--text-dark);">Press</a></li>
</ul>
@@ -36,7 +35,6 @@
<ul class="dropdown-menu" style="background-color: var(--white); border: 1px solid var(--border-color); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);">
<li><a class="dropdown-item" href="{{ url_for('public.help_center') }}" style="color: var(--text-dark);">Help Center</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.contact') }}" style="color: var(--text-dark);">Contact</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.status') }}" style="color: var(--text-dark);">Status</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.security') }}" style="color: var(--text-dark);">Security</a></li>
</ul>
</li>
@@ -47,8 +45,7 @@
<ul class="dropdown-menu" style="background-color: var(--white); border: 1px solid var(--border-color); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);">
<li><a class="dropdown-item" href="{{ url_for('public.privacy') }}" style="color: var(--text-dark);">Privacy</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.terms') }}" style="color: var(--text-dark);">Terms</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.gdpr') }}" style="color: var(--text-dark);">GDPR</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.compliance') }}" style="color: var(--text-dark);">Compliance</a></li>
<li><a class="dropdown-item" href="{{ url_for('public.cookies') }}" style="color: var(--text-dark);">Cookies</a></li>
</ul>
</li>
</ul>

View File

@@ -0,0 +1,73 @@
<!-- Reusable Hero Section Component -->
<section class="hero-section">
<div class="floating-elements">
<div class="floating-element"></div>
<div class="floating-element"></div>
<div class="floating-element"></div>
</div>
<div class="container text-center position-relative">
<h1 class="display-{{ title_size|default('3') }} fw-bold mb-4">{{ title }}</h1>
<p class="lead fs-{{ description_size|default('4') }} mb-5">{{ description }}</p>
{% if buttons %}
<div class="d-flex justify-content-center gap-3 flex-wrap">
{% for button in buttons %}
{% if button.type == 'link' %}
<a href="{{ button.url }}" class="btn btn-{{ button.style|default('light') }} btn-lg px-5 py-3">
{% if button.icon %}<i class="{{ button.icon }} me-2"></i>{% endif %}{{ button.text }}
</a>
{% elif button.type == 'modal' %}
<button type="button" class="btn btn-{{ button.style|default('outline-light') }} btn-lg px-5 py-3" data-bs-toggle="modal" data-bs-target="{{ button.target }}">
{% if button.icon %}<i class="{{ button.icon }} me-2"></i>{% endif %}{{ button.text }}
</button>
{% elif button.type == 'button' %}
<button type="button" class="btn btn-{{ button.style|default('light') }} btn-lg px-5 py-3" onclick="{{ button.onclick }}">
{% if button.icon %}<i class="{{ button.icon }} me-2"></i>{% endif %}{{ button.text }}
</button>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
</section>
<style>
.hero-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 120px 0 100px 0;
position: relative;
z-index: 1;
overflow: hidden;
}
.floating-elements {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
overflow: hidden;
}
.floating-element {
position: absolute;
width: 60px;
height: 60px;
background: linear-gradient(135deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%);
border-radius: 50%;
opacity: 0.3;
animation: float-around 8s ease-in-out infinite;
}
.floating-element:nth-child(1) { top: 20%; left: 10%; animation-delay: 0s; }
.floating-element:nth-child(2) { top: 60%; right: 15%; animation-delay: 2s; }
.floating-element:nth-child(3) { bottom: 30%; left: 20%; animation-delay: 4s; }
@keyframes float-around {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
25% { transform: translate(20px, -20px) rotate(90deg); }
50% { transform: translate(-10px, -40px) rotate(180deg); }
75% { transform: translate(-30px, -10px) rotate(270deg); }
}
</style>

View File

@@ -5,6 +5,72 @@
<h2 class="display-5 fw-bold mb-3">Simple, Transparent Pricing</h2>
<p class="lead text-muted">Choose the plan that fits your organization's needs</p>
</div>
{% set pricing_plans = PricingPlan.get_active_plans() %}
{% if pricing_plans %}
<div class="row g-4 justify-content-center">
{% for plan in pricing_plans %}
<div class="col-md-3">
<div class="card pricing-card h-100 d-flex flex-column {% if plan.is_popular %}border-primary position-relative{% endif %}"
{% if plan.is_popular %}style="border: 3px solid var(--primary-color) !important;"{% endif %}>
{% if plan.is_popular %}
<div class="position-absolute top-0 start-0" style="z-index: 10;">
<span class="badge px-3 py-2" style="background: var(--primary-color); color: white; font-size: 0.8rem; font-weight: 600; border-radius: 0 0 15px 0; margin-top: 0; border-top-left-radius: 10px;">
Most Popular
</span>
</div>
{% endif %}
<div class="card-body text-center p-5 d-flex flex-column">
<div class="flex-grow-1">
<h3 class="card-title">{{ plan.name }}</h3>
{% if plan.is_custom %}
<div class="display-4 fw-bold mb-3" style="color: var(--primary-color);">Custom</div>
{% else %}
<div class="display-4 fw-bold mb-3" style="color: var(--primary-color);">
<span class="monthly-price">€{{ "%.0f"|format(plan.monthly_price) }}</span>
<span class="annual-price" style="display: none;">€{{ "%.0f"|format(plan.annual_price) }}</span>
<span class="fs-6 text-muted">/month</span>
</div>
{% endif %}
{% if plan.description %}
<p class="text-muted mb-3">{{ plan.description }}</p>
{% endif %}
<ul class="list-unstyled mb-4">
{% for feature in plan.features %}
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>{{ feature }}</li>
{% endfor %}
</ul>
</div>
<a href="{{ plan.button_url }}" class="btn {% if plan.is_popular %}btn-primary{% else %}btn-outline-primary{% endif %} btn-lg w-100 mt-auto px-4 py-3">
{{ plan.button_text }}
</a>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Billing Toggle - Only show if there are non-custom plans -->
{% set has_non_custom_plans = pricing_plans | selectattr('is_custom', 'equalto', false) | list | length > 0 %}
{% if has_non_custom_plans %}
<div class="d-flex justify-content-center align-items-center mt-4 mb-3">
<span class="me-3">Monthly</span>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="annualBilling" style="width: 3rem; height: 1.5rem; background-color: var(--border-color); border-color: var(--border-color);">
<label class="form-check-label" for="annualBilling"></label>
</div>
<span class="ms-3">Annual <span class="badge text-white px-2 py-1 ms-1" style="background: var(--primary-color); font-size: 0.75rem;">Save 20%</span></span>
</div>
{% endif %}
{% else %}
<!-- Fallback to default pricing if no plans are configured -->
<div class="row g-4 justify-content-center">
<div class="col-md-3">
<div class="card pricing-card h-100 d-flex flex-column">
@@ -24,7 +90,7 @@
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Email support</li>
</ul>
</div>
<a href="{{ contact_url }}" class="btn btn-outline-primary w-100 mt-auto">Get Started</a>
<a href="{{ contact_url }}" class="btn btn-outline-primary btn-lg w-100 mt-auto px-4 py-3">Get Started</a>
</div>
</div>
</div>
@@ -51,7 +117,7 @@
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Priority support</li>
</ul>
</div>
<a href="{{ contact_url }}" class="btn btn-primary w-100 mt-auto">Get Started</a>
<a href="{{ contact_url }}" class="btn btn-primary btn-lg w-100 mt-auto px-4 py-3">Get Started</a>
</div>
</div>
</div>
@@ -73,7 +139,7 @@
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>24/7 dedicated support</li>
</ul>
</div>
<a href="{{ contact_url }}" class="btn btn-outline-primary w-100 mt-auto">Get Started</a>
<a href="{{ contact_url }}" class="btn btn-outline-primary btn-lg w-100 mt-auto px-4 py-3">Get Started</a>
</div>
</div>
</div>
@@ -92,7 +158,7 @@
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Dedicated account manager</li>
</ul>
</div>
<a href="{{ contact_url }}" class="btn btn-outline-primary w-100 mt-auto">Contact Sales</a>
<a href="{{ contact_url }}" class="btn btn-outline-primary btn-lg w-100 mt-auto px-4 py-3">Contact Sales</a>
</div>
</div>
</div>
@@ -107,12 +173,15 @@
</div>
<span class="ms-3">Annual <span class="badge text-white px-2 py-1 ms-1" style="background: var(--primary-color); font-size: 0.75rem;">Save 20%</span></span>
</div>
{% endif %}
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', function() {
const billingToggle = document.getElementById('annualBilling');
if (!billingToggle) return;
const monthlyPrices = document.querySelectorAll('.monthly-price');
const annualPrices = document.querySelectorAll('.annual-price');
@@ -126,19 +195,64 @@ document.addEventListener('DOMContentLoaded', function() {
.form-check-input:focus {
box-shadow: 0 0 0 0.25rem rgba(var(--primary-color-rgb), 0.25) !important;
}
.price-number {
display: inline-block;
transition: all 0.3s ease;
}
`;
document.head.appendChild(style);
// Function to animate number counting
function animateNumber(element, startValue, endValue, duration = 500) {
const start = performance.now();
const difference = endValue - startValue;
function updateNumber(currentTime) {
const elapsed = currentTime - start;
const progress = Math.min(elapsed / duration, 1);
// Easing function for smooth animation
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentValue = startValue + (difference * easeOutQuart);
element.textContent = '€' + Math.round(currentValue);
if (progress < 1) {
requestAnimationFrame(updateNumber);
} else {
// Ensure the final value is correct
element.textContent = '€' + endValue;
}
}
requestAnimationFrame(updateNumber);
}
billingToggle.addEventListener('change', function() {
if (this.checked) {
// Show annual prices
monthlyPrices.forEach(price => price.style.display = 'none');
annualPrices.forEach(price => price.style.display = 'inline');
// Switch to annual prices with animation
monthlyPrices.forEach((price, index) => {
const monthlyValue = parseInt(price.textContent.replace('€', ''));
const annualValue = parseInt(annualPrices[index].textContent.replace('€', ''));
// Store the original monthly value for later use
price.setAttribute('data-original-monthly', monthlyValue);
// Simply animate the number change
animateNumber(price, monthlyValue, annualValue);
});
} else {
// Show monthly prices
monthlyPrices.forEach(price => price.style.display = 'inline');
annualPrices.forEach(price => price.style.display = 'none');
// Switch to monthly prices with animation
monthlyPrices.forEach((price, index) => {
const currentValue = parseInt(price.textContent.replace('€', ''));
const originalMonthlyValue = parseInt(price.getAttribute('data-original-monthly'));
// Simply animate the number change back to monthly
animateNumber(price, currentValue, originalMonthlyValue);
});
}
});
});
</script>
</script>

View File

@@ -38,7 +38,9 @@
.hero-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 100px 0;
padding: 120px 0 100px 0;
position: relative;
z-index: 1;
}
.stats-section {
background-color: var(--bg-color);
@@ -64,16 +66,100 @@
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
color: white;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #5a6268 0%, #495057 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(108, 117, 125, 0.3);
color: white;
}
.btn-light {
background: rgba(255, 255, 255, 0.9);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-light:hover {
background: rgba(255, 255, 255, 1);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 255, 255, 0.4);
}
.btn-outline-light {
border: 2px solid rgba(255, 255, 255, 0.8);
color: white;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-light:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 1);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 255, 255, 0.2);
}
.btn-lg {
padding: 15px 40px;
font-size: 1.1rem;
}
.btn-close {
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
opacity: 0.8;
}
.btn-close:hover {
background: linear-gradient(135deg, #5a6268 0%, #495057 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(108, 117, 125, 0.3);
opacity: 1;
}
.btn-close::before {
content: '×';
color: white;
font-size: 24px;
font-weight: bold;
line-height: 1;
}
/* Navigation dropdown styles */
@@ -101,26 +187,46 @@
.dropdown-item {
color: var(--text-dark);
}
/* Ensure hero buttons are clickable */
.hero-section .d-flex {
position: relative;
z-index: 2;
}
.hero-section .btn {
position: relative;
z-index: 3;
cursor: pointer;
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
<section class="hero-section">
<div class="container text-center">
<h1 class="display-3 fw-bold mb-4">Enterprise Document Management<br>Made Simple</h1>
<p class="lead mb-5 fs-4">Secure, intelligent, and scalable document management platform designed for modern enterprises. Streamline workflows, enhance collaboration, and protect your data.</p>
<div class="d-flex justify-content-center gap-3 flex-wrap">
<a href="#contact" class="btn btn-light btn-lg px-5 py-3">
<i class="fas fa-rocket me-2"></i>Get Started
</a>
<a href="#features" class="btn btn-outline-light btn-lg px-5 py-3">
<i class="fas fa-play me-2"></i>Learn More
</a>
</div>
</div>
</section>
{% with
title="Enterprise Document Management Made Simple",
description="Secure, intelligent, and scalable document management platform designed for modern enterprises. Streamline workflows, enhance collaboration, and protect your data.",
buttons=[
{
'type': 'link',
'url': url_for('public.pricing'),
'text': 'Get Started',
'icon': 'fas fa-rocket',
'style': 'light'
},
{
'type': 'modal',
'target': '#explainerVideoModal',
'text': 'Learn More',
'icon': 'fas fa-play',
'style': 'outline-light'
}
]
%}
{% include 'components/hero_section.html' %}
{% endwith %}
<!-- Features Section -->
<section id="features" class="py-5">
@@ -287,7 +393,7 @@
<div class="col-md-8 text-center">
<h2 class="display-5 fw-bold mb-3">Ready to Get Started?</h2>
<p class="lead text-muted mb-5">Contact us today to learn how DocuPulse can transform your document management</p>
{% with primary_url=url_for('public.pricing'), primary_icon='fas fa-rocket', primary_text='Get Started', secondary_url='#contact', secondary_icon='fas fa-envelope', secondary_text='Contact Sales' %}
{% with primary_url=url_for('public.pricing'), primary_icon='fas fa-rocket', primary_text='Get Started', secondary_url=url_for('public.contact'), secondary_icon='fas fa-envelope', secondary_text='Contact Sales' %}
{% include 'components/cta_buttons.html' %}
{% endwith %}
</div>
@@ -297,6 +403,24 @@
{% include 'components/footer_nav.html' %}
<!-- Explainer Video Modal -->
{% with
modal_title='DocuPulse Overview',
video_title='Explainer Video',
video_placeholder='Video placeholder - Replace with actual explainer video',
learning_title='What you\'ll learn:',
learning_points=[
'How DocuPulse streamlines document management',
'Room-based collaboration features',
'Security and permission controls',
'Real-time messaging and notifications'
],
cta_url=url_for('public.pricing'),
cta_text='Get Started Now'
%}
{% include 'components/explainer_video_modal.html' %}
{% endwith %}
<!-- Hidden Admin Link -->
<div class="admin-link">
<a href="{{ url_for('auth.login') }}" title="Admin Login">

View File

@@ -130,6 +130,33 @@
<div class="company-value" id="company-description">Loading...</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;">Version:</div>
<div class="company-value" id="instance-version-value">Loading...</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;">Payment Plan:</div>
<div class="company-value" id="instance-payment-plan-value">Loading...</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;">Portainer Stack:</div>
<div class="company-value">
{% if instance.portainer_stack_name %}
<span class="badge bg-primary">{{ instance.portainer_stack_name }}</span>
{% if instance.portainer_stack_id %}
<small class="text-muted d-block mt-1">ID: {{ instance.portainer_stack_id }}</small>
{% endif %}
{% else %}
<span class="text-muted">Not set</span>
{% endif %}
</div>
</div>
</div>
</div>
<div class="col-md-6">
<h5 class="mb-3">Contact Information</h5>
@@ -1569,5 +1596,46 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
});
// Function to fetch version and payment plan info
async function fetchInstanceVersionAndPlan() {
const versionEl = document.getElementById('instance-version-value');
const planEl = document.getElementById('instance-payment-plan-value');
versionEl.textContent = 'Loading...';
planEl.textContent = 'Loading...';
try {
// Get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) throw new Error('Failed to get management token');
const tokenData = await tokenResponse.json();
if (!tokenData.token) throw new Error('No token received');
// Fetch version info
const response = await fetch(`{{ instance.main_url }}/api/admin/version-info`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!response.ok) throw new Error('Failed to fetch version info');
const data = await response.json();
versionEl.textContent = data.app_version || 'Unknown';
planEl.textContent = data.pricing_tier_name || 'Unknown';
} catch (error) {
versionEl.textContent = 'Error';
planEl.textContent = 'Error';
console.error('Error fetching version/plan info:', error);
}
}
document.addEventListener('DOMContentLoaded', function() {
// ... existing code ...
fetchInstanceVersionAndPlan();
});
</script>
{% endblock %}

View File

@@ -40,32 +40,127 @@
background-color: #d1ecf1 !important;
border-color: #bee5eb !important;
}
.badge.bg-orange {
background-color: #fd7e14 !important;
color: white !important;
}
.badge.bg-orange:hover {
background-color: #e55a00 !important;
}
/* Pricing tier selection styles */
.pricing-card {
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.pricing-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-color: var(--primary-color);
}
.pricing-card.selected {
border-color: var(--primary-color);
background-color: rgba(22, 118, 123, 0.05);
box-shadow: 0 4px 12px rgba(22, 118, 123, 0.2);
}
.pricing-card.selected::after {
content: '✓';
position: absolute;
top: 10px;
right: 10px;
background-color: var(--primary-color);
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
.pricing-card.border-primary {
border-color: var(--primary-color) !important;
}
.quota-info {
font-size: 0.75rem;
}
.features {
text-align: left;
}
</style>
{% endblock %}
{% block content %}
{{ header(
title="Instances",
description="Manage your DocuPulse instances",
title="Instance Management",
description="Manage and monitor your DocuPulse instances",
icon="fa-server",
buttons=[
{
'text': 'Launch New Instance',
'url': '#',
'onclick': 'showAddInstanceModal()',
'icon': 'fa-rocket',
'class': 'btn-primary',
'onclick': 'showAddInstanceModal()'
'class': 'btn-primary'
},
{
'text': 'Add Existing Instance',
'text': 'Link Existing Instance',
'url': '#',
'onclick': 'showAddExistingInstanceModal()',
'icon': 'fa-link',
'class': 'btn-primary',
'onclick': 'showAddExistingInstanceModal()'
'class': 'btn-secondary'
}
]
) }}
<!-- Latest Version Information -->
<div class="container-fluid mb-4">
<div class="row">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h5 class="card-title mb-2">
<i class="fas fa-code-branch me-2" style="color: var(--primary-color);"></i>
Latest Available Version
</h5>
<div class="d-flex align-items-center">
<div class="me-4">
<span class="badge bg-success fs-6" id="latestVersionBadge">
<i class="fas fa-spinner fa-spin me-1"></i> Loading...
</span>
</div>
<div>
<small class="text-muted">
<i class="fas fa-clock me-1"></i>
Last checked: <span id="lastChecked" class="text-muted">Loading...</span>
</small>
</div>
</div>
</div>
<div class="col-md-4 text-end">
<button class="btn btn-outline-primary btn-sm" onclick="refreshLatestVersion()">
<i class="fas fa-sync-alt me-1"></i> Refresh
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-12">
@@ -84,7 +179,7 @@
<th>Main URL</th>
<th>Status</th>
<th>Version</th>
<th>Branch</th>
<th>Portainer Stack</th>
<th>Connection Token</th>
<th>Actions</th>
</tr>
@@ -97,7 +192,7 @@
<td>{{ instance.rooms_count }}</td>
<td>{{ instance.conversations_count }}</td>
<td>{{ "%.1f"|format(instance.data_size) }} GB</td>
<td>{{ instance.payment_plan }}</td>
<td id="payment-plan-{{ instance.id }}">{{ instance.payment_plan }}</td>
<td>
<a href="{{ instance.main_url }}"
target="_blank"
@@ -127,13 +222,13 @@
{% endif %}
</td>
<td>
{% if instance.deployed_branch %}
<span class="badge bg-light text-dark branch-badge" data-bs-toggle="tooltip"
title="Deployed branch: {{ instance.deployed_branch }}">
{{ instance.deployed_branch }}
{% if instance.portainer_stack_name %}
<span class="badge bg-primary" data-bs-toggle="tooltip"
title="Stack ID: {{ instance.portainer_stack_id or 'N/A' }}">
{{ instance.portainer_stack_name }}
</span>
{% else %}
<span class="badge bg-secondary branch-badge">unknown</span>
<span class="badge bg-secondary">Not set</span>
{% endif %}
</td>
<td>
@@ -210,6 +305,10 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<!-- Hidden fields for pricing tier selection -->
<input type="hidden" id="selectedPricingTierId" value="">
<input type="hidden" id="selectedPricingTierName" value="">
<!-- Steps Navigation -->
<div class="d-flex justify-content-between mb-4">
<div class="step-item active" data-step="1">
@@ -234,6 +333,10 @@
</div>
<div class="step-item" data-step="6">
<div class="step-circle">6</div>
<div class="step-label">Pricing Tier</div>
</div>
<div class="step-item" data-step="7">
<div class="step-circle">7</div>
<div class="step-label">Launch</div>
</div>
</div>
@@ -443,8 +546,30 @@
</div>
</div>
<!-- Step 6 -->
<!-- Step 6: Pricing Tier Selection -->
<div class="step-pane" id="step6">
<div class="step-content">
<div class="card">
<div class="card-body">
<h5 class="card-title mb-4">Select Pricing Tier</h5>
<p class="text-muted mb-4">Choose the pricing tier that best fits your needs. This will determine the resource limits for your instance.</p>
<div id="pricingTiersContainer" class="row">
<!-- Pricing tiers will be loaded here -->
<div class="col-12 text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading pricing tiers...</span>
</div>
<p class="mt-2">Loading available pricing tiers...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Step 7 -->
<div class="step-pane" id="step7">
<div class="text-center">
<i class="fas fa-rocket fa-4x mb-4" style="color: var(--primary-color);"></i>
<h4>Ready to Launch!</h4>
@@ -458,6 +583,7 @@
<p><strong>Repository:</strong> <span id="reviewRepo"></span></p>
<p><strong>Branch:</strong> <span id="reviewBranch"></span></p>
<p><strong>Company:</strong> <span id="reviewCompany"></span></p>
<p><strong>Pricing Tier:</strong> <span id="reviewPricingTier"></span></p>
</div>
<div class="col-md-6">
<p><strong>Port:</strong> <span id="reviewPort"></span></p>
@@ -687,7 +813,7 @@ let launchStepsModal;
let currentStep = 1;
// Update the total number of steps
const totalSteps = 6;
const totalSteps = 7;
document.addEventListener('DOMContentLoaded', function() {
addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal'));
@@ -706,25 +832,37 @@ document.addEventListener('DOMContentLoaded', function() {
const headerButtons = document.querySelector('.header-buttons');
if (headerButtons) {
const refreshButton = document.createElement('button');
refreshButton.className = 'btn btn-outline-primary';
refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i> Refresh';
refreshButton.className = 'btn btn-outline-primary me-2';
refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i> Refresh All';
refreshButton.onclick = function() {
fetchCompanyNames();
};
headerButtons.appendChild(refreshButton);
const versionRefreshButton = document.createElement('button');
versionRefreshButton.className = 'btn btn-outline-info';
versionRefreshButton.innerHTML = '<i class="fas fa-code-branch"></i> Refresh Versions';
versionRefreshButton.onclick = function() {
refreshAllVersionInfo();
};
headerButtons.appendChild(versionRefreshButton);
}
// Wait a short moment to ensure the table is rendered
setTimeout(() => {
// Check statuses on page load
checkAllInstanceStatuses();
setTimeout(async () => {
// First fetch latest version information
await fetchLatestVersion();
// Fetch company names for all instances
// Then check statuses and fetch company names
checkAllInstanceStatuses();
fetchCompanyNames();
}, 100);
// Set up periodic status checks (every 30 seconds)
setInterval(checkAllInstanceStatuses, 30000);
// Set up periodic latest version checks (every 5 minutes)
setInterval(fetchLatestVersion, 300000);
// Update color picker functionality
const primaryColor = document.getElementById('primaryColor');
@@ -768,12 +906,25 @@ document.addEventListener('DOMContentLoaded', function() {
updateColorPreview();
});
// Function to check status of all instances
// Function to check all instance statuses
async function checkAllInstanceStatuses() {
const statusBadges = document.querySelectorAll('[data-instance-id]');
for (const badge of statusBadges) {
console.log('Checking all instance statuses...');
const instances = document.querySelectorAll('[data-instance-id]');
for (const badge of instances) {
const instanceId = badge.dataset.instanceId;
await checkInstanceStatus(instanceId);
// Also refresh version info when checking status
const instanceUrl = badge.closest('tr').querySelector('td:nth-child(7) a')?.href;
const apiKey = badge.dataset.token;
if (instanceUrl && apiKey) {
// Fetch version info in the background (don't await to avoid blocking status checks)
fetchVersionInfo(instanceUrl, instanceId).catch(error => {
console.error(`Error fetching version info for instance ${instanceId}:`, error);
});
}
}
}
@@ -890,6 +1041,225 @@ async function fetchInstanceStats(instanceUrl, instanceId, jwtToken) {
}
}
// Function to compare semantic versions and determine update type
function compareSemanticVersions(currentVersion, latestVersion) {
try {
// Parse versions into parts (handle cases like "1.0" or "1.0.0")
const parseVersion = (version) => {
const parts = version.split('.').map(part => {
const num = parseInt(part, 10);
return isNaN(num) ? 0 : num;
});
// Ensure we have at least 3 parts (major.minor.patch)
while (parts.length < 3) {
parts.push(0);
}
return parts.slice(0, 3); // Only take first 3 parts
};
const current = parseVersion(currentVersion);
const latest = parseVersion(latestVersion);
// Compare major version
if (current[0] < latest[0]) {
return 'major';
}
// Compare minor version
if (current[1] < latest[1]) {
return 'minor';
}
// Compare patch version
if (current[2] < latest[2]) {
return 'patch';
}
// If we get here, current version is newer or equal
return 'up_to_date';
} catch (error) {
console.error('Error comparing semantic versions:', error);
return 'unknown';
}
}
// Function to fetch version information for an instance
async function fetchVersionInfo(instanceUrl, instanceId) {
const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr');
const versionCell = row.querySelector('td:nth-child(9)'); // Version column (adjusted after removing branch)
const paymentPlanCell = row.querySelector('td:nth-child(6)'); // Payment Plan column
// Show loading state
if (versionCell) {
versionCell.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...';
}
try {
const apiKey = document.querySelector(`[data-instance-id="${instanceId}"]`).dataset.token;
if (!apiKey) {
throw new Error('No API key available');
}
console.log(`Getting JWT token for instance ${instanceId} for version info`);
const jwtToken = await getJWTToken(instanceUrl, apiKey);
console.log('Got JWT token for version info');
// Fetch version information
console.log(`Fetching version info for instance ${instanceId} from ${instanceUrl}/api/admin/version-info`);
const response = await fetch(`${instanceUrl}/api/admin/version-info`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${jwtToken}`
}
});
if (!response.ok) {
const errorText = await response.text();
console.error(`HTTP error ${response.status}:`, errorText);
throw new Error(`Server returned ${response.status}: ${errorText}`);
}
const data = await response.json();
console.log('Received version data:', data);
// Update payment plan cell with pricing tier name
if (paymentPlanCell) {
const pricingTierName = data.pricing_tier_name || 'unknown';
if (pricingTierName !== 'unknown') {
paymentPlanCell.innerHTML = `
<span class="badge bg-info" data-bs-toggle="tooltip" title="Pricing Tier: ${pricingTierName}">
<i class="fas fa-tag me-1"></i>${pricingTierName}
</span>`;
// Add tooltip for payment plan
const paymentPlanBadge = paymentPlanCell.querySelector('[data-bs-toggle="tooltip"]');
if (paymentPlanBadge) {
new bootstrap.Tooltip(paymentPlanBadge);
}
} else {
paymentPlanCell.innerHTML = '<span class="badge bg-secondary">unknown</span>';
}
}
// Update version cell
if (versionCell) {
const appVersion = data.app_version || 'unknown';
const gitCommit = data.git_commit || 'unknown';
const deployedAt = data.deployed_at || 'unknown';
if (appVersion !== 'unknown') {
// Get the latest version for comparison
const latestVersionBadge = document.getElementById('latestVersionBadge');
let latestVersion = latestVersionBadge ? latestVersionBadge.textContent.replace('Loading...', '').trim() : null;
// If latest version is not available yet, wait a bit and try again
if (!latestVersion || latestVersion === '') {
// Show loading state while waiting for latest version
versionCell.innerHTML = `
<span class="badge bg-secondary version-badge" data-bs-toggle="tooltip"
title="App Version: ${appVersion}<br>Git Commit: ${gitCommit}<br>Deployed: ${deployedAt}<br>Waiting for latest version...">
<i class="fas fa-spinner fa-spin me-1"></i>${appVersion.length > 8 ? appVersion.substring(0, 8) : appVersion}
</span>`;
// Wait a bit and retry the comparison
setTimeout(() => {
fetchVersionInfo(instanceUrl, instanceId);
}, 2000);
return;
}
// Determine if this instance is up to date
let badgeClass = 'bg-secondary';
let statusIcon = 'fas fa-tag';
let tooltipText = `App Version: ${appVersion}<br>Git Commit: ${gitCommit}<br>Deployed: ${deployedAt}`;
if (latestVersion && appVersion === latestVersion) {
// Exact match - green
badgeClass = 'bg-success';
statusIcon = 'fas fa-check-circle';
tooltipText += '<br><strong>✅ Up to date</strong>';
} else if (latestVersion && appVersion !== latestVersion) {
// Compare semantic versions
const versionComparison = compareSemanticVersions(appVersion, latestVersion);
switch (versionComparison) {
case 'patch':
// Only patch version different - yellow
badgeClass = 'bg-warning';
statusIcon = 'fas fa-exclamation-triangle';
tooltipText += `<br><strong>🟡 Patch update available (Latest: ${latestVersion})</strong>`;
break;
case 'minor':
// Minor version different - orange
badgeClass = 'bg-orange';
statusIcon = 'fas fa-exclamation-triangle';
tooltipText += `<br><strong>🟠 Minor update available (Latest: ${latestVersion})</strong>`;
break;
case 'major':
// Major version different - red
badgeClass = 'bg-danger';
statusIcon = 'fas fa-exclamation-triangle';
tooltipText += `<br><strong>🔴 Major update available (Latest: ${latestVersion})</strong>`;
break;
default:
// Unknown format or comparison failed - red
badgeClass = 'bg-danger';
statusIcon = 'fas fa-exclamation-triangle';
tooltipText += `<br><strong>🔴 Outdated (Latest: ${latestVersion})</strong>`;
break;
}
}
versionCell.innerHTML = `
<span class="badge ${badgeClass} version-badge" data-bs-toggle="tooltip"
title="${tooltipText}">
<i class="${statusIcon} me-1"></i>${appVersion.length > 8 ? appVersion.substring(0, 8) : appVersion}
</span>`;
} else {
versionCell.innerHTML = '<span class="badge bg-secondary version-badge">unknown</span>';
}
}
// Update tooltips
const versionBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]');
if (versionBadge) {
new bootstrap.Tooltip(versionBadge);
}
} catch (error) {
console.error(`Error fetching version info for instance ${instanceId}:`, error);
// Show error state
if (versionCell) {
versionCell.innerHTML = `
<span class="text-warning" data-bs-toggle="tooltip" title="Error: ${error.message}">
<i class="fas fa-exclamation-triangle"></i> Error
</span>`;
}
if (paymentPlanCell) {
paymentPlanCell.innerHTML = `
<span class="text-warning" data-bs-toggle="tooltip" title="Error: ${error.message}">
<i class="fas fa-exclamation-triangle"></i> Error
</span>`;
}
// Add tooltips for error states
const errorBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]');
if (errorBadge) {
new bootstrap.Tooltip(errorBadge);
}
const paymentPlanErrorBadge = paymentPlanCell?.querySelector('[data-bs-toggle="tooltip"]');
if (paymentPlanErrorBadge) {
new bootstrap.Tooltip(paymentPlanErrorBadge);
}
}
}
// Function to fetch company name from instance settings
async function fetchCompanyName(instanceUrl, instanceId) {
const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr');
@@ -977,53 +1347,30 @@ async function fetchCompanyName(instanceUrl, instanceId) {
// Function to fetch company names for all instances
async function fetchCompanyNames() {
console.log('Starting fetchCompanyNames...');
const instances = document.querySelectorAll('[data-instance-id]');
const loadingPromises = [];
console.log('Starting to fetch company names and stats for all instances');
for (const instance of instances) {
const instanceId = instance.dataset.instanceId;
const row = instance.closest('tr');
for (const badge of instances) {
const instanceId = badge.dataset.instanceId;
const instanceUrl = badge.closest('tr').querySelector('td:nth-child(7) a')?.href;
const apiKey = badge.dataset.token;
// Debug: Log all cells in the row
console.log(`Row for instance ${instanceId}:`, {
cells: Array.from(row.querySelectorAll('td')).map(td => ({
text: td.textContent.trim(),
html: td.innerHTML.trim()
}))
});
// Main URL is now the 9th column (after adding Version and Branch columns)
const urlCell = row.querySelector('td:nth-child(9)');
if (!urlCell) {
console.error(`Could not find URL cell for instance ${instanceId}`);
continue;
}
const urlLink = urlCell.querySelector('a');
if (!urlLink) {
console.error(`Could not find URL link for instance ${instanceId}`);
continue;
}
const instanceUrl = urlLink.getAttribute('href');
const token = instance.dataset.token;
console.log(`Instance ${instanceId}:`, {
url: instanceUrl,
hasToken: !!token
});
if (instanceUrl && token) {
loadingPromises.push(fetchCompanyName(instanceUrl, instanceId));
if (instanceUrl && apiKey) {
console.log(`Fetching data for instance ${instanceId}`);
loadingPromises.push(
fetchCompanyName(instanceUrl, instanceId),
fetchVersionInfo(instanceUrl, instanceId) // Add version info fetching
);
} else {
const row = badge.closest('tr');
const cells = [
row.querySelector('td:nth-child(2)'), // Company
row.querySelector('td:nth-child(3)'), // Rooms
row.querySelector('td:nth-child(4)'), // Conversations
row.querySelector('td:nth-child(5)') // Data
row.querySelector('td:nth-child(5)'), // Data
row.querySelector('td:nth-child(9)') // Version
];
cells.forEach(cell => {
@@ -1043,7 +1390,7 @@ async function fetchCompanyNames() {
try {
await Promise.all(loadingPromises);
console.log('Finished fetching all company names and stats');
console.log('Finished fetching all company names, stats, and version info');
} catch (error) {
console.error('Error in fetchCompanyNames:', error);
}
@@ -1425,6 +1772,11 @@ function updateStepDisplay() {
if (currentStep === 4) {
getNextAvailablePort();
}
// If we're on step 6, load pricing tiers
if (currentStep === 6) {
loadPricingTiers();
}
}
function nextStep() {
@@ -1437,6 +1789,9 @@ function nextStep() {
if (currentStep === 4 && !validateStep4()) {
return;
}
if (currentStep === 6 && !validateStep6()) {
return;
}
if (currentStep < totalSteps) {
currentStep++;
@@ -1841,6 +2196,7 @@ function updateReviewSection() {
const webAddresses = Array.from(document.querySelectorAll('.web-address'))
.map(input => input.value)
.join(', ');
const pricingTier = document.getElementById('selectedPricingTierName').value;
// Update the review section
document.getElementById('reviewRepo').textContent = repo;
@@ -1848,6 +2204,7 @@ function updateReviewSection() {
document.getElementById('reviewCompany').textContent = company;
document.getElementById('reviewPort').textContent = port;
document.getElementById('reviewWebAddresses').textContent = webAddresses;
document.getElementById('reviewPricingTier').textContent = pricingTier || 'Not selected';
}
// Function to launch the instance
@@ -1874,6 +2231,10 @@ function launchInstance() {
colors: {
primary: document.getElementById('primaryColor').value,
secondary: document.getElementById('secondaryColor').value
},
pricingTier: {
id: document.getElementById('selectedPricingTierId').value,
name: document.getElementById('selectedPricingTierName').value
}
};
@@ -1886,5 +2247,293 @@ function launchInstance() {
// Redirect to the launch progress page
window.location.href = '/instances/launch-progress';
}
// Function to refresh all version information
async function refreshAllVersionInfo() {
console.log('Refreshing all version information...');
const instances = document.querySelectorAll('[data-instance-id]');
const loadingPromises = [];
for (const badge of instances) {
const instanceId = badge.dataset.instanceId;
const instanceUrl = badge.closest('tr').querySelector('td:nth-child(7) a')?.href;
const apiKey = badge.dataset.token;
if (instanceUrl && apiKey) {
console.log(`Refreshing version info for instance ${instanceId}`);
loadingPromises.push(fetchVersionInfo(instanceUrl, instanceId));
}
}
try {
await Promise.all(loadingPromises);
console.log('Finished refreshing all version information');
} catch (error) {
console.error('Error refreshing version information:', error);
}
}
// Function to fetch latest version information
async function fetchLatestVersion() {
console.log('Fetching latest version information...');
const versionBadge = document.getElementById('latestVersionBadge');
const commitSpan = document.getElementById('latestCommit');
const checkedSpan = document.getElementById('lastChecked');
// Show loading state
if (versionBadge) {
versionBadge.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Loading...';
versionBadge.className = 'badge bg-secondary fs-6';
}
try {
const response = await fetch('/api/latest-version', {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
}
});
if (!response.ok) {
const errorText = await response.text();
console.error(`HTTP error ${response.status}:`, errorText);
throw new Error(`Server returned ${response.status}: ${errorText}`);
}
const data = await response.json();
console.log('Received latest version data:', data);
if (data.success) {
// Update version badge
if (versionBadge) {
const version = data.latest_version;
if (version !== 'unknown') {
versionBadge.innerHTML = `<i class="fas fa-tag me-1"></i>${version}`;
versionBadge.className = 'badge bg-success fs-6';
} else {
versionBadge.innerHTML = '<i class="fas fa-question-circle me-1"></i>Unknown';
versionBadge.className = 'badge bg-warning fs-6';
}
}
// Update commit information
if (commitSpan) {
const commit = data.latest_commit;
if (commit !== 'unknown') {
commitSpan.textContent = commit.substring(0, 8);
commitSpan.title = commit;
} else {
commitSpan.textContent = 'Unknown';
}
}
// Update last checked time
if (checkedSpan) {
const lastChecked = data.last_checked;
if (lastChecked) {
const date = new Date(lastChecked);
checkedSpan.textContent = date.toLocaleString();
} else {
checkedSpan.textContent = 'Never';
}
}
} else {
// Handle error response
if (versionBadge) {
versionBadge.innerHTML = '<i class="fas fa-exclamation-triangle me-1"></i>Error';
versionBadge.className = 'badge bg-danger fs-6';
}
if (commitSpan) {
commitSpan.textContent = 'Error';
}
if (checkedSpan) {
checkedSpan.textContent = 'Error';
}
console.error('Error in latest version response:', data.error);
}
} catch (error) {
console.error('Error fetching latest version:', error);
// Show error state
if (versionBadge) {
versionBadge.innerHTML = '<i class="fas fa-exclamation-triangle me-1"></i>Error';
versionBadge.className = 'badge bg-danger fs-6';
}
if (commitSpan) {
commitSpan.textContent = 'Error';
}
if (checkedSpan) {
checkedSpan.textContent = 'Error';
}
}
}
// Function to refresh latest version (called by button)
async function refreshLatestVersion() {
console.log('Manual refresh of latest version requested');
await fetchLatestVersion();
}
// Function to load pricing tiers
async function loadPricingTiers() {
const container = document.getElementById('pricingTiersContainer');
try {
const response = await fetch('/api/admin/pricing-plans', {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
}
});
if (!response.ok) {
throw new Error('Failed to load pricing tiers');
}
const data = await response.json();
if (data.plans && data.plans.length > 0) {
container.innerHTML = data.plans.map(plan => `
<div class="col-md-6 col-lg-4 mb-3">
<div class="card pricing-card h-100 ${plan.is_popular ? 'border-primary' : ''}"
onclick="selectPricingTier(${plan.id}, '${plan.name}')">
<div class="card-body text-center">
${plan.is_popular ? '<div class="badge bg-primary mb-2">Most Popular</div>' : ''}
<h5 class="card-title">${plan.name}</h5>
${plan.description ? `<p class="text-muted small mb-3">${plan.description}</p>` : ''}
<div class="pricing mb-3">
${plan.is_custom ?
'<span class="h4 text-primary">Custom Pricing</span>' :
`<span class="h4 text-primary">€${plan.monthly_price}</span><span class="text-muted">/month</span>`
}
</div>
<div class="quota-info small text-muted mb-3">
<div class="row">
<div class="col-6">
<i class="fas fa-door-open me-1"></i>${plan.format_quota_display.room_quota}<br>
<i class="fas fa-comments me-1"></i>${plan.format_quota_display.conversation_quota}<br>
<i class="fas fa-hdd me-1"></i>${plan.format_quota_display.storage_quota_gb}
</div>
<div class="col-6">
<i class="fas fa-user-tie me-1"></i>${plan.format_quota_display.manager_quota}<br>
<i class="fas fa-user-shield me-1"></i>${plan.format_quota_display.admin_quota}
</div>
</div>
</div>
<div class="features small">
${plan.features.slice(0, 3).map(feature => `<div>✓ ${feature}</div>`).join('')}
${plan.features.length > 3 ? `<div class="text-muted">+${plan.features.length - 3} more features</div>` : ''}
</div>
</div>
</div>
</div>
`).join('');
} else {
container.innerHTML = `
<div class="col-12 text-center">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
No pricing tiers available. Please contact your administrator.
</div>
</div>
`;
}
} catch (error) {
console.error('Error loading pricing tiers:', error);
container.innerHTML = `
<div class="col-12 text-center">
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Error loading pricing tiers: ${error.message}
</div>
</div>
`;
}
}
// Function to select a pricing tier
function selectPricingTier(planId, planName) {
// Remove selection from all cards
document.querySelectorAll('.pricing-card').forEach(card => {
card.classList.remove('selected');
});
// Add selection to clicked card
event.currentTarget.classList.add('selected');
// Store the selection
document.getElementById('selectedPricingTierId').value = planId;
document.getElementById('selectedPricingTierName').value = planName;
// Enable next button if not already enabled
const nextButton = document.querySelector('#launchStepsFooter .btn-primary');
if (nextButton.disabled) {
nextButton.disabled = false;
}
}
// Function to validate step 6 (pricing tier selection)
function validateStep6() {
const selectedTierId = document.getElementById('selectedPricingTierId').value;
if (!selectedTierId) {
alert('Please select a pricing tier before proceeding.');
return false;
}
return true;
}
document.addEventListener('DOMContentLoaded', function() {
// For each instance row, fetch the payment plan from the instance API
document.querySelectorAll('[data-instance-id]').forEach(function(badge) {
const instanceId = badge.getAttribute('data-instance-id');
const token = badge.getAttribute('data-token');
const row = badge.closest('tr');
const urlCell = row.querySelector('td:nth-child(7) a');
const paymentPlanCell = document.getElementById('payment-plan-' + instanceId);
if (!urlCell || !token || !paymentPlanCell) return;
const instanceUrl = urlCell.getAttribute('href');
// Get management token
fetch(instanceUrl.replace(/\/$/, '') + '/api/admin/management-token', {
method: 'POST',
headers: {
'X-API-Key': token,
'Accept': 'application/json'
}
})
.then(res => res.json())
.then(data => {
if (!data.token) throw new Error('No management token');
// Fetch version info (which includes pricing_tier_name)
return fetch(instanceUrl.replace(/\/$/, '') + '/api/admin/version-info', {
headers: {
'Authorization': 'Bearer ' + data.token,
'Accept': 'application/json'
}
});
})
.then(res => res.json())
.then(data => {
if (data.pricing_tier_name) {
paymentPlanCell.textContent = data.pricing_tier_name;
} else {
paymentPlanCell.textContent = 'Unknown';
}
})
.catch(err => {
paymentPlanCell.textContent = 'Unknown';
});
});
});
</script>
{% endblock %}
{% endblock %}

431
templates/public/about.html Normal file
View File

@@ -0,0 +1,431 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About Us - DocuPulse</title>
<meta name="description" content="Learn about DocuPulse - the team behind the enterprise document management platform that's transforming how businesses handle their documents.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.about-section {
padding: 80px 0;
}
.mission-card {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 15px 35px var(--shadow-color);
height: 100%;
border: none;
transition: transform 0.3s ease;
}
.mission-card:hover {
transform: translateY(-5px);
}
.mission-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 25px;
color: white;
font-size: 2rem;
}
.team-member {
text-align: center;
margin-bottom: 30px;
}
.team-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
font-size: 3rem;
font-weight: bold;
}
.timeline {
position: relative;
padding: 40px 0;
}
.timeline::before {
content: '';
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateX(-50%);
}
.timeline-item {
position: relative;
margin-bottom: 40px;
}
.timeline-item:nth-child(odd) .timeline-content {
margin-left: 0;
margin-right: 50%;
padding-right: 40px;
text-align: right;
}
.timeline-item:nth-child(even) .timeline-content {
margin-left: 50%;
margin-right: 0;
padding-left: 40px;
text-align: left;
}
.timeline-dot {
position: absolute;
left: 50%;
top: 20px;
width: 20px;
height: 20px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 50%;
transform: translateX(-50%);
z-index: 2;
}
.timeline-content {
background: var(--white);
border-radius: 15px;
padding: 25px;
box-shadow: 0 5px 15px var(--shadow-color);
position: relative;
}
.gradient-text {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.values-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
margin-top: 50px;
justify-items: center;
}
.value-card {
background: var(--white);
border-radius: 15px;
padding: 30px;
text-align: center;
box-shadow: 0 10px 25px var(--shadow-color);
transition: transform 0.3s ease;
border: none;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
.value-card:hover {
transform: translateY(-5px);
}
.value-card h4,
.value-card p {
text-align: center;
width: 100%;
}
.value-icon {
width: 60px;
height: 60px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
font-size: 1.5rem;
}
@media (max-width: 768px) {
.timeline::before {
left: 20px;
}
.timeline-item:nth-child(odd) .timeline-content,
.timeline-item:nth-child(even) .timeline-content {
margin-left: 0;
margin-right: 0;
padding-left: 50px;
padding-right: 20px;
text-align: left;
}
.timeline-dot {
left: 20px;
}
}
/* Button styles to match home page */
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
.btn-lg {
padding: 15px 40px;
font-size: 1.1rem;
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
{% with
title="About DocuPulse",
description="We're building the future of enterprise document management, one secure collaboration at a time.",
title_size="4",
description_size="5"
%}
{% include 'components/hero_section.html' %}
{% endwith %}
<!-- Mission & Vision -->
<section class="about-section">
<div class="container">
<div class="row g-4">
<div class="col-lg-6">
<div class="mission-card">
<div class="mission-icon">
<i class="fas fa-bullseye"></i>
</div>
<h3 class="h4 fw-bold mb-3">Our Mission</h3>
<p class="text-muted">To revolutionize how enterprises manage, collaborate on, and secure their documents by providing an intuitive, scalable, and secure platform that empowers teams to work more efficiently.</p>
<p class="text-muted mb-0">We believe that document management shouldn't be a barrier to productivity, but a catalyst for innovation and growth.</p>
</div>
</div>
<div class="col-lg-6">
<div class="mission-card">
<div class="mission-icon">
<i class="fas fa-eye"></i>
</div>
<h3 class="h4 fw-bold mb-3">Our Vision</h3>
<p class="text-muted">To become the world's most trusted enterprise document management platform, setting the standard for security, collaboration, and user experience in the digital workplace.</p>
<p class="text-muted mb-0">We envision a future where document management is seamless, intelligent, and empowers every organization to achieve their full potential.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Company Stats -->
{% with stats=[
{'value': 500, 'suffix': '+', 'display': '500+', 'label': 'Enterprise Clients'},
{'value': 50, 'suffix': 'K+', 'display': '50K+', 'label': 'Active Users'},
{'value': 99.9, 'suffix': '%', 'display': '99.9%', 'label': 'Uptime'},
{'value': 24, 'suffix': '/7', 'display': '24/7', 'label': 'Support'}
] %}
{% include 'components/animated_numbers.html' %}
{% endwith %}
<!-- Our Values -->
<section class="about-section" style="background-color: var(--bg-color);">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Our Values</h2>
<p class="lead text-muted">The principles that guide everything we do</p>
</div>
<div class="values-grid">
<div class="value-card">
<div class="value-icon">
<i class="fas fa-shield-alt"></i>
</div>
<h4 class="fw-bold mb-3">Security First</h4>
<p class="text-muted">We prioritize the security and privacy of our users' data above all else, implementing enterprise-grade security measures and maintaining strict compliance standards.</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-users"></i>
</div>
<h4 class="fw-bold mb-3">User-Centric Design</h4>
<p class="text-muted">Every feature we build is designed with the user in mind, ensuring intuitive experiences that enhance productivity rather than hinder it.</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-rocket"></i>
</div>
<h4 class="fw-bold mb-3">Innovation</h4>
<p class="text-muted">We continuously push the boundaries of what's possible in document management, embracing new technologies and methodologies.</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-handshake"></i>
</div>
<h4 class="fw-bold mb-3">Trust & Transparency</h4>
<p class="text-muted">We build lasting relationships with our clients through honest communication, transparent practices, and reliable service delivery.</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-cogs"></i>
</div>
<h4 class="fw-bold mb-3">Excellence</h4>
<p class="text-muted">We strive for excellence in everything we do, from code quality to customer support, ensuring the highest standards of service.</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-heart"></i>
</div>
<h4 class="fw-bold mb-3">Passion</h4>
<p class="text-muted">Our team is passionate about solving real-world problems and making a positive impact on how organizations work and collaborate.</p>
</div>
</div>
</div>
</section>
<!-- Our Story -->
<section class="about-section">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Our Story</h2>
<p class="lead text-muted">From startup to enterprise solution</p>
</div>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h4 class="fw-bold">2020 - The Beginning</h4>
<p class="text-muted">Founded by a team of experienced developers and enterprise architects who saw the need for a better document management solution.</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h4 class="fw-bold">2021 - First Release</h4>
<p class="text-muted">Launched our first version with core document management features and room-based collaboration.</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h4 class="fw-bold">2022 - Enterprise Growth</h4>
<p class="text-muted">Expanded to serve enterprise clients with advanced security features and compliance capabilities.</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h4 class="fw-bold">2023 - Global Expansion</h4>
<p class="text-muted">Reached 500+ enterprise clients worldwide and launched advanced AI-powered features.</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h4 class="fw-bold">2024 - Innovation Leader</h4>
<p class="text-muted">Continuing to innovate with cutting-edge features and expanding our global presence.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Leadership Team -->
<section class="about-section" style="background-color: var(--bg-color);">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Leadership Team</h2>
<p class="lead text-muted">Meet the people driving our mission forward</p>
</div>
<div class="row">
<div class="col-lg-4 col-md-6">
<div class="team-member">
<div class="team-avatar">EW</div>
<h4 class="fw-bold">Eric Wyns</h4>
<p class="text-primary fw-semibold">CEO & Co-Founder</p>
<p class="text-muted">Discription here</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="team-member">
<div class="team-avatar">KA</div>
<h4 class="fw-bold">Kobe Amerijckx</h4>
<p class="text-primary fw-semibold">CTO & Co-Founder</p>
<p class="text-muted">Expert in cloud architecture and security with a background in building scalable platforms.</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="team-member">
<div class="team-avatar">KT</div>
<h4 class="fw-bold">Kelly Tordeur</h4>
<p class="text-primary fw-semibold">VP of Product</p>
<p class="text-muted">Product leader with experience in user experience design and enterprise software development.</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="about-section">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8 text-center">
<h2 class="display-5 fw-bold mb-4">Join Us in Shaping the Future</h2>
<p class="lead text-muted mb-5">Ready to experience the next generation of document management? Let's work together to transform how your organization handles documents.</p>
{% with primary_url=url_for('public.contact'), primary_icon='fas fa-envelope', primary_text='Get in Touch', secondary_url=url_for('public.careers'), secondary_icon='fas fa-users', secondary_text='Join Our Team' %}
{% include 'components/cta_buttons.html' %}
{% endwith %}
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,391 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Careers - DocuPulse</title>
<meta name="description" content="Join the DocuPulse team and help us build the future of enterprise document management. Explore open positions and learn about our culture.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.careers-section {
padding: 80px 0;
}
.culture-card {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 15px 35px var(--shadow-color);
height: 100%;
border: none;
transition: transform 0.3s ease;
}
.culture-card:hover {
transform: translateY(-5px);
}
.culture-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 25px;
color: white;
font-size: 2rem;
}
.job-card {
background: var(--white);
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 25px var(--shadow-color);
transition: transform 0.3s ease;
border: none;
cursor: pointer;
margin-bottom: 20px;
}
.job-card:hover {
transform: translateY(-5px);
}
.job-tag {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
display: inline-block;
margin-right: 10px;
margin-bottom: 10px;
}
.application-form {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 15px 35px var(--shadow-color);
}
.form-control {
border-radius: 10px;
border: 2px solid var(--border-color);
padding: 12px 15px;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem var(--primary-opacity-15);
}
.gradient-text {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.team-photo {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 20px;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 4rem;
margin-bottom: 30px;
}
/* Button styles to match home page */
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
.btn-lg {
padding: 15px 40px;
font-size: 1.1rem;
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
{% with
title="Join Our Team",
description="Help us build the future of enterprise document management. We're looking for passionate individuals who want to make a difference.",
title_size="4",
description_size="5"
%}
{% include 'components/hero_section.html' %}
{% endwith %}
<!-- Company Culture -->
<section class="careers-section">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Our Culture</h2>
<p class="lead text-muted">What makes DocuPulse a great place to work</p>
</div>
<div class="row g-4">
<div class="col-lg-4">
<div class="culture-card">
<div class="culture-icon">
<i class="fas fa-users"></i>
</div>
<h3 class="h4 fw-bold mb-3">Collaborative Environment</h3>
<p class="text-muted">We believe in the power of teamwork. Every voice matters, and we encourage open communication and knowledge sharing across all levels.</p>
</div>
</div>
<div class="col-lg-4">
<div class="culture-card">
<div class="culture-icon">
<i class="fas fa-rocket"></i>
</div>
<h3 class="h4 fw-bold mb-3">Innovation First</h3>
<p class="text-muted">We're constantly pushing boundaries and exploring new technologies. If you have an idea, we want to hear it and help you bring it to life.</p>
</div>
</div>
<div class="col-lg-4">
<div class="culture-card">
<div class="culture-icon">
<i class="fas fa-heart"></i>
</div>
<h3 class="h4 fw-bold mb-3">Work-Life Balance</h3>
<p class="text-muted">We understand that great work comes from happy, well-rested people. We offer flexible schedules and encourage taking time to recharge.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Company Stats -->
{% with stats=[
{'value': 15, 'suffix': '+', 'display': '15+', 'label': 'Team Members'},
{'value': 10, 'suffix': '+', 'display': '10+', 'label': 'Countries'},
{'value': 4.8, 'suffix': '', 'display': '4.8', 'label': 'Glassdoor Rating'},
{'value': 95, 'suffix': '%', 'display': '95%', 'label': 'Retention Rate'}
] %}
{% include 'components/animated_numbers.html' %}
{% endwith %}
<!-- Open Positions -->
<section id="positions" class="careers-section">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Open Positions</h2>
<p class="lead text-muted">Find your perfect role at DocuPulse</p>
</div>
<div class="row">
<div class="col-lg-8">
<!-- Engineering Positions -->
<div class="job-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h4 class="fw-bold mb-2">Senior Full Stack Engineer</h4>
<p class="text-muted mb-2">Engineering • Full-time • Remote</p>
</div>
<span class="job-tag">Remote</span>
</div>
<p class="text-muted mb-3">Join our engineering team to build scalable, secure, and user-friendly features for our enterprise document management platform.</p>
<div class="d-flex gap-2">
<span class="job-tag">Python</span>
<span class="job-tag">React</span>
<span class="job-tag">PostgreSQL</span>
<span class="job-tag">AWS</span>
</div>
</div>
<div class="job-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h4 class="fw-bold mb-2">DevOps Engineer</h4>
<p class="text-muted mb-2">Engineering • Full-time • Hybrid</p>
</div>
<span class="job-tag">Hybrid</span>
</div>
<p class="text-muted mb-3">Help us build and maintain our cloud infrastructure, ensuring high availability and security for our enterprise customers.</p>
<div class="d-flex gap-2">
<span class="job-tag">Docker</span>
<span class="job-tag">Kubernetes</span>
<span class="job-tag">AWS</span>
<span class="job-tag">Terraform</span>
</div>
</div>
<div class="job-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h4 class="fw-bold mb-2">Frontend Engineer</h4>
<p class="text-muted mb-2">Engineering • Full-time • Remote</p>
</div>
<span class="job-tag">Remote</span>
</div>
<p class="text-muted mb-3">Create beautiful, intuitive user interfaces that make document management effortless for our users.</p>
<div class="d-flex gap-2">
<span class="job-tag">React</span>
<span class="job-tag">TypeScript</span>
<span class="job-tag">CSS3</span>
<span class="job-tag">Bootstrap</span>
</div>
</div>
<!-- Product Positions -->
<div class="job-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h4 class="fw-bold mb-2">Product Manager</h4>
<p class="text-muted mb-2">Product • Full-time • Hybrid</p>
</div>
<span class="job-tag">Hybrid</span>
</div>
<p class="text-muted mb-3">Drive product strategy and execution, working closely with engineering and design teams to deliver exceptional user experiences.</p>
<div class="d-flex gap-2">
<span class="job-tag">Product Strategy</span>
<span class="job-tag">User Research</span>
<span class="job-tag">Agile</span>
<span class="job-tag">Analytics</span>
</div>
</div>
<!-- Sales Positions -->
<div class="job-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h4 class="fw-bold mb-2">Enterprise Sales Representative</h4>
<p class="text-muted mb-2">Sales • Full-time • Remote</p>
</div>
<span class="job-tag">Remote</span>
</div>
<p class="text-muted mb-3">Help enterprise organizations transform their document management processes with our innovative platform.</p>
<div class="d-flex gap-2">
<span class="job-tag">B2B Sales</span>
<span class="job-tag">Enterprise</span>
<span class="job-tag">SaaS</span>
<span class="job-tag">CRM</span>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="application-form">
<h4 class="fw-bold mb-4">General Application</h4>
<p class="text-muted mb-4">Don't see a perfect fit? Send us your resume and we'll keep you in mind for future opportunities.</p>
<form>
<div class="mb-3">
<label for="name" class="form-label">Full Name *</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email Address *</label>
<input type="email" class="form-control" id="email" required>
</div>
<div class="mb-3">
<label for="position" class="form-label">Position of Interest</label>
<select class="form-control" id="position">
<option value="">Select a position</option>
<option value="engineering">Engineering</option>
<option value="product">Product</option>
<option value="sales">Sales</option>
<option value="marketing">Marketing</option>
<option value="other">Other</option>
</select>
</div>
<div class="mb-3">
<label for="message" class="form-label">Why DocuPulse?</label>
<textarea class="form-control" id="message" rows="4" placeholder="Tell us why you'd like to join our team..."></textarea>
</div>
<div class="mb-3">
<label for="resume" class="form-label">Resume/CV</label>
<input type="file" class="form-control" id="resume" accept=".pdf,.doc,.docx">
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-paper-plane me-2"></i>Submit Application
</button>
</form>
</div>
</div>
</div>
</div>
</section>
<!-- Team Photo Section -->
<section class="careers-section" style="background-color: var(--bg-color);">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<div class="team-photo">
<i class="fas fa-users"></i>
</div>
</div>
<div class="col-lg-6">
<h2 class="display-5 fw-bold mb-4">Meet Our Team</h2>
<p class="lead text-muted mb-4">We're a diverse group of passionate individuals from around the world, united by our mission to revolutionize document management.</p>
<p class="text-muted mb-4">Our team values creativity, collaboration, and continuous learning. We believe that the best solutions come from diverse perspectives and open dialogue.</p>
<a href="{{ url_for('public.about') }}" class="btn btn-outline-primary btn-lg">
<i class="fas fa-users me-2"></i>Learn More About Us
</a>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="careers-section">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8 text-center">
<h2 class="display-5 fw-bold mb-4">Ready to Join Us?</h2>
<p class="lead text-muted mb-5">Take the first step towards an exciting career at DocuPulse. We can't wait to meet you!</p>
<div class="d-flex justify-content-center gap-3 flex-wrap">
<a href="#positions" class="btn btn-primary btn-lg px-5 py-3">
<i class="fas fa-search me-2"></i>View All Positions
</a>
<a href="{{ url_for('public.contact') }}" class="btn btn-outline-primary btn-lg px-5 py-3">
<i class="fas fa-envelope me-2"></i>Get in Touch
</a>
</div>
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,535 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Compliance & Certifications - DocuPulse</title>
<meta name="description" content="Learn about DocuPulse's compliance certifications including SOC 2, ISO 27001, GDPR, and other industry standards.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.legal-section {
padding: 80px 0;
}
.legal-content {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 10px 25px var(--shadow-color);
margin-bottom: 30px;
}
.legal-header {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 80px 0;
position: relative;
overflow: hidden;
}
.legal-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.legal-header .container {
position: relative;
z-index: 1;
}
.section-title {
color: var(--primary-color);
border-bottom: 3px solid var(--primary-color);
padding-bottom: 10px;
margin-bottom: 25px;
}
.info-box {
background: rgba(var(--primary-color-rgb), 0.05);
border-left: 4px solid var(--primary-color);
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.info-box h5 {
color: var(--primary-color);
margin-bottom: 10px;
}
.certification-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 25px;
margin: 30px 0;
}
.certification-card {
background: var(--white);
border: 2px solid var(--border-color);
border-radius: 20px;
padding: 30px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.certification-card:hover {
border-color: var(--primary-color);
transform: translateY(-5px);
box-shadow: 0 15px 35px var(--shadow-color);
}
.certification-card h4 {
color: var(--primary-color);
margin-bottom: 15px;
}
.certification-icon {
width: 60px;
height: 60px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 2rem;
margin-bottom: 20px;
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
margin: 10px 0;
}
.status-certified {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
border: 1px solid #28a745;
}
.status-pending {
background: rgba(255, 193, 7, 0.1);
color: #ffc107;
border: 1px solid #ffc107;
}
.status-in-progress {
background: rgba(0, 123, 255, 0.1);
color: #007bff;
border: 1px solid #007bff;
}
.compliance-table {
background: var(--white);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px var(--shadow-color);
margin: 20px 0;
}
.compliance-table th {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border: none;
padding: 15px;
}
.compliance-table td {
padding: 15px;
border-bottom: 1px solid var(--border-color);
}
.compliance-table tr:last-child td {
border-bottom: none;
}
.contact-info {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 40px;
border-radius: 20px;
text-align: center;
}
.contact-info h3 {
margin-bottom: 20px;
}
.contact-info a {
color: white;
text-decoration: none;
}
.contact-info a:hover {
text-decoration: underline;
}
.last-updated {
background: var(--light-bg);
padding: 15px;
border-radius: 10px;
text-align: center;
margin-top: 30px;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
position: relative;
padding-left: 25px;
}
.feature-list li:before {
content: '✓';
position: absolute;
left: 0;
color: var(--primary-color);
font-weight: bold;
}
.feature-list li:last-child {
border-bottom: none;
}
@media (max-width: 768px) {
.legal-content {
padding: 25px;
}
.certification-grid {
grid-template-columns: 1fr;
}
.compliance-table {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Header Section -->
<section class="legal-header">
<div class="container">
<div class="text-center">
<h1 class="display-4 fw-bold mb-3">Compliance & Certifications</h1>
<p class="lead opacity-75">Meeting the highest standards of security and compliance</p>
<p class="opacity-75">Last updated: December 2024</p>
</div>
</div>
</section>
<!-- Compliance Content -->
<section class="legal-section">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="legal-content">
<h2 class="section-title">1. Our Compliance Commitment</h2>
<p>At DocuPulse, we understand that compliance is not just about meeting regulatory requirements—it's about building trust with our customers and ensuring the highest standards of security and data protection. We maintain rigorous compliance programs and regularly undergo third-party audits to validate our security practices.</p>
<div class="info-box">
<h5><i class="fas fa-certificate me-2"></i>Certification Status</h5>
<p class="mb-0">All our certifications are current and regularly audited. We provide compliance reports and documentation to enterprise customers upon request.</p>
</div>
<h2 class="section-title">2. Security Certifications</h2>
<div class="certification-grid">
<div class="certification-card">
<div class="certification-icon">
<i class="fas fa-shield-alt"></i>
</div>
<h4>SOC 2 Type II</h4>
<span class="status-badge status-certified">Certified</span>
<p>Service Organization Control 2 Type II certification demonstrates our commitment to security, availability, processing integrity, confidentiality, and privacy.</p>
<ul class="feature-list">
<li>Annual third-party audits</li>
<li>Security controls validation</li>
<li>Availability monitoring</li>
<li>Data protection measures</li>
</ul>
<p><strong>Last Audit:</strong> December 2024</p>
<p><strong>Next Audit:</strong> December 2025</p>
</div>
<div class="certification-card">
<div class="certification-icon">
<i class="fas fa-lock"></i>
</div>
<h4>ISO 27001</h4>
<span class="status-badge status-certified">Certified</span>
<p>International standard for information security management systems, ensuring systematic approach to managing sensitive company information.</p>
<ul class="feature-list">
<li>Information security framework</li>
<li>Risk management processes</li>
<li>Security controls implementation</li>
<li>Continuous improvement</li>
</ul>
<p><strong>Certification Date:</strong> March 2024</p>
<p><strong>Valid Until:</strong> March 2027</p>
</div>
<div class="certification-card">
<div class="certification-icon">
<i class="fas fa-cloud"></i>
</div>
<h4>Cloud Security Alliance</h4>
<span class="status-badge status-certified">Certified</span>
<p>CSA STAR certification demonstrates our compliance with cloud security best practices and industry standards.</p>
<ul class="feature-list">
<li>Cloud security controls</li>
<li>Data protection standards</li>
<li>Transparency reporting</li>
<li>Security assessment</li>
</ul>
<p><strong>Certification Date:</strong> June 2024</p>
<p><strong>Valid Until:</strong> June 2025</p>
</div>
</div>
<h2 class="section-title">3. Privacy & Data Protection</h2>
<div class="certification-grid">
<div class="certification-card">
<div class="certification-icon">
<i class="fas fa-user-shield"></i>
</div>
<h4>GDPR Compliance</h4>
<span class="status-badge status-certified">Compliant</span>
<p>Full compliance with the General Data Protection Regulation, ensuring the protection of EU residents' personal data.</p>
<ul class="feature-list">
<li>Data subject rights</li>
<li>Privacy by design</li>
<li>Data protection impact assessments</li>
<li>Breach notification procedures</li>
</ul>
<p><strong>Compliance Date:</strong> May 2018</p>
<p><strong>Status:</strong> Continuously monitored</p>
</div>
<div class="certification-card">
<div class="certification-icon">
<i class="fas fa-california"></i>
</div>
<h4>CCPA/CPRA</h4>
<span class="status-badge status-certified">Compliant</span>
<p>California Consumer Privacy Act and California Privacy Rights Act compliance for California residents.</p>
<ul class="feature-list">
<li>Consumer rights management</li>
<li>Data disclosure requirements</li>
<li>Opt-out mechanisms</li>
<li>Privacy notices</li>
</ul>
<p><strong>Compliance Date:</strong> January 2020</p>
<p><strong>Status:</strong> Continuously monitored</p>
</div>
<div class="certification-card">
<div class="certification-icon">
<i class="fas fa-globe"></i>
</div>
<h4>International Standards</h4>
<span class="status-badge status-certified">Compliant</span>
<p>Compliance with various international privacy and data protection regulations.</p>
<ul class="feature-list">
<li>LGPD (Brazil)</li>
<li>PIPEDA (Canada)</li>
<li>POPIA (South Africa)</li>
<li>APEC Privacy Framework</li>
</ul>
<p><strong>Status:</strong> Continuously monitored</p>
<p><strong>Updates:</strong> As regulations evolve</p>
</div>
</div>
<h2 class="section-title">4. Industry-Specific Compliance</h2>
<div class="compliance-table">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Standard</th>
<th>Description</th>
<th>Status</th>
<th>Last Review</th>
</tr>
</thead>
<tbody>
<tr>
<td>HIPAA</td>
<td>Health Insurance Portability and Accountability Act</td>
<td><span class="status-badge status-certified">Compliant</span></td>
<td>November 2024</td>
</tr>
<tr>
<td>SOX</td>
<td>Sarbanes-Oxley Act for financial reporting</td>
<td><span class="status-badge status-certified">Compliant</span></td>
<td>October 2024</td>
</tr>
<tr>
<td>PCI DSS</td>
<td>Payment Card Industry Data Security Standard</td>
<td><span class="status-badge status-certified">Compliant</span></td>
<td>September 2024</td>
</tr>
<tr>
<td>FedRAMP</td>
<td>Federal Risk and Authorization Management Program</td>
<td><span class="status-badge status-in-progress">In Progress</span></td>
<td>Q1 2025</td>
</tr>
<tr>
<td>NIST</td>
<td>National Institute of Standards and Technology</td>
<td><span class="status-badge status-certified">Compliant</span></td>
<td>August 2024</td>
</tr>
</tbody>
</table>
</div>
<h2 class="section-title">5. Security Controls & Measures</h2>
<p>Our comprehensive security program includes the following controls and measures:</p>
<div class="row">
<div class="col-md-6">
<h4>Technical Controls</h4>
<ul>
<li>Multi-factor authentication (MFA)</li>
<li>End-to-end encryption (AES-256)</li>
<li>Network security and firewalls</li>
<li>Intrusion detection and prevention</li>
<li>Vulnerability management</li>
<li>Security monitoring and alerting</li>
</ul>
</div>
<div class="col-md-6">
<h4>Organizational Controls</h4>
<ul>
<li>Security policies and procedures</li>
<li>Employee security training</li>
<li>Background checks and screening</li>
<li>Incident response procedures</li>
<li>Business continuity planning</li>
<li>Regular security assessments</li>
</ul>
</div>
</div>
<h2 class="section-title">6. Audit & Assessment Schedule</h2>
<p>We maintain a regular schedule of internal and external audits to ensure ongoing compliance:</p>
<div class="compliance-table">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Audit Type</th>
<th>Frequency</th>
<th>Next Due</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>SOC 2 Type II</td>
<td>Annual</td>
<td>December 2025</td>
<td><span class="status-badge status-certified">Scheduled</span></td>
</tr>
<tr>
<td>ISO 27001</td>
<td>Annual Surveillance</td>
<td>March 2025</td>
<td><span class="status-badge status-certified">Scheduled</span></td>
</tr>
<tr>
<td>Penetration Testing</td>
<td>Quarterly</td>
<td>March 2025</td>
<td><span class="status-badge status-certified">Scheduled</span></td>
</tr>
<tr>
<td>Vulnerability Assessment</td>
<td>Monthly</td>
<td>January 2025</td>
<td><span class="status-badge status-certified">Ongoing</span></td>
</tr>
<tr>
<td>Security Training</td>
<td>Quarterly</td>
<td>March 2025</td>
<td><span class="status-badge status-certified">Scheduled</span></td>
</tr>
</tbody>
</table>
</div>
<h2 class="section-title">7. Compliance Documentation</h2>
<p>We provide comprehensive compliance documentation to our enterprise customers:</p>
<ul>
<li><strong>SOC 2 Type II Reports:</strong> Available to enterprise customers under NDA</li>
<li><strong>ISO 27001 Certificates:</strong> Available upon request</li>
<li><strong>Security Questionnaires:</strong> Standardized responses for common frameworks</li>
<li><strong>Compliance Whitepapers:</strong> Detailed documentation of our controls</li>
<li><strong>Audit Reports:</strong> Third-party assessment results</li>
</ul>
<div class="info-box">
<h5><i class="fas fa-file-contract me-2"></i>Documentation Requests</h5>
<p class="mb-0">Enterprise customers can request compliance documentation by contacting our compliance team at compliance@docupulse.com. We typically respond within 2-3 business days.</p>
</div>
<h2 class="section-title">8. Continuous Improvement</h2>
<p>Our compliance program is continuously evolving to meet new requirements and best practices:</p>
<ul>
<li>Regular review and updates of security policies</li>
<li>Monitoring of emerging threats and vulnerabilities</li>
<li>Adoption of new security technologies and practices</li>
<li>Participation in industry working groups and forums</li>
<li>Regular training and awareness programs for staff</li>
</ul>
<div class="contact-info">
<h3><i class="fas fa-certificate me-2"></i>Compliance Team</h3>
<p>For compliance-related questions or documentation requests, contact our compliance team:</p>
<p><strong>Email:</strong> <a href="mailto:compliance@docupulse.com">compliance@docupulse.com</a></p>
<p><strong>Address:</strong> DocuPulse Inc., 123 Business Ave, Suite 100, City, State 12345</p>
<p><strong>Phone:</strong> <a href="tel:+1-555-123-4567">+1 (555) 123-4567</a></p>
</div>
<div class="last-updated">
<p class="mb-0"><strong>Last Updated:</strong> December 2024</p>
</div>
</div>
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -46,16 +46,40 @@
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem var(--primary-opacity-15);
}
/* Button styles to match other pages */
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 10px;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-light) 0%, var(--secondary-light) 100%);
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
.btn-lg {
padding: 15px 40px;
font-size: 1.1rem;
}
</style>
</head>
@@ -63,12 +87,14 @@
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
<section class="hero-section">
<div class="container text-center">
<h1 class="display-4 fw-bold mb-4">Get in Touch</h1>
<p class="lead fs-5">We're here to help with your enterprise document management needs</p>
</div>
</section>
{% with
title="Get in Touch",
description="We're here to help with your enterprise document management needs",
title_size="4",
description_size="5"
%}
{% include 'components/hero_section.html' %}
{% endwith %}
<!-- Contact Information -->
<section class="contact-section">

View File

View File

@@ -9,22 +9,48 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.feature-section {
padding: 80px 0;
/* Enhanced Features Page Styles */
.hero-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 120px 0 80px 0;
position: relative;
overflow: hidden;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.feature-section {
padding: 100px 0;
position: relative;
}
.feature-section:nth-child(even) {
background: linear-gradient(135deg, var(--bg-color) 0%, rgba(var(--primary-color-rgb), 0.02) 100%);
}
.feature-card {
background: var(--white);
border-radius: 20px;
padding: 40px 30px;
box-shadow: 0 10px 30px var(--shadow-color);
padding: 40px;
box-shadow: 0 15px 35px var(--shadow-color);
transition: all 0.4s ease;
height: 100%;
border: none;
position: relative;
overflow: hidden;
transform: translateY(0);
}
.feature-card::before {
content: '';
position: absolute;
@@ -36,27 +62,35 @@
transform: scaleX(0);
transition: transform 0.3s ease;
}
.feature-card:hover::before {
transform: scaleX(1);
}
.feature-card:hover {
transform: translateY(-10px);
box-shadow: 0 20px 40px var(--shadow-color-light);
box-shadow: 0 25px 50px var(--shadow-color-light);
}
.feature-icon {
width: 90px;
height: 90px;
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 50%;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 30px;
margin-bottom: 25px;
color: white;
font-size: 2.2rem;
font-size: 2rem;
position: relative;
z-index: 2;
transition: all 0.3s ease;
}
.feature-card:hover .feature-icon {
transform: scale(1.1) rotate(5deg);
}
.feature-icon::after {
content: '';
position: absolute;
@@ -65,87 +99,27 @@
right: -5px;
bottom: -5px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 50%;
border-radius: 25px;
z-index: -1;
opacity: 0.3;
opacity: 0.2;
transition: all 0.3s ease;
}
.feature-card:hover .feature-icon::after {
transform: scale(1.2);
opacity: 0.5;
}
.hero-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 120px 0 80px 0;
position: relative;
overflow: hidden;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.section-title {
color: var(--text-dark);
font-weight: 700;
margin-bottom: 1rem;
position: relative;
}
.section-title::after {
content: '';
position: absolute;
bottom: -10px;
left: 0;
width: 60px;
height: 3px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 2px;
}
.section-subtitle {
color: var(--text-muted);
font-size: 1.1rem;
margin-bottom: 3rem;
}
.visual-guide-placeholder {
border-radius: 20px;
padding: 40px;
text-align: center;
min-height: 350px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
box-shadow: 0 15px 35px var(--shadow-color);
}
.visual-guide-placeholder::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%);
transform: translateX(-100%);
transition: transform 0.6s ease;
}
.visual-guide-placeholder:hover::before {
transform: translateX(100%);
}
.feature-highlight {
.feature-showcase {
background: linear-gradient(135deg, var(--primary-bg-light) 0%, var(--secondary-bg-light) 100%);
border-radius: 25px;
padding: 60px 40px;
position: relative;
overflow: hidden;
box-shadow: 0 20px 40px var(--shadow-color);
}
.feature-highlight::before {
.feature-showcase::before {
content: '';
position: absolute;
top: -50%;
@@ -155,66 +129,253 @@
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(180deg); }
}
.staggered-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 50px;
}
.staggered-item:nth-child(odd) {
transform: translateY(30px);
}
.floating-elements {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
overflow: hidden;
}
.floating-element {
position: absolute;
width: 60px;
height: 60px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 50%;
opacity: 0.1;
animation: float-around 8s ease-in-out infinite;
}
.floating-element:nth-child(1) { top: 20%; left: 10%; animation-delay: 0s; }
.floating-element:nth-child(2) { top: 60%; right: 15%; animation-delay: 2s; }
.floating-element:nth-child(3) { bottom: 30%; left: 20%; animation-delay: 4s; }
@keyframes float-around {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
25% { transform: translate(20px, -20px) rotate(90deg); }
50% { transform: translate(-10px, -40px) rotate(180deg); }
75% { transform: translate(-30px, -10px) rotate(270deg); }
}
.gradient-text {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.feature-badge {
.section-title {
color: var(--text-dark);
font-weight: 700;
margin-bottom: 1rem;
position: relative;
}
.section-subtitle {
color: var(--text-muted);
font-size: 1.1rem;
margin-bottom: 3rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 50px;
}
.feature-highlight {
background: linear-gradient(135deg, var(--white) 0%, rgba(var(--primary-color-rgb), 0.02) 100%);
border-radius: 20px;
padding: 40px;
border: 2px solid transparent;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.feature-highlight::before {
content: '';
position: absolute;
top: -15px;
right: 20px;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 0;
}
.feature-highlight:hover::before {
opacity: 0.05;
}
.feature-highlight > * {
position: relative;
z-index: 1;
}
.feature-highlight:hover {
border-color: var(--primary-color);
transform: translateY(-5px);
box-shadow: 0 15px 35px var(--shadow-color);
}
.interactive-demo {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 15px 35px var(--shadow-color);
position: relative;
overflow: hidden;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.interactive-demo::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 30%, rgba(var(--primary-color-rgb), 0.05) 50%, transparent 70%);
transform: translateX(-100%);
transition: transform 0.6s ease;
}
.interactive-demo:hover::before {
transform: translateX(100%);
}
.demo-placeholder {
text-align: center;
color: var(--text-muted);
}
.demo-placeholder i {
font-size: 4rem;
margin-bottom: 20px;
opacity: 0.5;
}
.stats-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
transform: rotate(3deg);
padding: 80px 0;
position: relative;
overflow: hidden;
}
.additional-features .section-title::after {
display: none;
.stats-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.stat-item {
text-align: center;
position: relative;
z-index: 1;
}
.stat-number {
font-size: 3rem;
font-weight: 700;
margin-bottom: 10px;
display: block;
}
.stat-label {
font-size: 1.1rem;
opacity: 0.9;
}
.btn-outline-light {
border: 2px solid rgba(255, 255, 255, 0.8);
color: white;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-light:hover {
background: rgba(255, 255, 255, 0.1);
border-color: white;
transform: translateY(-2px);
}
.btn-lg {
padding: 15px 40px;
font-size: 1.1rem;
}
/* Unified button styling */
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
color: white;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #5a6268 0%, #495057 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(108, 117, 125, 0.3);
color: white;
}
/* Animation classes */
.fade-in-up {
opacity: 0;
transform: translateY(30px);
transition: all 0.6s ease;
}
.fade-in-up.visible {
opacity: 1;
transform: translateY(0);
}
.stagger-delay-1 { transition-delay: 0.1s; }
.stagger-delay-2 { transition-delay: 0.2s; }
.stagger-delay-3 { transition-delay: 0.3s; }
.stagger-delay-4 { transition-delay: 0.4s; }
@media (max-width: 768px) {
.hero-section {
padding: 100px 0 60px 0;
}
.feature-section {
padding: 60px 0;
}
.feature-showcase {
padding: 40px 20px;
}
.stat-number {
font-size: 2.5rem;
}
}
</style>
</head>
@@ -222,201 +383,171 @@
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
<section class="hero-section">
<div class="floating-elements">
<div class="floating-element"></div>
<div class="floating-element"></div>
<div class="floating-element"></div>
</div>
<div class="container text-center position-relative">
<h1 class="display-4 fw-bold mb-4">Powerful Features for Modern Enterprises</h1>
<p class="lead fs-5">Discover how DocuPulse transforms document management with intelligent workflows, secure collaboration, and scalable architecture.</p>
</div>
</section>
{% with
title="Powerful Features for Modern Enterprises",
description="Discover how DocuPulse transforms document management with intelligent workflows, secure collaboration, and scalable architecture.",
title_size="4",
description_size="5",
buttons=[
{
'type': 'modal',
'target': '#explainerVideoModal',
'text': 'Watch Demo',
'icon': 'fas fa-play',
'style': 'outline-light'
}
]
%}
{% include 'components/hero_section.html' %}
{% endwith %}
<!-- Room-Based Workspaces Feature -->
<!-- Key Features Grid -->
<section class="feature-section">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<div class="feature-highlight">
<h2 class="section-title gradient-text">Room-Based Workspaces</h2>
<p class="section-subtitle">Create isolated collaboration environments with granular permissions, file storage, and real-time messaging for teams and projects.</p>
<ul class="list-unstyled">
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Isolated team spaces for each project</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Granular permissions and access controls</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Integrated file storage and messaging</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Real-time collaboration tools</li>
</ul>
</div>
</div>
<div class="col-lg-6">
<div class="visual-guide-placeholder" style="background: var(--bg-color);">
<div>
<i class="fas fa-image fa-4x text-muted mb-3"></i>
<p class="text-muted fw-bold">Visual Guide: Room Workspace Interface</p>
<small class="text-muted">Screenshot or demo of room creation and management</small>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Client Document Requests Feature -->
<section class="feature-section" style="background-color: var(--bg-color);">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6 order-lg-2">
<div class="feature-highlight">
<h2 class="section-title gradient-text">Client Document Requests</h2>
<p class="section-subtitle">Streamlined communication system for requesting documents from clients with file attachments and conversation tracking.</p>
<ul class="list-unstyled">
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Document request workflows</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Conversation tracking and history</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>File attachments and sharing</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Status tracking and notifications</li>
</ul>
</div>
</div>
<div class="col-lg-6 order-lg-1">
<div class="visual-guide-placeholder" style="background: var(--white);">
<div>
<i class="fas fa-image fa-4x text-muted mb-3"></i>
<p class="text-muted fw-bold">Visual Guide: Document Request Flow</p>
<small class="text-muted">Workflow diagram or interface screenshot</small>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Advanced File Management Feature -->
<section class="feature-section">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<div class="feature-highlight">
<h2 class="section-title gradient-text">Advanced File Management</h2>
<p class="section-subtitle">Upload, organize, search, and manage files with hierarchical folders and comprehensive metadata tracking.</p>
<ul class="list-unstyled">
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Hierarchical folder structure</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Advanced search and filtering</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Metadata tracking and organization</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>File versioning and history</li>
</ul>
</div>
</div>
<div class="col-lg-6">
<div class="visual-guide-placeholder" style="background: var(--bg-color);">
<div>
<i class="fas fa-image fa-4x text-muted mb-3"></i>
<p class="text-muted fw-bold">Visual Guide: File Management Interface</p>
<small class="text-muted">File browser and organization interface</small>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Security & Compliance Feature -->
<section class="feature-section" style="background-color: var(--bg-color);">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6 order-lg-2">
<div class="feature-highlight">
<h2 class="section-title gradient-text">Security & Compliance</h2>
<p class="section-subtitle">Enterprise-grade security with comprehensive compliance features for data protection and regulatory requirements.</p>
<ul class="list-unstyled">
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Role-based access controls</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>End-to-end encryption</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>GDPR and HIPAA compliance</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Comprehensive audit logging</li>
</ul>
</div>
</div>
<div class="col-lg-6 order-lg-1">
<div class="visual-guide-placeholder" style="background: var(--white);">
<div>
<i class="fas fa-image fa-4x text-muted mb-3"></i>
<p class="text-muted fw-bold">Visual Guide: Security Dashboard</p>
<small class="text-muted">Security settings and compliance reports</small>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- White Labeling Feature -->
<section class="feature-section">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<div class="feature-highlight">
<h2 class="section-title gradient-text">White Labeling</h2>
<p class="section-subtitle">Complete brand customization with custom logos, company colors, and personalized branding throughout the platform.</p>
<ul class="list-unstyled">
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Custom company logo upload</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Brand color customization</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Personalized email templates</li>
<li class="mb-3"><i class="fas fa-check text-success me-2"></i>Complete platform rebranding</li>
</ul>
</div>
</div>
<div class="col-lg-6">
<div class="visual-guide-placeholder" style="background: var(--bg-color);">
<div>
<i class="fas fa-image fa-4x text-muted mb-3"></i>
<p class="text-muted fw-bold">Visual Guide: Brand Customization</p>
<small class="text-muted">Logo upload and color customization interface</small>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Additional Features Grid -->
<section class="feature-section additional-features" style="background-color: var(--bg-color);">
<div class="container">
<div class="text-center mb-5">
<h2 class="section-title gradient-text" style="position: relative;">Additional Features</h2>
<p class="section-subtitle">More powerful tools to enhance your document management workflow</p>
<h2 class="section-title gradient-text">Core Features</h2>
<p class="section-subtitle">Everything you need to revolutionize your document management workflow</p>
</div>
<div class="feature-grid">
<div class="feature-card fade-in-up">
<div class="feature-icon">
<i class="fas fa-door-open"></i>
</div>
<h3 class="h4 fw-bold mb-3">Room-Based Workspaces</h3>
<p class="text-muted mb-4">Create isolated collaboration environments with granular permissions, file storage, and real-time messaging for teams and projects.</p>
<ul class="list-unstyled">
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Isolated team spaces</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Granular permissions</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Real-time messaging</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Integrated file storage</li>
</ul>
</div>
<div class="feature-card fade-in-up stagger-delay-1">
<div class="feature-icon">
<i class="fas fa-comments"></i>
</div>
<h3 class="h4 fw-bold mb-3">Client Document Requests</h3>
<p class="text-muted mb-4">Streamlined communication system for requesting documents from clients with file attachments and conversation tracking.</p>
<ul class="list-unstyled">
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Document workflows</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Conversation tracking</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>File attachments</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Status notifications</li>
</ul>
</div>
<div class="feature-card fade-in-up stagger-delay-2">
<div class="feature-icon">
<i class="fas fa-folder-tree"></i>
</div>
<h3 class="h4 fw-bold mb-3">Advanced File Management</h3>
<p class="text-muted mb-4">Upload, organize, search, and manage files with hierarchical folders and comprehensive metadata tracking.</p>
<ul class="list-unstyled">
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Hierarchical folders</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Advanced search</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>Metadata tracking</li>
<li class="mb-2"><i class="fas fa-check text-success me-2"></i>File versioning</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Interactive Demo Section -->
<section class="feature-section" style="background-color: var(--bg-color);">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<div class="feature-showcase">
<h2 class="section-title gradient-text">See It In Action</h2>
<p class="section-subtitle">Experience the intuitive interface and powerful features that make DocuPulse the choice for modern enterprises.</p>
<div class="row g-3">
<div class="col-6">
<div class="text-center p-3">
<i class="fas fa-users fa-2x mb-2" style="color: var(--primary-color);"></i>
<h6 class="fw-bold">Team Collaboration</h6>
<small class="text-muted">Real-time messaging and file sharing</small>
</div>
</div>
<div class="col-6">
<div class="text-center p-3">
<i class="fas fa-shield-alt fa-2x mb-2" style="color: var(--primary-color);"></i>
<h6 class="fw-bold">Enterprise Security</h6>
<small class="text-muted">Role-based access controls</small>
</div>
</div>
<div class="col-6">
<div class="text-center p-3">
<i class="fas fa-search fa-2x mb-2" style="color: var(--primary-color);"></i>
<h6 class="fw-bold">Smart Search</h6>
<small class="text-muted">Find files instantly</small>
</div>
</div>
<div class="col-6">
<div class="text-center p-3">
<i class="fas fa-bell fa-2x mb-2" style="color: var(--primary-color);"></i>
<h6 class="fw-bold">Notifications</h6>
<small class="text-muted">Stay updated in real-time</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="interactive-demo">
<div class="demo-placeholder">
<i class="fas fa-laptop"></i>
<h5 class="fw-bold">Interactive Demo</h5>
<p>Experience the platform firsthand</p>
<button class="btn btn-primary btn-lg px-4 py-3">
<i class="fas fa-play me-2"></i>Launch Demo
</button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Advanced Features -->
<section class="feature-section">
<div class="container">
<div class="text-center mb-5">
<h2 class="section-title gradient-text">Advanced Capabilities</h2>
<p class="section-subtitle">Enterprise-grade features for complex document management needs</p>
</div>
<div class="row g-4">
<div class="col-lg-4 col-md-6">
<div class="feature-card">
<div class="col-lg-4">
<div class="feature-highlight fade-in-up">
<div class="feature-icon">
<i class="fas fa-users"></i>
<i class="fas fa-user-shield"></i>
</div>
<h3 class="h5 fw-bold mb-3">Team Management</h3>
<p class="text-muted">Efficient team management with member roles, permissions, and collaborative workflows for seamless project execution.</p>
<h3 class="h5 fw-bold mb-3">Security & Compliance</h3>
<p class="text-muted">Enterprise-grade security with comprehensive compliance features for data protection and regulatory requirements.</p>
<ul class="list-unstyled mt-3">
<li><i class="fas fa-check text-success me-2"></i>Member roles</li>
<li><i class="fas fa-check text-success me-2"></i>Collaborative workflows</li>
<li><i class="fas fa-check text-success me-2"></i>Project execution</li>
<li><i class="fas fa-check text-success me-2"></i>Role-based access controls</li>
<li><i class="fas fa-check text-success me-2"></i>End-to-end encryption</li>
<li><i class="fas fa-check text-success me-2"></i>GDPR and HIPAA compliance</li>
</ul>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="feature-card">
<div class="col-lg-4">
<div class="feature-highlight fade-in-up stagger-delay-1">
<div class="feature-icon">
<i class="fas fa-star"></i>
<i class="fas fa-palette"></i>
</div>
<h3 class="h5 fw-bold mb-3">File Organization</h3>
<p class="text-muted">Advanced file organization with starring, tagging, and intelligent search capabilities for quick access to important documents.</p>
<h3 class="h5 fw-bold mb-3">White Labeling</h3>
<p class="text-muted">Complete brand customization with custom logos, company colors, and personalized branding throughout the platform.</p>
<ul class="list-unstyled mt-3">
<li><i class="fas fa-check text-success me-2"></i>File starring</li>
<li><i class="fas fa-check text-success me-2"></i>Intelligent search</li>
<li><i class="fas fa-check text-success me-2"></i>Quick access</li>
<li><i class="fas fa-check text-success me-2"></i>Custom company logo</li>
<li><i class="fas fa-check text-success me-2"></i>Brand color customization</li>
<li><i class="fas fa-check text-success me-2"></i>Personalized templates</li>
</ul>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="feature-card">
<div class="col-lg-4">
<div class="feature-highlight fade-in-up stagger-delay-2">
<div class="feature-icon">
<i class="fas fa-bell"></i>
</div>
@@ -433,6 +564,16 @@
</div>
</section>
<!-- Stats Section -->
{% with stats=[
{'value': 99.9, 'suffix': '%', 'display': '99.9%', 'label': 'Uptime'},
{'value': 256, 'suffix': '-bit', 'display': '256-bit', 'label': 'Encryption'},
{'value': 24, 'suffix': '/7', 'display': '24/7', 'label': 'Support'},
{'value': 1000, 'suffix': '+', 'display': '1000+', 'label': 'Organizations'}
] %}
{% include 'components/animated_numbers.html' %}
{% endwith %}
<!-- CTA Section -->
<section class="py-5">
<div class="container text-center">
@@ -446,6 +587,48 @@
{% include 'components/footer_nav.html' %}
<!-- Explainer Video Modal -->
{% with
modal_title='DocuPulse Features Overview',
video_title='Features Demo Video',
video_placeholder='Video placeholder - Replace with actual features demo video',
learning_title='What you\'ll see:',
learning_points=[
'Room-based workspace creation and management',
'Document request workflows and client communication',
'Advanced file management and organization',
'Security features and permission controls',
'White labeling and brand customization'
],
cta_url=url_for('public.pricing'),
cta_text='Get Started Now'
%}
{% include 'components/explainer_video_modal.html' %}
{% endwith %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Intersection Observer for animations
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, observerOptions);
// Observe elements for fade-in animation
document.addEventListener('DOMContentLoaded', function() {
const fadeElements = document.querySelectorAll('.fade-in-up');
fadeElements.forEach(el => {
observer.observe(el);
});
});
</script>
</body>
</html>

464
templates/public/gdpr.html Normal file
View File

@@ -0,0 +1,464 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GDPR Compliance - DocuPulse</title>
<meta name="description" content="Learn about DocuPulse's GDPR compliance measures and how we protect your data rights under European data protection regulations.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.legal-section {
padding: 80px 0;
}
.legal-content {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 10px 25px var(--shadow-color);
margin-bottom: 30px;
}
.legal-header {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 80px 0;
position: relative;
overflow: hidden;
}
.legal-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.legal-header .container {
position: relative;
z-index: 1;
}
.section-title {
color: var(--primary-color);
border-bottom: 3px solid var(--primary-color);
padding-bottom: 10px;
margin-bottom: 25px;
}
.info-box {
background: rgba(var(--primary-color-rgb), 0.05);
border-left: 4px solid var(--primary-color);
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.info-box h5 {
color: var(--primary-color);
margin-bottom: 10px;
}
.rights-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 30px 0;
}
.right-card {
background: var(--white);
border: 2px solid var(--border-color);
border-radius: 15px;
padding: 25px;
transition: all 0.3s ease;
}
.right-card:hover {
border-color: var(--primary-color);
transform: translateY(-5px);
box-shadow: 0 10px 25px var(--shadow-color);
}
.right-card h4 {
color: var(--primary-color);
margin-bottom: 15px;
}
.right-icon {
width: 50px;
height: 50px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
margin-bottom: 15px;
}
.compliance-table {
background: var(--white);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px var(--shadow-color);
margin: 20px 0;
}
.compliance-table th {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border: none;
padding: 15px;
}
.compliance-table td {
padding: 15px;
border-bottom: 1px solid var(--border-color);
}
.compliance-table tr:last-child td {
border-bottom: none;
}
.contact-info {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 40px;
border-radius: 20px;
text-align: center;
}
.contact-info h3 {
margin-bottom: 20px;
}
.contact-info a {
color: white;
text-decoration: none;
}
.contact-info a:hover {
text-decoration: underline;
}
.last-updated {
background: var(--light-bg);
padding: 15px;
border-radius: 10px;
text-align: center;
margin-top: 30px;
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
margin: 5px;
}
.status-compliant {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
border: 1px solid #28a745;
}
.status-pending {
background: rgba(255, 193, 7, 0.1);
color: #ffc107;
border: 1px solid #ffc107;
}
@media (max-width: 768px) {
.legal-content {
padding: 25px;
}
.rights-grid {
grid-template-columns: 1fr;
}
.compliance-table {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Header Section -->
<section class="legal-header">
<div class="container">
<div class="text-center">
<h1 class="display-4 fw-bold mb-3">GDPR Compliance</h1>
<p class="lead opacity-75">Your data rights under European law</p>
<p class="opacity-75">Last updated: December 2024</p>
</div>
</div>
</section>
<!-- GDPR Content -->
<section class="legal-section">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="legal-content">
<h2 class="section-title">1. GDPR Overview</h2>
<p>The General Data Protection Regulation (GDPR) is a comprehensive data protection law that applies to organizations processing personal data of individuals in the European Union (EU) and European Economic Area (EEA). DocuPulse is committed to full compliance with GDPR requirements.</p>
<div class="info-box">
<h5><i class="fas fa-shield-alt me-2"></i>Our Commitment</h5>
<p class="mb-0">We are fully committed to protecting your privacy and ensuring compliance with GDPR. Our data processing activities are designed with privacy by design and default principles.</p>
</div>
<h2 class="section-title">2. Legal Basis for Processing</h2>
<p>Under GDPR, we process your personal data based on the following legal grounds:</p>
<div class="compliance-table">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Processing Purpose</th>
<th>Legal Basis</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Service Provision</td>
<td>Contract Performance</td>
<td>Processing necessary to provide our services</td>
</tr>
<tr>
<td>Account Management</td>
<td>Contract Performance</td>
<td>Managing your account and billing</td>
</tr>
<tr>
<td>Customer Support</td>
<td>Legitimate Interest</td>
<td>Providing support and improving service</td>
</tr>
<tr>
<td>Security & Fraud Prevention</td>
<td>Legitimate Interest</td>
<td>Protecting our systems and users</td>
</tr>
<tr>
<td>Marketing Communications</td>
<td>Consent</td>
<td>Only with your explicit consent</td>
</tr>
<tr>
<td>Legal Compliance</td>
<td>Legal Obligation</td>
<td>Complying with applicable laws</td>
</tr>
</tbody>
</table>
</div>
<h2 class="section-title">3. Your Data Subject Rights</h2>
<p>Under GDPR, you have the following rights regarding your personal data:</p>
<div class="rights-grid">
<div class="right-card">
<div class="right-icon">
<i class="fas fa-eye"></i>
</div>
<h4>Right of Access</h4>
<p>You have the right to request access to your personal data and information about how we process it.</p>
</div>
<div class="right-card">
<div class="right-icon">
<i class="fas fa-edit"></i>
</div>
<h4>Right to Rectification</h4>
<p>You can request correction of inaccurate or incomplete personal data we hold about you.</p>
</div>
<div class="right-card">
<div class="right-icon">
<i class="fas fa-trash"></i>
</div>
<h4>Right to Erasure</h4>
<p>You can request deletion of your personal data in certain circumstances (the "right to be forgotten").</p>
</div>
<div class="right-card">
<div class="right-icon">
<i class="fas fa-pause"></i>
</div>
<h4>Right to Restriction</h4>
<p>You can request that we limit how we process your personal data in certain situations.</p>
</div>
<div class="right-card">
<div class="right-icon">
<i class="fas fa-download"></i>
</div>
<h4>Right to Portability</h4>
<p>You can request a copy of your personal data in a structured, machine-readable format.</p>
</div>
<div class="right-card">
<div class="right-icon">
<i class="fas fa-ban"></i>
</div>
<h4>Right to Object</h4>
<p>You can object to processing of your personal data based on legitimate interests.</p>
</div>
</div>
<h2 class="section-title">4. How to Exercise Your Rights</h2>
<p>To exercise any of your GDPR rights, you can:</p>
<ul>
<li><strong>Use our self-service tools:</strong> Access and manage your data through your account settings</li>
<li><strong>Contact our Data Protection Officer:</strong> Email us at dpo@docupulse.com</li>
<li><strong>Submit a formal request:</strong> Use our data request form</li>
<li><strong>Contact us directly:</strong> Reach out to our support team</li>
</ul>
<div class="info-box">
<h5><i class="fas fa-clock me-2"></i>Response Time</h5>
<p class="mb-0">We will respond to your requests within 30 days. In complex cases, we may extend this period by up to 60 days, but we will notify you of any delay.</p>
</div>
<h2 class="section-title">5. Data Processing Details</h2>
<h4>5.1 Categories of Personal Data</h4>
<p>We process the following categories of personal data:</p>
<ul>
<li><strong>Identity Data:</strong> Name, email address, contact information</li>
<li><strong>Account Data:</strong> Username, password, profile information</li>
<li><strong>Usage Data:</strong> How you interact with our services</li>
<li><strong>Technical Data:</strong> IP address, browser type, device information</li>
<li><strong>Content Data:</strong> Documents and files you upload</li>
<li><strong>Communication Data:</strong> Messages and support requests</li>
</ul>
<h4>5.2 Data Retention Periods</h4>
<p>We retain your personal data for the following periods:</p>
<ul>
<li><strong>Account Data:</strong> Until account deletion or 2 years of inactivity</li>
<li><strong>Usage Data:</strong> 24 months for analytics purposes</li>
<li><strong>Support Communications:</strong> 3 years from last contact</li>
<li><strong>Billing Data:</strong> 7 years for tax and accounting purposes</li>
<li><strong>Security Logs:</strong> 12 months for security monitoring</li>
</ul>
<h2 class="section-title">6. International Data Transfers</h2>
<p>Your personal data may be transferred to and processed in countries outside the EU/EEA. We ensure appropriate safeguards are in place:</p>
<ul>
<li><strong>Standard Contractual Clauses:</strong> We use EU-approved SCCs for transfers</li>
<li><strong>Adequacy Decisions:</strong> We transfer to countries with adequate protection</li>
<li><strong>Certification Schemes:</strong> We rely on approved certification mechanisms</li>
<li><strong>Binding Corporate Rules:</strong> Where applicable, we use BCRs for intra-group transfers</li>
</ul>
<h2 class="section-title">7. Data Protection Measures</h2>
<p>We implement comprehensive technical and organizational measures to protect your data:</p>
<div class="compliance-table">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Measure</th>
<th>Implementation</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Encryption</td>
<td>AES-256 encryption at rest and in transit</td>
<td><span class="status-badge status-compliant">Compliant</span></td>
</tr>
<tr>
<td>Access Controls</td>
<td>Role-based access and multi-factor authentication</td>
<td><span class="status-badge status-compliant">Compliant</span></td>
</tr>
<tr>
<td>Data Minimization</td>
<td>Only collect data necessary for service provision</td>
<td><span class="status-badge status-compliant">Compliant</span></td>
</tr>
<tr>
<td>Privacy by Design</td>
<td>Privacy considerations built into all systems</td>
<td><span class="status-badge status-compliant">Compliant</span></td>
</tr>
<tr>
<td>Regular Audits</td>
<td>Annual privacy and security assessments</td>
<td><span class="status-badge status-compliant">Compliant</span></td>
</tr>
<tr>
<td>Staff Training</td>
<td>Regular GDPR and privacy training</td>
<td><span class="status-badge status-compliant">Compliant</span></td>
</tr>
</tbody>
</table>
</div>
<h2 class="section-title">8. Data Breach Procedures</h2>
<p>In the unlikely event of a data breach, we have established procedures to:</p>
<ul>
<li>Detect and assess the breach within 72 hours</li>
<li>Notify the relevant supervisory authority</li>
<li>Inform affected individuals when required</li>
<li>Document all breach incidents and remedial actions</li>
<li>Implement measures to prevent future breaches</li>
</ul>
<h2 class="section-title">9. Third-Party Processors</h2>
<p>We work with carefully selected third-party processors who help us provide our services. All processors:</p>
<ul>
<li>Are bound by data processing agreements</li>
<li>Implement appropriate security measures</li>
<li>Process data only as instructed by us</li>
<li>Are regularly audited for compliance</li>
</ul>
<h2 class="section-title">10. Supervisory Authority</h2>
<p>You have the right to lodge a complaint with your local data protection supervisory authority if you believe we have not addressed your concerns adequately.</p>
<div class="info-box">
<h5><i class="fas fa-info-circle me-2"></i>EU Representative</h5>
<p class="mb-0">For EU residents, you can contact our EU representative at: DocuPulse EU Representative, [Address], [Email]</p>
</div>
<div class="contact-info">
<h3><i class="fas fa-user-shield me-2"></i>Data Protection Officer</h3>
<p>For any GDPR-related questions or to exercise your rights, contact our Data Protection Officer:</p>
<p><strong>Email:</strong> <a href="mailto:dpo@docupulse.com">dpo@docupulse.com</a></p>
<p><strong>Address:</strong> DocuPulse Inc., 123 Business Ave, Suite 100, City, State 12345</p>
<p><strong>Phone:</strong> <a href="tel:+1-555-123-4567">+1 (555) 123-4567</a></p>
</div>
<div class="last-updated">
<p class="mb-0"><strong>Last Updated:</strong> December 2024</p>
</div>
</div>
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

437
templates/public/help.html Normal file
View File

@@ -0,0 +1,437 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Help Center - DocuPulse</title>
<meta name="description" content="Get help with DocuPulse. Find answers to common questions, tutorials, and support resources.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.help-section {
padding: 80px 0;
}
.search-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 80px 0;
position: relative;
overflow: hidden;
}
.search-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.search-box {
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50px;
padding: 15px 25px;
color: white;
font-size: 1.1rem;
width: 100%;
max-width: 600px;
margin: 0 auto;
backdrop-filter: blur(10px);
}
.search-box::placeholder {
color: rgba(255, 255, 255, 0.8);
}
.search-box:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 0 0.2rem rgba(255, 255, 255, 0.25);
}
.category-card {
background: var(--white);
border-radius: 20px;
padding: 40px 30px;
box-shadow: 0 10px 25px var(--shadow-color);
height: 100%;
border: none;
transition: transform 0.3s ease;
text-align: center;
}
.category-card:hover {
transform: translateY(-5px);
}
.category-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 25px;
color: white;
font-size: 2rem;
}
.faq-item {
background: var(--white);
border-radius: 15px;
margin-bottom: 15px;
box-shadow: 0 5px 15px var(--shadow-color);
overflow: hidden;
}
.faq-question {
background: var(--white);
border: none;
padding: 20px 25px;
width: 100%;
text-align: left;
font-weight: 600;
color: var(--text-dark);
transition: all 0.3s ease;
cursor: pointer;
}
.faq-question:hover {
background: var(--bg-color);
}
.faq-question[aria-expanded="true"] {
background: var(--primary-bg-light);
color: var(--primary-color);
}
.faq-answer {
padding: 0 25px 20px;
color: var(--text-muted);
line-height: 1.6;
}
.contact-card {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 15px 35px var(--shadow-color);
text-align: center;
}
.contact-icon {
width: 60px;
height: 60px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
font-size: 1.5rem;
}
/* Button styles to match other pages */
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
.btn-lg {
padding: 15px 40px;
font-size: 1.1rem;
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
{% with
title="Help Center",
description="Find answers to your questions and get the support you need",
title_size="4",
description_size="5"
%}
{% include 'components/hero_section.html' %}
{% endwith %}
<!-- Help Categories -->
<section class="help-section">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Browse by Category</h2>
<p class="lead text-muted">Find help organized by topic</p>
</div>
<div class="row g-4">
<div class="col-lg-4 col-md-6">
<div class="category-card">
<div class="category-icon">
<i class="fas fa-rocket"></i>
</div>
<h4 class="fw-bold mb-3">Getting Started</h4>
<p class="text-muted mb-4">Learn the basics of DocuPulse and set up your workspace</p>
<a href="{{ url_for('public.help_articles', category='getting-started') }}" class="btn btn-outline-primary">View Articles</a>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="category-card">
<div class="category-icon">
<i class="fas fa-users"></i>
</div>
<h4 class="fw-bold mb-3">User Management</h4>
<p class="text-muted mb-4">Manage users, permissions, and team collaboration</p>
<a href="{{ url_for('public.help_articles', category='user-management') }}" class="btn btn-outline-primary">View Articles</a>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="category-card">
<div class="category-icon">
<i class="fas fa-folder"></i>
</div>
<h4 class="fw-bold mb-3">File Management</h4>
<p class="text-muted mb-4">Upload, organize, and manage your documents</p>
<a href="{{ url_for('public.help_articles', category='file-management') }}" class="btn btn-outline-primary">View Articles</a>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="category-card">
<div class="category-icon">
<i class="fas fa-comments"></i>
</div>
<h4 class="fw-bold mb-3">Communication</h4>
<p class="text-muted mb-4">Use messaging and collaboration features</p>
<a href="{{ url_for('public.help_articles', category='communication') }}" class="btn btn-outline-primary">View Articles</a>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="category-card">
<div class="category-icon">
<i class="fas fa-shield-alt"></i>
</div>
<h4 class="fw-bold mb-3">Security & Privacy</h4>
<p class="text-muted mb-4">Learn about security features and data protection</p>
<a href="{{ url_for('public.help_articles', category='security') }}" class="btn btn-outline-primary">View Articles</a>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="category-card">
<div class="category-icon">
<i class="fas fa-cog"></i>
</div>
<h4 class="fw-bold mb-3">Administration</h4>
<p class="text-muted mb-4">Configure settings and manage your organization</p>
<a href="{{ url_for('public.help_articles', category='administration') }}" class="btn btn-outline-primary">View Articles</a>
</div>
</div>
</div>
</div>
</section>
<!-- FAQ Section -->
<section class="help-section" style="background-color: var(--bg-color);">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Frequently Asked Questions</h2>
<p class="lead text-muted">Quick answers to common questions</p>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="accordion" id="faqAccordion">
<div class="faq-item">
<button class="faq-question" type="button" data-bs-toggle="collapse" data-bs-target="#faq1" aria-expanded="true" aria-controls="faq1">
What is DocuPulse and what does it do?
</button>
<div id="faq1" class="collapse show" data-bs-parent="#faqAccordion">
<div class="faq-answer">
DocuPulse is an enterprise document management platform that helps organizations securely store, organize, and collaborate on documents. It features room-based file organization, real-time messaging, user permissions, and comprehensive audit trails. The platform supports multi-tenant instances and provides both document management and team communication capabilities in one integrated solution.
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq2" aria-expanded="false" aria-controls="faq2">
How do rooms work in DocuPulse?
</button>
<div id="faq2" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer">
Rooms are the primary organizational units in DocuPulse. Each room can contain files, folders, and have specific members with granular permissions. You can create rooms for different projects, departments, or teams. Room members can have different permission levels including view, download, upload, delete, rename, move, and share capabilities. Rooms provide a secure, organized way to manage documents and control access.
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq3" aria-expanded="false" aria-controls="faq3">
What file types and sizes are supported?
</button>
<div id="faq3" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer">
DocuPulse supports a wide range of file types including documents (PDF, DOCX, DOC, TXT, RTF), spreadsheets (XLSX, XLS, ODS), presentations (PPTX, PPT), images (JPG, PNG, GIF, SVG), archives (ZIP, RAR, 7Z), code files (PY, JS, HTML, CSS), audio/video files, and CAD/design files.
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq4" aria-expanded="false" aria-controls="faq4">
How does the conversation and messaging system work?
</button>
<div id="faq4" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer">
DocuPulse includes a built-in messaging system where you can create conversations with team members. Conversations support text messages and file attachments, allowing you to discuss documents and share files directly within the platform. Only administrators and managers can create conversations, and members receive notifications when added to conversations or when new messages are sent.
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq5" aria-expanded="false" aria-controls="faq5">
What are the different user roles and permissions?
</button>
<div id="faq5" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer">
DocuPulse has three main user roles: Administrators (full system access), Managers (can create conversations and manage room members), and Regular Users. Within rooms, users can have granular permissions: view, download, upload, delete, rename, move, and share. These permissions are managed per room, allowing flexible access control based on project needs and user responsibilities.
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq6" aria-expanded="false" aria-controls="faq6">
How does file organization and the trash system work?
</button>
<div id="faq6" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer">
Files in DocuPulse are organized in a hierarchical folder structure within rooms. You can create folders, move files between locations, and use the search function to find documents quickly. When files are deleted, they go to the trash where they remain for 30 days before permanent deletion. You can restore files from trash or permanently delete them. The platform also includes a starring system to mark important files for quick access.
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq7" aria-expanded="false" aria-controls="faq7">
What security and audit features are available?
</button>
<div id="faq7" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer">
DocuPulse provides comprehensive security and audit capabilities. All user actions are logged as events, including file operations, user management, and system changes. The platform supports password policies, secure file storage, and detailed activity tracking. Administrators can view audit logs, monitor system usage, and export event data for compliance purposes. The system also includes notification features to keep users informed of important activities.
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq8" aria-expanded="false" aria-controls="faq8">
Can I customize the platform appearance and settings?
</button>
<div id="faq8" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer">
Yes, DocuPulse offers extensive customization options. Administrators can customize the platform's color scheme, company branding, and logo. The system includes configurable SMTP settings for email notifications, customizable email templates, and various system settings. You can also configure storage limits, room limits, and other platform parameters to match your organization's needs.
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq9" aria-expanded="false" aria-controls="faq9">
How does the notification system work?
</button>
<div id="faq9" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer">
DocuPulse includes a comprehensive notification system that alerts users to various events. You'll receive notifications for room invitations, conversation messages, file activities, and system events. Notifications can be marked as read/unread, filtered by type, and managed through the notifications panel. The system also supports email notifications for important events, which can be configured by administrators.
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#faq10" aria-expanded="false" aria-controls="faq10">
What administrative tools and monitoring features are available?
</button>
<div id="faq10" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer">
Administrators have access to comprehensive management tools including user management, system monitoring, usage statistics, and audit logs. The platform provides dashboard views showing file counts, storage usage, user activity, and system health. Administrators can also manage email templates, configure SMTP settings, monitor events, and export data for reporting. The system includes tools for database verification and file system synchronization.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Contact Support -->
<section class="help-section">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="contact-card">
<h2 class="text-center mb-4">Still Need Help?</h2>
<p class="text-muted text-center mb-5">Our support team is here to help you get the most out of DocuPulse</p>
<div class="row g-4">
<div class="col-md-6">
<div class="text-center">
<div class="contact-icon">
<i class="fas fa-envelope"></i>
</div>
<h5 class="fw-bold">Email Support</h5>
<p class="text-muted">Get help via email</p>
<a href="mailto:support@docupulse.com" class="btn btn-outline-primary btn-sm">Contact Us</a>
</div>
</div>
<div class="col-md-6">
<div class="text-center">
<div class="contact-icon">
<i class="fas fa-phone"></i>
</div>
<h5 class="fw-bold">Phone Support</h5>
<p class="text-muted">Call us directly</p>
<a href="tel:+15551234567" class="btn btn-outline-primary btn-sm">Call Now</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Search functionality
document.querySelector('.search-box').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const faqItems = document.querySelectorAll('.faq-item');
faqItems.forEach(item => {
const question = item.querySelector('.faq-question').textContent.toLowerCase();
const answer = item.querySelector('.faq-answer').textContent.toLowerCase();
if (question.includes(searchTerm) || answer.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if category_name %}{{ category_name }} - {% endif %}Help Articles - DocuPulse</title>
<meta name="description" content="Browse help articles and documentation for DocuPulse organized by category.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.category-nav {
background: var(--white);
border-radius: 15px;
padding: 20px;
box-shadow: 0 5px 15px var(--shadow-color);
margin-bottom: 30px;
}
.category-link {
display: block;
padding: 12px 20px;
margin: 5px 0;
border-radius: 10px;
text-decoration: none;
color: var(--text-dark);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.category-link:hover {
background-color: var(--bg-color);
color: var(--primary-color);
transform: translateX(5px);
}
.category-link.active {
background-color: var(--primary-color);
color: var(--white);
border-color: var(--primary-color);
}
.article-card {
background: var(--white);
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px var(--shadow-color);
margin-bottom: 25px;
border: none;
transition: transform 0.3s ease;
}
.article-card:hover {
transform: translateY(-2px);
}
.article-title {
color: var(--primary-color);
margin-bottom: 15px;
}
.article-content {
line-height: 1.7;
color: var(--text-dark);
}
.article-content h1, .article-content h2, .article-content h3,
.article-content h4, .article-content h5, .article-content h6 {
color: var(--primary-color);
margin-top: 25px;
margin-bottom: 15px;
}
.article-content p {
margin-bottom: 15px;
}
.article-content ul, .article-content ol {
margin-bottom: 15px;
padding-left: 20px;
}
.article-content li {
margin-bottom: 5px;
}
.article-content code {
background-color: var(--bg-color);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.article-content pre {
background-color: var(--bg-color);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
margin: 15px 0;
}
.article-content blockquote {
border-left: 4px solid var(--primary-color);
padding-left: 20px;
margin: 20px 0;
font-style: italic;
color: var(--text-muted);
}
.article-content table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.article-content th, .article-content td {
border: 1px solid var(--border-color);
padding: 10px;
text-align: left;
}
.article-content th {
background-color: var(--bg-color);
font-weight: 600;
}
.breadcrumb-nav {
background: var(--white);
border-radius: 10px;
padding: 15px 20px;
margin-bottom: 30px;
box-shadow: 0 2px 8px var(--shadow-color);
}
.breadcrumb-item a {
color: var(--primary-color);
text-decoration: none;
}
.breadcrumb-item.active {
color: var(--text-muted);
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-state i {
font-size: 4rem;
color: var(--text-muted);
opacity: 0.5;
margin-bottom: 20px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Breadcrumb Navigation -->
<div class="container mt-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb breadcrumb-nav">
<li class="breadcrumb-item"><a href="{{ url_for('public.help_center') }}">Help Center</a></li>
<li class="breadcrumb-item active" aria-current="page">
{% if category_name %}{{ category_name }}{% else %}All Articles{% endif %}
</li>
</ol>
</nav>
</div>
<!-- Main Content -->
<div class="container">
<div class="row">
<!-- Category Navigation -->
<div class="col-lg-3">
<div class="category-nav">
<h5 class="mb-3" style="color: var(--primary-color);">
<i class="fas fa-folder me-2"></i>Categories
</h5>
<a href="{{ url_for('public.help_articles') }}"
class="category-link {% if not current_category %}active{% endif %}">
<i class="fas fa-th-large me-2"></i>All Articles
</a>
{% for category_key, category_name in categories.items() %}
<a href="{{ url_for('public.help_articles', category=category_key) }}"
class="category-link {% if current_category == category_key %}active{% endif %}">
<i class="fas fa-{{ 'rocket' if category_key == 'getting-started' else 'users' if category_key == 'user-management' else 'folder' if category_key == 'file-management' else 'comments' if category_key == 'communication' else 'shield-alt' if category_key == 'security' else 'cog' }} me-2"></i>
{{ category_name }}
{% if all_articles.get(category_key) %}
<span class="badge bg-secondary float-end">{{ all_articles[category_key]|length }}</span>
{% endif %}
</a>
{% endfor %}
</div>
</div>
<!-- Articles Content -->
<div class="col-lg-9">
{% if category_name %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 mb-0" style="color: var(--primary-color);">
<i class="fas fa-{{ 'rocket' if current_category == 'getting-started' else 'users' if current_category == 'user-management' else 'folder' if current_category == 'file-management' else 'comments' if current_category == 'communication' else 'shield-alt' if current_category == 'security' else 'cog' }} me-2"></i>
{{ category_name }}
</h1>
<a href="{{ url_for('public.help_center') }}" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Help Center
</a>
</div>
{% else %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 mb-0" style="color: var(--primary-color);">
<i class="fas fa-book me-2"></i>All Help Articles
</h1>
<a href="{{ url_for('public.help_center') }}" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Help Center
</a>
</div>
{% endif %}
{% if articles %}
{% for article in articles %}
<div class="article-card">
<h2 class="article-title">{{ article.title }}</h2>
<div class="article-content">
{{ article.body|safe }}
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="fas fa-calendar me-1"></i>
Updated: {{ article.updated_at.strftime('%B %d, %Y') if article.updated_at else article.created_at.strftime('%B %d, %Y') }}
</small>
<span class="badge" style="background-color: var(--primary-color);">
{{ categories[article.category] }}
</span>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<i class="fas fa-file-alt"></i>
<h3 class="text-muted">No Articles Found</h3>
<p class="text-muted">
{% if current_category %}
No articles are available in the "{{ category_name }}" category yet.
{% else %}
No help articles are available yet.
{% endif %}
</p>
<a href="{{ url_for('public.help_center') }}" class="btn btn-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Help Center
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

452
templates/public/press.html Normal file
View File

@@ -0,0 +1,452 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Press - DocuPulse</title>
<meta name="description" content="Press releases, media kit, and company information for journalists and media professionals covering DocuPulse.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.press-section {
padding: 80px 0;
}
.press-release {
background: var(--white);
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 25px var(--shadow-color);
transition: transform 0.3s ease;
border: none;
margin-bottom: 20px;
}
.press-release:hover {
transform: translateY(-5px);
}
.press-date {
color: var(--text-muted);
font-size: 0.9rem;
margin-bottom: 10px;
}
.press-tag {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
display: inline-block;
margin-right: 10px;
margin-bottom: 10px;
}
.media-kit-card {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 15px 35px var(--shadow-color);
height: 100%;
border: none;
transition: transform 0.3s ease;
text-align: center;
}
.media-kit-card:hover {
transform: translateY(-5px);
}
.media-kit-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 25px;
color: white;
font-size: 2rem;
}
.company-info {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 60px 0;
position: relative;
overflow: hidden;
}
.company-info::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.info-item {
text-align: center;
position: relative;
z-index: 1;
margin-bottom: 30px;
}
.info-icon {
width: 60px;
height: 60px;
background: rgba(255, 255, 255, 0.2);
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
font-size: 1.5rem;
}
.contact-form {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 15px 35px var(--shadow-color);
}
.form-control {
border-radius: 10px;
border: 2px solid var(--border-color);
padding: 12px 15px;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem var(--primary-opacity-15);
}
.gradient-text {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo-showcase {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 15px 35px var(--shadow-color);
text-align: center;
}
.logo-placeholder {
width: 200px;
height: 100px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
font-size: 1.5rem;
font-weight: bold;
}
.stats-section {
background-color: var(--bg-color);
padding: 60px 0;
}
.stat-item {
text-align: center;
margin-bottom: 30px;
}
.stat-number {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 10px;
display: block;
}
.stat-label {
font-size: 1rem;
color: var(--text-muted);
}
.download-btn {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 10px;
padding: 12px 25px;
color: white;
font-weight: 600;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.download-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
color: white;
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
{% with
title="Press & Media",
description="Press releases, media kit, and company information for journalists and media professionals.",
title_size="4",
description_size="5"
%}
{% include 'components/hero_section.html' %}
{% endwith %}
<!-- Company Stats -->
<section class="stats-section">
<div class="container">
<div class="row text-center">
<div class="col-md-3">
<div class="stat-item">
<span class="stat-number" data-value="75" data-suffix="+">75+</span>
<div class="stat-label">Enterprise Clients</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-item">
<span class="stat-number" data-value="1000" data-suffix="K+">1000+</span>
<div class="stat-label">Active Users</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-item">
<span class="stat-number" data-value="1.1" data-suffix="M" data-prefix="$">$1.1M</span>
<div class="stat-label">Annual Revenue</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-item">
<span class="stat-number" data-value="15" data-suffix="+">15+</span>
<div class="stat-label">Countries Served</div>
</div>
</div>
</div>
</div>
</section>
<!-- Media Kit -->
<section class="press-section">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Media Kit</h2>
<p class="lead text-muted">Download our official media assets and company information</p>
</div>
<div class="row g-4">
<div class="col-lg-4">
<div class="media-kit-card">
<div class="media-kit-icon">
<i class="fas fa-image"></i>
</div>
<h4 class="fw-bold mb-3">Logos & Brand Assets</h4>
<p class="text-muted mb-4">High-resolution logos, brand guidelines, and visual assets for media use.</p>
<a href="#" class="download-btn">
<i class="fas fa-download me-2"></i>Download Assets
</a>
</div>
</div>
<div class="col-lg-4">
<div class="media-kit-card">
<div class="media-kit-icon">
<i class="fas fa-file-alt"></i>
</div>
<h4 class="fw-bold mb-3">Company Fact Sheet</h4>
<p class="text-muted mb-4">Key facts, statistics, and company information for press coverage.</p>
<a href="#" class="download-btn">
<i class="fas fa-download me-2"></i>Download Fact Sheet
</a>
</div>
</div>
<div class="col-lg-4">
<div class="media-kit-card">
<div class="media-kit-icon">
<i class="fas fa-camera"></i>
</div>
<h4 class="fw-bold mb-3">Product Screenshots</h4>
<p class="text-muted mb-4">High-quality screenshots and product images for media use.</p>
<a href="#" class="download-btn">
<i class="fas fa-download me-2"></i>Download Images
</a>
</div>
</div>
</div>
</div>
</section>
<!-- Company Information -->
<section class="company-info">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Company Information</h2>
<p class="lead">Essential information for journalists and media professionals</p>
</div>
<div class="row">
<div class="col-lg-3 col-md-6">
<div class="info-item">
<div class="info-icon">
<i class="fas fa-building"></i>
</div>
<h5 class="fw-bold">Founded</h5>
<p class="opacity-75">1991</p>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-item">
<div class="info-icon">
<i class="fas fa-map-marker-alt"></i>
</div>
<h5 class="fw-bold">Headquarters</h5>
<p class="opacity-75">Vilvoorde, Belgium</p>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-item">
<div class="info-icon">
<i class="fas fa-users"></i>
</div>
<h5 class="fw-bold">Employees</h5>
<p class="opacity-75">15+</p>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-item">
<div class="info-icon">
<i class="fas fa-globe"></i>
</div>
<h5 class="fw-bold">Global Presence</h5>
<p class="opacity-75">15+ Countries</p>
</div>
</div>
</div>
</div>
</section>
<!-- Logo Showcase -->
<section class="press-section" style="background-color: var(--bg-color);">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="logo-showcase">
<h3 class="fw-bold mb-4">Our Logo</h3>
<div class="logo-placeholder">
DocuPulse
</div>
<p class="text-muted mb-4">Download our logo in various formats and sizes for media use. Please follow our brand guidelines when using the DocuPulse logo.</p>
<div class="d-flex justify-content-center gap-3 flex-wrap">
<a href="#" class="download-btn">
<i class="fas fa-download me-2"></i>PNG Format
</a>
<a href="#" class="download-btn">
<i class="fas fa-download me-2"></i>SVG Format
</a>
<a href="#" class="download-btn">
<i class="fas fa-download me-2"></i>Brand Guidelines
</a>
</div>
</div>
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Function to animate number counting
function animateNumber(element, endValue, suffix = '', prefix = '', duration = 2000) {
const start = performance.now();
const startValue = 0;
const difference = endValue - startValue;
function updateNumber(currentTime) {
const elapsed = currentTime - start;
const progress = Math.min(elapsed / duration, 1);
// Easing function for smooth animation
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentValue = startValue + (difference * easeOutQuart);
// Format the number based on the suffix and prefix
let displayValue;
if (suffix === 'K+') {
displayValue = Math.round(currentValue) + suffix;
} else if (suffix === 'M') {
displayValue = prefix + Math.round(currentValue) + suffix;
} else if (suffix === '+') {
displayValue = Math.round(currentValue) + suffix;
} else {
displayValue = Math.round(currentValue) + (suffix || '');
}
element.textContent = displayValue;
if (progress < 1) {
requestAnimationFrame(updateNumber);
} else {
// Ensure the final value is correct
if (suffix === 'M') {
element.textContent = prefix + element.getAttribute('data-value') + suffix;
} else {
element.textContent = element.getAttribute('data-value') + (suffix || '');
}
}
}
requestAnimationFrame(updateNumber);
}
// Initialize animated numbers when component is loaded
document.addEventListener('DOMContentLoaded', function() {
const statsObserver = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const statNumbers = entry.target.querySelectorAll('.stat-number');
statNumbers.forEach((stat, index) => {
setTimeout(() => {
const value = parseFloat(stat.getAttribute('data-value'));
const suffix = stat.getAttribute('data-suffix') || '';
const prefix = stat.getAttribute('data-prefix') || '';
if (!isNaN(value)) {
animateNumber(stat, value, suffix, prefix, 2000);
}
}, index * 300); // Stagger the animations
});
// Only trigger once
statsObserver.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
// Observe the stats section
const statsSection = document.querySelector('.stats-section');
if (statsSection) {
statsObserver.observe(statsSection);
}
});
</script>
</body>
</html>

View File

@@ -51,16 +51,29 @@
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
</style>
</head>
@@ -68,12 +81,14 @@
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
<section class="hero-section">
<div class="container text-center">
<h1 class="display-4 fw-bold mb-4">Simple, Transparent Pricing</h1>
<p class="lead fs-5">Choose the perfect plan for your enterprise. No hidden fees, no surprises.</p>
</div>
</section>
{% with
title="Simple, Transparent Pricing",
description="Choose the perfect plan for your enterprise. No hidden fees, no surprises.",
title_size="4",
description_size="5"
%}
{% include 'components/hero_section.html' %}
{% endwith %}
<!-- Pricing Plans -->
{% with contact_url=url_for('public.contact') %}

View File

@@ -0,0 +1,322 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy - DocuPulse</title>
<meta name="description" content="Learn how DocuPulse collects, uses, and protects your personal information in accordance with privacy laws and regulations.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.legal-section {
padding: 80px 0;
}
.legal-content {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 10px 25px var(--shadow-color);
margin-bottom: 30px;
}
.legal-header {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 80px 0;
position: relative;
overflow: hidden;
}
.legal-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.legal-header .container {
position: relative;
z-index: 1;
}
.section-title {
color: var(--primary-color);
border-bottom: 3px solid var(--primary-color);
padding-bottom: 10px;
margin-bottom: 25px;
}
.info-box {
background: rgba(var(--primary-color-rgb), 0.05);
border-left: 4px solid var(--primary-color);
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.info-box h5 {
color: var(--primary-color);
margin-bottom: 10px;
}
.data-table {
background: var(--white);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px var(--shadow-color);
margin: 20px 0;
}
.data-table th {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border: none;
padding: 15px;
}
.data-table td {
padding: 15px;
border-bottom: 1px solid var(--border-color);
}
.data-table tr:last-child td {
border-bottom: none;
}
.contact-info {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 40px;
border-radius: 20px;
text-align: center;
}
.contact-info h3 {
margin-bottom: 20px;
}
.contact-info a {
color: white;
text-decoration: none;
}
.contact-info a:hover {
text-decoration: underline;
}
.last-updated {
background: var(--light-bg);
padding: 15px;
border-radius: 10px;
text-align: center;
margin-top: 30px;
}
@media (max-width: 768px) {
.legal-content {
padding: 25px;
}
.data-table {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Header Section -->
<section class="legal-header">
<div class="container">
<div class="text-center">
<h1 class="display-4 fw-bold mb-3">Privacy Policy</h1>
<p class="lead opacity-75">How we collect, use, and protect your information</p>
<p class="opacity-75">Last updated: December 2024</p>
</div>
</div>
</section>
<!-- Privacy Policy Content -->
<section class="legal-section">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="legal-content">
<h2 class="section-title">1. Introduction</h2>
<p>DocuPulse ("we," "our," or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our document management platform and related services.</p>
<div class="info-box">
<h5><i class="fas fa-info-circle me-2"></i>Important Notice</h5>
<p class="mb-0">By using DocuPulse, you agree to the collection and use of information in accordance with this policy. If you do not agree with our policies and practices, please do not use our service.</p>
</div>
<h2 class="section-title">2. Information We Collect</h2>
<h4>2.1 Personal Information</h4>
<p>We collect information you provide directly to us, including:</p>
<ul>
<li>Name, email address, and contact information</li>
<li>Company and job title information</li>
<li>Account credentials and profile information</li>
<li>Payment and billing information</li>
<li>Communications with our support team</li>
</ul>
<h4>2.2 Usage Information</h4>
<p>We automatically collect certain information about your use of our services:</p>
<ul>
<li>Log data (IP address, browser type, access times)</li>
<li>Device information (device type, operating system)</li>
<li>Usage patterns and feature interactions</li>
</ul>
<h4>2.3 Document and Content Data</h4>
<p>When you use our document management features, we may process:</p>
<ul>
<li>Documents and files you upload</li>
<li>Metadata associated with your documents</li>
<li>Collaboration and sharing information</li>
</ul>
<h2 class="section-title">3. How We Use Your Information</h2>
<div class="data-table">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Purpose</th>
<th>Information Used</th>
<th>Legal Basis</th>
</tr>
</thead>
<tbody>
<tr>
<td>Provide our services</td>
<td>Account info, documents, usage data</td>
<td>Contract performance</td>
</tr>
<tr>
<td>Customer support</td>
<td>Contact info, communications</td>
<td>Legitimate interest</td>
</tr>
<tr>
<td>Security and fraud prevention</td>
<td>Log data, device info</td>
<td>Legitimate interest</td>
</tr>
<tr>
<td>Service improvement</td>
<td>Usage patterns, feedback</td>
<td>Legitimate interest</td>
</tr>
</tbody>
</table>
</div>
<h2 class="section-title">4. Information Sharing and Disclosure</h2>
<p>We do not sell, trade, or rent your personal information to third parties. We may share your information in the following circumstances:</p>
<h4>4.1 Service Providers</h4>
<p>We work with trusted third-party service providers who assist us in operating our platform, such as:</p>
<ul>
<li>Cloud hosting and storage providers</li>
<li>Payment processors</li>
<li>Email and communication services</li>
</ul>
<h4>4.2 Legal Requirements</h4>
<p>We may disclose your information if required by law or in response to:</p>
<ul>
<li>Valid legal requests or court orders</li>
<li>Government investigations</li>
<li>Protection of our rights and safety</li>
<li>Prevention of fraud or security threats</li>
</ul>
<h4>4.3 Business Transfers</h4>
<p>In the event of a merger, acquisition, or sale of assets, your information may be transferred as part of the business transaction, subject to the same privacy protections.</p>
<h2 class="section-title">5. Data Security</h2>
<p>We implement comprehensive security measures to protect your information:</p>
<ul>
<li><strong>Encryption:</strong> All data is encrypted in transit and at rest using AES-256</li>
<li><strong>Access Controls:</strong> Role-based access controls and multi-factor authentication</li>
<li><strong>Monitoring:</strong> Continuous security monitoring and threat detection</li>
<li><strong>Backup:</strong> Regular encrypted backups with disaster recovery</li>
</ul>
<h2 class="section-title">6. Your Rights and Choices</h2>
<p>You have the right to:</p>
<ul>
<li>Access your personal information</li>
<li>Correct inaccurate information</li>
<li>Delete your account and data</li>
<li>Export your data</li>
<li>Object to processing</li>
<li>Withdraw consent</li>
</ul>
<h2 class="section-title">7. Data Retention</h2>
<p>We retain your information for as long as necessary to:</p>
<ul>
<li>Provide our services to you</li>
<li>Comply with legal obligations</li>
<li>Resolve disputes and enforce agreements</li>
</ul>
<p>When you delete your account, we will delete your personal information within 30 days, except where retention is required by law.</p>
<h2 class="section-title">8. International Data Transfers</h2>
<p>Your information may be transferred to and processed in countries other than your own when you provide access to your DocuPulse instance to clients and users. We ensure appropriate safeguards are in place for international transfers, including:</p>
<ul>
<li>Standard contractual clauses</li>
<li>Adequacy decisions</li>
<li>Other appropriate safeguards</li>
</ul>
<h2 class="section-title">9. Cookies and Tracking</h2>
<p>We use cookies and similar technologies to:</p>
<ul>
<li>Remember your preferences and settings</li>
<li>Provide personalized content and features</li>
<li>Ensure security and prevent fraud</li>
</ul>
<h2 class="section-title">10. Children's Privacy</h2>
<p>Our services are not intended for children under 13 years of age. We do not knowingly collect personal information from children under 13. If you believe we have collected information from a child under 13, please contact us immediately.</p>
<h2 class="section-title">11. Changes to This Policy</h2>
<p>We may update this Privacy Policy from time to time. We will notify you of any material changes by:</p>
<ul>
<li>Sending email notifications to registered users</li>
<li>Displaying prominent notices in our application</li>
</ul>
<p>Your continued use of our services after changes become effective constitutes acceptance of the updated policy.</p>
<div class="contact-info">
<h3><i class="fas fa-envelope me-2"></i>Contact Us</h3>
<p>If you have questions about this Privacy Policy or our privacy practices, please contact us:</p>
<p><strong>Email:</strong> <a href="mailto:privacy@docupulse.com">privacy@docupulse.com</a></p>
<p><strong>Address:</strong> DocuPulse Inc., 123 Business Ave, Suite 100, City, State 12345</p>
<p><strong>Phone:</strong> <a href="tel:+1-555-123-4567">+1 (555) 123-4567</a></p>
</div>
<div class="last-updated">
<p class="mb-0"><strong>Last Updated:</strong> June 2025</p>
</div>
</div>
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,368 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Security - DocuPulse</title>
<meta name="description" content="Learn about DocuPulse's enterprise-grade security measures, compliance certifications, and data protection practices.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.security-section {
padding: 80px 0;
}
.security-card {
background: var(--white);
border-radius: 20px;
padding: 40px 30px;
box-shadow: 0 10px 25px var(--shadow-color);
height: 100%;
border: none;
transition: transform 0.3s ease;
}
.security-card:hover {
transform: translateY(-5px);
}
.security-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 25px;
color: white;
font-size: 2rem;
}
.compliance-badge {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 15px 25px;
border-radius: 15px;
text-align: center;
margin-bottom: 20px;
}
.compliance-badge i {
font-size: 2rem;
margin-bottom: 10px;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
position: relative;
padding-left: 30px;
}
.feature-list li:before {
content: '✓';
position: absolute;
left: 0;
color: var(--primary-color);
font-weight: bold;
font-size: 1.2rem;
}
.feature-list li:last-child {
border-bottom: none;
}
/* Make checkmarks white in security features section */
.security-section[style*="gradient"] .feature-list li:before {
color: white;
}
.security-overview {
background: var(--white);
color: var(--text-dark);
padding: 80px 0;
position: relative;
overflow: hidden;
}
.security-overview::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.overview-item {
text-align: center;
position: relative;
z-index: 1;
margin-bottom: 30px;
}
.overview-icon {
width: 60px;
height: 60px;
background: rgba(255, 255, 255, 0.2);
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
font-size: 1.5rem;
}
.timeline {
position: relative;
padding: 40px 0;
}
.timeline::before {
content: '';
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateX(-50%);
}
.timeline-item {
position: relative;
margin-bottom: 40px;
}
.timeline-item:nth-child(odd) .timeline-content {
margin-left: 0;
margin-right: 50%;
padding-right: 40px;
text-align: right;
}
.timeline-item:nth-child(even) .timeline-content {
margin-left: 50%;
margin-right: 0;
padding-left: 40px;
text-align: left;
}
.timeline-dot {
position: absolute;
left: 50%;
top: 20px;
width: 20px;
height: 20px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border-radius: 50%;
transform: translateX(-50%);
z-index: 2;
}
.timeline-content {
background: var(--white);
border-radius: 15px;
padding: 25px;
box-shadow: 0 5px 15px var(--shadow-color);
position: relative;
}
.gradient-text {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Button styles to match other pages */
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
.btn-lg {
padding: 15px 40px;
font-size: 1.1rem;
}
@media (max-width: 768px) {
.timeline::before {
left: 20px;
}
.timeline-item:nth-child(odd) .timeline-content,
.timeline-item:nth-child(even) .timeline-content {
margin-left: 0;
margin-right: 0;
padding-left: 50px;
padding-right: 20px;
text-align: left;
}
.timeline-dot {
left: 20px;
}
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Hero Section -->
{% with
title="Security & Compliance",
description="Enterprise-grade security measures to protect your data and ensure compliance with industry standards",
title_size="4",
description_size="5"
%}
{% include 'components/hero_section.html' %}
{% endwith %}
<!-- Security Overview -->
<section class="security-overview" style="background-color: var(--white); color: var(--text-dark);">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Security at the Core</h2>
<p class="lead text-muted">We prioritize the security of your data with multiple layers of protection</p>
</div>
<div class="row">
<div class="col-lg-4">
<div class="overview-item">
<div class="overview-icon" style="background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);">
<i class="fas fa-lock"></i>
</div>
<h5 class="fw-bold">End-to-End Encryption</h5>
<p class="text-muted">All data encrypted in transit and at rest</p>
</div>
</div>
<div class="col-lg-4">
<div class="overview-item">
<div class="overview-icon" style="background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);">
<i class="fas fa-shield-alt"></i>
</div>
<h5 class="fw-bold">SOC 2 Type II</h5>
<p class="text-muted">Certified compliance with industry standards</p>
</div>
</div>
<div class="col-lg-4">
<div class="overview-item">
<div class="overview-icon" style="background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);">
<i class="fas fa-user-shield"></i>
</div>
<h5 class="fw-bold">Role-Based Access</h5>
<p class="text-muted">Granular permissions and access controls</p>
</div>
</div>
</div>
</div>
</section>
<!-- Security Features -->
<section class="security-section" style="background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); color: white;">
<div class="container">
<div class="text-center mb-5">
<h2 class="display-5 fw-bold mb-3">Security Features</h2>
<p class="lead opacity-75">Comprehensive security measures to protect your organization</p>
</div>
<div class="row g-4">
<div class="col-lg-4">
<div class="security-card text-center" style="background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2);">
<div class="security-icon" style="background: rgba(255, 255, 255, 0.2);">
<i class="fas fa-key"></i>
</div>
<h4 class="fw-bold mb-3">Data Encryption</h4>
<p class="opacity-75 mb-4">All data is encrypted using AES-256 encryption both in transit and at rest.</p>
<ul class="feature-list text-start" style="color: rgba(255, 255, 255, 0.9);">
<li>AES-256 encryption at rest</li>
<li>TLS 1.3 for data in transit</li>
<li>Encrypted backups</li>
</ul>
</div>
</div>
<div class="col-lg-4">
<div class="security-card text-center" style="background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2);">
<div class="security-icon" style="background: rgba(255, 255, 255, 0.2);">
<i class="fas fa-user-lock"></i>
</div>
<h4 class="fw-bold mb-3">Access Control</h4>
<p class="opacity-75 mb-4">Granular role-based access controls allow you to manage who can access what data.</p>
<ul class="feature-list text-start" style="color: rgba(255, 255, 255, 0.9);">
<li>Role-based permissions</li>
<li>Multi-factor authentication</li>
<li>Session management</li>
</ul>
</div>
</div>
<div class="col-lg-4">
<div class="security-card text-center" style="background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2);">
<div class="security-icon" style="background: rgba(255, 255, 255, 0.2);">
<i class="fas fa-search"></i>
</div>
<h4 class="fw-bold mb-3">Audit & Monitoring</h4>
<p class="opacity-75 mb-4">Comprehensive logging and monitoring to track all activities and detect threats.</p>
<ul class="feature-list text-start" style="color: rgba(255, 255, 255, 0.9);">
<li>Activity logging</li>
<li>Real-time monitoring</li>
<li>Security alerts</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="security-section">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8 text-center">
<h2 class="display-5 fw-bold mb-4">Ready to Get Started?</h2>
<p class="lead text-muted mb-5">Experience enterprise-grade security with DocuPulse</p>
{% with primary_url=url_for('public.contact'), primary_icon='fas fa-envelope', primary_text='Contact Team', secondary_url=url_for('public.pricing'), secondary_icon='fas fa-rocket', secondary_text='Get Started' %}
{% include 'components/cta_buttons.html' %}
{% endwith %}
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

365
templates/public/terms.html Normal file
View File

@@ -0,0 +1,365 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terms of Service - DocuPulse</title>
<meta name="description" content="Read DocuPulse's Terms of Service to understand your rights and obligations when using our document management platform.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.legal-section {
padding: 80px 0;
}
.legal-content {
background: var(--white);
border-radius: 20px;
padding: 40px;
box-shadow: 0 10px 25px var(--shadow-color);
margin-bottom: 30px;
}
.legal-header {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 80px 0;
position: relative;
overflow: hidden;
}
.legal-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.legal-header .container {
position: relative;
z-index: 1;
}
.section-title {
color: var(--primary-color);
border-bottom: 3px solid var(--primary-color);
padding-bottom: 10px;
margin-bottom: 25px;
}
.warning-box {
background: rgba(255, 193, 7, 0.1);
border-left: 4px solid #ffc107;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.warning-box h5 {
color: #856404;
margin-bottom: 10px;
}
.info-box {
background: rgba(var(--primary-color-rgb), 0.05);
border-left: 4px solid var(--primary-color);
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.info-box h5 {
color: var(--primary-color);
margin-bottom: 10px;
}
.terms-table {
background: var(--white);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px var(--shadow-color);
margin: 20px 0;
}
.terms-table th {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border: none;
padding: 15px;
}
.terms-table td {
padding: 15px;
border-bottom: 1px solid var(--border-color);
}
.terms-table tr:last-child td {
border-bottom: none;
}
.contact-info {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 40px;
border-radius: 20px;
text-align: center;
}
.contact-info h3 {
margin-bottom: 20px;
}
.contact-info a {
color: white;
text-decoration: none;
}
.contact-info a:hover {
text-decoration: underline;
}
.last-updated {
background: var(--light-bg);
padding: 15px;
border-radius: 10px;
text-align: center;
margin-top: 30px;
}
.prohibited-list {
background: rgba(220, 53, 69, 0.1);
border-left: 4px solid #dc3545;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.prohibited-list h5 {
color: #721c24;
margin-bottom: 15px;
}
.prohibited-list ul {
margin-bottom: 0;
}
@media (max-width: 768px) {
.legal-content {
padding: 25px;
}
.terms-table {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Header Section -->
<section class="legal-header">
<div class="container">
<div class="text-center">
<h1 class="display-4 fw-bold mb-3">Terms of Service</h1>
<p class="lead opacity-75">Your agreement with DocuPulse</p>
<p class="opacity-75">Last updated: December 2024</p>
</div>
</div>
</section>
<!-- Terms of Service Content -->
<section class="legal-section">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="legal-content">
<h2 class="section-title">1. Acceptance of Terms</h2>
<p>By accessing and using DocuPulse ("the Service"), you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to abide by the above, please do not use this service.</p>
<div class="warning-box">
<h5><i class="fas fa-exclamation-triangle me-2"></i>Important Notice</h5>
<p class="mb-0">These Terms of Service constitute a legally binding agreement between you and DocuPulse Inc. Please read them carefully before using our services.</p>
</div>
<h2 class="section-title">2. Description of Service</h2>
<p>DocuPulse is a cloud-based document management platform that provides:</p>
<ul>
<li>Document storage and organization</li>
<li>Security and access controls</li>
<li>Document management and sharing</li>
<li>Document requests</li>
<li>Secure conversations</li>
</ul>
<h2 class="section-title">3. User Accounts</h2>
<h4>3.1 Account Creation</h4>
<p>To use our Service, you must create an account by providing accurate, current, and complete information. You are responsible for maintaining the confidentiality of your account credentials.</p>
<h4>3.2 Account Responsibilities</h4>
<p>You are responsible for:</p>
<ul>
<li>All activities that occur under your account</li>
<li>Maintaining the security of your password</li>
<li>Notifying us immediately of any unauthorized use</li>
<li>Ensuring your account information remains accurate</li>
</ul>
<h2 class="section-title">4. Acceptable Use Policy</h2>
<p>You agree to use the Service only for lawful purposes and in accordance with these Terms. You agree not to use the Service:</p>
<div class="prohibited-list">
<h5><i class="fas fa-ban me-2"></i>Prohibited Activities</h5>
<ul>
<li>To violate any applicable laws or regulations</li>
<li>To infringe upon the rights of others</li>
<li>To upload malicious software or content</li>
<li>To attempt to gain unauthorized access to our systems</li>
<li>To interfere with or disrupt the Service</li>
<li>To use the Service for spam or unsolicited communications</li>
<li>To store or transmit illegal content</li>
<li>To reverse engineer or attempt to extract source code</li>
</ul>
</div>
<h2 class="section-title">5. Content and Data</h2>
<h4>5.1 Your Content</h4>
<p>You retain ownership of all content you upload to the Service. By uploading content, you grant us a limited license to store, process, and display your content as necessary to provide the Service.</p>
<h4>5.2 Content Standards</h4>
<p>You represent and warrant that all content you upload:</p>
<ul>
<li>Does not violate any laws or regulations</li>
<li>Does not infringe on third-party rights</li>
<li>Is not harmful, offensive, or inappropriate</li>
<li>Complies with our content guidelines</li>
</ul>
<h4>5.3 Data Processing</h4>
<p>We process your data in accordance with our Privacy Policy and applicable data protection laws. You are responsible for ensuring you have the right to share any data you upload.</p>
<h2 class="section-title">6. Subscription and Payment</h2>
<h4>6.1 Subscription Plans</h4>
<p>We offer various subscription plans with different features and pricing. Current plans and pricing are available on our website and may be updated from time to time.</p>
<h4>6.2 Payment Terms</h4>
<ul>
<li>Payments are billed in advance on a recurring basis</li>
<li>All fees are non-refundable except as required by law</li>
<li>We may change our pricing with 30 days' notice</li>
<li>Failed payments may result in service suspension</li>
</ul>
<h4>6.3 Cancellation</h4>
<p>You may cancel your subscription at any time through your account settings. Cancellation will take effect at the end of your current billing period.</p>
<h2 class="section-title">7. Service Availability</h2>
<div class="info-box">
<h5><i class="fas fa-info-circle me-2"></i>Service Level</h5>
<p class="mb-0">We strive to maintain 99.9% uptime but cannot guarantee uninterrupted service. We may perform maintenance that temporarily affects availability.</p>
</div>
<p>We reserve the right to:</p>
<ul>
<li>Modify or discontinue features at any time</li>
<li>Perform maintenance that may affect service availability</li>
<li>Limit or suspend access for security or legal reasons</li>
<li>Update the Service to improve functionality</li>
</ul>
<h2 class="section-title">8. Intellectual Property</h2>
<h4>8.1 Our Rights</h4>
<p>DocuPulse and its original content, features, and functionality are owned by DocuPulse Inc. and are protected by international copyright, trademark, patent, trade secret, and other intellectual property laws.</p>
<h4>8.2 Your Rights</h4>
<p>You retain ownership of your content. You grant us a limited, non-exclusive license to use your content solely to provide the Service.</p>
<h2 class="section-title">9. Privacy and Data Protection</h2>
<p>Your privacy is important to us. Our collection and use of personal information is governed by our Privacy Policy, which is incorporated into these Terms by reference.</p>
<h2 class="section-title">10. Limitation of Liability</h2>
<div class="warning-box">
<h5><i class="fas fa-shield-alt me-2"></i>Liability Limitations</h5>
<p class="mb-0">To the maximum extent permitted by law, DocuPulse shall not be liable for any indirect, incidental, special, consequential, or punitive damages, including but not limited to loss of profits, data, or use.</p>
</div>
<p>Our total liability to you for any claims arising from these Terms or your use of the Service shall not exceed the amount you paid us in the 12 months preceding the claim.</p>
<h2 class="section-title">11. Indemnification</h2>
<p>You agree to indemnify and hold harmless DocuPulse and its officers, directors, employees, and agents from any claims, damages, losses, or expenses arising from:</p>
<ul>
<li>Your use of the Service</li>
<li>Your violation of these Terms</li>
<li>Your content or data</li>
<li>Your violation of any third-party rights</li>
</ul>
<h2 class="section-title">12. Termination</h2>
<h4>12.1 Termination by You</h4>
<p>You may terminate your account at any time by contacting us or using the account deletion feature in your settings.</p>
<h4>12.2 Termination by Us</h4>
<p>We may terminate or suspend your account immediately if:</p>
<ul>
<li>You violate these Terms</li>
<li>You engage in fraudulent or illegal activities</li>
<li>We are required to do so by law</li>
<li>You fail to pay applicable fees</li>
</ul>
<h4>12.3 Effect of Termination</h4>
<p>Upon termination, your right to use the Service ceases immediately. We may delete your account and data in accordance with our data retention policies.</p>
<h2 class="section-title">13. Dispute Resolution</h2>
<h4>13.1 Governing Law</h4>
<p>These Terms are governed by the laws of [Jurisdiction], without regard to conflict of law principles.</p>
<h4>13.2 Dispute Resolution Process</h4>
<p>Before pursuing formal legal action, we encourage you to contact us directly to resolve any disputes. If we cannot resolve the dispute informally, you agree to submit to binding arbitration.</p>
<h2 class="section-title">14. Changes to Terms</h2>
<p>We may update these Terms from time to time. We will notify you of any material changes by:</p>
<ul>
<li>Posting the updated Terms on our website</li>
<li>Sending email notifications to registered users</li>
<li>Displaying prominent notices in our application</li>
</ul>
<p>Your continued use of the Service after changes become effective constitutes acceptance of the updated Terms.</p>
<h2 class="section-title">15. Miscellaneous</h2>
<h4>15.1 Entire Agreement</h4>
<p>These Terms, together with our Privacy Policy, constitute the entire agreement between you and DocuPulse regarding the Service.</p>
<h4>15.2 Severability</h4>
<p>If any provision of these Terms is found to be unenforceable, the remaining provisions will continue in full force and effect.</p>
<h4>15.3 Waiver</h4>
<p>Our failure to enforce any right or provision of these Terms will not constitute a waiver of that right or provision.</p>
<div class="contact-info">
<h3><i class="fas fa-envelope me-2"></i>Contact Us</h3>
<p>If you have questions about these Terms of Service, please contact us:</p>
<p><strong>Email:</strong> <a href="mailto:legal@docupulse.com">legal@docupulse.com</a></p>
<p><strong>Address:</strong> DocuPulse Inc., 123 Business Ave, Suite 100, City, State 12345</p>
<p><strong>Phone:</strong> <a href="tel:+1-555-123-4567">+1 (555) 123-4567</a></p>
</div>
<div class="last-updated">
<p class="mb-0"><strong>Last Updated:</strong> December 2024</p>
</div>
</div>
</div>
</div>
</div>
</section>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -9,6 +9,7 @@
{% from "settings/tabs/mails.html" import mails_tab %}
{% from "settings/tabs/smtp_settings.html" import smtp_settings_tab %}
{% from "settings/tabs/connections.html" import connections_tab %}
{% from "settings/tabs/pricing.html" import pricing_tab %}
{% from "settings/components/reset_colors_modal.html" import reset_colors_modal %}
{% block title %}Settings - DocuPulse{% endblock %}
@@ -82,6 +83,11 @@
<i class="fas fa-plug me-2"></i>Connections
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'pricing' %}active{% endif %}" id="pricing-tab" data-bs-toggle="tab" data-bs-target="#pricing" type="button" role="tab" aria-controls="pricing" aria-selected="{{ 'true' if active_tab == 'pricing' else 'false' }}">
<i class="fas fa-tags me-2"></i>Pricing
</button>
</li>
{% endif %}
</ul>
</div>
@@ -136,6 +142,11 @@
<div class="tab-pane fade {% if active_tab == 'connections' %}show active{% endif %}" id="connections" role="tabpanel" aria-labelledby="connections-tab">
{{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings) }}
</div>
<!-- Pricing Tab -->
<div class="tab-pane fade {% if active_tab == 'pricing' %}show active{% endif %}" id="pricing" role="tabpanel" aria-labelledby="pricing-tab">
{{ pricing_tab(pricing_plans, csrf_token) }}
</div>
{% endif %}
</div>
</div>
@@ -150,4 +161,7 @@
{% block extra_js %}
<script src="{{ url_for('static', filename='js/settings.js') }}?v={{ 'js/settings.js'|asset_version }}"></script>
<script src="{{ url_for('static', filename='js/events.js') }}?v={{ 'js/events.js'|asset_version }}"></script>
{% if is_master and active_tab == 'pricing' %}
<script src="{{ url_for('static', filename='js/settings/pricing.js') }}?v={{ 'js/settings/pricing.js'|asset_version }}"></script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,427 @@
{% macro pricing_tab(pricing_plans, csrf_token) %}
<div class="pricing-configuration">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">Pricing Plans Configuration</h4>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPricingPlanModal">
<i class="fas fa-plus me-2"></i>Add New Plan
</button>
</div>
<!-- Pricing Plans List -->
<div class="row" id="pricingPlansContainer">
{% for plan in pricing_plans %}
<div class="col-md-6 col-lg-4 mb-4" data-plan-id="{{ plan.id }}">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">{{ plan.name }}</h5>
<div class="form-check form-switch">
<input class="form-check-input plan-status-toggle" type="checkbox"
data-plan-id="{{ plan.id }}"
{% if plan.is_active %}checked{% endif %}>
</div>
</div>
<div class="card-body">
<div class="mb-3">
<strong>Monthly Price:</strong> €{{ "%.2f"|format(plan.monthly_price) }}<br>
<strong>Annual Price:</strong> €{{ "%.2f"|format(plan.annual_price) }}
</div>
{% if plan.description %}
<div class="mb-3">
<strong>Description:</strong><br>
<small class="text-muted">{{ plan.description }}</small>
</div>
{% endif %}
<!-- Quotas Section -->
<div class="mb-3">
<strong>Quotas:</strong>
<div class="row mt-2">
<div class="col-6">
<small class="text-muted">
<i class="fas fa-door-open me-1"></i>Rooms: {{ plan.format_quota_display('room_quota') }}<br>
<i class="fas fa-comments me-1"></i>Conversations: {{ plan.format_quota_display('conversation_quota') }}<br>
<i class="fas fa-hdd me-1"></i>Storage: {{ plan.format_quota_display('storage_quota_gb') }}
</small>
</div>
<div class="col-6">
<small class="text-muted">
<i class="fas fa-user-tie me-1"></i>Managers: {{ plan.format_quota_display('manager_quota') }}<br>
<i class="fas fa-user-shield me-1"></i>Admins: {{ plan.format_quota_display('admin_quota') }}
</small>
</div>
</div>
</div>
<div class="mb-3">
<strong>Features:</strong>
<ul class="list-unstyled mt-2">
{% for feature in plan.features %}
<li><i class="fas fa-check text-success me-2"></i>{{ feature }}</li>
{% endfor %}
</ul>
</div>
<div class="mb-3">
<strong>Button:</strong> {{ plan.button_text }} → {{ plan.button_url }}
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input plan-popular-toggle" type="checkbox"
data-plan-id="{{ plan.id }}"
{% if plan.is_popular %}checked{% endif %}>
<label class="form-check-label">Most Popular</label>
</div>
<div class="form-check">
<input class="form-check-input plan-custom-toggle" type="checkbox"
data-plan-id="{{ plan.id }}"
{% if plan.is_custom %}checked{% endif %}>
<label class="form-check-label">Custom Plan</label>
</div>
</div>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-sm btn-outline-primary edit-plan-btn"
data-plan-id="{{ plan.id }}"
data-bs-toggle="modal" data-bs-target="#editPricingPlanModal">
<i class="fas fa-edit me-1"></i>Edit
</button>
<button type="button" class="btn btn-sm btn-outline-danger delete-plan-btn"
data-plan-id="{{ plan.id }}">
<i class="fas fa-trash me-1"></i>Delete
</button>
</div>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="text-center py-5">
<i class="fas fa-tags fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No pricing plans configured</h5>
<p class="text-muted">Click "Add New Plan" to create your first pricing plan.</p>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Add Pricing Plan Modal -->
<div class="modal fade" id="addPricingPlanModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Pricing Plan</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="addPricingPlanForm">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="planName" class="form-label">Plan Name *</label>
<input type="text" class="form-control" id="planName" name="name" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="planDescription" class="form-label">Description</label>
<textarea class="form-control" id="planDescription" name="description" rows="2"></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="monthlyPrice" class="form-label">Monthly Price (€) *</label>
<input type="number" class="form-control" id="monthlyPrice" name="monthly_price"
step="0.01" min="0" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="annualPrice" class="form-label">Annual Price (€) *</label>
<input type="number" class="form-control" id="annualPrice" name="annual_price"
step="0.01" min="0" required>
</div>
</div>
</div>
<!-- Quotas Section -->
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-chart-pie me-2"></i>Resource Quotas</h6>
<small class="text-muted">Set to 0 for unlimited</small>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="roomQuota" class="form-label">Room Quota</label>
<input type="number" class="form-control" id="roomQuota" name="room_quota"
min="0" value="0">
<small class="text-muted">Number of rooms allowed</small>
</div>
<div class="mb-3">
<label for="conversationQuota" class="form-label">Conversation Quota</label>
<input type="number" class="form-control" id="conversationQuota" name="conversation_quota"
min="0" value="0">
<small class="text-muted">Number of conversations allowed</small>
</div>
<div class="mb-3">
<label for="storageQuota" class="form-label">Storage Quota (GB)</label>
<input type="number" class="form-control" id="storageQuota" name="storage_quota_gb"
min="0" value="0">
<small class="text-muted">Storage limit in gigabytes</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="managerQuota" class="form-label">Manager Quota</label>
<input type="number" class="form-control" id="managerQuota" name="manager_quota"
min="0" value="0">
<small class="text-muted">Number of manager users allowed</small>
</div>
<div class="mb-3">
<label for="adminQuota" class="form-label">Admin Quota</label>
<input type="number" class="form-control" id="adminQuota" name="admin_quota"
min="0" value="0">
<small class="text-muted">Number of admin users allowed</small>
</div>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="planFeatures" class="form-label">Features *</label>
<div id="featuresContainer">
<div class="input-group mb-2">
<input type="text" class="form-control feature-input" name="features[]" required>
<button type="button" class="btn btn-outline-danger remove-feature-btn">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeatureBtn">
<i class="fas fa-plus me-1"></i>Add Feature
</button>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="buttonText" class="form-label">Button Text</label>
<input type="text" class="form-control" id="buttonText" name="button_text"
value="Get Started">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="buttonUrl" class="form-label">Button URL</label>
<input type="text" class="form-control" id="buttonUrl" name="button_url"
value="#">
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isPopular" name="is_popular">
<label class="form-check-label" for="isPopular">Most Popular</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isCustom" name="is_custom">
<label class="form-check-label" for="isCustom">Custom Plan</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isActive" name="is_active" checked>
<label class="form-check-label" for="isActive">Active</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Create Plan</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Pricing Plan Modal -->
<div class="modal fade" id="editPricingPlanModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Pricing Plan</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="editPricingPlanForm">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" id="editPlanId" name="plan_id">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editPlanName" class="form-label">Plan Name *</label>
<input type="text" class="form-control" id="editPlanName" name="name" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editPlanDescription" class="form-label">Description</label>
<textarea class="form-control" id="editPlanDescription" name="description" rows="2"></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editMonthlyPrice" class="form-label">Monthly Price (€) *</label>
<input type="number" class="form-control" id="editMonthlyPrice" name="monthly_price"
step="0.01" min="0" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editAnnualPrice" class="form-label">Annual Price (€) *</label>
<input type="number" class="form-control" id="editAnnualPrice" name="annual_price"
step="0.01" min="0" required>
</div>
</div>
</div>
<!-- Quotas Section -->
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-chart-pie me-2"></i>Resource Quotas</h6>
<small class="text-muted">Set to 0 for unlimited</small>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editRoomQuota" class="form-label">Room Quota</label>
<input type="number" class="form-control" id="editRoomQuota" name="room_quota"
min="0" value="0">
<small class="text-muted">Number of rooms allowed</small>
</div>
<div class="mb-3">
<label for="editConversationQuota" class="form-label">Conversation Quota</label>
<input type="number" class="form-control" id="editConversationQuota" name="conversation_quota"
min="0" value="0">
<small class="text-muted">Number of conversations allowed</small>
</div>
<div class="mb-3">
<label for="editStorageQuota" class="form-label">Storage Quota (GB)</label>
<input type="number" class="form-control" id="editStorageQuota" name="storage_quota_gb"
min="0" value="0">
<small class="text-muted">Storage limit in gigabytes</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editManagerQuota" class="form-label">Manager Quota</label>
<input type="number" class="form-control" id="editManagerQuota" name="manager_quota"
min="0" value="0">
<small class="text-muted">Number of manager users allowed</small>
</div>
<div class="mb-3">
<label for="editAdminQuota" class="form-label">Admin Quota</label>
<input type="number" class="form-control" id="editAdminQuota" name="admin_quota"
min="0" value="0">
<small class="text-muted">Number of admin users allowed</small>
</div>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="editPlanFeatures" class="form-label">Features *</label>
<div id="editFeaturesContainer">
<!-- Features will be populated dynamically -->
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="editAddFeatureBtn">
<i class="fas fa-plus me-1"></i>Add Feature
</button>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editButtonText" class="form-label">Button Text</label>
<input type="text" class="form-control" id="editButtonText" name="button_text">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editButtonUrl" class="form-label">Button URL</label>
<input type="text" class="form-control" id="editButtonUrl" name="button_url">
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="editIsPopular" name="is_popular">
<label class="form-check-label" for="editIsPopular">Most Popular</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="editIsCustom" name="is_custom">
<label class="form-check-label" for="editIsCustom">Custom Plan</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="editIsActive" name="is_active">
<label class="form-check-label" for="editIsActive">Active</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update Plan</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deletePricingPlanModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Pricing Plan</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this pricing plan? This action cannot be undone.</p>
<p class="text-danger"><strong>Plan: <span id="deletePlanName"></span></strong></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeletePlan">Delete Plan</button>
</div>
</div>
</div>
</div>
{% endmacro %}