version
This commit is contained in:
Binary file not shown.
24
migrations/versions/a1fd98e6630d_merge_heads.py
Normal file
24
migrations/versions/a1fd98e6630d_merge_heads.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""merge heads
|
||||||
|
|
||||||
|
Revision ID: a1fd98e6630d
|
||||||
|
Revises: 761908f0cacf, add_password_reset_tokens
|
||||||
|
Create Date: 2025-06-23 09:12:50.264151
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a1fd98e6630d'
|
||||||
|
down_revision = ('761908f0cacf', 'add_password_reset_tokens')
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
||||||
@@ -7,6 +7,7 @@ Create Date: 2024-01-01 12:00:00.000000
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'add_password_reset_tokens'
|
revision = 'add_password_reset_tokens'
|
||||||
@@ -15,19 +16,28 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# Create password_reset_tokens table
|
conn = op.get_bind()
|
||||||
op.create_table('password_reset_tokens',
|
result = conn.execute(text("""
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
SELECT EXISTS (
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
SELECT FROM information_schema.tables
|
||||||
sa.Column('token', sa.String(length=100), nullable=False),
|
WHERE table_name = 'password_reset_tokens'
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
);
|
||||||
sa.Column('expires_at', sa.DateTime(), nullable=False),
|
"""))
|
||||||
sa.Column('used', sa.Boolean(), nullable=True),
|
exists = result.scalar()
|
||||||
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
if not exists:
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
|
# Create password_reset_tokens table
|
||||||
sa.PrimaryKeyConstraint('id'),
|
op.create_table('password_reset_tokens',
|
||||||
sa.UniqueConstraint('token')
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
)
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('token', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('expires_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('used', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('token')
|
||||||
|
)
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# Drop password_reset_tokens table
|
# Drop password_reset_tokens table
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""add version tracking fields to instances
|
||||||
|
|
||||||
|
Revision ID: c94c2b2b9f2e
|
||||||
|
Revises: a1fd98e6630d
|
||||||
|
Create Date: 2025-06-23 09:15:13.092801
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c94c2b2b9f2e'
|
||||||
|
down_revision = 'a1fd98e6630d'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('email_template')
|
||||||
|
op.drop_table('notification')
|
||||||
|
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))
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
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'])
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
op.create_table('notification',
|
||||||
|
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('title', sa.VARCHAR(length=200), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('message', sa.TEXT(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('type', sa.VARCHAR(length=50), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('read', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('notification_user_id_fkey')),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('notification_pkey'))
|
||||||
|
)
|
||||||
|
op.create_table('email_template',
|
||||||
|
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('subject', sa.VARCHAR(length=200), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('body', sa.TEXT(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('email_template_pkey')),
|
||||||
|
sa.UniqueConstraint('name', name=op.f('email_template_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -528,6 +528,11 @@ class Instance(db.Model):
|
|||||||
status = db.Column(db.String(20), nullable=False, default='inactive')
|
status = db.Column(db.String(20), nullable=False, default='inactive')
|
||||||
status_details = db.Column(db.Text, nullable=True)
|
status_details = db.Column(db.Text, nullable=True)
|
||||||
connection_token = db.Column(db.String(64), unique=True, nullable=True)
|
connection_token = db.Column(db.String(64), unique=True, nullable=True)
|
||||||
|
# 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
|
||||||
|
latest_version = db.Column(db.String(50), nullable=True) # Latest version available in Git
|
||||||
|
version_checked_at = db.Column(db.DateTime, nullable=True) # When version was last checked
|
||||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP'))
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP'))
|
||||||
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP'), onupdate=db.text('CURRENT_TIMESTAMP'))
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP'), onupdate=db.text('CURRENT_TIMESTAMP'))
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -13,6 +13,9 @@ from email.utils import formatdate
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import requests
|
import requests
|
||||||
import base64
|
import base64
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
|
from functools import wraps
|
||||||
|
import os
|
||||||
|
|
||||||
launch_api = Blueprint('launch_api', __name__)
|
launch_api = Blueprint('launch_api', __name__)
|
||||||
|
|
||||||
@@ -657,6 +660,60 @@ def download_docker_compose():
|
|||||||
if not git_settings:
|
if not git_settings:
|
||||||
return jsonify({'message': 'Git settings not configured'}), 400
|
return jsonify({'message': 'Git settings not configured'}), 400
|
||||||
|
|
||||||
|
# Get the current commit hash and latest tag for the branch
|
||||||
|
commit_hash = None
|
||||||
|
latest_tag = None
|
||||||
|
if git_settings['provider'] == 'gitea':
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': f'token {git_settings["token"]}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the latest commit for the branch
|
||||||
|
commit_response = requests.get(
|
||||||
|
f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/commits/{data["branch"]}',
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if commit_response.status_code == 200:
|
||||||
|
commit_data = commit_response.json()
|
||||||
|
commit_hash = commit_data.get('sha')
|
||||||
|
else:
|
||||||
|
# Try token as query parameter if header auth fails
|
||||||
|
commit_response = requests.get(
|
||||||
|
f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/commits/{data["branch"]}?token={git_settings["token"]}',
|
||||||
|
headers={'Accept': 'application/json'}
|
||||||
|
)
|
||||||
|
if commit_response.status_code == 200:
|
||||||
|
commit_data = commit_response.json()
|
||||||
|
commit_hash = commit_data.get('sha')
|
||||||
|
|
||||||
|
# Get the latest tag
|
||||||
|
tags_response = requests.get(
|
||||||
|
f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/tags',
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
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')
|
||||||
|
else:
|
||||||
|
# Try token as query parameter if header auth fails
|
||||||
|
tags_response = requests.get(
|
||||||
|
f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/tags?token={git_settings["token"]}',
|
||||||
|
headers={'Accept': 'application/json'}
|
||||||
|
)
|
||||||
|
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')
|
||||||
|
|
||||||
# Determine the provider and set up the appropriate API call
|
# Determine the provider and set up the appropriate API call
|
||||||
if git_settings['provider'] == 'gitea':
|
if git_settings['provider'] == 'gitea':
|
||||||
# For Gitea
|
# For Gitea
|
||||||
@@ -704,9 +761,53 @@ def download_docker_compose():
|
|||||||
else:
|
else:
|
||||||
content = response.text
|
content = response.text
|
||||||
|
|
||||||
|
# Add version.txt creation to the docker-compose content
|
||||||
|
if commit_hash:
|
||||||
|
# Create version information with both tag and commit hash
|
||||||
|
version_info = {
|
||||||
|
'tag': latest_tag or 'unknown',
|
||||||
|
'commit': commit_hash,
|
||||||
|
'branch': data['branch'],
|
||||||
|
'deployed_at': datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
version_json = json.dumps(version_info, indent=2)
|
||||||
|
|
||||||
|
# Add a command to create version.txt with the version information
|
||||||
|
version_command = f'echo \'{version_json}\' > /app/version.txt'
|
||||||
|
|
||||||
|
# Find the web service and add the command
|
||||||
|
if 'web:' in content:
|
||||||
|
# Add the command to create version.txt before the main command
|
||||||
|
lines = content.split('\n')
|
||||||
|
new_lines = []
|
||||||
|
in_web_service = False
|
||||||
|
command_added = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
new_lines.append(line)
|
||||||
|
|
||||||
|
if line.strip() == 'web:':
|
||||||
|
in_web_service = True
|
||||||
|
elif in_web_service and line.strip().startswith('command:'):
|
||||||
|
# Add the version.txt creation command before the main command
|
||||||
|
new_lines.append(f' - sh -c "{version_command} && {line.split("command:")[1].strip()}"')
|
||||||
|
command_added = True
|
||||||
|
continue
|
||||||
|
elif in_web_service and line.strip() and not line.startswith(' ') and not line.startswith('#'):
|
||||||
|
# We've left the web service section
|
||||||
|
if not command_added:
|
||||||
|
# If no command was found, add a new command section
|
||||||
|
new_lines.append(f' command: sh -c "{version_command} && python app.py"')
|
||||||
|
command_added = True
|
||||||
|
in_web_service = False
|
||||||
|
|
||||||
|
content = '\n'.join(new_lines)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'content': content
|
'content': content,
|
||||||
|
'commit_hash': commit_hash,
|
||||||
|
'latest_tag': latest_tag
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -981,26 +1082,77 @@ def save_instance():
|
|||||||
missing_fields = [field for field in required_fields if field not in data]
|
missing_fields = [field for field in required_fields if field not in data]
|
||||||
return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400
|
return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400
|
||||||
|
|
||||||
# Save instance data
|
# Check if instance already exists
|
||||||
instance_data = {
|
existing_instance = Instance.query.filter_by(name=data['name']).first()
|
||||||
'name': data['name'],
|
|
||||||
'port': data['port'],
|
|
||||||
'domains': data['domains'],
|
|
||||||
'stack_id': data['stack_id'],
|
|
||||||
'stack_name': data['stack_name'],
|
|
||||||
'status': data['status'],
|
|
||||||
'repository': data['repository'],
|
|
||||||
'branch': data['branch'],
|
|
||||||
'created_at': datetime.utcnow().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
# Save to database using KeyValueSettings
|
if existing_instance:
|
||||||
KeyValueSettings.set_value(f'instance_{data["name"]}', instance_data)
|
# 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.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()
|
||||||
|
|
||||||
return jsonify({
|
db.session.commit()
|
||||||
'message': 'Instance data saved successfully',
|
|
||||||
'data': instance_data
|
return jsonify({
|
||||||
})
|
'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,
|
||||||
|
'status': existing_instance.status,
|
||||||
|
'repository': existing_instance.repository,
|
||||||
|
'branch': existing_instance.branch,
|
||||||
|
'deployed_version': existing_instance.deployed_version,
|
||||||
|
'deployed_branch': existing_instance.deployed_branch
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Create new instance
|
||||||
|
instance = Instance(
|
||||||
|
name=data['name'],
|
||||||
|
company='Loading...', # Will be updated later
|
||||||
|
rooms_count=0,
|
||||||
|
conversations_count=0,
|
||||||
|
data_size=0.0,
|
||||||
|
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'],
|
||||||
|
deployed_version=data.get('deployed_version', 'unknown'),
|
||||||
|
deployed_branch=data.get('deployed_branch', data['branch'])
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(instance)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'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,
|
||||||
|
'status': instance.status,
|
||||||
|
'repository': instance.repository,
|
||||||
|
'branch': instance.branch,
|
||||||
|
'deployed_version': instance.deployed_version,
|
||||||
|
'deployed_branch': instance.deployed_branch
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error saving instance data: {str(e)}")
|
current_app.logger.error(f"Error saving instance data: {str(e)}")
|
||||||
|
|||||||
@@ -379,18 +379,58 @@ def init_routes(main_bp):
|
|||||||
|
|
||||||
instances = Instance.query.order_by(Instance.name.asc()).all()
|
instances = Instance.query.order_by(Instance.name.asc()).all()
|
||||||
|
|
||||||
# Check status for each instance
|
# Get Git settings
|
||||||
|
git_settings = KeyValueSettings.get_value('git_settings')
|
||||||
|
gitea_url = git_settings.get('url') if git_settings else None
|
||||||
|
gitea_token = git_settings.get('token') if git_settings else None
|
||||||
|
gitea_repo = git_settings.get('repo') if git_settings else None
|
||||||
|
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
|
# 1. Check status
|
||||||
status_info = check_instance_status(instance)
|
status_info = check_instance_status(instance)
|
||||||
instance.status = status_info['status']
|
instance.status = status_info['status']
|
||||||
instance.status_details = status_info['details']
|
instance.status_details = status_info['details']
|
||||||
|
|
||||||
|
# 2. Check deployed version
|
||||||
|
deployed_version = None
|
||||||
|
deployed_tag = None
|
||||||
|
deployed_commit = None
|
||||||
|
try:
|
||||||
|
version_url = f"{instance.main_url.rstrip('/')}/api/version"
|
||||||
|
resp = requests.get(version_url, timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
version_data = resp.json()
|
||||||
|
deployed_version = version_data.get('version', 'unknown')
|
||||||
|
deployed_tag = version_data.get('tag', 'unknown')
|
||||||
|
deployed_commit = version_data.get('commit', 'unknown')
|
||||||
|
except Exception as e:
|
||||||
|
deployed_version = None
|
||||||
|
deployed_tag = None
|
||||||
|
deployed_commit = None
|
||||||
|
|
||||||
|
instance.deployed_version = deployed_tag or deployed_version or 'unknown'
|
||||||
|
instance.deployed_branch = instance.deployed_branch or 'master'
|
||||||
|
|
||||||
|
# 3. Check latest version from Gitea (if settings available)
|
||||||
|
latest_version = None
|
||||||
|
deployed_branch = instance.deployed_branch or 'master'
|
||||||
|
if gitea_url and gitea_token and gitea_repo:
|
||||||
|
try:
|
||||||
|
headers = {'Accept': 'application/json', 'Authorization': f'token {gitea_token}'}
|
||||||
|
# Gitea API: /api/v1/repos/{owner}/{repo}/commits/{branch}
|
||||||
|
commit_url = f"{gitea_url}/api/v1/repos/{gitea_repo}/commits/{deployed_branch}"
|
||||||
|
commit_resp = requests.get(commit_url, headers=headers, timeout=5)
|
||||||
|
if commit_resp.status_code == 200:
|
||||||
|
latest_version = commit_resp.json().get('sha')
|
||||||
|
except Exception as e:
|
||||||
|
latest_version = None
|
||||||
|
instance.latest_version = latest_version or 'unknown'
|
||||||
|
instance.version_checked_at = datetime.utcnow()
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Get connection settings
|
|
||||||
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
||||||
nginx_settings = KeyValueSettings.get_value('nginx_settings')
|
nginx_settings = KeyValueSettings.get_value('nginx_settings')
|
||||||
git_settings = KeyValueSettings.get_value('git_settings')
|
|
||||||
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
|
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
|
||||||
|
|
||||||
return render_template('main/instances.html',
|
return render_template('main/instances.html',
|
||||||
@@ -1976,3 +2016,35 @@ def init_routes(main_bp):
|
|||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
return render_template('wiki/base.html')
|
return render_template('wiki/base.html')
|
||||||
|
|
||||||
|
@main_bp.route('/api/version')
|
||||||
|
def api_version():
|
||||||
|
version_file = os.path.join(current_app.root_path, 'version.txt')
|
||||||
|
version = 'unknown'
|
||||||
|
version_data = {}
|
||||||
|
|
||||||
|
if os.path.exists(version_file):
|
||||||
|
with open(version_file, 'r') as f:
|
||||||
|
content = f.read().strip()
|
||||||
|
|
||||||
|
# Try to parse as JSON first (new format)
|
||||||
|
try:
|
||||||
|
version_data = json.loads(content)
|
||||||
|
version = version_data.get('tag', 'unknown')
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Fallback to old format (just commit hash)
|
||||||
|
version = content
|
||||||
|
version_data = {
|
||||||
|
'tag': 'unknown',
|
||||||
|
'commit': content,
|
||||||
|
'branch': 'unknown',
|
||||||
|
'deployed_at': 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'version': version,
|
||||||
|
'tag': version_data.get('tag', 'unknown'),
|
||||||
|
'commit': version_data.get('commit', 'unknown'),
|
||||||
|
'branch': version_data.get('branch', 'unknown'),
|
||||||
|
'deployed_at': version_data.get('deployed_at', 'unknown')
|
||||||
|
})
|
||||||
@@ -546,12 +546,19 @@ async function startLaunch(data) {
|
|||||||
stack_name: stackResult.data.name,
|
stack_name: stackResult.data.name,
|
||||||
status: stackResult.data.status,
|
status: stackResult.data.status,
|
||||||
repository: data.repository,
|
repository: data.repository,
|
||||||
branch: data.branch
|
branch: data.branch,
|
||||||
|
deployed_version: dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown',
|
||||||
|
deployed_branch: data.branch
|
||||||
};
|
};
|
||||||
console.log('Saving instance data:', instanceData);
|
console.log('Saving instance data:', instanceData);
|
||||||
const saveResult = await saveInstanceData(instanceData);
|
const saveResult = await saveInstanceData(instanceData);
|
||||||
console.log('Save result:', saveResult);
|
console.log('Save result:', saveResult);
|
||||||
await updateStep(10, 'Saving Instance Data', 'Instance data saved successfully');
|
|
||||||
|
// Update step with version information
|
||||||
|
const versionInfo = dockerComposeResult.commit_hash ?
|
||||||
|
`Instance data saved successfully. Version: ${dockerComposeResult.commit_hash.substring(0, 8)}` :
|
||||||
|
'Instance data saved successfully';
|
||||||
|
await updateStep(10, 'Saving Instance Data', versionInfo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving instance data:', error);
|
console.error('Error saving instance data:', error);
|
||||||
await updateStep(10, 'Saving Instance Data', `Error: ${error.message}`);
|
await updateStep(10, 'Saving Instance Data', `Error: ${error.message}`);
|
||||||
|
|||||||
@@ -3,6 +3,27 @@
|
|||||||
|
|
||||||
{% block title %}Instances - DocuPulse{% endblock %}
|
{% block title %}Instances - DocuPulse{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.version-info {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.version-info code {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.badge-sm {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
.table td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ header(
|
{{ header(
|
||||||
title="Instances",
|
title="Instances",
|
||||||
@@ -43,6 +64,7 @@
|
|||||||
<th>Payment Plan</th>
|
<th>Payment Plan</th>
|
||||||
<th>Main URL</th>
|
<th>Main URL</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Version</th>
|
||||||
<th>Connection Token</th>
|
<th>Connection Token</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -74,6 +96,44 @@
|
|||||||
{{ instance.status|title }}
|
{{ instance.status|title }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if instance.deployed_version and instance.deployed_version != 'unknown' %}
|
||||||
|
<div class="version-info">
|
||||||
|
<div class="small">
|
||||||
|
<strong>Version:</strong>
|
||||||
|
<code class="text-primary">{{ instance.deployed_version }}</code>
|
||||||
|
</div>
|
||||||
|
{% if instance.latest_version and instance.latest_version != 'unknown' %}
|
||||||
|
<div class="small">
|
||||||
|
<strong>Latest:</strong>
|
||||||
|
<code class="text-secondary">{{ instance.latest_version }}</code>
|
||||||
|
</div>
|
||||||
|
{% if instance.deployed_version == instance.latest_version %}
|
||||||
|
<span class="badge bg-success badge-sm" data-bs-toggle="tooltip" title="Instance is up to date">
|
||||||
|
<i class="fas fa-check"></i> Up-to-date
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning badge-sm" data-bs-toggle="tooltip" title="Instance is outdated">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> Outdated
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary badge-sm" data-bs-toggle="tooltip" title="Latest version unknown">
|
||||||
|
<i class="fas fa-question"></i> Unknown
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.version_checked_at %}
|
||||||
|
<div class="small text-muted">
|
||||||
|
<i class="fas fa-clock"></i> {{ instance.version_checked_at.strftime('%H:%M') }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary" data-bs-toggle="tooltip" title="Version information not available">
|
||||||
|
<i class="fas fa-question"></i> Unknown
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if instance.connection_token %}
|
{% if instance.connection_token %}
|
||||||
<span class="badge bg-success" data-bs-toggle="tooltip" title="Instance is authenticated">
|
<span class="badge bg-success" data-bs-toggle="tooltip" title="Instance is authenticated">
|
||||||
|
|||||||
6
version.txt
Normal file
6
version.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tag": "v1.2.3",
|
||||||
|
"commit": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
|
||||||
|
"branch": "main",
|
||||||
|
"deployed_at": "2024-01-15T10:30:00.000000"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user