version v2

This commit is contained in:
2025-06-23 14:11:11 +02:00
parent 1a6741ec10
commit 1370bef1f1
11 changed files with 315 additions and 115 deletions

View File

@@ -10,8 +10,9 @@ DocuPulse is a powerful document management system designed to streamline docume
### Prerequisites ### Prerequisites
- Node.js (version 18 or higher) - Python 3.11 or higher
- npm or yarn - PostgreSQL 13 or higher
- Docker and Docker Compose (for containerized deployment)
### Installation ### Installation
@@ -23,18 +24,50 @@ cd docupulse
2. Install dependencies: 2. Install dependencies:
```bash ```bash
npm install pip install -r requirements.txt
# or
yarn install
``` ```
3. Start the development server: 3. Set up environment variables:
```bash ```bash
npm run dev # Copy example environment file
# or cp .env.example .env
yarn dev
# Set version information for local development
python set_version.py
``` ```
4. Initialize the database:
```bash
flask db upgrade
flask create-admin
```
5. Start the development server:
```bash
python app.py
```
## Version Tracking
DocuPulse uses a database-only approach for version tracking:
- **Environment Variables**: Version information is passed via environment variables (`APP_VERSION`, `GIT_COMMIT`, `GIT_BRANCH`, `DEPLOYED_AT`)
- **Database Storage**: Instance version information is stored in the `instances` table
- **API Endpoint**: Version information is available via `/api/version`
### Setting Version Information
For local development:
```bash
python set_version.py
```
For production deployments, set the following environment variables:
- `APP_VERSION`: Application version/tag
- `GIT_COMMIT`: Git commit hash
- `GIT_BRANCH`: Git branch name
- `DEPLOYED_AT`: Deployment timestamp
## Features ## Features
- Document upload and management - Document upload and management
@@ -42,6 +75,8 @@ yarn dev
- Secure document storage - Secure document storage
- User authentication and authorization - User authentication and authorization
- Document version control - Document version control
- Multi-tenant instance management
- RESTful API
## Contributing ## Contributing

View File

@@ -21,6 +21,10 @@ services:
- POSTGRES_PASSWORD=docupulse_${PORT:-10335} - POSTGRES_PASSWORD=docupulse_${PORT:-10335}
- POSTGRES_DB=docupulse_${PORT:-10335} - POSTGRES_DB=docupulse_${PORT:-10335}
- MASTER=${ISMASTER:-false} - MASTER=${ISMASTER:-false}
- APP_VERSION=${APP_VERSION:-unknown}
- GIT_COMMIT=${GIT_COMMIT:-unknown}
- GIT_BRANCH=${GIT_BRANCH:-unknown}
- DEPLOYED_AT=${DEPLOYED_AT:-unknown}
volumes: volumes:
- docupulse_uploads:/app/uploads - docupulse_uploads:/app/uploads
depends_on: depends_on:

View File

@@ -761,48 +761,6 @@ 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,

View File

@@ -1983,32 +1983,16 @@ def init_routes(main_bp):
@main_bp.route('/api/version') @main_bp.route('/api/version')
def api_version(): def api_version():
version_file = os.path.join(current_app.root_path, 'version.txt') # Get version information from environment variables
version = 'unknown' version = os.getenv('APP_VERSION', 'unknown')
version_data = {} commit = os.getenv('GIT_COMMIT', 'unknown')
branch = os.getenv('GIT_BRANCH', 'unknown')
if os.path.exists(version_file): deployed_at = os.getenv('DEPLOYED_AT', 'unknown')
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({ return jsonify({
'version': version, 'version': version,
'tag': version_data.get('tag', 'unknown'), 'tag': version,
'commit': version_data.get('commit', 'unknown'), 'commit': commit,
'branch': version_data.get('branch', 'unknown'), 'branch': branch,
'deployed_at': version_data.get('deployed_at', 'unknown') 'deployed_at': deployed_at
}) })

59
set_version.py Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Utility script to set version environment variables for local development.
This replaces the need for version.txt file creation.
"""
import os
import subprocess
import json
from datetime import datetime
def get_git_info():
"""Get current git commit hash and branch"""
try:
# Get current commit hash
commit_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD'],
text=True, stderr=subprocess.DEVNULL).strip()
# Get current branch
branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
text=True, stderr=subprocess.DEVNULL).strip()
# Get latest tag
try:
latest_tag = subprocess.check_output(['git', 'describe', '--tags', '--abbrev=0'],
text=True, stderr=subprocess.DEVNULL).strip()
except subprocess.CalledProcessError:
latest_tag = 'unknown'
return {
'commit': commit_hash,
'branch': branch,
'tag': latest_tag
}
except (subprocess.CalledProcessError, FileNotFoundError):
return {
'commit': 'unknown',
'branch': 'unknown',
'tag': 'unknown'
}
def set_version_env():
"""Set version environment variables"""
git_info = get_git_info()
# Set environment variables
os.environ['APP_VERSION'] = git_info['tag']
os.environ['GIT_COMMIT'] = git_info['commit']
os.environ['GIT_BRANCH'] = git_info['branch']
os.environ['DEPLOYED_AT'] = datetime.utcnow().isoformat()
print("Version environment variables set:")
print(f"APP_VERSION: {os.environ['APP_VERSION']}")
print(f"GIT_COMMIT: {os.environ['GIT_COMMIT']}")
print(f"GIT_BRANCH: {os.environ['GIT_BRANCH']}")
print(f"DEPLOYED_AT: {os.environ['DEPLOYED_AT']}")
if __name__ == '__main__':
set_version_env()

View File

@@ -451,6 +451,11 @@ async function startLaunch(data) {
throw new Error(dockerComposeResult.error || 'Failed to download docker-compose.yml'); throw new Error(dockerComposeResult.error || 'Failed to download docker-compose.yml');
} }
// Set global version variables for deployment
window.currentDeploymentVersion = dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown';
window.currentDeploymentCommit = dockerComposeResult.commit_hash || 'unknown';
window.currentDeploymentBranch = data.branch;
// Update the step to show success // Update the step to show success
const dockerComposeStep = document.querySelectorAll('.step-item')[7]; const dockerComposeStep = document.querySelectorAll('.step-item')[7];
dockerComposeStep.classList.remove('active'); dockerComposeStep.classList.remove('active');
@@ -2551,6 +2556,22 @@ async function deployStack(dockerComposeContent, stackName, port) {
{ {
name: 'ISMASTER', name: 'ISMASTER',
value: 'false' value: 'false'
},
{
name: 'APP_VERSION',
value: window.currentDeploymentVersion || 'unknown'
},
{
name: 'GIT_COMMIT',
value: window.currentDeploymentCommit || 'unknown'
},
{
name: 'GIT_BRANCH',
value: window.currentDeploymentBranch || 'unknown'
},
{
name: 'DEPLOYED_AT',
value: new Date().toISOString()
} }
] ]
}), }),
@@ -2573,34 +2594,9 @@ async function deployStack(dockerComposeContent, stackName, port) {
} catch (error) { } catch (error) {
console.error('Error deploying stack:', error); console.error('Error deploying stack:', error);
// Check if this is a timeout error
if (error.name === 'AbortError') {
return {
success: false,
error: 'Stack deployment timed out after 10 minutes. The operation may still be in progress.'
};
}
// Check if this is a network error
if (error.message && error.message.includes('fetch')) {
return {
success: false,
error: 'Network error during stack deployment. Please check your connection and try again.'
};
}
// Check if this is a response error
if (error.message && error.message.includes('Failed to deploy stack')) {
return { return {
success: false, success: false,
error: error.message error: error.message
}; };
} }
return {
success: false,
error: error.message || 'Unknown error during stack deployment'
};
}
} }

View File

@@ -8,6 +8,38 @@
.table td { .table td {
vertical-align: middle; vertical-align: middle;
} }
/* Version column styling */
.version-badge {
font-family: monospace;
font-size: 0.85em;
}
.branch-badge {
font-size: 0.85em;
}
/* Make table responsive */
.table-responsive {
overflow-x: auto;
}
/* Tooltip styling for version info */
.tooltip-inner {
max-width: 300px;
text-align: left;
}
/* Version comparison styling */
.version-outdated {
background-color: #fff3cd !important;
border-color: #ffeaa7 !important;
}
.version-current {
background-color: #d1ecf1 !important;
border-color: #bee5eb !important;
}
</style> </style>
{% endblock %} {% endblock %}
@@ -51,6 +83,8 @@
<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>Branch</th>
<th>Connection Token</th> <th>Connection Token</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
@@ -82,6 +116,26 @@
{{ instance.status|title }} {{ instance.status|title }}
</span> </span>
</td> </td>
<td>
{% if instance.deployed_version %}
<span class="badge bg-info version-badge" data-bs-toggle="tooltip"
title="Deployed: {{ instance.deployed_version }}{% if instance.version_checked_at %}<br>Checked: {{ instance.version_checked_at.strftime('%Y-%m-%d %H:%M') }}{% endif %}">
{{ instance.deployed_version[:8] if instance.deployed_version != 'unknown' else 'unknown' }}
</span>
{% else %}
<span class="badge bg-secondary version-badge">unknown</span>
{% 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 }}
</span>
{% else %}
<span class="badge bg-secondary branch-badge">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">
@@ -809,19 +863,19 @@ async function fetchInstanceStats(instanceUrl, instanceId, jwtToken) {
const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr'); const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr');
// Update rooms count // Update rooms count (3rd column)
const roomsCell = row.querySelector('td:nth-child(3)'); const roomsCell = row.querySelector('td:nth-child(3)');
if (roomsCell) { if (roomsCell) {
roomsCell.textContent = data.rooms || '0'; roomsCell.textContent = data.rooms || '0';
} }
// Update conversations count // Update conversations count (4th column)
const conversationsCell = row.querySelector('td:nth-child(4)'); const conversationsCell = row.querySelector('td:nth-child(4)');
if (conversationsCell) { if (conversationsCell) {
conversationsCell.textContent = data.conversations || '0'; conversationsCell.textContent = data.conversations || '0';
} }
// Update data usage // Update data usage (5th column)
const dataCell = row.querySelector('td:nth-child(5)'); const dataCell = row.querySelector('td:nth-child(5)');
if (dataCell) { if (dataCell) {
const dataSize = data.total_storage || 0; const dataSize = data.total_storage || 0;
@@ -940,8 +994,8 @@ async function fetchCompanyNames() {
})) }))
}); });
// Changed from nth-child(8) to nth-child(7) since Main URL is the 7th column // Main URL is now the 9th column (after adding Version and Branch columns)
const urlCell = row.querySelector('td:nth-child(7)'); const urlCell = row.querySelector('td:nth-child(9)');
if (!urlCell) { if (!urlCell) {
console.error(`Could not find URL cell for instance ${instanceId}`); console.error(`Could not find URL cell for instance ${instanceId}`);
@@ -1056,8 +1110,8 @@ async function editInstance(id) {
// Get the name from the first cell // Get the name from the first cell
const name = row.querySelector('td:first-child').textContent.trim(); const name = row.querySelector('td:first-child').textContent.trim();
// Get the main URL from the link in the URL cell (7th column) // Get the main URL from the link in the URL cell (9th column after adding Version and Branch)
const urlCell = row.querySelector('td:nth-child(7)'); const urlCell = row.querySelector('td:nth-child(9)');
const urlLink = urlCell.querySelector('a'); const urlLink = urlCell.querySelector('a');
const mainUrl = urlLink ? urlLink.getAttribute('href') : urlCell.textContent.trim(); const mainUrl = urlLink ? urlLink.getAttribute('href') : urlCell.textContent.trim();

View File

@@ -297,6 +297,67 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Version Management -->
<div class="mb-5">
<h5 style="color: var(--primary-color);" class="mb-4">Version Management</h5>
<!-- Version Tracking -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white">
<h6 class="mb-0" style="color: var(--primary-color);">
<i class="fas fa-tags me-2"></i>Version Tracking
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<h6 class="text-muted mb-2">Environment Variables</h6>
<ul class="list-unstyled small">
<li class="mb-1"><code>APP_VERSION</code> - Application version/tag</li>
<li class="mb-1"><code>GIT_COMMIT</code> - Git commit hash</li>
<li class="mb-1"><code>GIT_BRANCH</code> - Git branch name</li>
<li class="mb-1"><code>DEPLOYED_AT</code> - Deployment timestamp</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">Database Storage</h6>
<ul class="list-unstyled small">
<li class="mb-1">• Instance version tracking</li>
<li class="mb-1">• Version comparison</li>
<li class="mb-1">• Update notifications</li>
<li class="mb-1">• Deployment history</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Version API -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white">
<h6 class="mb-0" style="color: var(--primary-color);">
<i class="fas fa-code me-2"></i>Version API
</h6>
</div>
<div class="card-body">
<div class="mb-3">
<h6 class="text-muted mb-2">Endpoint</h6>
<code class="bg-light p-2 rounded d-block">GET /api/version</code>
</div>
<div class="mb-3">
<h6 class="text-muted mb-2">Response</h6>
<pre class="bg-light p-2 rounded small"><code>{
"version": "v1.2.3",
"tag": "v1.2.3",
"commit": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
"branch": "main",
"deployed_at": "2024-01-15T10:30:00.000000"
}</code></pre>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Sidebar --> <!-- Sidebar -->

55
test_version_api.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""
Test script for the new version API endpoint.
This verifies that the database-only version tracking works correctly.
"""
import os
import requests
import json
from datetime import datetime
def test_version_api():
"""Test the version API endpoint"""
# Set test environment variables
os.environ['APP_VERSION'] = 'v1.2.3'
os.environ['GIT_COMMIT'] = 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0'
os.environ['GIT_BRANCH'] = 'main'
os.environ['DEPLOYED_AT'] = datetime.utcnow().isoformat()
print("Testing version API endpoint...")
print(f"APP_VERSION: {os.environ['APP_VERSION']}")
print(f"GIT_COMMIT: {os.environ['GIT_COMMIT']}")
print(f"GIT_BRANCH: {os.environ['GIT_BRANCH']}")
print(f"DEPLOYED_AT: {os.environ['DEPLOYED_AT']}")
try:
# Test the API endpoint (assuming it's running on localhost:5000)
response = requests.get('http://localhost:5000/api/version')
if response.status_code == 200:
data = response.json()
print("\n✅ Version API test successful!")
print("Response:")
print(json.dumps(data, indent=2))
# Verify the response matches our environment variables
assert data['version'] == os.environ['APP_VERSION'], f"Version mismatch: {data['version']} != {os.environ['APP_VERSION']}"
assert data['commit'] == os.environ['GIT_COMMIT'], f"Commit mismatch: {data['commit']} != {os.environ['GIT_COMMIT']}"
assert data['branch'] == os.environ['GIT_BRANCH'], f"Branch mismatch: {data['branch']} != {os.environ['GIT_BRANCH']}"
assert data['deployed_at'] == os.environ['DEPLOYED_AT'], f"Deployed at mismatch: {data['deployed_at']} != {os.environ['DEPLOYED_AT']}"
print("\n✅ All version information matches environment variables!")
else:
print(f"\n❌ Version API test failed with status code: {response.status_code}")
print(f"Response: {response.text}")
except requests.exceptions.ConnectionError:
print("\n❌ Could not connect to the API. Make sure the application is running on localhost:5000")
except Exception as e:
print(f"\n❌ Test failed with error: {str(e)}")
if __name__ == '__main__':
test_version_api()

View File

@@ -1,6 +0,0 @@
{
"tag": "v1.2.3",
"commit": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
"branch": "main",
"deployed_at": "2024-01-15T10:30:00.000000"
}