Just a whole lot of crap

This commit is contained in:
2025-08-14 16:36:12 -05:00
parent 66b7e42275
commit 62f6d6f279
144 changed files with 41421 additions and 2458 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,101 +1,47 @@
const React = require("react");
const { Box, Text, useInput, useApp } = require("ink");
const { useAppState } = require("../../providers/AppProvider.jsx");
const SelectInput = require("ink-select-input").default;
const LogReaderService = require("../../../services/logReader");
/**
* Log Viewer Screen Component
* Displays application logs with filtering and navigation capabilities
* Requirements: 7.5, 7.6
* Displays application logs with pagination, filtering and navigation capabilities
* Requirements: 6.1, 6.4, 10.3
*/
const LogViewerScreen = () => {
const { appState, navigateBack } = useAppState();
const { exit } = useApp();
// Mock log data (in a real implementation, this would read from log files)
const mockLogs = [
{
timestamp: new Date(Date.now() - 1000 * 60 * 5),
level: "INFO",
message: "Application started",
details: "Shopify Price Updater v1.0.0",
},
{
timestamp: new Date(Date.now() - 1000 * 60 * 4),
level: "INFO",
message: "Configuration loaded",
details: "Target tag: 'sale', Adjustment: 10%",
},
{
timestamp: new Date(Date.now() - 1000 * 60 * 3),
level: "INFO",
message: "Testing Shopify connection",
details: "Connecting to store...",
},
{
timestamp: new Date(Date.now() - 1000 * 60 * 2),
level: "SUCCESS",
message: "Connection successful",
details: "API version: 2023-01",
},
{
timestamp: new Date(Date.now() - 1000 * 60 * 1),
level: "INFO",
message: "Fetching products",
details: "Query: tag:sale",
},
{
timestamp: new Date(Date.now() - 1000 * 60 * 1),
level: "INFO",
message: "Products found",
details: "Found 15 products with tag 'sale'",
},
{
timestamp: new Date(Date.now() - 1000 * 30),
level: "INFO",
message: "Starting price updates",
details: "Processing 15 products, 42 variants total",
},
{
timestamp: new Date(Date.now() - 1000 * 25),
level: "INFO",
message: "Updating product",
details: "Product: 'Summer T-Shirt' - New price: $22.00",
},
{
timestamp: new Date(Date.now() - 1000 * 20),
level: "INFO",
message: "Updating product",
details: "Product: 'Winter Jacket' - New price: $110.00",
},
{
timestamp: new Date(Date.now() - 1000 * 15),
level: "WARNING",
message: "Price update failed",
details: "Product: 'Limited Edition Sneaker' - Insufficient permissions",
},
{
timestamp: new Date(Date.now() - 1000 * 10),
level: "INFO",
message: "Updating product",
details: "Product: 'Casual Jeans' - New price: $45.00",
},
{
timestamp: new Date(Date.now() - 1000 * 5),
level: "SUCCESS",
message: "Operation completed",
details: "Updated 40 of 42 variants (95.2% success rate)",
},
];
// Initialize log reader service
const [logReader] = React.useState(() => new LogReaderService());
// State for log viewing
const [logs, setLogs] = React.useState(mockLogs);
const [filteredLogs, setFilteredLogs] = React.useState(mockLogs);
const [filterLevel, setFilterLevel] = React.useState("ALL");
const [selectedLog, setSelectedLog] = React.useState(null);
// State for log viewing with pagination
const [logData, setLogData] = React.useState({
entries: [],
pagination: {
currentPage: 0,
pageSize: 10,
totalEntries: 0,
totalPages: 0,
hasNextPage: false,
hasPreviousPage: false,
startIndex: 1,
endIndex: 0,
},
filters: {
levelFilter: "ALL",
searchTerm: "",
},
});
const [selectedLog, setSelectedLog] = React.useState(0);
const [showDetails, setShowDetails] = React.useState(false);
const [scrollPosition, setScrollPosition] = React.useState(0);
const [maxScroll, setMaxScroll] = React.useState(0);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const [stats, setStats] = React.useState(null);
const [autoRefresh, setAutoRefresh] = React.useState(true);
const [lastRefresh, setLastRefresh] = React.useState(new Date());
const [refreshing, setRefreshing] = React.useState(false);
// Filter options
const filterOptions = [
@@ -106,17 +52,99 @@ const LogViewerScreen = () => {
{ value: "SUCCESS", label: "Success" },
];
// Filter logs based on selected level
const filterLogs = () => {
if (filterLevel === "ALL") {
setFilteredLogs(logs);
} else {
setFilteredLogs(logs.filter((log) => log.level === filterLevel));
// Load log data with current filters and pagination
const loadLogData = React.useCallback(
async (options = {}, isAutoRefresh = false) => {
try {
if (isAutoRefresh) {
setRefreshing(true);
} else {
setLoading(true);
}
setError(null);
const loadOptions = {
page: logData.pagination.currentPage,
pageSize: logData.pagination.pageSize,
levelFilter: logData.filters.levelFilter,
searchTerm: logData.filters.searchTerm,
...options,
};
const result = await logReader.getPaginatedEntries(loadOptions);
setLogData(result);
// Reset selection if current selection is out of bounds
if (selectedLog >= result.entries.length) {
setSelectedLog(Math.max(0, result.entries.length - 1));
}
setShowDetails(false);
setLastRefresh(new Date());
} catch (err) {
setError(`Failed to load logs: ${err.message}`);
} finally {
setLoading(false);
setRefreshing(false);
}
},
[
logReader,
logData.pagination.currentPage,
logData.pagination.pageSize,
logData.filters.levelFilter,
logData.filters.searchTerm,
selectedLog,
]
);
// Load statistics
const loadStats = React.useCallback(async () => {
try {
const statistics = await logReader.getLogStatistics();
setStats(statistics);
} catch (err) {
console.warn("Failed to load log statistics:", err.message);
}
};
}, [logReader]);
// Initial load
React.useEffect(() => {
loadLogData();
loadStats();
}, []);
// Auto-refresh functionality with file watching
React.useEffect(() => {
if (!autoRefresh) return;
const cleanup = logReader.watchFile(() => {
loadLogData({}, true); // Mark as auto-refresh
loadStats();
});
return cleanup;
}, [logReader, loadLogData, loadStats, autoRefresh]);
// Periodic refresh as backup (every 30 seconds)
React.useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(() => {
// Only refresh if not currently loading
if (!loading && !refreshing) {
logReader.clearCache();
loadLogData({}, true);
loadStats();
}
}, 30000); // 30 seconds
return () => clearInterval(interval);
}, [logReader, loadLogData, loadStats, autoRefresh, loading, refreshing]);
// Handle keyboard input
useInput((input, key) => {
if (loading) return; // Ignore input while loading
if (key.escape) {
// Go back to main menu
navigateBack();
@@ -128,29 +156,80 @@ const LogViewerScreen = () => {
}
} else if (key.downArrow) {
// Navigate down in log list
if (selectedLog < filteredLogs.length - 1) {
if (selectedLog < logData.entries.length - 1) {
setSelectedLog(selectedLog + 1);
setShowDetails(false);
}
} else if (key.leftArrow) {
// Previous page
if (logData.pagination.hasPreviousPage) {
loadLogData({ page: logData.pagination.currentPage - 1 });
}
} else if (key.rightArrow) {
// Next page
if (logData.pagination.hasNextPage) {
loadLogData({ page: logData.pagination.currentPage + 1 });
}
} else if (key.return || key.enter) {
// Toggle log details
if (selectedLog !== null) {
if (selectedLog < logData.entries.length) {
setShowDetails(!showDetails);
}
} else if (key.r) {
} else if (key.r || input === "r") {
// Refresh logs
setLogs(mockLogs);
setFilteredLogs(mockLogs);
setSelectedLog(null);
setShowDetails(false);
logReader.clearCache();
loadLogData();
loadStats();
} else if (input >= "1" && input <= "5") {
// Quick filter by number
const filterMap = {
1: "ALL",
2: "ERROR",
3: "WARNING",
4: "INFO",
5: "SUCCESS",
};
const newFilter = filterMap[input];
if (newFilter !== logData.filters.levelFilter) {
loadLogData({
levelFilter: newFilter,
page: 0, // Reset to first page when filtering
});
}
} else if (input === "s") {
// Toggle search mode (simplified - cycle through common search terms)
const searchTerms = ["", "error", "update", "rollback", "product"];
const currentIndex = searchTerms.indexOf(logData.filters.searchTerm);
const nextIndex = (currentIndex + 1) % searchTerms.length;
const newSearchTerm = searchTerms[nextIndex];
loadLogData({
searchTerm: newSearchTerm,
page: 0, // Reset to first page when searching
});
} else if (input === "c") {
// Clear all filters
loadLogData({
levelFilter: "ALL",
searchTerm: "",
page: 0,
});
} else if (input === "a") {
// Toggle auto-refresh
setAutoRefresh(!autoRefresh);
} else if (key.pageUp) {
// Jump to first page
if (logData.pagination.currentPage > 0) {
loadLogData({ page: 0 });
}
} else if (key.pageDown) {
// Jump to last page
if (logData.pagination.currentPage < logData.pagination.totalPages - 1) {
loadLogData({ page: logData.pagination.totalPages - 1 });
}
}
});
// Handle filter change
const handleFilterChange = (option) => {
setFilterLevel(option.value);
};
// Get log level color
const getLogLevelColor = (level) => {
switch (level) {
@@ -176,22 +255,91 @@ const LogViewerScreen = () => {
});
};
// Format date for display
const formatDate = (date) => {
return date.toLocaleDateString([], {
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
};
// Truncate text for display
const truncateText = (text, maxLength = 60) => {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + "...";
};
// Show loading state
if (loading && logData.entries.length === 0) {
return React.createElement(
Box,
{
flexDirection: "column",
padding: 2,
justifyContent: "center",
alignItems: "center",
},
React.createElement(Text, { color: "blue" }, "Loading logs..."),
React.createElement(
Text,
{ color: "gray" },
"Please wait while we read the log files"
)
);
}
// Show error state
if (error) {
return React.createElement(
Box,
{
flexDirection: "column",
padding: 2,
justifyContent: "center",
alignItems: "center",
},
React.createElement(
Text,
{ color: "red", bold: true },
"Error Loading Logs"
),
React.createElement(Text, { color: "gray" }, error),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
"Press 'r' to retry or Esc to go back"
)
);
}
return React.createElement(
Box,
{ flexDirection: "column", padding: 2, flexGrow: 1 },
// Header
// Header with statistics
React.createElement(
Box,
{ flexDirection: "column", marginBottom: 2 },
React.createElement(Text, { bold: true, color: "cyan" }, "📋 Log Viewer"),
React.createElement(
Text,
{ color: "gray" },
"View application logs and operation history"
Box,
{ flexDirection: "row", justifyContent: "space-between" },
React.createElement(
Text,
{ color: "gray" },
"View application logs and operation history"
),
stats &&
React.createElement(
Text,
{ color: "gray" },
`${stats.totalEntries} entries | ${stats.operations.total} operations`
)
)
),
// Filter controls
// Filter and pagination controls
React.createElement(
Box,
{
@@ -204,36 +352,54 @@ const LogViewerScreen = () => {
React.createElement(
Box,
{ flexDirection: "column" },
React.createElement(
Text,
{ bold: true, color: "blue" },
"Log Filters:"
),
React.createElement(
Box,
{ flexDirection: "row", alignItems: "center" },
React.createElement(Text, { color: "white" }, "Level: "),
React.createElement(SelectInput, {
items: filterOptions,
selectedIndex: filterOptions.findIndex(
(opt) => opt.value === filterLevel
{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
React.createElement(
Box,
{ flexDirection: "row", alignItems: "center" },
React.createElement(
Text,
{ color: "white", bold: true },
"Filter: "
),
onSelect: handleFilterChange,
itemComponent: ({ label, isSelected }) =>
React.createElement(
Text,
{
color: isSelected ? "blue" : "white",
bold: isSelected,
},
label
),
})
React.createElement(
Text,
{ color: "blue" },
logData.filters.levelFilter
),
React.createElement(
Text,
{ color: "gray", marginLeft: 2 },
"(1-5 to change)"
)
),
React.createElement(
Box,
{ flexDirection: "row", alignItems: "center" },
React.createElement(Text, { color: "white", bold: true }, "Page: "),
React.createElement(
Text,
{ color: "blue" },
`${logData.pagination.currentPage + 1}/${
logData.pagination.totalPages
}`
),
React.createElement(
Text,
{ color: "gray", marginLeft: 2 },
"(←/→ to navigate)"
)
)
),
React.createElement(
Text,
{ color: "gray", fontSize: "small" },
`Showing ${filteredLogs.length} of ${logs.length} log entries`
{ color: "gray" },
`Showing ${logData.pagination.startIndex}-${logData.pagination.endIndex} of ${logData.pagination.totalEntries} entries`
)
)
),
@@ -246,58 +412,74 @@ const LogViewerScreen = () => {
borderStyle: "single",
borderColor: "gray",
flexDirection: "column",
minHeight: 10,
},
filteredLogs.map((log, index) => {
const isSelected = selectedLog === index;
const isHighlighted = isSelected && !showDetails;
return React.createElement(
Box,
{
key: index,
borderStyle: "single",
borderColor: isSelected ? "blue" : "transparent",
paddingX: 1,
paddingY: 0.5,
backgroundColor: isHighlighted ? "blue" : undefined,
},
React.createElement(
logData.entries.length === 0
? React.createElement(
Box,
{ flexDirection: "row", alignItems: "center" },
{ justifyContent: "center", alignItems: "center", padding: 2 },
React.createElement(
Text,
{
color: getLogLevelColor(log.level),
bold: true,
width: 8,
},
log.level
{ color: "gray" },
"No log entries found"
),
React.createElement(
Text,
{
color: isHighlighted ? "white" : "gray",
width: 8,
},
formatTimestamp(log.timestamp)
),
React.createElement(
Text,
{
color: isHighlighted ? "white" : "white",
flexGrow: 1,
},
log.message
{ color: "gray" },
"Try changing the filter or refresh with 'r'"
)
)
);
})
)
: logData.entries.map((log, index) => {
const isSelected = selectedLog === index;
const isHighlighted = isSelected && !showDetails;
return React.createElement(
Box,
{
key: log.id || index,
borderStyle: isSelected ? "single" : "none",
borderColor: isSelected ? "blue" : "transparent",
paddingX: 1,
paddingY: 0,
backgroundColor: isHighlighted ? "blue" : undefined,
},
React.createElement(
Box,
{ flexDirection: "row", alignItems: "center" },
React.createElement(
Text,
{
color: getLogLevelColor(log.level),
bold: true,
width: 8,
},
log.level
),
React.createElement(
Text,
{
color: isHighlighted ? "white" : "gray",
width: 12,
},
formatDate(log.timestamp)
),
React.createElement(
Text,
{
color: isHighlighted ? "white" : "white",
flexGrow: 1,
},
truncateText(log.message, 50)
)
)
);
})
),
// Log details (when selected)
showDetails &&
selectedLog !== null &&
filteredLogs[selectedLog] &&
selectedLog < logData.entries.length &&
logData.entries[selectedLog] &&
React.createElement(
Box,
{
@@ -306,6 +488,7 @@ const LogViewerScreen = () => {
paddingX: 1,
paddingY: 1,
marginTop: 2,
maxHeight: 8,
},
React.createElement(
Box,
@@ -325,8 +508,19 @@ const LogViewerScreen = () => {
),
React.createElement(
Text,
{ color: getLogLevelColor(filteredLogs[selectedLog].level) },
filteredLogs[selectedLog].level
{ color: getLogLevelColor(logData.entries[selectedLog].level) },
logData.entries[selectedLog].level
),
React.createElement(Text, { color: "gray", marginLeft: 2 }, "|"),
React.createElement(
Text,
{ color: "white", bold: true, marginLeft: 1 },
"Type: "
),
React.createElement(
Text,
{ color: "cyan" },
logData.entries[selectedLog].type || "unknown"
)
),
React.createElement(
@@ -336,7 +530,7 @@ const LogViewerScreen = () => {
React.createElement(
Text,
{ color: "gray" },
filteredLogs[selectedLog].timestamp.toLocaleString()
logData.entries[selectedLog].timestamp.toLocaleString()
)
),
React.createElement(
@@ -350,23 +544,24 @@ const LogViewerScreen = () => {
React.createElement(
Text,
{ color: "white" },
filteredLogs[selectedLog].message
logData.entries[selectedLog].message
)
),
React.createElement(
Box,
{ flexDirection: "column", marginTop: 1 },
logData.entries[selectedLog].details &&
React.createElement(
Text,
{ color: "white", bold: true },
"Details:"
),
React.createElement(
Text,
{ color: "gray", italic: true },
filteredLogs[selectedLog].details
Box,
{ flexDirection: "column", marginTop: 1 },
React.createElement(
Text,
{ color: "white", bold: true },
"Details:"
),
React.createElement(
Text,
{ color: "gray", italic: true },
truncateText(logData.entries[selectedLog].details, 200)
)
)
)
)
),
@@ -378,13 +573,40 @@ const LogViewerScreen = () => {
marginTop: 2,
borderTopStyle: "single",
borderColor: "gray",
paddingTop: 2,
paddingTop: 1,
},
React.createElement(Text, { color: "gray" }, "Controls:"),
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate logs"),
React.createElement(Text, { color: "gray" }, " Enter - View details"),
React.createElement(Text, { color: "gray" }, " R - Refresh logs"),
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
React.createElement(
Box,
{ flexDirection: "row", justifyContent: "space-between" },
React.createElement(
Box,
{ flexDirection: "column" },
React.createElement(
Text,
{ color: "gray" },
"Navigation: ↑/↓ entries | ←/→ pages | Enter details"
),
React.createElement(
Text,
{ color: "gray" },
"Filters: 1=All 2=Error 3=Warning 4=Info 5=Success"
)
),
React.createElement(
Box,
{ flexDirection: "column" },
React.createElement(
Text,
{ color: "gray" },
"Search: S cycle terms | C clear | A auto-refresh"
),
React.createElement(
Text,
{ color: "gray" },
"Actions: R refresh | PgUp/PgDn jump | Esc back"
)
)
)
),
// Status bar
@@ -394,15 +616,34 @@ const LogViewerScreen = () => {
borderStyle: "single",
borderColor: "gray",
paddingX: 1,
paddingY: 0.5,
paddingY: 0,
marginTop: 1,
},
React.createElement(
Text,
{ color: "gray", fontSize: "small" },
`Selected: ${
selectedLog !== null ? `Log #${selectedLog + 1}` : "None"
} | Details: ${showDetails ? "Visible" : "Hidden"}`
Box,
{ flexDirection: "row", justifyContent: "space-between" },
React.createElement(
Text,
{ color: "gray" },
`Entry ${selectedLog + 1}/${logData.entries.length} | Details: ${
showDetails ? "ON" : "OFF"
}`
),
React.createElement(
Text,
{ color: loading ? "yellow" : refreshing ? "cyan" : "gray" },
loading
? "Loading..."
: refreshing
? "Refreshing..."
: `Filter: ${logData.filters.levelFilter}${
logData.filters.searchTerm
? ` | Search: "${logData.filters.searchTerm}"`
: ""
} | Auto: ${
autoRefresh ? "ON" : "OFF"
} | ${lastRefresh.toLocaleTimeString()}`
)
)
)
);

View File

@@ -1,6 +1,13 @@
const React = require("react");
const { Box, Text, useInput } = require("ink");
const { useAppState } = require("../../providers/AppProvider.jsx");
const {
createKeyboardHandler,
navigationKeys,
} = require("../../utils/keyboardHandlers.js");
const ResponsiveContainer = require("../common/ResponsiveContainer.jsx");
const ResponsiveText = require("../common/ResponsiveText.jsx");
const ScrollableContainer = require("../common/ScrollableContainer.jsx");
/**
* Main Menu Screen Component
@@ -8,7 +15,15 @@ const { useAppState } = require("../../providers/AppProvider.jsx");
* Requirements: 5.1, 5.3, 7.1
*/
const MainMenuScreen = () => {
const { appState, navigateTo, updateUIState } = useAppState();
const {
appState,
navigateTo,
navigateBack,
updateUIState,
toggleHelp,
showHelp,
hideHelp,
} = useAppState();
// Menu items configuration
const menuItems = [
@@ -40,161 +55,206 @@ const MainMenuScreen = () => {
{ id: "exit", label: "Exit", description: "Quit the application" },
];
// Handle keyboard input
useInput((input, key) => {
if (key.upArrow) {
// Navigate up in menu
const newIndex = Math.max(0, appState.uiState.selectedMenuIndex - 1);
updateUIState({ selectedMenuIndex: newIndex });
} else if (key.downArrow) {
// Navigate down in menu
const newIndex = Math.min(
menuItems.length - 1,
appState.uiState.selectedMenuIndex + 1
);
updateUIState({ selectedMenuIndex: newIndex });
} else if (key.return || key.enter || input === " ") {
// Select menu item
// Create screen-specific keyboard handler
const screenKeyboardHandler = (input, key) => {
// Handle menu navigation
const wasNavigationHandled = navigationKeys.handleMenuNavigation(
key,
appState.uiState.selectedMenuIndex,
menuItems.length - 1,
(newIndex) => updateUIState({ selectedMenuIndex: newIndex })
);
if (wasNavigationHandled) return;
// Handle menu selection
if (key.return || key.enter || input === " ") {
const selectedItem = menuItems[appState.uiState.selectedMenuIndex];
if (selectedItem.id === "exit") {
// Exit the application
process.exit(0);
} else {
// Navigate to selected screen
navigateTo(selectedItem.id);
}
} else if (input === "q" || input === "Q") {
// Quick exit with 'q'
process.exit(0);
}
});
};
// Use global keyboard handler with screen-specific handler
const context = {
appState,
navigateTo,
navigateBack,
updateUIState,
toggleHelp,
showHelp,
hideHelp,
};
useInput(createKeyboardHandler(screenKeyboardHandler, context));
return React.createElement(
Box,
{ flexDirection: "column", padding: 1 },
// Header
React.createElement(
Box,
{ flexDirection: "column", marginBottom: 1 },
React.createElement(
Text,
{ bold: true, color: "cyan" },
"🛍️ Shopify Price Updater"
),
React.createElement(
Text,
{ color: "gray", fontSize: "small" },
"Terminal User Interface"
)
),
// Welcome message
React.createElement(
Box,
{ flexDirection: "column", marginBottom: 1 },
React.createElement(
Text,
{ color: "green", fontSize: "small" },
"Shopify Price Updater TUI"
),
React.createElement(
Text,
{ color: "gray", fontSize: "small" },
"Arrow keys: Navigate | Enter: Select"
)
),
// Menu items
ResponsiveContainer,
{ componentType: "menu" },
React.createElement(
Box,
{ flexDirection: "column" },
menuItems.map((item, index) => {
const isSelected = index === appState.uiState.selectedMenuIndex;
const isConfigured = appState.configuration.isValid;
// Header
React.createElement(
Box,
{ flexDirection: "column", marginBottom: 1 },
React.createElement(
ResponsiveText,
{ styleType: "title" },
"🛍️ Shopify Price Updater"
),
React.createElement(
ResponsiveText,
{ styleType: "subtitle" },
"Terminal User Interface"
)
),
return React.createElement(
// Welcome message
React.createElement(
Box,
{ flexDirection: "column", marginBottom: 1 },
React.createElement(
ResponsiveText,
{ styleType: "emphasis" },
"Shopify Price Updater TUI"
),
React.createElement(
ResponsiveText,
{ styleType: "normal" },
"Arrow keys: Navigate | Enter: Select"
)
),
// Menu items with scrollable container
React.createElement(ScrollableContainer, {
items: menuItems,
itemHeight: 3,
renderItem: (item, index) => {
const isSelected = index === appState.uiState.selectedMenuIndex;
const isConfigured = appState.configuration.isValid;
return React.createElement(
Box,
{
borderStyle: "single",
borderColor: isSelected ? "blue" : "gray",
paddingX: 1,
paddingY: 1,
flexDirection: "column",
},
React.createElement(
Box,
{ flexDirection: "row", alignItems: "center" },
React.createElement(
ResponsiveText,
{
styleType: isSelected ? "emphasis" : "normal",
bold: isSelected,
},
`${isSelected ? "▶" : " "} ${item.label}`
),
// Configuration status indicator
item.id === "operation" &&
!isConfigured &&
React.createElement(
Box,
{ marginLeft: 2 },
React.createElement(
ResponsiveText,
{ styleType: "error" },
"⚠️ Not Configured"
)
)
),
React.createElement(
ResponsiveText,
{
styleType: "subtitle",
truncate: true,
},
` ${item.description}`
)
);
},
}),
// Footer with instructions (hide on small screens)
React.createElement(
ResponsiveContainer,
{
componentType: "secondary-info",
hideOnSmall: true,
padding: false,
},
React.createElement(
Box,
{
key: item.id,
borderStyle: "single",
borderColor: isSelected ? "blue" : "gray",
paddingX: 1,
paddingY: 1,
marginBottom: 1,
flexDirection: "column",
marginTop: 2,
borderTopStyle: "single",
borderColor: "gray",
paddingTop: 1,
},
React.createElement(
Box,
{ flexDirection: "row", alignItems: "center" },
React.createElement(
Text,
{
bold: isSelected,
color: isSelected ? "blue" : "white",
},
`${isSelected ? "▶" : " "} ${item.label}`
),
// Configuration status indicator
item.id === "operation" &&
!isConfigured &&
React.createElement(
Box,
{ marginLeft: 2 },
React.createElement(Text, { color: "red" }, "⚠️ Not Configured")
)
ResponsiveText,
{ styleType: "subtitle" },
"Navigation:"
),
React.createElement(
Text,
{
color: isSelected ? "cyan" : "gray",
italic: true,
},
` ${item.description}`
ResponsiveText,
{ styleType: "normal" },
" ↑/↓ - Navigate menu"
),
React.createElement(
ResponsiveText,
{ styleType: "normal" },
" Enter/Space - Select item"
),
React.createElement(
ResponsiveText,
{ styleType: "normal" },
" h - Show help"
),
React.createElement(
ResponsiveText,
{ styleType: "normal" },
" q - Quick exit"
),
React.createElement(
ResponsiveText,
{ styleType: "normal" },
" Esc - Back (when available)"
)
);
})
),
// Footer with instructions
React.createElement(
Box,
{
flexDirection: "column",
marginTop: 3,
borderTopStyle: "single",
borderColor: "gray",
paddingTop: 2,
},
React.createElement(Text, { color: "gray" }, "Navigation:"),
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate menu"),
React.createElement(
Text,
{ color: "gray" },
" Enter/Space - Select item"
)
),
React.createElement(Text, { color: "gray" }, " q - Quick exit"),
React.createElement(
Text,
{ color: "gray" },
" Esc - Back (when available)"
)
),
// Configuration status
React.createElement(
Box,
{ flexDirection: "row", justifyContent: "space-between", marginTop: 2 },
// Configuration status
React.createElement(
Text,
{ color: appState.configuration.isValid ? "green" : "red" },
`Configuration: ${
appState.configuration.isValid ? "✓ Complete" : "⚠ Incomplete"
}`
),
React.createElement(
Text,
{ color: "gray" },
`Mode: ${appState.configuration.operationMode.toUpperCase()}`
Box,
{ flexDirection: "row", justifyContent: "space-between", marginTop: 1 },
React.createElement(
ResponsiveText,
{
styleType: appState.configuration.isValid ? "success" : "error",
truncate: true,
maxWidth: 30,
},
`Configuration: ${
appState.configuration.isValid ? "✓ Complete" : "⚠ Incomplete"
}`
),
React.createElement(
ResponsiveText,
{
styleType: "normal",
truncate: true,
maxWidth: 20,
},
`Mode: ${appState.configuration.operationMode.toUpperCase()}`
)
)
)
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,582 @@
const React = require("react");
const { Box, Text, useInput, useApp } = require("ink");
const { useAppState } = require("../../providers/AppProvider.jsx");
const LogReaderService = require("../../../services/logReader");
const VirtualScrollableContainer = require("../common/VirtualScrollableContainer.jsx");
/**
* Optimized Log Viewer Screen Component with virtual scrolling and performance enhancements
* Requirements: 4.1, 4.3, 4.4, 6.1, 6.4, 10.3
*/
// Memoized log entry component to prevent unnecessary re-renders
const LogEntry = React.memo(
({
log,
index,
isSelected,
isHighlighted,
getLogLevelColor,
formatDate,
truncateText,
}) => (
<Box
borderStyle={isSelected ? "single" : "none"}
borderColor={isSelected ? "blue" : "transparent"}
paddingX={1}
paddingY={0}
backgroundColor={isHighlighted ? "blue" : undefined}
>
<Box flexDirection="row" alignItems="center">
<Text color={getLogLevelColor(log.level)} bold={true} width={8}>
{log.level}
</Text>
<Text color={isHighlighted ? "white" : "gray"} width={12}>
{formatDate(log.timestamp)}
</Text>
<Text color={isHighlighted ? "white" : "white"} flexGrow={1}>
{truncateText(log.message, 50)}
</Text>
</Box>
</Box>
)
);
// Memoized log details component
const LogDetails = React.memo(({ log, getLogLevelColor }) => (
<Box
borderStyle="single"
borderColor="green"
paddingX={1}
paddingY={1}
marginTop={2}
maxHeight={8}
>
<Box flexDirection="column">
<Text bold={true} color="green">
Log Details:
</Text>
<Box flexDirection="row" alignItems="center">
<Text color="white" bold={true}>
Level:
</Text>
<Text color={getLogLevelColor(log.level)}>{log.level}</Text>
<Text color="gray" marginLeft={2}>
|
</Text>
<Text color="white" bold={true} marginLeft={1}>
Type:
</Text>
<Text color="cyan">{log.type || "unknown"}</Text>
</Box>
<Box flexDirection="row" alignItems="center">
<Text color="white" bold={true}>
Time:{" "}
</Text>
<Text color="gray">{log.timestamp.toLocaleString()}</Text>
</Box>
<Box flexDirection="column" marginTop={1}>
<Text color="white" bold={true}>
Message:
</Text>
<Text color="white">{log.message}</Text>
</Box>
{log.details && (
<Box flexDirection="column" marginTop={1}>
<Text color="white" bold={true}>
Details:
</Text>
<Text color="gray" italic={true}>
{log.details.length > 200
? log.details.substring(0, 200) + "..."
: log.details}
</Text>
</Box>
)}
</Box>
</Box>
));
// Debounced state update hook
const useDebouncedState = (initialValue, delay = 100) => {
const [value, setValue] = React.useState(initialValue);
const [debouncedValue, setDebouncedValue] = React.useState(initialValue);
const timeoutRef = React.useRef(null);
const updateValue = React.useCallback(
(newValue) => {
setValue(newValue);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setDebouncedValue(newValue);
}, delay);
},
[delay]
);
return [value, debouncedValue, updateValue];
};
const OptimizedLogViewerScreen = React.memo(() => {
const { appState, navigateBack } = useAppState();
const { exit } = useApp();
// Initialize log reader service with memoization
const [logReader] = React.useState(() => new LogReaderService());
// Optimized state management with debouncing
const [logData, setLogData] = React.useState({
entries: [],
pagination: {
currentPage: 0,
pageSize: 50, // Increased for better virtual scrolling performance
totalEntries: 0,
totalPages: 0,
hasNextPage: false,
hasPreviousPage: false,
startIndex: 1,
endIndex: 0,
},
filters: {
levelFilter: "ALL",
searchTerm: "",
},
});
const [selectedLog, setSelectedLog] = useDebouncedState(0, 50);
const [showDetails, setShowDetails] = React.useState(false);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const [stats, setStats] = React.useState(null);
const [autoRefresh, setAutoRefresh] = React.useState(true);
const [lastRefresh, setLastRefresh] = React.useState(new Date());
const [refreshing, setRefreshing] = React.useState(false);
// Memoized filter options
const filterOptions = React.useMemo(
() => [
{ value: "ALL", label: "All Levels" },
{ value: "ERROR", label: "Errors" },
{ value: "WARNING", label: "Warnings" },
{ value: "INFO", label: "Info" },
{ value: "SUCCESS", label: "Success" },
],
[]
);
// Memoized utility functions
const getLogLevelColor = React.useCallback((level) => {
switch (level) {
case "ERROR":
return "red";
case "WARNING":
return "yellow";
case "INFO":
return "blue";
case "SUCCESS":
return "green";
default:
return "white";
}
}, []);
const formatTimestamp = React.useCallback((date) => {
return date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}, []);
const formatDate = React.useCallback((date) => {
return date.toLocaleDateString([], {
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}, []);
const truncateText = React.useCallback((text, maxLength = 60) => {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + "...";
}, []);
// Optimized load log data function with memoization
const loadLogData = React.useCallback(
async (options = {}, isAutoRefresh = false) => {
try {
if (isAutoRefresh) {
setRefreshing(true);
} else {
setLoading(true);
}
setError(null);
const loadOptions = {
page: logData.pagination.currentPage,
pageSize: logData.pagination.pageSize,
levelFilter: logData.filters.levelFilter,
searchTerm: logData.filters.searchTerm,
...options,
};
const result = await logReader.getPaginatedEntries(loadOptions);
setLogData(result);
// Reset selection if current selection is out of bounds
if (selectedLog >= result.entries.length) {
setSelectedLog(Math.max(0, result.entries.length - 1));
}
setShowDetails(false);
setLastRefresh(new Date());
} catch (err) {
setError(`Failed to load logs: ${err.message}`);
} finally {
setLoading(false);
setRefreshing(false);
}
},
[
logReader,
logData.pagination.currentPage,
logData.pagination.pageSize,
logData.filters.levelFilter,
logData.filters.searchTerm,
selectedLog,
]
);
// Optimized load statistics function
const loadStats = React.useCallback(async () => {
try {
const statistics = await logReader.getLogStatistics();
setStats(statistics);
} catch (err) {
console.warn("Failed to load log statistics:", err.message);
}
}, [logReader]);
// Initial load with optimization
React.useEffect(() => {
const loadInitialData = async () => {
await Promise.all([loadLogData(), loadStats()]);
};
loadInitialData();
}, []);
// Optimized auto-refresh with file watching
React.useEffect(() => {
if (!autoRefresh) return;
const cleanup = logReader.watchFile(() => {
loadLogData({}, true);
loadStats();
});
return cleanup;
}, [logReader, loadLogData, loadStats, autoRefresh]);
// Optimized periodic refresh
React.useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(() => {
if (!loading && !refreshing) {
logReader.clearCache();
loadLogData({}, true);
loadStats();
}
}, 30000);
return () => clearInterval(interval);
}, [logReader, loadLogData, loadStats, autoRefresh, loading, refreshing]);
// Optimized keyboard input handler with debouncing
const handleKeyboardInput = React.useCallback(
(input, key) => {
if (loading) return;
if (key.escape) {
navigateBack();
} else if (key.upArrow) {
if (selectedLog > 0) {
setSelectedLog(selectedLog - 1);
setShowDetails(false);
}
} else if (key.downArrow) {
if (selectedLog < logData.entries.length - 1) {
setSelectedLog(selectedLog + 1);
setShowDetails(false);
}
} else if (key.leftArrow) {
if (logData.pagination.hasPreviousPage) {
loadLogData({ page: logData.pagination.currentPage - 1 });
}
} else if (key.rightArrow) {
if (logData.pagination.hasNextPage) {
loadLogData({ page: logData.pagination.currentPage + 1 });
}
} else if (key.return || key.enter) {
if (selectedLog < logData.entries.length) {
setShowDetails(!showDetails);
}
} else if (key.r || input === "r") {
logReader.clearCache();
loadLogData();
loadStats();
} else if (input >= "1" && input <= "5") {
const filterMap = {
1: "ALL",
2: "ERROR",
3: "WARNING",
4: "INFO",
5: "SUCCESS",
};
const newFilter = filterMap[input];
if (newFilter !== logData.filters.levelFilter) {
loadLogData({ levelFilter: newFilter, page: 0 });
}
} else if (input === "s") {
const searchTerms = ["", "error", "update", "rollback", "product"];
const currentIndex = searchTerms.indexOf(logData.filters.searchTerm);
const nextIndex = (currentIndex + 1) % searchTerms.length;
const newSearchTerm = searchTerms[nextIndex];
loadLogData({ searchTerm: newSearchTerm, page: 0 });
} else if (input === "c") {
loadLogData({ levelFilter: "ALL", searchTerm: "", page: 0 });
} else if (input === "a") {
setAutoRefresh(!autoRefresh);
} else if (key.pageUp) {
if (logData.pagination.currentPage > 0) {
loadLogData({ page: 0 });
}
} else if (key.pageDown) {
if (
logData.pagination.currentPage <
logData.pagination.totalPages - 1
) {
loadLogData({ page: logData.pagination.totalPages - 1 });
}
}
},
[
loading,
navigateBack,
selectedLog,
logData,
showDetails,
loadLogData,
loadStats,
logReader,
autoRefresh,
]
);
useInput(handleKeyboardInput);
// Memoized render function for log entries
const renderLogEntry = React.useCallback(
(log, index) => {
const isSelected = selectedLog === index;
const isHighlighted = isSelected && !showDetails;
return (
<LogEntry
log={log}
index={index}
isSelected={isSelected}
isHighlighted={isHighlighted}
getLogLevelColor={getLogLevelColor}
formatDate={formatDate}
truncateText={truncateText}
/>
);
},
[selectedLog, showDetails, getLogLevelColor, formatDate, truncateText]
);
// Show loading state
if (loading && logData.entries.length === 0) {
return (
<Box
flexDirection="column"
padding={2}
justifyContent="center"
alignItems="center"
>
<Text color="blue">Loading logs...</Text>
<Text color="gray">Please wait while we read the log files</Text>
</Box>
);
}
// Show error state
if (error) {
return (
<Box
flexDirection="column"
padding={2}
justifyContent="center"
alignItems="center"
>
<Text color="red" bold={true}>
Error Loading Logs
</Text>
<Text color="gray">{error}</Text>
<Text color="gray" marginTop={1}>
Press 'r' to retry or Esc to go back
</Text>
</Box>
);
}
return (
<Box flexDirection="column" padding={2} flexGrow={1}>
{/* Header with statistics */}
<Box flexDirection="column" marginBottom={2}>
<Text bold={true} color="cyan">
📋 Log Viewer
</Text>
<Box flexDirection="row" justifyContent="space-between">
<Text color="gray">View application logs and operation history</Text>
{stats && (
<Text color="gray">
{stats.totalEntries} entries | {stats.operations.total} operations
</Text>
)}
</Box>
</Box>
{/* Filter and pagination controls */}
<Box
borderStyle="single"
borderColor="blue"
paddingX={1}
paddingY={1}
marginBottom={2}
>
<Box flexDirection="column">
<Box
flexDirection="row"
justifyContent="space-between"
alignItems="center"
>
<Box flexDirection="row" alignItems="center">
<Text color="white" bold={true}>
Filter:{" "}
</Text>
<Text color="blue">{logData.filters.levelFilter}</Text>
<Text color="gray" marginLeft={2}>
(1-5 to change)
</Text>
</Box>
<Box flexDirection="row" alignItems="center">
<Text color="white" bold={true}>
Page:{" "}
</Text>
<Text color="blue">
{logData.pagination.currentPage + 1}/
{logData.pagination.totalPages}
</Text>
<Text color="gray" marginLeft={2}>
(/ to navigate)
</Text>
</Box>
</Box>
<Text color="gray">
Showing {logData.pagination.startIndex}-
{logData.pagination.endIndex} of {logData.pagination.totalEntries}{" "}
entries
</Text>
</Box>
</Box>
{/* Virtual scrolled log list */}
<VirtualScrollableContainer
items={logData.entries}
renderItem={renderLogEntry}
itemHeight={1}
showScrollIndicators={true}
overscan={10}
borderStyle="single"
borderColor="gray"
flexGrow={1}
minHeight={10}
/>
{/* Log details */}
{showDetails &&
selectedLog < logData.entries.length &&
logData.entries[selectedLog] && (
<LogDetails
log={logData.entries[selectedLog]}
getLogLevelColor={getLogLevelColor}
/>
)}
{/* Instructions */}
<Box
flexDirection="column"
marginTop={2}
borderTopStyle="single"
borderColor="gray"
paddingTop={1}
>
<Box flexDirection="row" justifyContent="space-between">
<Box flexDirection="column">
<Text color="gray">
Navigation: / entries | / pages | Enter details
</Text>
<Text color="gray">
Filters: 1=All 2=Error 3=Warning 4=Info 5=Success
</Text>
</Box>
<Box flexDirection="column">
<Text color="gray">
Search: S cycle terms | C clear | A auto-refresh
</Text>
<Text color="gray">
Actions: R refresh | PgUp/PgDn jump | Esc back
</Text>
</Box>
</Box>
</Box>
{/* Status bar */}
<Box
borderStyle="single"
borderColor="gray"
paddingX={1}
paddingY={0}
marginTop={1}
>
<Box flexDirection="row" justifyContent="space-between">
<Text color="gray">
Entry {selectedLog + 1}/{logData.entries.length} | Details:{" "}
{showDetails ? "ON" : "OFF"}
</Text>
<Text color={loading ? "yellow" : refreshing ? "cyan" : "gray"}>
{loading
? "Loading..."
: refreshing
? "Refreshing..."
: `Filter: ${logData.filters.levelFilter}${
logData.filters.searchTerm
? ` | Search: "${logData.filters.searchTerm}"`
: ""
} | Auto: ${
autoRefresh ? "ON" : "OFF"
} | ${lastRefresh.toLocaleTimeString()}`}
</Text>
</Box>
</Box>
</Box>
);
});
module.exports = OptimizedLogViewerScreen;

File diff suppressed because it is too large Load Diff

View File

@@ -2,69 +2,29 @@ const React = require("react");
const { Box, Text, useInput, useApp } = require("ink");
const { useAppState } = require("../../providers/AppProvider.jsx");
const SelectInput = require("ink-select-input").default;
const TagAnalysisService = require("../../../services/tagAnalysis");
/**
* Tag Analysis Screen Component
* Analyzes product tags and provides insights for price update operations
* Requirements: 7.7, 7.8
* Requirements: 7.1, 7.2, 7.3
*/
const TagAnalysisScreen = () => {
const { appState, navigateBack } = useAppState();
const { exit } = useApp();
// Mock tag analysis data
const mockTagAnalysis = {
totalProducts: 150,
tagCounts: [
{ tag: "sale", count: 45, percentage: 30.0 },
{ tag: "new", count: 32, percentage: 21.3 },
{ tag: "featured", count: 28, percentage: 18.7 },
{ tag: "clearance", count: 22, percentage: 14.7 },
{ tag: "limited", count: 15, percentage: 10.0 },
{ tag: "seasonal", count: 8, percentage: 5.3 },
],
priceRanges: {
sale: { min: 9.99, max: 199.99, average: 59.5 },
new: { min: 19.99, max: 299.99, average: 89.75 },
featured: { min: 29.99, max: 399.99, average: 129.5 },
clearance: { min: 4.99, max: 149.99, average: 39.25 },
limited: { min: 49.99, max: 499.99, average: 199.5 },
seasonal: { min: 14.99, max: 249.99, average: 74.25 },
},
recommendations: [
{
type: "high_impact",
title: "High-Impact Tags",
description:
"Tags with many products that would benefit most from price updates",
tags: ["sale", "clearance"],
reason:
"These tags have the highest product counts and are most likely to need price adjustments",
},
{
type: "high_value",
title: "High-Value Tags",
description: "Tags with products having higher average prices",
tags: ["limited", "featured"],
reason:
"These tags contain premium products where price adjustments have the most financial impact",
},
{
type: "caution",
title: "Use Caution",
description: "Tags that may require special handling",
tags: ["new", "seasonal"],
reason:
"These tags may have products with special pricing strategies that shouldn't be automatically adjusted",
},
],
};
// State for tag analysis
const [analysisData, setAnalysisData] = React.useState(mockTagAnalysis);
const [analysisData, setAnalysisData] = React.useState(null);
const [selectedTag, setSelectedTag] = React.useState(null);
const [showDetails, setShowDetails] = React.useState(false);
const [analysisType, setAnalysisType] = React.useState("overview");
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
const [sampleProducts, setSampleProducts] = React.useState([]);
const [loadingSamples, setLoadingSamples] = React.useState(false);
// Initialize tag analysis service
const tagAnalysisService = React.useMemo(() => new TagAnalysisService(), []);
// Analysis type options
const analysisOptions = [
@@ -74,33 +34,84 @@ const TagAnalysisScreen = () => {
{ value: "recommendations", label: "Recommendations" },
];
// Load tag analysis data on component mount
React.useEffect(() => {
loadTagAnalysis();
}, []);
// Load tag analysis data
const loadTagAnalysis = async () => {
setLoading(true);
setError(null);
try {
const analysis = await tagAnalysisService.getTagAnalysis();
setAnalysisData(analysis);
setSelectedTag(null);
setShowDetails(false);
setSampleProducts([]);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// Load sample products for selected tag
const loadSampleProducts = async (tag) => {
if (!tag) return;
setLoadingSamples(true);
try {
const samples = await tagAnalysisService.getSampleProductsForTag(tag, 3);
setSampleProducts(samples);
} catch (err) {
setSampleProducts([]);
} finally {
setLoadingSamples(false);
}
};
// Handle keyboard input
useInput((input, key) => {
if (loading) return; // Ignore input while loading
if (key.escape) {
// Go back to main menu
navigateBack();
} else if (key.upArrow) {
} else if (key.upArrow && analysisData) {
// Navigate up in list
if (selectedTag > 0) {
if (selectedTag === null) {
setSelectedTag(analysisData.tagCounts.length - 1);
} else if (selectedTag > 0) {
setSelectedTag(selectedTag - 1);
setShowDetails(false);
}
} else if (key.downArrow) {
// Navigate down in list
if (selectedTag < analysisData.tagCounts.length - 1) {
setSelectedTag(selectedTag + 1);
setShowDetails(false);
}
} else if (key.return || key.enter) {
// Toggle tag details
if (selectedTag !== null) {
setShowDetails(!showDetails);
}
} else if (key.r) {
// Refresh analysis
setAnalysisData(mockTagAnalysis);
setSelectedTag(null);
setShowDetails(false);
setSampleProducts([]);
} else if (key.downArrow && analysisData) {
// Navigate down in list
if (selectedTag === null) {
setSelectedTag(0);
} else if (selectedTag < analysisData.tagCounts.length - 1) {
setSelectedTag(selectedTag + 1);
}
setShowDetails(false);
setSampleProducts([]);
} else if ((key.return || key.enter) && analysisData) {
// Toggle tag details and load samples
if (selectedTag !== null) {
const newShowDetails = !showDetails;
setShowDetails(newShowDetails);
if (newShowDetails) {
const tagName = analysisData.tagCounts[selectedTag].tag;
loadSampleProducts(tagName);
} else {
setSampleProducts([]);
}
}
} else if (input === "r" || input === "R") {
// Refresh analysis
loadTagAnalysis();
}
});
@@ -117,9 +128,30 @@ const TagAnalysisScreen = () => {
return "green";
};
// Render overview section
const renderOverview = () =>
// Render loading state
const renderLoading = () =>
React.createElement(
Box,
{ flexDirection: "column", alignItems: "center", justifyContent: "center", height: 10 },
React.createElement(Text, { color: "blue" }, "🔄 Loading tag analysis..."),
React.createElement(Text, { color: "gray" }, "This may take a moment...")
);
// Render error state
const renderError = () =>
React.createElement(
Box,
{ flexDirection: "column", alignItems: "center", justifyContent: "center", height: 10 },
React.createElement(Text, { color: "red", bold: true }, "❌ Error loading tag analysis"),
React.createElement(Text, { color: "white" }, error),
React.createElement(Text, { color: "gray", marginTop: 1 }, "Press 'R' to retry")
);
// Render overview section
const renderOverview = () => {
if (!analysisData) return null;
return React.createElement(
Box,
{ flexDirection: "column" },
React.createElement(
@@ -155,6 +187,11 @@ const TagAnalysisScreen = () => {
`Most Common Tag: ${analysisData.tagCounts[0]?.tag || "N/A"} (${
analysisData.tagCounts[0]?.count || 0
} products)`
),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
`Last Updated: ${new Date(analysisData.analyzedAt).toLocaleString()}`
)
)
),
@@ -226,8 +263,10 @@ const TagAnalysisScreen = () => {
);
// Render pricing analysis
const renderPricingAnalysis = () =>
React.createElement(
const renderPricingAnalysis = () => {
if (!analysisData) return null;
return React.createElement(
Box,
{ flexDirection: "column" },
React.createElement(
@@ -269,7 +308,7 @@ const TagAnalysisScreen = () => {
color: isSelected ? "white" : "gray",
fontSize: "small",
},
`${tagInfo.count} products`
`${tagInfo.count} products (${analysisData.priceRanges[tagInfo.tag]?.count || 0} variants)`
),
React.createElement(
Box,
@@ -280,14 +319,14 @@ const TagAnalysisScreen = () => {
color: isSelected ? "white" : "cyan",
bold: true,
},
"Range: "
"Range: $"
),
React.createElement(
Text,
{
color: isSelected ? "white" : "white",
},
`$${priceRange.min} - $${priceRange.max}`
`${priceRange.min.toFixed(2)} - $${priceRange.max.toFixed(2)}`
)
),
React.createElement(
@@ -299,7 +338,7 @@ const TagAnalysisScreen = () => {
color: isSelected ? "white" : "cyan",
bold: true,
},
"Avg: "
"Avg: $"
),
React.createElement(
Text,
@@ -313,10 +352,13 @@ const TagAnalysisScreen = () => {
);
})
);
};
// Render recommendations
const renderRecommendations = () =>
React.createElement(
const renderRecommendations = () => {
if (!analysisData) return null;
return React.createElement(
Box,
{ flexDirection: "column" },
React.createElement(
@@ -331,8 +373,14 @@ const TagAnalysisScreen = () => {
return "green";
case "high_value":
return "blue";
case "optimal":
return "magenta";
case "consistency":
return "red";
case "caution":
return "yellow";
case "low_count":
return "gray";
default:
return "white";
}
@@ -344,13 +392,33 @@ const TagAnalysisScreen = () => {
return "⭐";
case "high_value":
return "💎";
case "optimal":
return "🎯";
case "consistency":
return "⚖️";
case "caution":
return "⚠️";
case "low_count":
return "🔍";
default:
return "";
}
};
const getPriorityBadge = (priority) => {
const colors = {
high: "red",
medium: "yellow",
low: "blue",
info: "gray"
};
return React.createElement(
Text,
{ color: colors[priority] || "white", bold: true },
`[${priority.toUpperCase()}]`
);
};
return React.createElement(
Box,
{
@@ -364,6 +432,7 @@ const TagAnalysisScreen = () => {
React.createElement(
Box,
{ flexDirection: "column" },
// Header with icon, title, and priority
React.createElement(
Box,
{ flexDirection: "row", alignItems: "center", marginBottom: 1 },
@@ -373,31 +442,96 @@ const TagAnalysisScreen = () => {
color: getTypeColor(rec.type),
bold: true,
},
`${getTypeIcon(rec.type)} ${rec.title}`
`${getTypeIcon(rec.type)} ${rec.title} `
),
getPriorityBadge(rec.priority),
rec.actionable && React.createElement(
Text,
{ color: "green", marginLeft: 1 },
"[ACTIONABLE]"
)
),
// Description
React.createElement(
Text,
{ color: "white", marginBottom: 1 },
rec.description
),
// Tags
React.createElement(
Text,
{ color: "gray", italic: true, marginBottom: 1 },
rec.reason
Box,
{ flexDirection: "row", marginBottom: 1 },
React.createElement(
Text,
{ color: "cyan", bold: true },
"Tags: "
),
React.createElement(
Text,
{ color: "white" },
rec.tags.join(", ")
)
),
// Estimated impact
rec.estimatedImpact && React.createElement(
Box,
{ flexDirection: "row", marginBottom: 1 },
React.createElement(
Text,
{ color: "green", bold: true },
"Impact: "
),
React.createElement(
Text,
{ color: "white" },
rec.estimatedImpact
)
),
// Detailed information for some recommendation types
rec.details && rec.details.length > 0 && React.createElement(
Box,
{ flexDirection: "column", marginTop: 1, marginLeft: 2 },
React.createElement(
Text,
{ color: "gray", bold: true },
"Details:"
),
rec.details.slice(0, 3).map((detail, detailIdx) =>
React.createElement(
Text,
{ key: detailIdx, color: "gray" },
rec.type === 'high_impact' ?
`${detail.tag}: ${detail.count} products (${detail.percentage.toFixed(1)}%)` :
rec.type === 'high_value' ?
`${detail.tag}: $${detail.averagePrice.toFixed(2)} avg, ${detail.count} products` :
rec.type === 'optimal' ?
`${detail.tag}: Score ${detail.score.toFixed(1)}, ${detail.count} products` :
rec.type === 'consistency' ?
`${detail.tag}: ${detail.issue} (${detail.variationRatio}x variation)` :
rec.type === 'caution' ?
`${detail.tag}: ${detail.count} products (${detail.riskLevel} risk)` :
`${detail.tag}: ${detail.count} products`
)
)
),
// Reason
React.createElement(
Text,
{ color: "cyan", bold: true },
"Tags: " + rec.tags.join(", ")
{ color: "gray", italic: true, marginTop: 1 },
rec.reason
)
)
);
})
);
};
// Render current analysis view
const renderCurrentView = () => {
if (loading) return renderLoading();
if (error) return renderError();
if (!analysisData) return renderError();
switch (analysisType) {
case "overview":
return renderOverview();
@@ -599,7 +733,39 @@ const TagAnalysisScreen = () => {
].average.toFixed(2)}`
)
)
),
// Sample products section
sampleProducts.length > 0 && React.createElement(
Box,
{ flexDirection: "column", marginTop: 1 },
React.createElement(
Text,
{ color: "white", bold: true },
"Sample Products:"
),
sampleProducts.map((product, idx) =>
React.createElement(
Box,
{ key: idx, flexDirection: "column", marginLeft: 2, marginTop: 0.5 },
React.createElement(
Text,
{ color: "cyan" },
`${product.title}`
),
product.variants.length > 0 && React.createElement(
Text,
{ color: "gray", marginLeft: 2 },
`Price: $${product.variants[0].price}${product.variants[0].compareAtPrice ? ` (was $${product.variants[0].compareAtPrice})` : ''}`
)
)
)
),
// Loading indicator for samples
loadingSamples && React.createElement(
Box,
{ flexDirection: "row", alignItems: "center", marginTop: 1 },
React.createElement(Text, { color: "blue" }, "🔄 Loading sample products...")
)
)
),
@@ -615,7 +781,7 @@ const TagAnalysisScreen = () => {
},
React.createElement(Text, { color: "gray" }, "Controls:"),
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate items"),
React.createElement(Text, { color: "gray" }, " Enter - View details"),
React.createElement(Text, { color: "gray" }, " Enter - View details & sample products"),
React.createElement(Text, { color: "gray" }, " R - Refresh analysis"),
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
),

View File

@@ -0,0 +1,525 @@
const React = require("react");
const { Box, Text, useInput } = require("ink");
const { useAppState } = require("../../providers/AppProvider.jsx");
const { useServices } = require("../../hooks/useServices.js");
const { LoadingIndicator } = require("../common/LoadingIndicator.jsx");
const ErrorDisplay = require("../common/ErrorDisplay.jsx");
const { Pagination } = require("../common/Pagination.jsx");
/**
* View Logs Screen Component
* Log file list view with keyboard navigation and metadata display
* Requirements: 2.1, 2.8, 4.1, 4.2
*/
const ViewLogsScreen = () => {
const { navigateBack } = useAppState();
const { getLogFiles, readLogFile } = useServices();
// State management for log files, selected file, and content
const [logFiles, setLogFiles] = React.useState([]);
const [selectedFileIndex, setSelectedFileIndex] = React.useState(0);
const [selectedFile, setSelectedFile] = React.useState(null);
const [logContent, setLogContent] = React.useState("");
const [parsedLogs, setParsedLogs] = React.useState([]);
const [currentPage, setCurrentPage] = React.useState(0);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const [loadingContent, setLoadingContent] = React.useState(false);
const [contentError, setContentError] = React.useState(null);
const [viewMode, setViewMode] = React.useState("parsed"); // "parsed" or "raw"
// Load log files on component mount
React.useEffect(() => {
const loadLogFiles = async () => {
try {
setLoading(true);
setError(null);
const files = await getLogFiles();
setLogFiles(files);
// Auto-select the main Progress.md file if it exists
const mainLogIndex = files.findIndex((file) => file.isMainLog);
if (mainLogIndex !== -1) {
setSelectedFileIndex(mainLogIndex);
}
} catch (err) {
setError(`Failed to discover log files: ${err.message}`);
} finally {
setLoading(false);
}
};
loadLogFiles();
}, [getLogFiles]);
// Load content for selected file
const loadFileContent = React.useCallback(
async (file) => {
if (!file) return;
try {
setLoadingContent(true);
setContentError(null);
setCurrentPage(0); // Reset pagination
const content = await readLogFile(file.filename);
setLogContent(content);
// Parse the content into structured log entries
const { parseLogContent } = useServices();
const parsed = parseLogContent(content);
setParsedLogs(parsed);
setSelectedFile(file);
} catch (err) {
setContentError(`Failed to read log file: ${err.message}`);
setLogContent("");
setParsedLogs([]);
} finally {
setLoadingContent(false);
}
},
[readLogFile, useServices]
);
// Helper function to format file size
const formatFileSize = (bytes) => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
};
// Helper function to format date
const formatDate = (date) => {
try {
return date.toLocaleString();
} catch (error) {
return "Invalid date";
}
};
// Helper function to get relative time
const getRelativeTime = (date) => {
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
};
// Keyboard navigation for log file selection
useInput((input, key) => {
if (loading) return; // Ignore input while loading
if (key.escape) {
navigateBack();
} else if (key.upArrow) {
setSelectedFileIndex((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {
setSelectedFileIndex((prev) => Math.min(logFiles.length - 1, prev + 1));
} else if (key.return) {
// Load content for selected file
if (logFiles.length > 0 && selectedFileIndex < logFiles.length) {
loadFileContent(logFiles[selectedFileIndex]);
}
} else if (input === "r") {
// Refresh log files list
setLoading(true);
setError(null);
getLogFiles()
.then(setLogFiles)
.catch((err) =>
setError(`Failed to discover log files: ${err.message}`)
)
.finally(() => setLoading(false));
}
});
// Show loading state
if (loading) {
return React.createElement(
Box,
{ flexDirection: "column", padding: 2, flexGrow: 1 },
React.createElement(Text, { bold: true, color: "cyan" }, "📋 View Logs"),
React.createElement(LoadingIndicator, {
message: "Discovering log files...",
})
);
}
// Show error state
if (error) {
return React.createElement(
Box,
{ flexDirection: "column", padding: 2, flexGrow: 1 },
React.createElement(Text, { bold: true, color: "cyan" }, "📋 View Logs"),
React.createElement(ErrorDisplay, {
error: { message: error },
onRetry: () => {
setError(null);
setLoading(true);
getLogFiles()
.then(setLogFiles)
.catch((err) =>
setError(`Failed to discover log files: ${err.message}`)
)
.finally(() => setLoading(false));
},
})
);
}
// Show file content view if a file is selected
if (selectedFile) {
return React.createElement(
Box,
{ flexDirection: "column", padding: 2, flexGrow: 1 },
// Header
React.createElement(
Box,
{ flexDirection: "column", marginBottom: 2 },
React.createElement(
Text,
{ bold: true, color: "cyan" },
"📄 Log File Content"
),
React.createElement(
Text,
{ color: "gray" },
`Viewing: ${selectedFile.filename}`
)
),
// File metadata
React.createElement(
Box,
{
borderStyle: "single",
borderColor: "blue",
paddingX: 2,
paddingY: 1,
marginBottom: 2,
},
React.createElement(
Box,
{ flexDirection: "column" },
React.createElement(
Box,
{ flexDirection: "row", justifyContent: "space-between" },
React.createElement(
Text,
{ color: "white", bold: true },
selectedFile.filename
),
React.createElement(
Text,
{ color: selectedFile.isMainLog ? "green" : "gray" },
selectedFile.isMainLog ? "MAIN LOG" : "ARCHIVE"
)
),
React.createElement(
Box,
{ flexDirection: "row", marginTop: 1 },
React.createElement(
Box,
{ flexDirection: "column", width: "50%" },
React.createElement(
Text,
{ color: "cyan" },
`Size: ${formatFileSize(selectedFile.size)}`
),
React.createElement(
Text,
{ color: "cyan" },
`Operations: ${selectedFile.operationCount}`
)
),
React.createElement(
Box,
{ flexDirection: "column", width: "50%" },
React.createElement(
Text,
{ color: "cyan" },
`Created: ${getRelativeTime(selectedFile.createdAt)}`
),
React.createElement(
Text,
{ color: "cyan" },
`Modified: ${getRelativeTime(selectedFile.modifiedAt)}`
)
)
)
)
),
// Content display
React.createElement(
Box,
{
borderStyle: "single",
borderColor: "gray",
paddingX: 1,
paddingY: 1,
flexGrow: 1,
marginBottom: 2,
},
loadingContent
? React.createElement(LoadingIndicator, {
message: "Loading file content...",
})
: contentError
? React.createElement(ErrorDisplay, {
error: { message: contentError },
onRetry: () => loadFileContent(selectedFile),
})
: logContent
? React.createElement(
Box,
{ flexDirection: "column" },
React.createElement(
Text,
{ color: "white", wrap: "wrap" },
logContent.substring(0, 2000) // Show first 2000 characters
),
logContent.length > 2000 &&
React.createElement(
Text,
{ color: "yellow", marginTop: 1 },
`... (${logContent.length - 2000} more characters)`
)
)
: React.createElement(
Text,
{ color: "gray" },
"File is empty or could not be read"
)
),
// Instructions
React.createElement(
Box,
{
flexDirection: "column",
borderTopStyle: "single",
borderColor: "gray",
paddingTop: 1,
},
React.createElement(Text, { color: "gray", bold: true }, "Navigation:"),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
" Esc - Back to file list R - Refresh content"
)
)
);
}
return React.createElement(
Box,
{ flexDirection: "column", padding: 2, flexGrow: 1 },
// Header
React.createElement(
Box,
{ flexDirection: "column", marginBottom: 2 },
React.createElement(Text, { bold: true, color: "cyan" }, "📋 View Logs"),
React.createElement(
Text,
{ color: "gray" },
"Select a log file to view its contents and operation history"
)
),
// Log file list view
React.createElement(
Box,
{
borderStyle: "single",
borderColor: "blue",
paddingX: 1,
paddingY: 1,
marginBottom: 2,
flexGrow: 1,
},
React.createElement(
Box,
{ flexDirection: "column" },
React.createElement(
Text,
{ bold: true, color: "blue", marginBottom: 1 },
`📁 Available Log Files (${logFiles.length})`
),
logFiles.length === 0
? React.createElement(
Box,
{ justifyContent: "center", alignItems: "center", padding: 2 },
React.createElement(
Text,
{ color: "gray" },
"No log files found"
),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
"Log files are created when operations are performed"
),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
"Run some price update operations to generate logs"
)
)
: React.createElement(
Box,
{ flexDirection: "column" },
logFiles.map((file, index) => {
const isSelected = selectedFileIndex === index;
return React.createElement(
Box,
{
key: file.filename,
flexDirection: "column",
paddingY: 1,
paddingX: 1,
backgroundColor: isSelected ? "blue" : undefined,
borderStyle: isSelected ? "single" : "none",
borderColor: isSelected ? "cyan" : "gray",
marginBottom: 1,
},
// File name and status
React.createElement(
Box,
{ flexDirection: "row", justifyContent: "space-between" },
React.createElement(
Text,
{
color: isSelected ? "white" : "white",
bold: isSelected,
},
`${isSelected ? "► " : " "}${file.filename}`
),
React.createElement(
Text,
{ color: file.isMainLog ? "green" : "gray", bold: true },
file.isMainLog ? "MAIN" : "ARCHIVE"
)
),
// File metadata
React.createElement(
Box,
{
flexDirection: "row",
justifyContent: "space-between",
marginTop: 1,
},
React.createElement(
Box,
{ flexDirection: "row" },
React.createElement(
Text,
{
color: isSelected ? "gray" : "gray",
marginLeft: isSelected ? 2 : 2,
},
`${formatFileSize(file.size)}`
),
React.createElement(
Text,
{
color: isSelected ? "gray" : "gray",
marginLeft: 2,
},
`${file.operationCount} ops`
)
),
React.createElement(
Text,
{
color: isSelected ? "gray" : "gray",
},
getRelativeTime(file.modifiedAt)
)
),
// Creation date
React.createElement(
Text,
{
color: isSelected ? "gray" : "gray",
marginLeft: isSelected ? 2 : 2,
marginTop: 1,
},
`Created: ${formatDate(file.createdAt)}`
)
);
})
)
)
),
// Instructions
React.createElement(
Box,
{
flexDirection: "column",
borderTopStyle: "single",
borderColor: "gray",
paddingTop: 1,
},
React.createElement(Text, { color: "gray", bold: true }, "Navigation:"),
React.createElement(
Box,
{ flexDirection: "row", marginTop: 1 },
React.createElement(
Box,
{ flexDirection: "column", width: "50%" },
React.createElement(Text, { color: "gray" }, " ↑/↓ - Select file"),
React.createElement(Text, { color: "gray" }, " Enter - View content")
),
React.createElement(
Box,
{ flexDirection: "column", width: "50%" },
React.createElement(Text, { color: "gray" }, " R - Refresh list"),
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
)
)
),
// Status bar
React.createElement(
Box,
{
borderStyle: "single",
borderColor: "gray",
paddingX: 1,
paddingY: 0,
marginTop: 1,
},
React.createElement(
Box,
{ flexDirection: "row", justifyContent: "space-between" },
React.createElement(
Text,
{ color: "gray" },
logFiles.length > 0
? `File ${selectedFileIndex + 1}/${logFiles.length}`
: "No files available"
),
React.createElement(
Text,
{ color: "gray" },
"Select a file to view detailed log content"
)
)
)
);
};
module.exports = ViewLogsScreen;