139 lines
4.4 KiB
Plaintext
139 lines
4.4 KiB
Plaintext
import SwiftUI
|
|
|
|
struct ScanProgressView: View {
|
|
@ObservedObject var store: AuditStore
|
|
|
|
@State private var pulse = false
|
|
|
|
private var elapsed: String {
|
|
let seconds = max(0, Int(Date().timeIntervalSince(store.scanStartedAt)))
|
|
return String(format: "%02d:%02d", seconds / 60, seconds % 60)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.accentColor.opacity(0.09))
|
|
.frame(width: 108, height: 108)
|
|
.scaleEffect(pulse ? 1.12 : 0.94)
|
|
|
|
Circle()
|
|
.stroke(Color.accentColor.opacity(0.18), lineWidth: 2)
|
|
.frame(width: 94, height: 94)
|
|
|
|
Image(systemName: "shield.lefthalf.filled")
|
|
.font(.system(size: 42, weight: .medium))
|
|
.foregroundStyle(Color.accentColor)
|
|
.symbolRenderingMode(.hierarchical)
|
|
}
|
|
.padding(.top, 34)
|
|
|
|
Text("Security Assessment")
|
|
.font(.system(size: 25, weight: .bold))
|
|
.padding(.top, 22)
|
|
|
|
Text("PHPGuard is inspecting your server")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.padding(.top, 6)
|
|
|
|
VStack(alignment: .leading, spacing: 15) {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: "server.rack")
|
|
.font(.title3)
|
|
.foregroundStyle(Color.accentColor)
|
|
.frame(width: 30)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(store.scanServerName)
|
|
.font(.headline)
|
|
|
|
Text(store.scanServerHost)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "checkmark.shield")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Divider()
|
|
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 5) {
|
|
Text("Scanning")
|
|
.font(.subheadline.weight(.semibold))
|
|
|
|
Text("Collecting PHP, package, Docker and web-server evidence")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
ProgressView()
|
|
.controlSize(.regular)
|
|
}
|
|
|
|
ProgressView()
|
|
.progressViewStyle(.linear)
|
|
.tint(Color.accentColor)
|
|
|
|
HStack {
|
|
Label(
|
|
"\(store.scanning.count) active",
|
|
systemImage: "network"
|
|
)
|
|
|
|
Spacer()
|
|
|
|
TimelineView(.periodic(from: .now, by: 1)) { context in
|
|
let seconds = max(
|
|
0,
|
|
Int(context.date.timeIntervalSince(store.scanStartedAt))
|
|
)
|
|
|
|
Text(
|
|
String(
|
|
format: "%02d:%02d",
|
|
seconds / 60,
|
|
seconds % 60
|
|
)
|
|
)
|
|
.monospacedDigit()
|
|
}
|
|
}
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(22)
|
|
.background(
|
|
Color.primary.opacity(0.035),
|
|
in: RoundedRectangle(cornerRadius: 16)
|
|
)
|
|
.padding(.horizontal, 30)
|
|
.padding(.top, 26)
|
|
|
|
Text("The assessment runs remotely over SSH. No server changes are made.")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 35)
|
|
.padding(.top, 22)
|
|
.padding(.bottom, 30)
|
|
}
|
|
.frame(width: 540)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.onAppear {
|
|
pulse = true
|
|
}
|
|
.animation(
|
|
.easeInOut(duration: 1.7).repeatForever(autoreverses: true),
|
|
value: pulse
|
|
)
|
|
}
|
|
}
|