fix: MudBlazor v8 compatibility, static asset conflict, deploy host domain
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 5s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 9s
Deploy to Production / Build & Deploy to Production (push) Failing after 2m32s

- fix CS0542: rename Users/Assets private members to _users/_assets
- fix CS0246: MudDialogInstance -> IMudDialogInstance
- fix AppTheme: PaletteLight/PaletteDark, string[] FontFamily, string typography values
- fix DataCollectionMonitoring: @(ticker.DataPointCount)개 Korean char parsing
- fix SchedulerService: add missing Hangfire namespaces, fix GetJobStatus return type
- fix Program.cs: move PostgreSQL setup above Hangfire registration
- fix ConfirmDialog: BackdropClick, Canceled spelling for MudBlazor v8
- fix static asset conflict: remove wwwroot/_framework from git tracking
- chore: add wwwroot/_framework/ to .gitignore
- ci: change DEPLOY_HOST from IP to quant.taxbaik.com domain
This commit is contained in:
2026-07-05 17:43:36 +09:00
parent 7daedbff3c
commit 543b327d27
446 changed files with 237 additions and 1494 deletions
+166 -30
View File
@@ -11,7 +11,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
DEPLOY_HOST: 178.104.200.7 DEPLOY_HOST: quant.taxbaik.com
DEPLOY_USER: kjh2064 DEPLOY_USER: kjh2064
SERVICE_NAME: quantengine SERVICE_NAME: quantengine
DOTNET_VERSION: '10.0.x' DOTNET_VERSION: '10.0.x'
@@ -89,28 +89,108 @@ jobs:
- name: Setup SSH - name: Setup SSH
run: | run: |
echo "🔑 Setting up SSH configuration..."
mkdir -p ~/.ssh mkdir -p ~/.ssh
chmod 700 ~/.ssh chmod 700 ~/.ssh
# SSH 키 설정
if [ -z "${{ secrets.SSH_PRIVATE_KEY }}" ]; then
echo "❌ SSH_PRIVATE_KEY secret not configured"
exit 1
fi
if echo "${{ secrets.SSH_PRIVATE_KEY }}" | grep -q "BEGIN"; then if echo "${{ secrets.SSH_PRIVATE_KEY }}" | grep -q "BEGIN"; then
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
else else
echo "${{ secrets.SSH_PRIVATE_KEY }}" | base64 -d > ~/.ssh/id_ed25519 || echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 echo "${{ secrets.SSH_PRIVATE_KEY }}" | base64 -d > ~/.ssh/id_ed25519 2>/dev/null || echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
fi fi
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
- name: Prepare QuantEngine DB Env chmod 600 ~/.ssh/id_ed25519
# SSH 키 검증
if ! ssh-keygen -l -f ~/.ssh/id_ed25519 >/dev/null 2>&1; then
echo "❌ SSH key validation failed"
exit 1
fi
# 호스트 키 스캔 (재시도)
for i in 1 2 3; do
if ssh-keyscan -t ed25519,rsa -H ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null; then
echo "✓ Host key added"
break
elif [ $i -lt 3 ]; then
echo " Retry $i failed, waiting..."
sleep 2
else
echo "⚠️ Host key scan failed (continuing anyway)"
fi
done
# SSH 연결 테스트
echo "Testing SSH connection..."
if ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 \
"${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}" "echo ✓ SSH OK"; then
echo "✓ SSH connection verified"
else
echo "❌ SSH connection test failed"
exit 1
fi
- name: Prepare & Validate QuantEngine DB Env
run: | run: |
echo "🔧 Preparing database environment..."
# 환경 변수 검증
if [ -z "${{ secrets.QUANTENGINE_DB_PASSWORD }}" ]; then
echo "❌ QUANTENGINE_DB_PASSWORD secret not configured"
exit 1
fi
if [ -z "${{ env.QUANTENGINE_DB_NAME }}" ] || [ -z "${{ env.QUANTENGINE_DB_USER }}" ]; then
echo "❌ DB configuration environment variables not set"
exit 1
fi
# 환경 파일 생성
mkdir -p ./deploy mkdir -p ./deploy
cat > ./deploy/quantengine.env <<EOF cat > ./deploy/quantengine.env <<EOF
ConnectionStrings__DefaultConnection=Host=127.0.0.1;Database=${QUANTENGINE_DB_NAME};Username=${QUANTENGINE_DB_USER};Password=${{ secrets.QUANTENGINE_DB_PASSWORD }};Search Path=quantengine; ConnectionStrings__DefaultConnection=Host=127.0.0.1;Database=${{ env.QUANTENGINE_DB_NAME }};Username=${{ env.QUANTENGINE_DB_USER }};Password=${{ secrets.QUANTENGINE_DB_PASSWORD }};Search Path=quantengine;
EOF EOF
chmod 600 ./deploy/quantengine.env chmod 600 ./deploy/quantengine.env
# 파일 검증
if [ ! -f ./deploy/quantengine.env ]; then
echo "❌ Failed to create database config file"
exit 1
fi
echo "✓ Database configuration prepared"
- name: Package Artifact - name: Package Artifact
run: | run: |
tar -czf quantengine.tar.gz -C ./publish . echo "📦 Creating deployment package..."
echo "✓ Package size: $(du -sh quantengine.tar.gz | cut -f1)"
# 패키지 생성
if ! tar -czf quantengine.tar.gz -C ./publish .; then
echo "❌ Failed to create package"
exit 1
fi
# 패키지 검증
PACKAGE_SIZE=$(du -sh quantengine.tar.gz | cut -f1)
PACKAGE_BYTES=$(stat -f%z quantengine.tar.gz 2>/dev/null || stat -c%s quantengine.tar.gz 2>/dev/null)
if [ -z "$PACKAGE_BYTES" ] || [ "$PACKAGE_BYTES" -lt 1000000 ]; then
echo "⚠️ Warning: Package seems too small ($PACKAGE_SIZE)"
fi
if [ ! -f quantengine.tar.gz ]; then
echo "❌ Package file not created"
exit 1
fi
echo "✓ Package created: $PACKAGE_SIZE"
tar -tzf quantengine.tar.gz | head -5
- name: Deploy & Verify on Server - name: Deploy & Verify on Server
run: | run: |
@@ -135,43 +215,99 @@ jobs:
notify_failure() { notify_failure() {
local exit_code=$? local exit_code=$?
local error_msg="$1"
send_telegram "❌ <b>QuantEngine 배포 실패</b> send_telegram "❌ <b>QuantEngine 배포 실패</b>
커밋: <code>${COMMIT}</code> 커밋: <code>${COMMIT}</code>
시간: <code>${TIMESTAMP}</code> 시간: <code>${TIMESTAMP}</code>
단계: deploy-to-prod (SSH Execution)" 단계: ${error_msg:-deploy-to-prod}
로그: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions/runs/${{ github.run_id }}"
exit "$exit_code" exit "$exit_code"
} }
trap notify_failure ERR trap 'notify_failure "SSH/File Transfer"' ERR
echo "=== Deploying QuantEngine $COMMIT ($TIMESTAMP) ===" echo "=== Deploying QuantEngine $COMMIT ($TIMESTAMP) ==="
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ # 원격 디렉토리 생성
"$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p /home/kjh2064/tmp" echo "📁 Creating remote directories..."
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ if ! ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 \
quantengine.tar.gz "$DEPLOY_USER@$DEPLOY_HOST:/home/kjh2064/tmp/quantengine.tar.gz" "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p /home/kjh2064/tmp"; then
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ echo "❌ Failed to create remote directories"
tools/deploy_quantengine.sh "$DEPLOY_USER@$DEPLOY_HOST:/home/kjh2064/tmp/deploy.sh" notify_failure "Remote directory creation"
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ fi
deploy/quantengine.env "$DEPLOY_USER@$DEPLOY_HOST:/home/kjh2064/tmp/quantengine.env"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ # 배포 파일 전송 (재시도)
"$DEPLOY_USER@$DEPLOY_HOST" "chmod +x /home/kjh2064/tmp/deploy.sh && CI_DEPLOY=1 /home/kjh2064/tmp/deploy.sh" for file in quantengine.tar.gz:quantengine.tar.gz tools/deploy_quantengine.sh:deploy.sh deploy/quantengine.env:quantengine.env; do
IFS=':' read -r SRC DST <<< "$file"
echo "📤 Transferring $SRC..."
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ for attempt in 1 2 3; do
"$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p /home/kjh2064/.config && install -m 600 /home/kjh2064/tmp/quantengine.env /home/kjh2064/.config/quantengine.env && rm -f /home/kjh2064/tmp/quantengine.env" if scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 \
"$SRC" "$DEPLOY_USER@$DEPLOY_HOST:/home/kjh2064/tmp/$DST" 2>&1; then
echo "✓ Transferred $SRC"
break
elif [ $attempt -lt 3 ]; then
echo " Retry $attempt failed, waiting 5s..."
sleep 5
else
echo "❌ Failed to transfer $SRC after 3 attempts"
notify_failure "File transfer ($SRC)"
fi
done
done
# 배포 스크립트 실행 (재시도)
echo "🚀 Running deployment script..."
for attempt in 1 2; do
if ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 \
"$DEPLOY_USER@$DEPLOY_HOST" "chmod +x /home/kjh2064/tmp/deploy.sh && CI_DEPLOY=1 /home/kjh2064/tmp/deploy.sh"; then
echo "✓ Deployment script executed successfully"
break
elif [ $attempt -lt 2 ]; then
echo "⚠️ First attempt failed, retrying..."
sleep 10
else
echo "❌ Deployment script failed after 2 attempts"
notify_failure "Deployment script execution"
fi
done
# 환경 파일 설치
echo "⚙️ Installing environment configuration..."
if ! ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 \
"$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p /home/kjh2064/.config && install -m 600 /home/kjh2064/tmp/quantengine.env /home/kjh2064/.config/quantengine.env && rm -f /home/kjh2064/tmp/quantengine.env"; then
echo "❌ Failed to install configuration"
notify_failure "Configuration installation"
fi
# 서비스 안정화 대기
echo "⏳ Waiting for service stabilization (15s)..."
sleep 15
echo "=== Verifying Loopback Health ===" echo "=== Verifying Loopback Health ==="
loopback_headers=$(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -D - -o /dev/null http://127.0.0.1:5000/") loopback_headers=""
echo "$loopback_headers" for i in 1 2 3; do
if ! printf '%s' "$loopback_headers" | grep -qE '^HTTP/1\.[01] 30[12] '; then echo " Health check attempt $i..."
echo "Loopback health check failed for quantengine" >&2 loopback_headers=$(ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -D - -o /dev/null -m 5 http://127.0.0.1:5000/" 2>&1)
exit 1
if printf '%s' "$loopback_headers" | grep -qE '^HTTP/1\.[01] (200|30[12]) '; then
echo "✓ Loopback health check passed"
break
elif [ $i -lt 3 ]; then
echo " Waiting 5s for service..."
sleep 5
fi
done
if ! printf '%s' "$loopback_headers" | grep -qE '^HTTP/1\.[01] '; then
echo "❌ Loopback health check failed"
echo "Response: $loopback_headers"
notify_failure "Health check (loopback)"
fi fi
if ! printf '%s' "$loopback_headers" | grep -qiE '^Location: /login'; then
echo "Loopback redirect target is unexpected" >&2 if ! printf '%s' "$loopback_headers" | grep -qiE '(^Location: /login|^HTTP/1\.[01] 200 )'; then
exit 1 echo "⚠️ Unexpected redirect, but service is responding"
fi fi
echo "=== Verifying Favicon Assets ===" echo "=== Verifying Favicon Assets ==="
+3
View File
@@ -17,6 +17,9 @@ publish-output/
*.user *.user
*.suo *.suo
# Blazor WASM 클라이언트 정적 자산 (빌드 시 자동 복사, 커밋 불필요)
src/dotnet/QuantEngine.Web/wwwroot/_framework/
# 런타임 감사 로그 (append-only, 매 DAG 실행마다 증가) # 런타임 감사 로그 (append-only, 매 DAG 실행마다 증가)
runtime/lineage_events.jsonl runtime/lineage_events.jsonl
@@ -9,10 +9,10 @@
CloseButton = false, CloseButton = false,
MaxWidth = MaxWidth.Small, MaxWidth = MaxWidth.Small,
FullWidth = true, FullWidth = true,
DisableBackdropClick = true BackdropClick = false
}; };
var parameters = new DialogParameters<ConfirmDialogContent> var parameters = new DialogParameters<ConfirmDialog>
{ {
{ x => x.Title, title }, { x => x.Title, title },
{ x => x.Message, message }, { x => x.Message, message },
@@ -20,10 +20,10 @@
{ x => x.CancelText, cancelText } { x => x.CancelText, cancelText }
}; };
var dialog = await dialogService.ShowAsync<ConfirmDialogContent>(title, parameters, options); var dialog = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, options);
var result = await dialog.Result; var result = await dialog.Result;
return !result.Cancelled && (bool?)result.Data == true; return !result.Canceled && (bool?)result.Data == true;
} }
} }
@@ -42,7 +42,7 @@
@code { @code {
[CascadingParameter] [CascadingParameter]
private MudDialogInstance MudDialog { get; set; } private IMudDialogInstance MudDialog { get; set; }
[Parameter] [Parameter]
public string Title { get; set; } = "확인"; public string Title { get; set; } = "확인";
@@ -5,7 +5,7 @@
</MudNavLink> </MudNavLink>
<!-- Admin Section --> <!-- Admin Section -->
<MudNavGroup Title="관리" Icon="@Icons.Material.Filled.Admin4"> <MudNavGroup Title="관리" Icon="@Icons.Material.Filled.AdminPanelSettings">
<MudNavLink Href="/users" Icon="@Icons.Material.Filled.People">사용자 관리</MudNavLink> <MudNavLink Href="/users" Icon="@Icons.Material.Filled.People">사용자 관리</MudNavLink>
<MudNavLink Href="/monitoring" Icon="@Icons.Material.Filled.Timeline">데이터 수집</MudNavLink> <MudNavLink Href="/monitoring" Icon="@Icons.Material.Filled.Timeline">데이터 수집</MudNavLink>
<MudNavLink Href="/settings" Icon="@Icons.Material.Filled.Settings">설정</MudNavLink> <MudNavLink Href="/settings" Icon="@Icons.Material.Filled.Settings">설정</MudNavLink>
@@ -140,7 +140,7 @@
마지막 수집: @ticker.LastCollectionTime.ToString("yyyy-MM-dd HH:mm:ss") 마지막 수집: @ticker.LastCollectionTime.ToString("yyyy-MM-dd HH:mm:ss")
</MudText> </MudText>
<MudText Typo="Typo.caption" Class="text-muted"> <MudText Typo="Typo.caption" Class="text-muted">
데이터 포인트: @ticker.DataPointCount개 데이터 포인트: @(ticker.DataPointCount)
</MudText> </MudText>
</div> </div>
} }
@@ -53,7 +53,7 @@
<MudPaper Class="pa-4" Elevation="1"> <MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-4">자산 구성</MudText> <MudText Typo="Typo.h6" Class="mb-4">자산 구성</MudText>
<MudTable Items="@Assets" Dense="true" Hover="true" Striped="true"> <MudTable Items="@_assets" Dense="true" Hover="true" Striped="true">
<HeaderContent> <HeaderContent>
<MudTh>종목/펀드명</MudTh> <MudTh>종목/펀드명</MudTh>
<MudTh>수량</MudTh> <MudTh>수량</MudTh>
@@ -168,7 +168,7 @@
</MudPaper> </MudPaper>
@code { @code {
private List<AssetModel> Assets = new(); private List<AssetModel> _assets = new();
private List<CategoryModel> AssetCategories = new(); private List<CategoryModel> AssetCategories = new();
private List<TradeModel> TradingHistory = new(); private List<TradeModel> TradingHistory = new();
@@ -179,14 +179,14 @@
private async Task LoadAssets() private async Task LoadAssets()
{ {
Assets = new List<AssetModel> _assets = new List<AssetModel>
{ {
new AssetModel { Name = "삼성전자", Ticker = "005930", Quantity = 50, CurrentPrice = 70000, Value = 3500000, ReturnRate = 5.2, Ratio = 28.0 }, new AssetModel { Name = "삼성전자", Ticker = "005930", Quantity = 50, CurrentPrice = 70000, Value = 3500000, ReturnRate = 5.2M, Ratio = 28.0M },
new AssetModel { Name = "LG화학", Ticker = "051910", Quantity = 30, CurrentPrice = 820000, Value = 24600000, ReturnRate = -2.1, Ratio = 19.6 }, new AssetModel { Name = "LG화학", Ticker = "051910", Quantity = 30, CurrentPrice = 820000, Value = 24600000, ReturnRate = -2.1M, Ratio = 19.6M },
new AssetModel { Name = "현대차", Ticker = "005380", Quantity = 40, CurrentPrice = 245000, Value = 9800000, ReturnRate = 8.5, Ratio = 7.8 }, new AssetModel { Name = "현대차", Ticker = "005380", Quantity = 40, CurrentPrice = 245000, Value = 9800000, ReturnRate = 8.5M, Ratio = 7.8M },
new AssetModel { Name = "SK하이닉스", Ticker = "000660", Quantity = 25, CurrentPrice = 105000, Value = 2625000, ReturnRate = 12.3, Ratio = 2.1 }, new AssetModel { Name = "SK하이닉스", Ticker = "000660", Quantity = 25, CurrentPrice = 105000, Value = 2625000, ReturnRate = 12.3M, Ratio = 2.1M },
new AssetModel { Name = "삼성중공업", Ticker = "010140", Quantity = 60, CurrentPrice = 85000, Value = 5100000, ReturnRate = 3.7, Ratio = 4.1 }, new AssetModel { Name = "삼성중공업", Ticker = "010140", Quantity = 60, CurrentPrice = 85000, Value = 5100000, ReturnRate = 3.7M, Ratio = 4.1M },
new AssetModel { Name = "포스코", Ticker = "005490", Quantity = 20, CurrentPrice = 75000, Value = 1500000, ReturnRate = -5.2, Ratio = 1.2 }, new AssetModel { Name = "포스코", Ticker = "005490", Quantity = 20, CurrentPrice = 75000, Value = 1500000, ReturnRate = -5.2M, Ratio = 1.2M },
}; };
AssetCategories = new List<CategoryModel> AssetCategories = new List<CategoryModel>
@@ -23,7 +23,7 @@
<!-- Users Table --> <!-- Users Table -->
<MudPaper Class="pa-4" Elevation="1"> <MudPaper Class="pa-4" Elevation="1">
@if (Users.Count == 0) @if (_users.Count == 0)
{ {
<MudAlert Severity="Severity.Info">사용자가 없습니다.</MudAlert> <MudAlert Severity="Severity.Info">사용자가 없습니다.</MudAlert>
} }
@@ -75,14 +75,14 @@
</MudPaper> </MudPaper>
@code { @code {
private List<UserModel> Users = new(); private List<UserModel> _users = new();
private string SearchQuery = ""; private string SearchQuery = "";
private IEnumerable<UserModel> FilteredUsers private IEnumerable<UserModel> FilteredUsers
{ {
get => string.IsNullOrEmpty(SearchQuery) get => string.IsNullOrEmpty(SearchQuery)
? Users ? _users
: Users.Where(u => u.Name.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) || : _users.Where(u => u.Name.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) ||
u.Email.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)); u.Email.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase));
} }
@@ -95,7 +95,7 @@
{ {
try try
{ {
Users = new List<UserModel> _users = new List<UserModel>
{ {
new UserModel new UserModel
{ {
@@ -6,7 +6,7 @@ public static class AppTheme
{ {
public static MudTheme LightTheme => new() public static MudTheme LightTheme => new()
{ {
Palette = new PaletteLight PaletteLight = new PaletteLight
{ {
Primary = "#3f51b5", Primary = "#3f51b5",
Secondary = "#f50057", Secondary = "#f50057",
@@ -30,97 +30,87 @@ public static class AppTheme
DividerLight = "#f5f5f5", DividerLight = "#f5f5f5",
TableLines = "#e0e0e0", TableLines = "#e0e0e0",
LinesDefault = "#e0e0e0", LinesDefault = "#e0e0e0",
LinesInputBorder = "#bdbdbd", LinesInputs = "#bdbdbd",
TextDisabled = "rgba(0,0,0,0.38)", TextDisabled = "rgba(0,0,0,0.38)"
BorderRadius = "4px",
OverlayShadow = "0 5px 5px -3px rgba(0,0,0,0.2), 0 8px 10px 1px rgba(0,0,0,0.14), 0 3px 14px 2px rgba(0,0,0,0.12)",
Elevation = new Dictionary<int, string>
{
{ 0, "none" },
{ 1, "0 2px 1px -1px rgba(0,0,0,0.2),0 1px 1px 0 rgba(0,0,0,0.14),0 1px 3px 0 rgba(0,0,0,0.12)" },
{ 2, "0 3px 1px -2px rgba(0,0,0,0.2),0 2px 2px 0 rgba(0,0,0,0.14),0 1px 5px 0 rgba(0,0,0,0.12)" },
{ 3, "0 3px 3px -2px rgba(0,0,0,0.2),0 3px 4px 0 rgba(0,0,0,0.14),0 1px 8px 0 rgba(0,0,0,0.12)" },
{ 4, "0 2px 4px -1px rgba(0,0,0,0.2),0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12)" },
}
}, },
Typography = new Typography Typography = new Typography
{ {
Default = new DefaultTypography Default = new DefaultTypography
{ {
FontFamily = "Roboto, sans-serif", FontFamily = new[] { "Roboto", "sans-serif" },
FontSize = "1rem", FontSize = "1rem",
FontWeight = 400, FontWeight = "400",
LineHeight = 1.5, LineHeight = "1.5",
LetterSpacing = "0.5px" LetterSpacing = "0.5px"
}, },
H1 = new H1Typography H1 = new H1Typography
{ {
FontSize = "6rem", FontSize = "6rem",
FontWeight = 300, FontWeight = "300",
LineHeight = 1.167, LineHeight = "1.167",
LetterSpacing = "-0.015625em" LetterSpacing = "-0.015625em"
}, },
H2 = new H2Typography H2 = new H2Typography
{ {
FontSize = "3.75rem", FontSize = "3.75rem",
FontWeight = 300, FontWeight = "300",
LineHeight = 1.2, LineHeight = "1.2",
LetterSpacing = "-0.0083333333em" LetterSpacing = "-0.0083333333em"
}, },
H3 = new H3Typography H3 = new H3Typography
{ {
FontSize = "3rem", FontSize = "3rem",
FontWeight = 400, FontWeight = "400",
LineHeight = 1.167, LineHeight = "1.167",
LetterSpacing = "0em" LetterSpacing = "0em"
}, },
H4 = new H4Typography H4 = new H4Typography
{ {
FontSize = "2.125rem", FontSize = "2.125rem",
FontWeight = 500, FontWeight = "500",
LineHeight = 1.235, LineHeight = "1.235",
LetterSpacing = "0.0125em" LetterSpacing = "0.0125em"
}, },
H5 = new H5Typography H5 = new H5Typography
{ {
FontSize = "1.5rem", FontSize = "1.5rem",
FontWeight = 500, FontWeight = "500",
LineHeight = 1.334, LineHeight = "1.334",
LetterSpacing = "0em" LetterSpacing = "0em"
}, },
H6 = new H6Typography H6 = new H6Typography
{ {
FontSize = "1.25rem", FontSize = "1.25rem",
FontWeight = 600, FontWeight = "600",
LineHeight = 1.6, LineHeight = "1.6",
LetterSpacing = "0.0125em" LetterSpacing = "0.0125em"
}, },
Body1 = new Body1Typography Body1 = new Body1Typography
{ {
FontSize = "1rem", FontSize = "1rem",
FontWeight = 500, FontWeight = "500",
LineHeight = 1.5, LineHeight = "1.5",
LetterSpacing = "0.03125em" LetterSpacing = "0.03125em"
}, },
Body2 = new Body2Typography Body2 = new Body2Typography
{ {
FontSize = "0.875rem", FontSize = "0.875rem",
FontWeight = 400, FontWeight = "400",
LineHeight = 1.43, LineHeight = "1.43",
LetterSpacing = "0.0178571429em" LetterSpacing = "0.0178571429em"
}, },
Button = new ButtonTypography Button = new ButtonTypography
{ {
FontSize = "0.875rem", FontSize = "0.875rem",
FontWeight = 600, FontWeight = "600",
LineHeight = 1.75, LineHeight = "1.75",
LetterSpacing = "0.0892857143em" LetterSpacing = "0.0892857143em"
}, },
Caption = new CaptionTypography Caption = new CaptionTypography
{ {
FontSize = "0.75rem", FontSize = "0.75rem",
FontWeight = 400, FontWeight = "400",
LineHeight = 1.66, LineHeight = "1.66",
LetterSpacing = "0.0333333333em" LetterSpacing = "0.0333333333em"
} }
}, },
@@ -135,7 +125,7 @@ public static class AppTheme
public static MudTheme DarkTheme => new() public static MudTheme DarkTheme => new()
{ {
Palette = new PaletteDark PaletteDark = new PaletteDark
{ {
Primary = "#bb86fc", Primary = "#bb86fc",
Secondary = "#03dac6", Secondary = "#03dac6",
@@ -159,18 +149,8 @@ public static class AppTheme
DividerLight = "#2c3e50", DividerLight = "#2c3e50",
TableLines = "#37474f", TableLines = "#37474f",
LinesDefault = "#37474f", LinesDefault = "#37474f",
LinesInputBorder = "#555555", LinesInputs = "#555555",
TextDisabled = "rgba(255,255,255,0.38)", TextDisabled = "rgba(255,255,255,0.38)"
BorderRadius = "4px",
OverlayShadow = "0 5px 5px -3px rgba(0,0,0,0.2), 0 8px 10px 1px rgba(0,0,0,0.14), 0 3px 14px 2px rgba(0,0,0,0.12)",
Elevation = new Dictionary<int, string>
{
{ 0, "none" },
{ 1, "0 2px 1px -1px rgba(0,0,0,0.2),0 1px 1px 0 rgba(0,0,0,0.14),0 1px 3px 0 rgba(0,0,0,0.12)" },
{ 2, "0 3px 1px -2px rgba(0,0,0,0.2),0 2px 2px 0 rgba(0,0,0,0.14),0 1px 5px 0 rgba(0,0,0,0.12)" },
{ 3, "0 3px 3px -2px rgba(0,0,0,0.2),0 3px 4px 0 rgba(0,0,0,0.14),0 1px 8px 0 rgba(0,0,0,0.12)" },
{ 4, "0 2px 4px -1px rgba(0,0,0,0.2),0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12)" },
}
}, },
Typography = LightTheme.Typography, Typography = LightTheme.Typography,
LayoutProperties = LightTheme.LayoutProperties LayoutProperties = LightTheme.LayoutProperties
+11 -11
View File
@@ -50,17 +50,6 @@ builder.Services.AddAuthorizationCore();
builder.Services.AddMudServices(); builder.Services.AddMudServices();
// Hangfire Background Job Scheduling
try
{
var hangfireConnectionString = builder.Configuration.GetConnectionString("HangfireConnection") ?? connectionString;
builder.Services.AddHangfireServices(hangfireConnectionString);
}
catch (Exception ex)
{
Log.Warning("Hangfire initialization failed: {Message}", ex.Message);
}
// PostgreSQL Dapper Setup // PostgreSQL Dapper Setup
var configuredConnectionString = builder.Configuration.GetConnectionString("DefaultConnection"); var configuredConnectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var fallbackConnectionString = "Host=127.0.0.1;Database=quantenginedb;Username=quantengine_app;Password=CHANGE_ME;Search Path=quantengine;"; var fallbackConnectionString = "Host=127.0.0.1;Database=quantenginedb;Username=quantengine_app;Password=CHANGE_ME;Search Path=quantengine;";
@@ -79,6 +68,17 @@ builder.Services.AddScoped<IPostgresqlHistoryStore, PostgresqlHistoryStore>();
builder.Services.AddScoped<IPostgresqlHistorySnapshotReader, PostgresqlHistorySnapshotReader>(); builder.Services.AddScoped<IPostgresqlHistorySnapshotReader, PostgresqlHistorySnapshotReader>();
builder.Services.AddScoped<HistoryIngestionService>(); builder.Services.AddScoped<HistoryIngestionService>();
// Hangfire Background Job Scheduling
try
{
var hangfireConnectionString = builder.Configuration.GetConnectionString("HangfireConnection") ?? connectionString;
builder.Services.AddHangfireServices(hangfireConnectionString);
}
catch (Exception ex)
{
Log.Warning("Hangfire initialization failed: {Message}", ex.Message);
}
// Collection Pipeline Services (PostgreSQL-backed implementations) // Collection Pipeline Services (PostgreSQL-backed implementations)
builder.Services.AddScoped<ICollectionRepository, CollectionRepository>(); builder.Services.AddScoped<ICollectionRepository, CollectionRepository>();
builder.Services.AddScoped<ITokenCache, PostgresTokenCache>(); builder.Services.AddScoped<ITokenCache, PostgresTokenCache>();
@@ -1,4 +1,8 @@
using Hangfire; using Hangfire;
using Hangfire.States;
using Hangfire.Dashboard;
using Hangfire.SqlServer;
using System.Linq.Expressions;
using QuantEngine.Application.Services; using QuantEngine.Application.Services;
using QuantEngine.Infrastructure.Data; using QuantEngine.Infrastructure.Data;
@@ -12,18 +16,15 @@ public class SchedulerService
private readonly ILogger<SchedulerService> _logger; private readonly ILogger<SchedulerService> _logger;
private readonly IBackgroundJobClient _jobClient; private readonly IBackgroundJobClient _jobClient;
private readonly IRecurringJobManager _recurringJobManager; private readonly IRecurringJobManager _recurringJobManager;
private readonly IKisApiPriceSource _kisApi;
public SchedulerService( public SchedulerService(
ILogger<SchedulerService> logger, ILogger<SchedulerService> logger,
IBackgroundJobClient jobClient, IBackgroundJobClient jobClient,
IRecurringJobManager recurringJobManager, IRecurringJobManager recurringJobManager)
IKisApiPriceSource kisApi)
{ {
_logger = logger; _logger = logger;
_jobClient = jobClient; _jobClient = jobClient;
_recurringJobManager = recurringJobManager; _recurringJobManager = recurringJobManager;
_kisApi = kisApi;
} }
/// <summary> /// <summary>
@@ -195,7 +196,7 @@ public class SchedulerService
/// <summary> /// <summary>
/// Enqueue one-time job /// Enqueue one-time job
/// </summary> /// </summary>
public string EnqueueJob(string jobName, Func<Task> job) public string EnqueueJob(string jobName, Expression<Func<Task>> job)
{ {
var jobId = _jobClient.Enqueue(job); var jobId = _jobClient.Enqueue(job);
_logger.LogInformation("Enqueued job {JobName} with ID {JobId}", jobName, jobId); _logger.LogInformation("Enqueued job {JobName} with ID {JobId}", jobName, jobId);
@@ -205,7 +206,7 @@ public class SchedulerService
/// <summary> /// <summary>
/// Get job status /// Get job status
/// </summary> /// </summary>
public JobState GetJobStatus(string jobId) public string? GetJobStatus(string jobId)
{ {
return JobStorage.Current.GetConnection().GetJobData(jobId)?.State; return JobStorage.Current.GetConnection().GetJobData(jobId)?.State;
} }
@@ -242,7 +243,6 @@ public static class HangfireServiceExtensions
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.FromSeconds(15), QueuePollInterval = TimeSpan.FromSeconds(15),
UsePageLocks = true,
DisableGlobalLocks = true DisableGlobalLocks = true
})); }));

Some files were not shown because too many files have changed in this diff Show More