Start on step 17 next

This commit is contained in:
2025-08-15 15:39:28 -05:00
parent 62f6d6f279
commit 7be928a5be
18 changed files with 3163 additions and 1347 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,13 @@ const { Pagination } = require("../common/Pagination.jsx");
*/
const ViewLogsScreen = () => {
const { navigateBack } = useAppState();
const { getLogFiles, readLogFile } = useServices();
const {
getLogFiles,
readLogFile,
parseLogContent,
getFilteredLogs,
filterLogs,
} = useServices();
// State management for log files, selected file, and content
const [logFiles, setLogFiles] = React.useState([]);
@@ -21,12 +27,28 @@ const ViewLogsScreen = () => {
const [selectedFile, setSelectedFile] = React.useState(null);
const [logContent, setLogContent] = React.useState("");
const [parsedLogs, setParsedLogs] = React.useState([]);
const [filteredLogs, setFilteredLogs] = 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"
const [scrollOffset, setScrollOffset] = React.useState(0);
const [selectedLogIndex, setSelectedLogIndex] = React.useState(0);
const [showingContent, setShowingContent] = React.useState(false);
// Filter and search state
const [showFilters, setShowFilters] = React.useState(false);
const [filters, setFilters] = React.useState({
dateRange: "all",
operationType: "all",
status: "all",
searchTerm: "",
});
const [filterInputMode, setFilterInputMode] = React.useState(null); // "search", "dateRange", "operationType", "status"
const [searchInput, setSearchInput] = React.useState("");
const [totalUnfilteredEntries, setTotalUnfilteredEntries] = React.useState(0);
// Load log files on component mount
React.useEffect(() => {
@@ -35,10 +57,24 @@ const ViewLogsScreen = () => {
setLoading(true);
setError(null);
const files = await getLogFiles();
setLogFiles(files);
// Transform the files to match expected format
const transformedFiles = files.map((file) => ({
filename: file.name,
path: file.path,
size: file.size,
createdAt: file.created,
modifiedAt: file.modified,
isMainLog: file.isMain,
operationCount: 0, // Will be calculated from content if needed
}));
setLogFiles(transformedFiles);
// Auto-select the main Progress.md file if it exists
const mainLogIndex = files.findIndex((file) => file.isMainLog);
const mainLogIndex = transformedFiles.findIndex(
(file) => file.isMainLog
);
if (mainLogIndex !== -1) {
setSelectedFileIndex(mainLogIndex);
}
@@ -52,34 +88,60 @@ const ViewLogsScreen = () => {
loadLogFiles();
}, [getLogFiles]);
// Load content for selected file
// Load content for selected file with filtering
const loadFileContent = React.useCallback(
async (file) => {
async (file, filterOptions = null) => {
if (!file) return;
try {
setLoadingContent(true);
setContentError(null);
setCurrentPage(0); // Reset pagination
setScrollOffset(0); // Reset scroll
setSelectedLogIndex(0); // Reset selection
const content = await readLogFile(file.filename);
setLogContent(content);
const currentFilters = filterOptions || filters;
// Parse the content into structured log entries
const { parseLogContent } = useServices();
const parsed = parseLogContent(content);
setParsedLogs(parsed);
// Use getFilteredLogs if we have filtering service available
if (getFilteredLogs) {
const result = await getFilteredLogs({
filePath: file.filename,
page: 0,
pageSize: 1000, // Load all entries for client-side pagination
...currentFilters,
});
setLogContent(result.metadata?.rawContent || "");
setParsedLogs(result.entries || []);
setFilteredLogs(result.entries || []);
setTotalUnfilteredEntries(
result.metadata?.totalUnfilteredEntries || 0
);
} else {
// Fallback to original method
const content = await readLogFile(file.filename);
setLogContent(content);
// Parse the content into structured log entries
const parsed = parseLogContent(content);
setParsedLogs(parsed);
setFilteredLogs(parsed);
setTotalUnfilteredEntries(parsed.length);
}
setSelectedFile(file);
setShowingContent(true);
} catch (err) {
setContentError(`Failed to read log file: ${err.message}`);
setLogContent("");
setParsedLogs([]);
setFilteredLogs([]);
setTotalUnfilteredEntries(0);
} finally {
setLoadingContent(false);
}
},
[readLogFile, useServices]
[readLogFile, parseLogContent, getFilteredLogs, filterLogs, filters]
);
// Helper function to format file size
@@ -115,31 +177,276 @@ const ViewLogsScreen = () => {
return date.toLocaleDateString();
};
// Keyboard navigation for log file selection
// Helper function to get color for log level
const getLogLevelColor = (level) => {
switch (level?.toUpperCase()) {
case "ERROR":
return "red";
case "WARNING":
return "yellow";
case "SUCCESS":
return "green";
case "INFO":
default:
return "white";
}
};
// Helper function to get status icon
const getStatusIcon = (level) => {
switch (level?.toUpperCase()) {
case "ERROR":
return "❌";
case "WARNING":
return "⚠️";
case "SUCCESS":
return "✅";
case "INFO":
default:
return "";
}
};
// Helper function to format timestamp
const formatTimestamp = (timestamp) => {
try {
return timestamp.toLocaleString();
} catch (error) {
return "Invalid date";
}
};
// Helper function to truncate text
const truncateText = (text, maxLength = 80) => {
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + "...";
};
// Apply filters to current log content
const applyFilters = React.useCallback(
async (newFilters) => {
if (!selectedFile) return;
try {
setLoadingContent(true);
setContentError(null);
setCurrentPage(0); // Reset pagination
setSelectedLogIndex(0); // Reset selection
// Use getFilteredLogs if available
if (getFilteredLogs) {
const result = await getFilteredLogs({
filePath: selectedFile.filename,
page: 0,
pageSize: 1000, // Load all entries for client-side pagination
...newFilters,
});
setFilteredLogs(result.entries || []);
setTotalUnfilteredEntries(
result.metadata?.totalUnfilteredEntries || 0
);
} else {
// Fallback: filter parsed logs client-side
if (filterLogs) {
const filtered = filterLogs(parsedLogs, newFilters);
setFilteredLogs(filtered);
} else {
setFilteredLogs(parsedLogs);
}
}
setFilters(newFilters);
} catch (err) {
setContentError(`Failed to apply filters: ${err.message}`);
} finally {
setLoadingContent(false);
}
},
[selectedFile, getFilteredLogs, parsedLogs, filterLogs]
);
// Clear all filters
const clearFilters = React.useCallback(() => {
const defaultFilters = {
dateRange: "all",
operationType: "all",
status: "all",
searchTerm: "",
};
setSearchInput("");
applyFilters(defaultFilters);
}, [applyFilters]);
// Handle search input
const handleSearchInput = React.useCallback(
(newSearchTerm) => {
setSearchInput(newSearchTerm);
const newFilters = { ...filters, searchTerm: newSearchTerm };
applyFilters(newFilters);
},
[filters, applyFilters]
);
// Handle filter changes
const handleFilterChange = React.useCallback(
(filterType, value) => {
const newFilters = { ...filters, [filterType]: value };
applyFilters(newFilters);
},
[filters, applyFilters]
);
// Keyboard navigation for log file selection and content viewing
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]);
// Handle filter input mode
if (filterInputMode === "search") {
if (key.escape) {
setFilterInputMode(null);
setSearchInput(filters.searchTerm); // Reset to current filter
} else if (key.return) {
handleSearchInput(searchInput);
setFilterInputMode(null);
} else if (key.backspace) {
setSearchInput((prev) => prev.slice(0, -1));
} else if (input && input.length === 1 && !key.ctrl && !key.meta) {
setSearchInput((prev) => prev + input);
}
return;
}
if (key.escape) {
if (showFilters && showingContent) {
// Close filters panel
setShowFilters(false);
} else if (showingContent) {
// Go back to file list
setShowingContent(false);
setSelectedFile(null);
setLogContent("");
setParsedLogs([]);
setFilteredLogs([]);
setCurrentPage(0);
setScrollOffset(0);
setSelectedLogIndex(0);
setShowFilters(false);
} else {
navigateBack();
}
} else if (showingContent) {
// Handle filter panel navigation
if (showFilters) {
if (input === "s") {
// Start search input
setFilterInputMode("search");
setSearchInput(filters.searchTerm);
} else if (input === "d") {
// Cycle through date range options
const dateOptions = ["all", "today", "yesterday", "week", "month"];
const currentIndex = dateOptions.indexOf(filters.dateRange);
const nextIndex = (currentIndex + 1) % dateOptions.length;
handleFilterChange("dateRange", dateOptions[nextIndex]);
} else if (input === "t") {
// Cycle through operation type options
const typeOptions = ["all", "update", "rollback", "scheduled"];
const currentIndex = typeOptions.indexOf(filters.operationType);
const nextIndex = (currentIndex + 1) % typeOptions.length;
handleFilterChange("operationType", typeOptions[nextIndex]);
} else if (input === "l") {
// Cycle through status/level options
const statusOptions = ["all", "error", "warning", "success", "info"];
const currentIndex = statusOptions.indexOf(filters.status);
const nextIndex = (currentIndex + 1) % statusOptions.length;
handleFilterChange("status", statusOptions[nextIndex]);
} else if (input === "c") {
// Clear all filters
clearFilters();
}
}
// Handle content viewing navigation
if (viewMode === "parsed") {
// Navigation for parsed log entries
if (key.upArrow) {
setSelectedLogIndex((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {
setSelectedLogIndex((prev) =>
Math.min(filteredLogs.length - 1, prev + 1)
);
} else if (key.pageUp) {
setCurrentPage((prev) => Math.max(0, prev - 1));
} else if (key.pageDown) {
const totalPages = Math.ceil(filteredLogs.length / 10);
setCurrentPage((prev) => Math.min(totalPages - 1, prev + 1));
} else if (input === "v") {
// Toggle view mode
setViewMode(viewMode === "parsed" ? "raw" : "parsed");
} else if (input === "f") {
// Toggle filters panel
setShowFilters(!showFilters);
} else if (input === "r") {
// Refresh content
if (selectedFile) {
loadFileContent(selectedFile);
}
}
} else {
// Navigation for raw content
if (key.upArrow) {
setScrollOffset((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {
const maxLines = logContent.split("\n").length;
setScrollOffset((prev) => Math.min(maxLines - 10, prev + 1));
} else if (key.pageUp) {
setScrollOffset((prev) => Math.max(0, prev - 10));
} else if (key.pageDown) {
const maxLines = logContent.split("\n").length;
setScrollOffset((prev) => Math.min(maxLines - 10, prev + 10));
} else if (input === "v") {
// Toggle view mode
setViewMode(viewMode === "parsed" ? "raw" : "parsed");
} else if (input === "r") {
// Refresh content
if (selectedFile) {
loadFileContent(selectedFile);
}
}
}
} else {
// Handle file list navigation
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((files) => {
const transformedFiles = files.map((file) => ({
filename: file.name,
path: file.path,
size: file.size,
createdAt: file.created,
modifiedAt: file.modified,
isMainLog: file.isMain,
operationCount: 0,
}));
setLogFiles(transformedFiles);
})
.catch((err) =>
setError(`Failed to discover log files: ${err.message}`)
)
.finally(() => setLoading(false));
}
} 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));
}
});
@@ -178,87 +485,118 @@ const ViewLogsScreen = () => {
}
// Show file content view if a file is selected
if (selectedFile) {
if (showingContent && selectedFile) {
const pageSize = 10;
const totalPages = Math.ceil(filteredLogs.length / pageSize);
const startIndex = currentPage * pageSize;
const endIndex = Math.min(startIndex + pageSize, filteredLogs.length);
const currentPageLogs = filteredLogs.slice(startIndex, endIndex);
return React.createElement(
Box,
{ flexDirection: "column", padding: 2, flexGrow: 1 },
// Header
React.createElement(
Box,
{ flexDirection: "column", marginBottom: 2 },
{ flexDirection: "column", marginBottom: 1 },
React.createElement(
Text,
{ bold: true, color: "cyan" },
"📄 Log File Content"
Box,
{ flexDirection: "row", justifyContent: "space-between" },
React.createElement(
Text,
{ bold: true, color: "cyan" },
"📄 Log File Content"
),
React.createElement(
Text,
{ color: "yellow", bold: true },
viewMode === "parsed" ? "PARSED VIEW" : "RAW VIEW"
)
),
React.createElement(
Text,
{ color: "gray" },
`Viewing: ${selectedFile.filename}`
`Viewing: ${selectedFile.filename} (${filteredLogs.length}/${totalUnfilteredEntries} entries)`
)
),
// File metadata
React.createElement(
Box,
{
borderStyle: "single",
borderColor: "blue",
paddingX: 2,
paddingY: 1,
marginBottom: 2,
},
// Filter panel (if enabled)
showFilters &&
React.createElement(
Box,
{ flexDirection: "column" },
{
borderStyle: "single",
borderColor: "yellow",
paddingX: 1,
paddingY: 1,
marginBottom: 1,
},
React.createElement(
Box,
{ flexDirection: "row", justifyContent: "space-between" },
{ flexDirection: "column" },
React.createElement(
Text,
{ color: "white", bold: true },
selectedFile.filename
{ color: "yellow", bold: true },
"🔍 Filters & Search"
),
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%" },
{ flexDirection: "row", marginTop: 1 },
React.createElement(
Text,
{ color: "cyan" },
`Size: ${formatFileSize(selectedFile.size)}`
Box,
{ flexDirection: "column", width: "50%" },
React.createElement(
Text,
{ color: "gray" },
`Date Range: ${filters.dateRange}`
),
React.createElement(
Text,
{ color: "gray" },
`Operation: ${filters.operationType}`
)
),
React.createElement(
Text,
{ color: "cyan" },
`Operations: ${selectedFile.operationCount}`
Box,
{ flexDirection: "column", width: "50%" },
React.createElement(
Text,
{ color: "gray" },
`Status: ${filters.status}`
),
React.createElement(
Text,
{ color: "gray" },
`Search: ${filters.searchTerm || "(none)"}`
)
)
),
filterInputMode === "search" &&
React.createElement(
Box,
{
borderStyle: "single",
borderColor: "cyan",
paddingX: 1,
marginTop: 1,
},
React.createElement(
Text,
{ color: "cyan" },
`Search: ${searchInput}`
)
),
React.createElement(
Box,
{ flexDirection: "column", width: "50%" },
{ flexDirection: "row", marginTop: 1 },
React.createElement(
Text,
{ color: "cyan" },
`Created: ${getRelativeTime(selectedFile.createdAt)}`
),
React.createElement(
Text,
{ color: "cyan" },
`Modified: ${getRelativeTime(selectedFile.modifiedAt)}`
{ color: "gray" },
"S-Search D-Date T-Type L-Level C-Clear F-Close"
)
)
)
)
),
),
// Content display
React.createElement(
@@ -269,7 +607,7 @@ const ViewLogsScreen = () => {
paddingX: 1,
paddingY: 1,
flexGrow: 1,
marginBottom: 2,
marginBottom: 1,
},
loadingContent
? React.createElement(LoadingIndicator, {
@@ -280,29 +618,201 @@ const ViewLogsScreen = () => {
error: { message: contentError },
onRetry: () => loadFileContent(selectedFile),
})
: logContent
: !logContent
? React.createElement(
Box,
{ justifyContent: "center", alignItems: "center", padding: 2 },
React.createElement(
Text,
{ color: "gray", bold: true },
"📄 Empty Log File"
),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
"This log file is empty or could not be read."
),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
"Log entries are created when operations are performed."
)
)
: viewMode === "parsed"
? // Parsed view with syntax highlighting
React.createElement(
Box,
{ flexDirection: "column" },
filteredLogs.length === 0
? React.createElement(
Box,
{
justifyContent: "center",
alignItems: "center",
padding: 2,
},
React.createElement(
Text,
{ color: "gray", bold: true },
totalUnfilteredEntries === 0
? "📄 No Parsed Entries"
: "🔍 No Matching Entries"
),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
totalUnfilteredEntries === 0
? "The log file contains no parseable entries."
: `No entries match the current filters. (${totalUnfilteredEntries} total entries)`
),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
totalUnfilteredEntries === 0
? "Press 'v' to view raw content."
: "Press 'f' to adjust filters or 'c' to clear them."
)
)
: currentPageLogs.map((entry, index) => {
const globalIndex = startIndex + index;
const isSelected = selectedLogIndex === globalIndex;
const levelColor = getLogLevelColor(entry.level);
const statusIcon = getStatusIcon(entry.level);
return React.createElement(
Box,
{
key: entry.id,
flexDirection: "column",
paddingY: 1,
paddingX: 1,
backgroundColor: isSelected ? "blue" : undefined,
borderStyle: isSelected ? "single" : "none",
borderColor: isSelected ? "cyan" : "gray",
marginBottom: 1,
},
// Entry header
React.createElement(
Box,
{
flexDirection: "row",
justifyContent: "space-between",
},
React.createElement(
Box,
{ flexDirection: "row", alignItems: "center" },
React.createElement(
Text,
{ color: levelColor, bold: true },
`${statusIcon} ${entry.title}`
)
),
React.createElement(
Text,
{ color: "gray" },
formatTimestamp(entry.timestamp)
)
),
// Entry message
React.createElement(
Text,
{
color: isSelected ? "white" : "gray",
marginTop: 1,
marginLeft: 2,
},
truncateText(entry.message, 70)
),
// Entry details (if available and selected)
isSelected &&
entry.details &&
entry.details.trim() &&
React.createElement(
Box,
{
borderStyle: "single",
borderColor: "gray",
paddingX: 1,
paddingY: 1,
marginTop: 1,
marginLeft: 2,
},
React.createElement(
Text,
{ color: "white", wrap: "wrap" },
entry.details.trim()
)
)
);
})
)
: // Raw view with basic syntax highlighting
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"
Box,
{ flexDirection: "column" },
logContent
.split("\n")
.slice(scrollOffset, scrollOffset + 15)
.map((line, index) => {
const lineNumber = scrollOffset + index + 1;
const trimmedLine = line.trim();
// Determine line color based on content
let lineColor = "white";
if (trimmedLine.startsWith("##")) {
lineColor = "cyan"; // Operation headers
} else if (
trimmedLine.startsWith("**") &&
trimmedLine.endsWith("**")
) {
lineColor = "yellow"; // Section headers
} else if (trimmedLine.includes("❌")) {
lineColor = "red"; // Errors
} else if (trimmedLine.includes("⚠️")) {
lineColor = "yellow"; // Warnings
} else if (trimmedLine.includes("✅")) {
lineColor = "green"; // Success
} else if (trimmedLine.startsWith("-")) {
lineColor = "gray"; // List items
}
return React.createElement(
Box,
{
key: lineNumber,
flexDirection: "row",
},
React.createElement(
Text,
{ color: "gray", width: 4 },
String(lineNumber).padStart(3, " ") + " "
),
React.createElement(
Text,
{ color: lineColor, wrap: "wrap" },
line || " "
)
);
})
)
)
),
// Pagination for parsed view
viewMode === "parsed" &&
filteredLogs.length > pageSize &&
React.createElement(Pagination, {
currentPage,
totalPages,
totalItems: filteredLogs.length,
itemsPerPage: pageSize,
onPageChange: setCurrentPage,
compact: true,
}),
// Instructions
React.createElement(
Box,
@@ -311,13 +821,61 @@ const ViewLogsScreen = () => {
borderTopStyle: "single",
borderColor: "gray",
paddingTop: 1,
marginTop: 1,
},
React.createElement(Text, { color: "gray", bold: true }, "Navigation:"),
React.createElement(
Text,
{ color: "gray", marginTop: 1 },
" Esc - Back to file list R - Refresh content"
)
Box,
{ flexDirection: "row", marginTop: 1 },
React.createElement(
Box,
{ flexDirection: "column", width: "33%" },
React.createElement(
Text,
{ color: "gray" },
" ↑/↓ - Navigate entries"
),
React.createElement(
Text,
{ color: "gray" },
" PgUp/PgDn - Page navigation"
)
),
React.createElement(
Box,
{ flexDirection: "column", width: "33%" },
React.createElement(
Text,
{ color: "gray" },
" V - Toggle view mode"
),
React.createElement(Text, { color: "gray" }, " F - Toggle filters")
),
React.createElement(
Box,
{ flexDirection: "column", width: "34%" },
React.createElement(
Text,
{ color: "gray" },
" R - Refresh content"
),
React.createElement(
Text,
{ color: "gray" },
" Esc - Back to file list"
)
)
),
showFilters &&
React.createElement(
Box,
{ flexDirection: "row", marginTop: 1 },
React.createElement(
Text,
{ color: "yellow" },
"Filter Controls: S-Search D-Date T-Type L-Level C-Clear"
)
)
)
);
}

View File

@@ -0,0 +1,62 @@
const React = require("react");
const { ScreenErrorBoundary } = require("../common");
const { enhanceError, logError } = require("../../utils/errorHandler");
/**
* Higher-order component that wraps screens with error boundaries
* Requirements: 4.5, 16.1
*/
function withErrorBoundary(
WrappedComponent,
screenName,
screenSpecificTips = []
) {
const WrappedScreenComponent = (props) => {
const handleError = (error, errorInfo) => {
// Log the error with screen context
logError(error, {
screen: screenName,
errorInfo,
props: Object.keys(props),
});
};
const handleRetry = (retryCount) => {
console.info(`Retrying ${screenName}, attempt ${retryCount}`);
};
const handleReset = () => {
console.info(`Resetting ${screenName} after error`);
};
const handleExit = () => {
// Navigate back to main menu
if (props.navigateBack) {
props.navigateBack();
} else if (props.onExit) {
props.onExit();
}
};
return React.createElement(
ScreenErrorBoundary,
{
screenName,
screenSpecificTips,
onError: handleError,
onRetry: handleRetry,
onReset: handleReset,
onExit: handleExit,
maxRetries: 3,
},
React.createElement(WrappedComponent, props)
);
};
// Set display name for debugging
WrappedScreenComponent.displayName = `withErrorBoundary(${screenName})`;
return WrappedScreenComponent;
}
module.exports = withErrorBoundary;