Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
173 commits
Select commit Hold shift + click to select a range
e15eb3b
Templates: Add upload status indicators to index
simmsa Feb 16, 2026
95bfb5c
Service: Use SSE to list files for upload vs cache
simmsa Feb 16, 2026
acc3574
JS: Track folder/files/jobs in state
simmsa Feb 16, 2026
ec46e36
JS: Add toggles/ui for file listing/upload checking
simmsa Feb 16, 2026
789e630
Routes: Send less to browser for upload
simmsa Feb 16, 2026
59c9429
Templates; Add more columns to tables in place of more rows
simmsa Feb 16, 2026
fd0ca25
Service: Sends less data to browser from upload manager
simmsa Feb 16, 2026
9d7fe1b
Analysis: Send smaller updates to the browser
simmsa Feb 16, 2026
8eb2a75
JS: Allow different truncation styles of folders
simmsa Feb 16, 2026
1a141b9
Upload: Show at max 100 rows per page
simmsa Feb 16, 2026
cc034bd
JS: Add missing import
simmsa Feb 16, 2026
6be3766
Tests: Update completion summary test
simmsa Feb 16, 2026
7893d37
Tests: Remove files from upload api test
simmsa Feb 16, 2026
5a91a55
Cache: add info for uploaded files
simmsa Feb 17, 2026
c6399f2
S3: Set multipart threshold upload to 1GB
simmsa Feb 17, 2026
abbb2f9
Upload: Add the ability to skip folders or files
simmsa Feb 17, 2026
8a78d68
Logs: Add more details about csv data
simmsa Feb 17, 2026
35530ce
Route: Add exclusions to upload
simmsa Feb 17, 2026
65ac829
Settings: Add overall success message
simmsa Feb 17, 2026
26a97a1
File Manager: Add/leverage caching
simmsa Feb 17, 2026
3a1449a
Route: Add logs upload stats
simmsa Feb 17, 2026
93a74a6
Tests: Update routes for reactjs changes
simmsa Feb 17, 2026
bd84e27
Routes: Refactor main for React SPA
simmsa Feb 17, 2026
78138ad
Config: Rebuild npm on update
simmsa Feb 17, 2026
5ffa127
App: Add delete api route
simmsa Feb 17, 2026
bc741ed
App: Add delete route
simmsa Feb 17, 2026
d88eab1
Service: Add delete manager service
simmsa Feb 17, 2026
b11d77b
Ignore: Add frontend reactjs tsx ignore
simmsa Feb 17, 2026
09355e5
Ignore: Add more typescript/vite options
simmsa Feb 17, 2026
ee6431f
Frontend: Init package
simmsa Feb 17, 2026
043d7c4
Frontend: Enable linting
simmsa Feb 17, 2026
6135376
Frontend: Configure typescript
simmsa Feb 17, 2026
ce06ec9
Frontend: Configure vite
simmsa Feb 17, 2026
3dce849
Frontend: Add entrypoint index
simmsa Feb 17, 2026
45d0d3a
Frontend: Add NLR assets
simmsa Feb 17, 2026
cb65295
Frontend: Add baseline NLR/tailwind css
simmsa Feb 17, 2026
7ac629c
Frontend: Add tsx entry point
simmsa Feb 17, 2026
fd8ac4d
Frontend: Add api request/response functions
simmsa Feb 17, 2026
7f2e2ae
Frontend: Add byte formatters
simmsa Feb 17, 2026
1026b7b
Utils: add transfer speed formatters
simmsa Feb 17, 2026
3db7e58
Frontend: Add time/duration formatters
simmsa Feb 17, 2026
4db7dd4
Frontend: Add date formatters
simmsa Feb 17, 2026
8aed27c
Utils: Add csv download helper
simmsa Feb 17, 2026
aed4975
Frontend: Add common icons entry point
simmsa Feb 17, 2026
f23e03b
Frontend: Add alert banner component
simmsa Feb 17, 2026
1942686
Frontend: Add breadcrumb component
simmsa Feb 17, 2026
dca3fef
Frontend: Add modal component
simmsa Feb 17, 2026
fe13d4e
Frontend: Add Notification component
simmsa Feb 17, 2026
ba0d70a
Frontend: Add progress bar component
simmsa Feb 17, 2026
5cb15d5
Frontend: Add spinner icon
simmsa Feb 17, 2026
8cead7f
Frontend: Add spinner component
simmsa Feb 17, 2026
7258d92
Frontend: add SortableHeader component
simmsa Feb 17, 2026
8327c08
Frontend: Add StatCard component
simmsa Feb 17, 2026
fe237b1
Frontend: Add Stepper component
simmsa Feb 17, 2026
b05db5d
Frontend: Add app version store
simmsa Feb 17, 2026
a99b861
Frontend: Add file Store
simmsa Feb 17, 2026
43a4e20
Frontend: Add upload state store
simmsa Feb 17, 2026
c9114a9
Frontend: Add delete state store
simmsa Feb 17, 2026
4fbde71
Frontend: Define types for API data passing
simmsa Feb 17, 2026
d6ee4cd
Frontend: Add types for delete state passing with backend
simmsa Feb 17, 2026
4702472
Frontend: Add types for upload state passing
simmsa Feb 17, 2026
6347bb3
Frontend: Add common debounce hook
simmsa Feb 17, 2026
f7ed73a
Frontend: Add folderScan hook
simmsa Feb 17, 2026
dafe76a
Frontend: Add sorting hook
simmsa Feb 17, 2026
204cd85
Frontend: Add SSE hooks
simmsa Feb 17, 2026
cc2d6fa
Frontend: Add upload job hooks
simmsa Feb 17, 2026
0be6842
Frontend: Add pagination hook
simmsa Feb 17, 2026
bec1ba8
Frontend: Add file store hooks
simmsa Feb 17, 2026
c30b616
Frontend: Add delete job hooks
simmsa Feb 17, 2026
8d576a1
Frontend: Add delete scan hooks
simmsa Feb 17, 2026
bcf3ab1
Frontend: add plus icon
simmsa Feb 17, 2026
689c1d6
Clean: Remove S3 file search
simmsa Feb 17, 2026
1353808
Frontend: Add File List component
simmsa Feb 17, 2026
e2b0acd
Frontend: Add S3Browser component
simmsa Feb 17, 2026
cd95957
Frontend: Add NLR Branded components
simmsa Feb 17, 2026
02edd19
Frontend: Add application about modal
simmsa Feb 17, 2026
214048b
Frontend: Add Folder Browser Component
simmsa Feb 17, 2026
b13abdf
Frontend: Add UnifiedFileTable component
simmsa Feb 17, 2026
df475ec
Frontend: Add upload Stepper component
simmsa Feb 17, 2026
e5919a2
Frontend: Add ConfirmModal component
simmsa Feb 17, 2026
04585f8
Frontend: Add review toolbar component
simmsa Feb 17, 2026
dce194b
Frontend: Add icons for cloud and close/open
simmsa Feb 17, 2026
b680d6f
Frontend: Use lucide progress icons
simmsa Feb 17, 2026
5bea536
Frontend: Add lucide cloud icon
simmsa Feb 17, 2026
13b8b66
Frontend: Add Upload footer and header components
simmsa Feb 17, 2026
e8f8180
Frontend: Add DeleteStepper component
simmsa Feb 17, 2026
6cc7b5d
Frontend: Add DeleteConfirmation component
simmsa Feb 17, 2026
8111e47
Frontend: Add Settings components
simmsa Feb 17, 2026
db2e85b
Frontend: Add upload page leveraging components
simmsa Feb 17, 2026
a04ef95
Frontend: Add Settings page leveraging components
simmsa Feb 17, 2026
fcf6c0e
Frontend: Add S3 file browser page
simmsa Feb 17, 2026
1e14d47
Frontend: Add delete page
simmsa Feb 17, 2026
b980267
Frontend: Add Logs page
simmsa Feb 17, 2026
1ccf1ec
Refactor: Remove deprecated jinja templates
simmsa Feb 18, 2026
f99be6f
Refactor: Remove migrated assets
simmsa Feb 18, 2026
f33de36
Refactor: Remove old js tests
simmsa Feb 18, 2026
3dc1705
Refactor: Delete non react js modules
simmsa Feb 18, 2026
5c4d805
Refactor: Remove legacy js configuration
simmsa Feb 18, 2026
e134ed3
Frontend: Add dev testing packages
simmsa Feb 18, 2026
199a385
Frontend: Ignore _* unused variables
simmsa Feb 18, 2026
b8409c7
Delete: Add folder/file exclusion
simmsa Feb 18, 2026
34558c6
Frontent: Wrap refs in use effect
simmsa Feb 18, 2026
964eec8
Frontend: Add is cancelling state to upload
simmsa Feb 18, 2026
d8a52f0
Frontend: Add exclusions to delete scan
simmsa Feb 18, 2026
84c19d3
Frontend: Add isCancelling state to delete page
simmsa Feb 18, 2026
4745ece
Frontend: Add Cancel Confirmation modal
simmsa Feb 18, 2026
4ec3d5b
Frontend: Add is cancelling state to upload
simmsa Feb 18, 2026
174448f
Frontend: Add refresh button to FolderBrowser
simmsa Feb 18, 2026
3772592
Frontend: Add more context to uploaded success messages
simmsa Feb 18, 2026
e5f133c
Frontend: Ignore useVirtualizer type error
simmsa Feb 18, 2026
77b93c6
Frontend: Use darker green for success
simmsa Feb 18, 2026
40a2ba8
Frontend: Add link to uploaded files in delete screen
simmsa Feb 18, 2026
5723e0b
Frontend: Add is delete hooks for cancelling state
simmsa Feb 18, 2026
6ff0f9b
Frontend: Remove unused icon sizes const
simmsa Feb 18, 2026
14c8f2a
Frontend: Add basic react/dom tests
simmsa Feb 18, 2026
8fc4c63
Ignore: Don't ignore nested logs dirs
simmsa Feb 18, 2026
74d17f7
Frontend: Add CSV Preview component
simmsa Feb 18, 2026
8669060
Frontend: Add logs filter bar component
simmsa Feb 18, 2026
d69e04c
Frontend: Add logs stats bar component
simmsa Feb 18, 2026
68e6d90
Frontend: Add logs table component
simmsa Feb 18, 2026
93a26a4
Frontend: Add Upload List component
simmsa Feb 18, 2026
877ea27
Frontend: Add upload stats bar
simmsa Feb 18, 2026
df4e074
Backend: Use os.walk for error tolerant file tree walking
simmsa Feb 18, 2026
270b5fd
Backend: Add /media/m2 and /media to preset paths
simmsa Feb 18, 2026
1b63c27
Refactor: Validate and upload file by file
simmsa Feb 18, 2026
452df17
Backend: Send completion messages immediately during upload
simmsa Feb 18, 2026
925550b
Frontend: Improve UI/functionality of upload page
simmsa Feb 18, 2026
37f9897
Fix: Show individual rows as completed after they are done uploading
simmsa Feb 18, 2026
0bdea63
Linux: Add exfat4 mount permissions script
simmsa Feb 18, 2026
3cf2680
Linux: Point to correct modaq logo location
simmsa Feb 18, 2026
4c2f802
Backend: Add delete fix permissions end point
simmsa Feb 18, 2026
4de3600
Backend: Send writeable status to the frontend
simmsa Feb 18, 2026
ef9c338
Frontend: Add Lock icon
simmsa Feb 18, 2026
1b4bb36
Frontend: Add Fix permissions flow for delete
simmsa Feb 18, 2026
3359dc7
Fix: Actually stop upload when user requests cancellation
simmsa Feb 18, 2026
b85851b
Feature: Add graceful shutdown button
simmsa Feb 18, 2026
42d5faa
Fix: Make graceful shutdown work when ran via gunicorn
simmsa Feb 18, 2026
45bdd01
Backend: Refactor to use batch processing
simmsa Mar 2, 2026
fc545ee
Dev: Add psutil requirement
simmsa Mar 2, 2026
fe3b802
Config: Add batch processing spec
simmsa Mar 2, 2026
e4c45b6
Backend: Handle delete in 1k file batches
simmsa Mar 2, 2026
cc23c6a
Dev: Lint
simmsa Mar 2, 2026
7b4d930
Backend: Do a smart merge of settings from different locations
simmsa Mar 2, 2026
094945e
Backend: Refactor upload to be more efficient
simmsa Mar 2, 2026
6b38249
Backend: Handle delete in batches
simmsa Mar 2, 2026
5691b0b
Backend: Add additional information to logs
simmsa Mar 2, 2026
7c43ebc
Backend: Extract mcap dates from filename in "fast mode"
simmsa Mar 2, 2026
acadc65
Backend: Refactor upload with skip validation and more correct pendin…
simmsa Mar 2, 2026
8e00116
Frontend: Add hook support for folder scanStart
simmsa Mar 2, 2026
41381b6
Front: Update useSSE with heartbeat detection
simmsa Mar 2, 2026
da5eabb
Frontend: Add Settings for performance tuning
simmsa Mar 2, 2026
42e938d
Frontend: Add list of active files
simmsa Mar 2, 2026
c0e75bc
Frontend: Add new batch progress component
simmsa Mar 2, 2026
9d910cc
Frontend: Add full screen cancel modal that locks screen
simmsa Mar 2, 2026
3426624
Frontend: Add scan progress modal
simmsa Mar 2, 2026
1562209
Frontend: Add data source provenance to file derived settings
simmsa Mar 2, 2026
e10e0dd
Frontend: Add analyzing progress to file table
simmsa Mar 2, 2026
a0fee3c
Frontend: Refactor upload header with more actionable KPI
simmsa Mar 2, 2026
41862ff
Frontend: Add batching support to useUploadJob
simmsa Mar 2, 2026
7cd2158
Frontend: Add PerformanceSection to Settings
simmsa Mar 2, 2026
043b910
Frontend: Wire in batching to upload page
simmsa Mar 2, 2026
1a0f3ad
Frontend: Add batching support to uploadStore
simmsa Mar 2, 2026
bd74dc4
Frontend: Add batching and performance types
simmsa Mar 2, 2026
e102fd6
Frontend: Add settings provenance types
simmsa Mar 2, 2026
1065310
Settings: Add default batch processing settings
simmsa Mar 2, 2026
79d1dd1
Tests: Add logs and infra tests
simmsa Mar 2, 2026
792d3f7
Backend: Add sse registry/cleanup to init.py
simmsa Mar 2, 2026
c513777
Fix: Don't cancel futures
simmsa Mar 2, 2026
862b489
Actions: Set js tests working dir to frontend
simmsa Mar 2, 2026
732b1e0
Pre Commit: Skip json files with comments
simmsa Mar 2, 2026
4f6ccc3
Dev: Add psutil type stubs
simmsa Mar 2, 2026
210d152
Release: Bump to 1.0.0
simmsa Mar 2, 2026
355d68d
Tests: Add shell html index for flask to test against
simmsa Mar 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ jobs:
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm ci
working-directory: frontend

- name: Lint, type check, and test
run: npm run check
working-directory: frontend
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ settings.json
modaq_upload_cache.db

# Log files
logs/
/logs/

# Python
__pycache__/
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repos:
- id: check-merge-conflict
- id: check-yaml
- id: check-json
exclude: ^frontend/tsconfig\.
- id: no-commit-to-branch
args: [--branch, main]

Expand Down
53 changes: 53 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,56 @@
"""Flask application factory for MCAP S3 Uploader."""

import atexit
import os
import threading

from flask import Flask

from app.config import get_package_version, get_settings

# Background cleanup thread control
_cleanup_thread: threading.Thread | None = None
_cleanup_stop_event = threading.Event()


def _sse_cleanup_worker() -> None:
"""Background worker that periodically cleans up stale SSE queues."""
from app.routes.upload import _cleanup_old_sse_queues

while not _cleanup_stop_event.wait(timeout=300): # Check every 5 minutes
try:
removed = _cleanup_old_sse_queues()
if removed > 0:
from app.services.log_service import get_log_service

log = get_log_service()
log.info(
"sse",
"sse_cleanup",
f"Cleaned up {removed} stale SSE queues",
{"queues_removed": removed},
)
except Exception:
# Don't crash the cleanup thread on errors
pass


def _start_sse_cleanup() -> None:
"""Start the background SSE cleanup thread."""
global _cleanup_thread
if _cleanup_thread is None:
_cleanup_thread = threading.Thread(
target=_sse_cleanup_worker, daemon=True, name="SSECleanup"
)
_cleanup_thread.start()


def _stop_sse_cleanup() -> None:
"""Stop the background SSE cleanup thread."""
_cleanup_stop_event.set()
if _cleanup_thread:
_cleanup_thread.join(timeout=2.0)


def create_app() -> Flask:
"""Create and configure the Flask application."""
Expand All @@ -25,6 +70,7 @@ def inject_display_name() -> dict[str, str]:
return {"display_name": settings.display_name}

# Register blueprints
from app.routes.delete import delete_bp
from app.routes.files import files_bp
from app.routes.logs import logs_bp
from app.routes.main import main_bp
Expand All @@ -36,6 +82,13 @@ def inject_display_name() -> dict[str, str]:
app.register_blueprint(files_bp, url_prefix="/api/files")
app.register_blueprint(settings_bp, url_prefix="/api/settings")
app.register_blueprint(logs_bp, url_prefix="/api/logs")
app.register_blueprint(delete_bp, url_prefix="/api/delete")

# Start background SSE cleanup thread
_start_sse_cleanup()

# Register cleanup on shutdown
atexit.register(_stop_sse_cleanup)

# Log application startup
from app.services.log_service import get_log_service
Expand Down
79 changes: 64 additions & 15 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class Settings:

_instance: "Settings | None" = None
_settings: dict[str, Any]
_provenance: dict[str, dict[str, str]]

def __new__(cls) -> "Settings":
"""Singleton pattern to ensure only one settings instance exists."""
Expand All @@ -83,34 +84,58 @@ def _load_settings(self) -> None:
"default_upload_folder": "",
"display_name": "MODAQ Uploader",
"log_directory": "logs",
"batch_processing": {
"enabled": True,
"batch_size": 100,
"auto_tune_workers": True,
"max_workers": 4,
"target_cpu_percent": 70.0,
"skip_mcap_validation": False,
"use_database_for_large_jobs": True,
"large_job_threshold": 1000,
},
}

# Track the source of each setting value as it is applied layer by layer.
provenance: dict[str, dict[str, str]] = {k: {"source": "builtin"} for k in defaults}

# Load from settings.default.json if it exists
if SETTINGS_DEFAULT_FILE.exists():
with open(SETTINGS_DEFAULT_FILE, encoding="utf-8") as f:
defaults.update(json.load(f))
default_data = json.load(f)
defaults.update(default_data)
for k in default_data:
provenance[k] = {"source": "default_file", "path": str(SETTINGS_DEFAULT_FILE)}

# Load from settings.json if it exists
if SETTINGS_FILE.exists():
with open(SETTINGS_FILE, encoding="utf-8") as f:
defaults.update(json.load(f))
user_data = json.load(f)
defaults.update(user_data)
for k in user_data:
provenance[k] = {"source": "settings_file", "path": str(SETTINGS_FILE)}

# Override with environment variables (highest priority)
env_overrides = {
"aws_profile": os.environ.get(ENV_AWS_PROFILE),
"aws_region": os.environ.get(ENV_AWS_REGION),
"s3_bucket": os.environ.get(ENV_S3_BUCKET),
"default_upload_folder": os.environ.get(ENV_DEFAULT_UPLOAD_FOLDER),
"display_name": os.environ.get(ENV_DISPLAY_NAME),
"log_directory": os.environ.get(ENV_LOG_DIRECTORY),
env_overrides: dict[str, tuple[str | None, str]] = {
"aws_profile": (os.environ.get(ENV_AWS_PROFILE), ENV_AWS_PROFILE),
"aws_region": (os.environ.get(ENV_AWS_REGION), ENV_AWS_REGION),
"s3_bucket": (os.environ.get(ENV_S3_BUCKET), ENV_S3_BUCKET),
"default_upload_folder": (
os.environ.get(ENV_DEFAULT_UPLOAD_FOLDER),
ENV_DEFAULT_UPLOAD_FOLDER,
),
"display_name": (os.environ.get(ENV_DISPLAY_NAME), ENV_DISPLAY_NAME),
"log_directory": (os.environ.get(ENV_LOG_DIRECTORY), ENV_LOG_DIRECTORY),
}

# Only apply non-None environment values
for key, value in env_overrides.items():
for key, (value, env_var) in env_overrides.items():
if value is not None:
defaults[key] = value
provenance[key] = {"source": "env", "env_var": env_var}

self._settings = defaults
self._provenance = provenance

# Save settings.json if it doesn't exist
if not SETTINGS_FILE.exists():
Expand Down Expand Up @@ -139,6 +164,10 @@ def all(self) -> dict[str, Any]:
"""Get all settings as a dictionary."""
return self._settings.copy()

def to_response(self) -> dict[str, Any]:
"""Return settings with value_sources provenance for API responses."""
return {**self._settings, "value_sources": self._provenance}

def reload(self) -> None:
"""Reload settings from file."""
self._load_settings()
Expand Down Expand Up @@ -177,6 +206,15 @@ def log_directory(self) -> Path:
path = BASE_DIR / path
return path

@property
def batch_processing(self) -> dict[str, Any]:
"""Get batch processing configuration."""
return dict(self._settings.get("batch_processing", {}))

def get_batch_config(self) -> dict[str, Any]:
"""Get batch processing configuration (alias for batch_processing property)."""
return self.batch_processing


def get_settings() -> Settings:
"""Get the singleton Settings instance."""
Expand Down Expand Up @@ -241,11 +279,19 @@ def update_application(self) -> dict[str, Any]:
"git_pull": {"success": False, "output": ""},
"pip_install": {"success": False, "output": ""},
"modaq_toolkit": {"success": False, "output": ""},
"npm_install": {"success": False, "output": ""},
"frontend_build": {"success": False, "output": ""},
}

steps: list[tuple[str, list[str]]] = [
("git_pull", ["git", "pull"]),
("pip_install", [sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]),
frontend_dir = str(self.base_dir / "frontend")

steps: list[tuple[str, list[str], str | None]] = [
("git_pull", ["git", "pull"], None),
(
"pip_install",
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"],
None,
),
(
"modaq_toolkit",
[
Expand All @@ -257,14 +303,17 @@ def update_application(self) -> dict[str, Any]:
"--force-reinstall",
"git+https://github.com/MODAQ2/MODAQ_toolkit.git",
],
None,
),
("npm_install", ["npm", "install"], frontend_dir),
("frontend_build", ["npm", "run", "build"], frontend_dir),
]

for step_name, cmd in steps:
for step_name, cmd, cwd in steps:
try:
result = subprocess.run(
cmd,
cwd=self.base_dir,
cwd=cwd or self.base_dir,
capture_output=True,
text=True,
check=True,
Expand Down
Loading