adding files 2

This commit is contained in:
Oracle Public Cloud User
2026-09-04 13:54:16 +00:00
parent b7ec9db2bc
commit 8b3cc8dd10
83 changed files with 5045 additions and 0 deletions

44
order-service/pom.xml Normal file
View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.oracle.demo</groupId>
<artifactId>kagent-oci-devops-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>order-service</artifactId>
<name>order-service</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,19 @@
package com.oracle.demo.orders;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.client.RestClient;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
RestClient restClient() {
return RestClient.builder().build();
}
}

View File

@@ -0,0 +1,41 @@
package com.oracle.demo.orders.api;
import com.oracle.demo.orders.model.BusinessHealth;
import com.oracle.demo.orders.model.CreateOrderRequest;
import com.oracle.demo.orders.model.OrderRecord;
import com.oracle.demo.orders.service.OrderService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public OrderRecord createOrder(@Valid @RequestBody CreateOrderRequest request) {
return orderService.createOrder(request);
}
@GetMapping
public List<OrderRecord> listOrders() {
return orderService.listOrders();
}
@GetMapping("/business-health")
public BusinessHealth businessHealth() {
return orderService.businessHealth();
}
}

View File

@@ -0,0 +1,26 @@
package com.oracle.demo.orders.client;
import com.oracle.demo.orders.model.InventoryReservation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class InventoryClient {
private final RestClient restClient;
private final String inventoryBaseUrl;
public InventoryClient(RestClient restClient, @Value("${services.inventory.base-url}") String inventoryBaseUrl) {
this.restClient = restClient;
this.inventoryBaseUrl = inventoryBaseUrl;
}
public InventoryReservation reserve(String sku, int quantity) {
return restClient.get()
.uri(inventoryBaseUrl + "/api/inventory/{sku}?quantity={quantity}", sku, quantity)
.retrieve()
.body(InventoryReservation.class);
}
}

View File

@@ -0,0 +1,29 @@
package com.oracle.demo.orders.client;
import com.oracle.demo.orders.model.PaymentDecision;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.util.Map;
@Component
public class PaymentClient {
private final RestClient restClient;
private final String paymentBaseUrl;
public PaymentClient(RestClient restClient, @Value("${services.payment.base-url}") String paymentBaseUrl) {
this.restClient = restClient;
this.paymentBaseUrl = paymentBaseUrl;
}
public PaymentDecision authorize(String orderId, String cardToken, double amount) {
return restClient.post()
.uri(paymentBaseUrl + "/api/payments/authorize")
.body(Map.of("orderId", orderId, "cardToken", cardToken, "amount", amount))
.retrieve()
.body(PaymentDecision.class);
}
}

View File

@@ -0,0 +1,15 @@
package com.oracle.demo.orders.model;
public record BusinessHealth(
long totalOrders,
long confirmedOrders,
long pendingReviewOrders,
long failedOrders,
double manualReviewRate,
double totalRevenue,
double confirmedRevenue,
double revenueAtRisk,
boolean healthy,
String summary,
String businessImpact) {
}

View File

@@ -0,0 +1,13 @@
package com.oracle.demo.orders.model;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public record CreateOrderRequest(
@NotBlank String customerId,
@NotBlank String sku,
@Min(1) int quantity,
@Min(1) double amount,
@NotBlank String cardToken) {
}

View File

@@ -0,0 +1,5 @@
package com.oracle.demo.orders.model;
public record InventoryReservation(String sku, boolean reserved, int availableUnits, String message) {
}

View File

@@ -0,0 +1,15 @@
package com.oracle.demo.orders.model;
import java.time.Instant;
public record OrderRecord(
String orderId,
String customerId,
String sku,
int quantity,
double amount,
String status,
String reason,
Instant createdAt) {
}

View File

@@ -0,0 +1,5 @@
package com.oracle.demo.orders.model;
public record PaymentDecision(boolean approved, String status, String message) {
}

View File

@@ -0,0 +1,107 @@
package com.oracle.demo.orders.observability;
import com.oracle.demo.orders.model.BusinessHealth;
import com.oracle.demo.orders.model.OrderRecord;
import com.oracle.demo.orders.service.OrderService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/observability")
public class ObservabilityController {
private final OrderService orderService;
private final RestClient restClient;
private final String inventoryBaseUrl;
private final String paymentBaseUrl;
public ObservabilityController(
OrderService orderService,
RestClient restClient,
@Value("${services.inventory.base-url}") String inventoryBaseUrl,
@Value("${services.payment.base-url}") String paymentBaseUrl) {
this.orderService = orderService;
this.restClient = restClient;
this.inventoryBaseUrl = inventoryBaseUrl;
this.paymentBaseUrl = paymentBaseUrl;
}
@GetMapping
public ObservabilitySnapshot snapshot() {
BusinessHealth businessHealth = orderService.businessHealth();
List<OrderRecord> recentOrders = orderService.listOrders().stream().limit(12).toList();
List<ServiceSignal> services = new ArrayList<>();
services.add(new ServiceSignal(
"order-service",
"business-api",
businessHealth.healthy() ? "UP" : "DEGRADED",
businessHealth.summary(),
!businessHealth.healthy()));
services.add(probe("inventory-service", "dependency", inventoryBaseUrl + "/actuator/health"));
services.add(probe("payment-service", "dependency", paymentBaseUrl + "/actuator/health"));
return new ObservabilitySnapshot(
Instant.now(),
businessHealth,
services,
recentOrders,
recommendedActions(businessHealth, services));
}
private ServiceSignal probe(String name, String layer, String url) {
try {
Map<?, ?> body = restClient.get().uri(url).retrieve().body(Map.class);
Object status = body == null ? null : body.get("status");
status = status == null ? "UNKNOWN" : status;
boolean healthy = "UP".equals(String.valueOf(status));
return new ServiceSignal(
name,
layer,
String.valueOf(status),
healthy ? "Actuator health endpoint is reachable." : "Actuator returned a non-UP status.",
!healthy);
} catch (Exception ex) {
return new ServiceSignal(
name,
layer,
"DOWN",
ex.getClass().getSimpleName() + ": " + ex.getMessage(),
true);
}
}
private List<String> recommendedActions(BusinessHealth businessHealth, List<ServiceSignal> services) {
List<String> actions = new ArrayList<>();
services.stream()
.filter(ServiceSignal::kagentSignal)
.filter(signal -> !"order-service".equals(signal.name()))
.findFirst()
.ifPresent(signal -> actions.add("Inspect " + signal.name() + " pod logs, endpoints, and recent deployment changes."));
if (!businessHealth.healthy()) {
if (businessHealth.revenueAtRisk() > 0) {
actions.add("Revenue at risk is " + businessHealth.revenueAtRisk()
+ " USD with manual review rate " + businessHealth.manualReviewRate()
+ "%. Inspect payment risk rules and recent OCI DevOps changes.");
}
actions.add("Correlate pending, failed, and revenue-at-risk signals with the latest OCI DevOps deployment.");
actions.add("Recommend rollback or approved config correction through OCI DevOps if degradation started after the last release.");
}
if (actions.isEmpty()) {
actions.add("No remediation required. Continue monitoring pipeline and runtime signals.");
}
return actions;
}
}

View File

@@ -0,0 +1,16 @@
package com.oracle.demo.orders.observability;
import com.oracle.demo.orders.model.BusinessHealth;
import com.oracle.demo.orders.model.OrderRecord;
import java.time.Instant;
import java.util.List;
public record ObservabilitySnapshot(
Instant timestamp,
BusinessHealth businessHealth,
List<ServiceSignal> services,
List<OrderRecord> recentOrders,
List<String> recommendedKagentActions) {
}

View File

@@ -0,0 +1,10 @@
package com.oracle.demo.orders.observability;
public record ServiceSignal(
String name,
String layer,
String status,
String detail,
boolean kagentSignal) {
}

View File

@@ -0,0 +1,157 @@
package com.oracle.demo.orders.service;
import com.oracle.demo.orders.client.InventoryClient;
import com.oracle.demo.orders.client.PaymentClient;
import com.oracle.demo.orders.model.BusinessHealth;
import com.oracle.demo.orders.model.CreateOrderRequest;
import com.oracle.demo.orders.model.InventoryReservation;
import com.oracle.demo.orders.model.OrderRecord;
import com.oracle.demo.orders.model.PaymentDecision;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class OrderService {
private final InventoryClient inventoryClient;
private final PaymentClient paymentClient;
private final Map<String, OrderRecord> orders = new ConcurrentHashMap<>();
private final boolean stuckReviewMode;
private final double manualReviewRateThreshold;
private final double revenueAtRiskThreshold;
private final Counter createdCounter;
private final Counter reviewCounter;
private final Counter failedCounter;
private final AtomicInteger pendingReviewGauge = new AtomicInteger();
public OrderService(
InventoryClient inventoryClient,
PaymentClient paymentClient,
MeterRegistry meterRegistry,
@Value("${demo.orders.stuck-review-mode:false}") boolean stuckReviewMode,
@Value("${demo.orders.manual-review-rate-threshold:40}") double manualReviewRateThreshold,
@Value("${demo.orders.revenue-at-risk-threshold:5000}") double revenueAtRiskThreshold) {
this.inventoryClient = inventoryClient;
this.paymentClient = paymentClient;
this.stuckReviewMode = stuckReviewMode;
this.manualReviewRateThreshold = manualReviewRateThreshold;
this.revenueAtRiskThreshold = revenueAtRiskThreshold;
this.createdCounter = Counter.builder("demo_orders_created_total").register(meterRegistry);
this.reviewCounter = Counter.builder("demo_orders_review_required_total").register(meterRegistry);
this.failedCounter = Counter.builder("demo_orders_failed_total").register(meterRegistry);
Gauge.builder("demo_orders_pending_review", pendingReviewGauge, AtomicInteger::get).register(meterRegistry);
}
public OrderRecord createOrder(CreateOrderRequest request) {
String orderId = UUID.randomUUID().toString();
createdCounter.increment();
InventoryReservation inventory = inventoryClient.reserve(request.sku(), request.quantity());
if (!inventory.reserved()) {
failedCounter.increment();
return store(new OrderRecord(
orderId,
request.customerId(),
request.sku(),
request.quantity(),
request.amount(),
"FAILED",
"Inventory check failed: " + inventory.message(),
Instant.now()));
}
PaymentDecision payment = paymentClient.authorize(orderId, request.cardToken(), request.amount());
if (!payment.approved()) {
reviewCounter.increment();
String reason = stuckReviewMode
? "Orders are stuck in review because stuck-review-mode is enabled."
: "Payment requires manual review: " + payment.message();
return store(new OrderRecord(
orderId,
request.customerId(),
request.sku(),
request.quantity(),
request.amount(),
"PENDING_REVIEW",
reason,
Instant.now()));
}
return store(new OrderRecord(
orderId,
request.customerId(),
request.sku(),
request.quantity(),
request.amount(),
"CONFIRMED",
"Order confirmed.",
Instant.now()));
}
public List<OrderRecord> listOrders() {
return orders.values().stream()
.sorted(Comparator.comparing(OrderRecord::createdAt).reversed())
.toList();
}
public BusinessHealth businessHealth() {
long total = orders.size();
long confirmed = orders.values().stream().filter(order -> "CONFIRMED".equals(order.status())).count();
long pendingReview = orders.values().stream().filter(order -> "PENDING_REVIEW".equals(order.status())).count();
long failed = orders.values().stream().filter(order -> "FAILED".equals(order.status())).count();
double totalRevenue = orders.values().stream().mapToDouble(OrderRecord::amount).sum();
double confirmedRevenue = orders.values().stream()
.filter(order -> "CONFIRMED".equals(order.status()))
.mapToDouble(OrderRecord::amount)
.sum();
double revenueAtRisk = orders.values().stream()
.filter(order -> "PENDING_REVIEW".equals(order.status()))
.mapToDouble(OrderRecord::amount)
.sum();
double manualReviewRate = total == 0 ? 0 : (pendingReview * 100.0) / total;
boolean manualReviewRateIncident = total >= 10 && manualReviewRate >= manualReviewRateThreshold;
boolean healthy = pendingReview < 5
&& failed < 3
&& !manualReviewRateIncident
&& revenueAtRisk < revenueAtRiskThreshold;
String summary = healthy
? "Business flow healthy."
: "Business degradation detected. Review pending or failed orders before promoting.";
String businessImpact = healthy
? "No revenue protection action required."
: "Revenue at risk from manual-review backlog. Validate payment risk rules before continuing promotion.";
return new BusinessHealth(
total,
confirmed,
pendingReview,
failed,
round(manualReviewRate),
round(totalRevenue),
round(confirmedRevenue),
round(revenueAtRisk),
healthy,
summary,
businessImpact);
}
private OrderRecord store(OrderRecord order) {
orders.put(order.orderId(), order);
pendingReviewGauge.set((int) orders.values().stream().filter(item -> "PENDING_REVIEW".equals(item.status())).count());
return order;
}
private double round(double value) {
return Math.round(value * 100.0) / 100.0;
}
}

View File

@@ -0,0 +1,28 @@
spring:
application:
name: order-service
server:
port: 8080
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
probes:
enabled: true
services:
inventory:
base-url: http://localhost:8081
payment:
base-url: http://localhost:8082
demo:
orders:
stuck-review-mode: false
manual-review-rate-threshold: 40
revenue-at-risk-threshold: 5000

View File

@@ -0,0 +1,118 @@
const totalOrders = document.querySelector("#totalOrders");
const confirmedOrders = document.querySelector("#confirmedOrders");
const pendingOrders = document.querySelector("#pendingOrders");
const failedOrders = document.querySelector("#failedOrders");
const businessStatus = document.querySelector("#businessStatus");
const manualReviewRate = document.querySelector("#manualReviewRate");
const revenueAtRisk = document.querySelector("#revenueAtRisk");
const confirmedRevenue = document.querySelector("#confirmedRevenue");
const businessImpact = document.querySelector("#businessImpact");
const serviceSignals = document.querySelector("#serviceSignals");
const kagentActions = document.querySelector("#kagentActions");
const ordersTable = document.querySelector("#ordersTable");
const lastUpdated = document.querySelector("#lastUpdated");
document.querySelector("#refreshBtn").addEventListener("click", loadDashboard);
document.querySelector("#normalOrderBtn").addEventListener("click", () => createOrder("4111111111111111"));
document.querySelector("#reviewOrderBtn").addEventListener("click", () => createOrder("9999123412341234"));
async function createOrder(cardToken) {
await fetch("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
customerId: `CUST-${Math.floor(Math.random() * 900 + 100)}`,
sku: "LAPTOP-15",
quantity: 1,
amount: 1499.99,
cardToken
})
});
await loadDashboard();
}
async function loadDashboard() {
const response = await fetch("/api/observability");
const snapshot = await response.json();
const health = snapshot.businessHealth;
totalOrders.textContent = health.totalOrders;
confirmedOrders.textContent = health.confirmedOrders;
pendingOrders.textContent = health.pendingReviewOrders;
failedOrders.textContent = health.failedOrders;
businessStatus.textContent = health.healthy ? "HEALTHY" : "DEGRADED";
manualReviewRate.textContent = `${formatNumber(health.manualReviewRate)}%`;
revenueAtRisk.textContent = formatCurrency(health.revenueAtRisk);
confirmedRevenue.textContent = formatCurrency(health.confirmedRevenue);
businessImpact.textContent = health.businessImpact;
lastUpdated.textContent = new Date(snapshot.timestamp).toLocaleString();
serviceSignals.innerHTML = snapshot.services.map(signal => `
<div class="signal">
<div>
<strong>${escapeHtml(signal.name)} <span class="muted">/ ${escapeHtml(signal.layer)}</span></strong>
<p>${escapeHtml(signal.detail)}</p>
</div>
<span class="status ${statusClass(signal.status)}">${escapeHtml(signal.status)}</span>
</div>
`).join("");
kagentActions.innerHTML = snapshot.recommendedKagentActions
.map(action => `<li>${escapeHtml(action)}</li>`)
.join("");
if (snapshot.recentOrders.length === 0) {
ordersTable.innerHTML = `<tr><td colspan="5" class="empty">No orders yet. Create a normal or review order to generate signals.</td></tr>`;
return;
}
ordersTable.innerHTML = snapshot.recentOrders.map(order => `
<tr>
<td>${escapeHtml(order.orderId.slice(0, 8))}</td>
<td>${escapeHtml(order.customerId)}</td>
<td>${escapeHtml(order.sku)}</td>
<td><span class="status ${statusClass(order.status)}">${escapeHtml(order.status)}</span></td>
<td>${escapeHtml(order.reason)}</td>
</tr>
`).join("");
}
function statusClass(status) {
const normalized = String(status).toLowerCase();
if (normalized === "up" || normalized === "confirmed" || normalized === "healthy") {
return "up";
}
if (normalized.includes("pending") || normalized.includes("degraded")) {
return "degraded";
}
if (normalized === "down" || normalized === "failed") {
return "down";
}
return "unknown";
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function formatCurrency(value) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
}).format(Number(value ?? 0));
}
function formatNumber(value) {
return new Intl.NumberFormat("en-US", {
maximumFractionDigits: 2
}).format(Number(value ?? 0));
}
loadDashboard();
setInterval(loadDashboard, 10000);

View File

@@ -0,0 +1,106 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kagent Demo Observability</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<main class="shell">
<section class="topbar">
<div>
<p class="section-label">OKE business application</p>
<h1>Order Flow Observability</h1>
</div>
<div class="actions">
<button id="refreshBtn" type="button">Refresh</button>
<button id="normalOrderBtn" type="button">Create normal order</button>
<button id="reviewOrderBtn" type="button" class="danger">Create review order</button>
</div>
</section>
<section class="summary-grid">
<article class="metric">
<span>Total orders</span>
<strong id="totalOrders">0</strong>
</article>
<article class="metric">
<span>Confirmed orders</span>
<strong id="confirmedOrders">0</strong>
</article>
<article class="metric warning">
<span>Pending review</span>
<strong id="pendingOrders">0</strong>
</article>
<article class="metric danger">
<span>Failed orders</span>
<strong id="failedOrders">0</strong>
</article>
<article class="metric">
<span>Business status</span>
<strong id="businessStatus">UNKNOWN</strong>
</article>
</section>
<section class="summary-grid business-grid">
<article class="metric">
<span>Manual review rate</span>
<strong id="manualReviewRate">0%</strong>
</article>
<article class="metric warning">
<span>Revenue at risk</span>
<strong id="revenueAtRisk">$0</strong>
</article>
<article class="metric">
<span>Confirmed revenue</span>
<strong id="confirmedRevenue">$0</strong>
</article>
<article class="metric">
<span>Business impact</span>
<p id="businessImpact" class="metric-note">Waiting for data</p>
</article>
</section>
<section class="workspace">
<article class="panel">
<div class="panel-head">
<h2>Service signals</h2>
<span id="lastUpdated">Waiting for data</span>
</div>
<div id="serviceSignals" class="signals"></div>
</article>
<article class="panel">
<div class="panel-head">
<h2>Kagent action view</h2>
<span>diagnostic inputs</span>
</div>
<ul id="kagentActions" class="action-list"></ul>
</article>
</section>
<section class="panel">
<div class="panel-head">
<h2>Recent orders</h2>
<span>business symptoms visible to the agent</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Order</th>
<th>Customer</th>
<th>SKU</th>
<th>Status</th>
<th>Reason</th>
</tr>
</thead>
<tbody id="ordersTable"></tbody>
</table>
</div>
</section>
</main>
<script src="/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,273 @@
:root {
--bg: #f5f7fb;
--panel: #ffffff;
--panel-soft: #eef3f8;
--text: #15202b;
--muted: #5f6f82;
--line: #d8e0ea;
--accent: #126c59;
--accent-strong: #0f4f43;
--warn: #a26013;
--danger: #b42318;
--shadow: 0 18px 45px rgba(31, 45, 61, 0.10);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: linear-gradient(135deg, #f8fafc 0%, #e8eef6 55%, #edf5f1 100%);
color: var(--text);
font-family: "Aptos", "Segoe UI", sans-serif;
}
button {
border: 0;
border-radius: 6px;
background: var(--accent);
color: white;
cursor: pointer;
font-size: 14px;
font-weight: 700;
padding: 11px 14px;
}
button:hover {
background: var(--accent-strong);
}
button.danger {
background: var(--danger);
}
.shell {
margin: 0 auto;
max-width: 1180px;
padding: 32px 20px 48px;
}
.topbar {
align-items: flex-end;
display: flex;
gap: 24px;
justify-content: space-between;
margin-bottom: 24px;
}
.section-label {
color: var(--accent);
font-size: 13px;
font-weight: 800;
letter-spacing: 0;
margin: 0 0 8px;
text-transform: uppercase;
}
h1,
h2 {
margin: 0;
}
h1 {
font-size: 38px;
line-height: 1.05;
}
h2 {
font-size: 18px;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-end;
}
.summary-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-bottom: 16px;
}
.business-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.metric,
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.metric {
min-height: 110px;
padding: 18px;
}
.metric span,
.panel-head span {
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
.metric strong {
display: block;
font-size: 34px;
margin-top: 14px;
}
.metric-note {
color: var(--muted);
font-size: 14px;
line-height: 1.35;
margin: 14px 0 0;
}
.metric.warning strong {
color: var(--warn);
}
.metric.danger strong {
color: var(--danger);
}
.workspace {
display: grid;
gap: 16px;
grid-template-columns: 1.25fr 0.75fr;
margin-bottom: 16px;
}
.panel {
padding: 18px;
}
.panel-head {
align-items: center;
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
margin-bottom: 14px;
padding-bottom: 12px;
}
.signals {
display: grid;
gap: 10px;
}
.signal {
align-items: center;
background: var(--panel-soft);
border: 1px solid var(--line);
border-radius: 8px;
display: grid;
gap: 10px;
grid-template-columns: 1fr auto;
padding: 14px;
}
.signal strong {
display: block;
margin-bottom: 5px;
}
.muted {
color: var(--muted);
font-weight: 700;
}
.signal p {
color: var(--muted);
font-size: 13px;
line-height: 1.35;
margin: 0;
}
.status {
border-radius: 999px;
color: white;
font-size: 12px;
font-weight: 800;
padding: 6px 9px;
}
.status.up {
background: var(--accent);
}
.status.degraded,
.status.down {
background: var(--danger);
}
.status.unknown {
background: var(--muted);
}
.action-list {
margin: 0;
padding-left: 20px;
}
.action-list li {
line-height: 1.45;
margin: 0 0 10px;
}
.table-wrap {
overflow-x: auto;
}
table {
border-collapse: collapse;
min-width: 860px;
width: 100%;
}
th,
td {
border-bottom: 1px solid var(--line);
font-size: 14px;
padding: 12px 10px;
text-align: left;
vertical-align: top;
}
th {
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
}
.empty {
color: var(--muted);
padding: 18px 10px;
}
@media (max-width: 860px) {
.topbar {
align-items: stretch;
flex-direction: column;
}
.actions {
justify-content: flex-start;
}
.summary-grid,
.workspace {
grid-template-columns: 1fr;
}
h1 {
font-size: 30px;
}
}