Files
ServerKit/backend/app/models/project.py
T
Juan DenisandClaude Opus 4.8 478b24a3cb feat: Phase 3 team & scale — projects/environments, shared resources, PR previews, proxy stacks
- Project / Environment hierarchy (C2): Workspace -> Project -> Environment
  org model. New Project/Environment models + CRUD APIs, nullable
  applications.project_id/environment_id (opt-in, no destructive backfill),
  ProjectService.ensure_default, a Projects list + ProjectDetail (env tabs),
  a project/environment selector in the create wizard, and sidebar nav.

- Polymorphic shared resources facade (C9): ResourceTag + SharedVariableGroup
  /SharedVariable/attachment models keyed by (resource_type, resource_id),
  reusing the env-var Fernet encryption; SharedResourceService resolves merged
  variables (most-recent attachment wins) with secrets masked. /api/v1/shared
  endpoints + reusable TagsPanel/EnvironmentVariablesPanel + a Shared Variables
  management page. Existing per-resource tables untouched (facade first).

- PR preview environments (C3): ApplicationPreview + settings models, a
  PreviewService with pure domain-templating + reconcile (open PRs vs active
  previews) cores and best-effort provisioning (incl. guarded WordPress
  file+DB clone), a pull_request webhook endpoint, preview.* jobs, and a
  Previews tab on the app detail page.

- Per-server managed proxy stack (C6): opt-in Dockerized Traefik/Caddy as a
  Compose stack (host Nginx stays default and superior for PHP/WordPress).
  ProxyStack model + ProxyStackService with pure compose/config generators and
  versioned config backups, /api/v1/servers/<id>/proxy endpoints, and a Proxy
  tab on the server detail page.

Migrations 031-034 add the new tables/columns; the full chain applies cleanly
from an empty DB. Full backend suite: 871 passed, 14 skipped; frontend builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:33:44 -04:00

67 lines
2.4 KiB
Python

from datetime import datetime
from app import db
import json
class Project(db.Model):
"""A Project groups applications under a Workspace.
Hierarchy: Workspace -> Project -> Environment -> Applications.
OPT-IN and backward compatible: applications carry a nullable project_id, so
existing apps remain "unassigned" until a user organizes them into projects.
"""
__tablename__ = 'projects'
id = db.Column(db.Integer, primary_key=True)
workspace_id = db.Column(db.Integer, db.ForeignKey('workspaces.id'), nullable=False, index=True)
name = db.Column(db.String(128), nullable=False)
slug = db.Column(db.String(128), nullable=False)
description = db.Column(db.Text, nullable=True)
metadata_json = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Environments belonging to this project. Deleting a project cascades to its
# environments (apps are detached/blocked separately by the service layer).
environments = db.relationship(
'Environment',
backref='project',
lazy='dynamic',
cascade='all, delete-orphan',
order_by='Environment.order',
)
__table_args__ = (
db.UniqueConstraint('workspace_id', 'slug', name='uq_project_workspace_slug'),
)
@property
def metadata_(self):
return json.loads(self.metadata_json) if self.metadata_json else {}
@metadata_.setter
def metadata_(self, v):
self.metadata_json = json.dumps(v) if v else None
def to_dict(self, include_counts=False):
result = {
'id': self.id,
'workspace_id': self.workspace_id,
'name': self.name,
'slug': self.slug,
'description': self.description,
'metadata': self.metadata_,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}
if include_counts:
from app.models.application import Application
result['environment_count'] = self.environments.count()
result['app_count'] = Application.query.filter_by(project_id=self.id).count()
return result
def __repr__(self):
return f'<Project {self.name}>'