diff --git a/README.md b/README.md index 0262d00..8cf8a5a 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ DocuPulse is a powerful document management system designed to streamline docume ### Prerequisites -- Node.js (version 18 or higher) -- npm or yarn +- Python 3.11 or higher +- PostgreSQL 13 or higher +- Docker and Docker Compose (for containerized deployment) ### Installation @@ -23,18 +24,50 @@ cd docupulse 2. Install dependencies: ```bash -npm install -# or -yarn install +pip install -r requirements.txt ``` -3. Start the development server: +3. Set up environment variables: ```bash -npm run dev -# or -yarn dev +# Copy example environment file +cp .env.example .env + +# 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 - Document upload and management @@ -42,6 +75,8 @@ yarn dev - Secure document storage - User authentication and authorization - Document version control +- Multi-tenant instance management +- RESTful API ## Contributing diff --git a/docker-compose.yml b/docker-compose.yml index fd67692..3d01b32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,10 @@ services: - POSTGRES_PASSWORD=docupulse_${PORT:-10335} - POSTGRES_DB=docupulse_${PORT:-10335} - MASTER=${ISMASTER:-false} + - APP_VERSION=${APP_VERSION:-unknown} + - GIT_COMMIT=${GIT_COMMIT:-unknown} + - GIT_BRANCH=${GIT_BRANCH:-unknown} + - DEPLOYED_AT=${DEPLOYED_AT:-unknown} volumes: - docupulse_uploads:/app/uploads depends_on: diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index b35635b..f9d647c 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/launch_api.py b/routes/launch_api.py index 24894a5..323bd32 100644 --- a/routes/launch_api.py +++ b/routes/launch_api.py @@ -761,48 +761,6 @@ def download_docker_compose(): else: 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({ 'success': True, 'content': content, diff --git a/routes/main.py b/routes/main.py index 838ef6e..37a3e4f 100644 --- a/routes/main.py +++ b/routes/main.py @@ -1983,32 +1983,16 @@ def init_routes(main_bp): @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' - } + # Get version information from environment variables + version = os.getenv('APP_VERSION', 'unknown') + commit = os.getenv('GIT_COMMIT', 'unknown') + branch = os.getenv('GIT_BRANCH', 'unknown') + deployed_at = os.getenv('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') + 'tag': version, + 'commit': commit, + 'branch': branch, + 'deployed_at': deployed_at }) \ No newline at end of file diff --git a/set_version.py b/set_version.py new file mode 100644 index 0000000..97abb44 --- /dev/null +++ b/set_version.py @@ -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() \ No newline at end of file diff --git a/static/js/launch_progress.js b/static/js/launch_progress.js index 7fa9022..af34ca8 100644 --- a/static/js/launch_progress.js +++ b/static/js/launch_progress.js @@ -451,6 +451,11 @@ async function startLaunch(data) { 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 const dockerComposeStep = document.querySelectorAll('.step-item')[7]; dockerComposeStep.classList.remove('active'); @@ -2551,6 +2556,22 @@ async function deployStack(dockerComposeContent, stackName, port) { { name: 'ISMASTER', 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) { 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 { - success: false, - error: error.message - }; - } - return { success: false, - error: error.message || 'Unknown error during stack deployment' + error: error.message }; } } \ No newline at end of file diff --git a/templates/main/instances.html b/templates/main/instances.html index b9aad53..76a7b47 100644 --- a/templates/main/instances.html +++ b/templates/main/instances.html @@ -8,6 +8,38 @@ .table td { 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; + } {% endblock %} @@ -51,6 +83,8 @@ Payment Plan Main URL Status + Version + Branch Connection Token Actions @@ -82,6 +116,26 @@ {{ instance.status|title }} + + {% if instance.deployed_version %} + + {{ instance.deployed_version[:8] if instance.deployed_version != 'unknown' else 'unknown' }} + + {% else %} + unknown + {% endif %} + + + {% if instance.deployed_branch %} + + {{ instance.deployed_branch }} + + {% else %} + unknown + {% endif %} + {% if instance.connection_token %} @@ -809,19 +863,19 @@ async function fetchInstanceStats(instanceUrl, instanceId, jwtToken) { 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)'); if (roomsCell) { roomsCell.textContent = data.rooms || '0'; } - // Update conversations count + // Update conversations count (4th column) const conversationsCell = row.querySelector('td:nth-child(4)'); if (conversationsCell) { conversationsCell.textContent = data.conversations || '0'; } - // Update data usage + // Update data usage (5th column) const dataCell = row.querySelector('td:nth-child(5)'); if (dataCell) { 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 - const urlCell = row.querySelector('td:nth-child(7)'); + // 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}`); @@ -1056,8 +1110,8 @@ async function editInstance(id) { // Get the name from the first cell const name = row.querySelector('td:first-child').textContent.trim(); - // Get the main URL from the link in the URL cell (7th column) - const urlCell = row.querySelector('td:nth-child(7)'); + // 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(9)'); const urlLink = urlCell.querySelector('a'); const mainUrl = urlLink ? urlLink.getAttribute('href') : urlCell.textContent.trim(); diff --git a/templates/wiki/tabs/deployment.html b/templates/wiki/tabs/deployment.html index b37834c..ede1b58 100644 --- a/templates/wiki/tabs/deployment.html +++ b/templates/wiki/tabs/deployment.html @@ -297,6 +297,67 @@ + + +
+
Version Management
+ + +
+
+
+ Version Tracking +
+
+
+
+
+
Environment Variables
+
    +
  • APP_VERSION - Application version/tag
  • +
  • GIT_COMMIT - Git commit hash
  • +
  • GIT_BRANCH - Git branch name
  • +
  • DEPLOYED_AT - Deployment timestamp
  • +
+
+
+
Database Storage
+
    +
  • • Instance version tracking
  • +
  • • Version comparison
  • +
  • • Update notifications
  • +
  • • Deployment history
  • +
+
+
+
+
+ + +
+
+
+ Version API +
+
+
+
+
Endpoint
+ GET /api/version +
+
+
Response
+
{
+  "version": "v1.2.3",
+  "tag": "v1.2.3",
+  "commit": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
+  "branch": "main",
+  "deployed_at": "2024-01-15T10:30:00.000000"
+}
+
+
+
+
diff --git a/test_version_api.py b/test_version_api.py new file mode 100644 index 0000000..a824305 --- /dev/null +++ b/test_version_api.py @@ -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() \ No newline at end of file diff --git a/version.txt b/version.txt deleted file mode 100644 index e1fa3e4..0000000 --- a/version.txt +++ /dev/null @@ -1,6 +0,0 @@ -{ - "tag": "v1.2.3", - "commit": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", - "branch": "main", - "deployed_at": "2024-01-15T10:30:00.000000" -} \ No newline at end of file