【Claude 3.5 Sonnet】近未来の運用ドキュメントアプリを試作してみた
近未来の運用ドキュメントアプリ - 直感的な操作と臨場感で運用手順を革新!
はじめに
この記事はClaude 3.5 Sonnetで作成した近未来型運用ドキュメントアプリ(HTMLコード)をインプットにしてClaude 3.5 Sonnetより自動で生成されたものです。(一部、加筆、図の挿入を行っています)
従来の運用ドキュメントは時に分かりにくく、現場の状況とかけ離れたものになりがちでした。そこで、より直感的でインタラクティブな新しいタイプの運用アプリを開発しました。未来を想定した没入感のあるUI/UXで、まるで実際の運用現場にいるかのような体験ができます。
アプリ(運用ドキュメント)の画面
アプリのコンセプト
- 実際の運用手順をステップ・バイ・ステップで分かりやすく示す
- 各ステップごとに具体的なコマンドや手順を表示
- リアルタイムにサービスの状態をモニタリング
- 没入感のあるUI/UXで運用の臨場感を演出
従来のドキュメントでは実現が難しかった「手順の明確化」と「状況の可視化」を同時に叶えるのが、このアプリの最大の特長です。
今回は、Windowsサーバ再起動メンテナンス作業を想定したドキュメントアプリとなっています。
具体的な機能と特長
1. ステップ・バイ・ステップの手順案内
運用手順を細かなステップに分解し、1つ1つ確実に実行できるようにナビゲートします。現在の進捗も常に確認できるので、運用のどの段階にいるのかが一目瞭然です。
2. コマンドの表示とコピー機能
各ステップで実行が必要なコマンドを画面上に表示。さらにワンクリックでコピーできるので、手間なくターミナルに貼り付けられます。
3. リアルタイムなサービスステータスの更新
各サービスのステータスをモニター上にリアルタイムに反映。
ステップの進捗度合いや、ログチェックの結果問題があった場合はその内容をモニターに自動反映します。
4. 猫のアニメーションによる癒やしの提供
画面の片隅で猫のアニメーションが動いています。ユーザーは自由に猫を動かせるので、リラックスしながら作業に取り組めます。
5. 自動エラー検知とナビゲーション
ログチェックのステップでは、実際に実行したコマンドの実行結果を貼り付けることで実行結果の妥当性を自動チェックすることができます。
ログチェックのステップでエラーが見つかった場合、その内容を自動で判定して画面に表示します。
さらにエラーが発生したステップに自動でフォーカスし、画面をスクロールしてナビゲートします。
6. 作業完了時の魅力的なアニメーション
すべての作業ステップが完了すると、美しいパーティクルアニメーションが画面全体に広がります。達成感を味わいながら、次の業務にスムーズに移れます。
アプリコード
以下のHTML-コードを全部コピーして.htmlファイルとして保存するとアプリをブラウザで起動できます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r132/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/20.0.0/tween.umd.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Noto+Sans+JP" rel="stylesheet">
<title>近未来の作業手順書</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap');
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f0f8ff;
}
.fixed-header {
position: fixed;
top: 0;
left: 0;
width: 30%;
/* 幅を30%に設定 */
background-color: #ffffff;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 1000;
padding-left: 20px;
/* 左側の余白を追加 */
height: 100vh;
/* 高さを画面いっぱいに */
overflow-y: auto;
/* 縦方向のスクロールを可能に */
}
h1 {
color: #1e90ff;
text-align: center;
margin: 10px 0;
}
.progress-container {
width: 90%;
max-width: 800px;
margin: 10px auto;
height: 20px;
background-color: #cbffc1;
border-radius: 10px;
overflow: hidden;
position: relative;
}
.progress-bar {
width: 0%;
height: 100%;
background-color: #A100FF;
transition: width 0.5s;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #0c0c0c;
font-weight: bold;
}
.game-container {
margin-left: calc(30% + 40px);
/* fixed-headerの幅 + 左右の余白 */
width: calc(70% - 60px);
/* 残りの幅から左右の余白を引く */
padding: 20px;
background-color: white;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: relative;
}
.step {
margin-bottom: 20px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.step h3 {
margin-top: 0;
color: #1e90ff;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #1e90ff;
color: white;
text-decoration: none;
border-radius: 5px;
border: none;
cursor: pointer;
}
.button:disabled {
background-color: #cccccc;
}
.button.completed {
background-color: #A100FF;
}
.status {
margin-top: 20px;
text-align: center;
}
.complete {
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
#cat {
position: fixed;
width: 100px;
height: 100px;
cursor: move;
z-index: 1000;
transition: all 0.3s ease;
}
#cat svg {
width: 100%;
height: 100%;
}
.hidden {
display: none;
}
#task-complete {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgb(255, 238, 0);
color: rgb(61, 55, 55);
padding: 20px;
border-radius: 10px;
font-size: 24px;
z-index: 1001;
}
@keyframes earTwitch {
0%,
100% {
transform: rotate(0deg);
}
50% {
transform: rotate(-5deg);
}
}
.ear-left {
transform-origin: 35px 25px;
animation: earTwitch 4s infinite;
}
.ear-right {
transform-origin: 65px 25px;
animation: earTwitch 4s infinite reverse;
}
@keyframes tailWave {
0%,
100% {
d: path('M70 75 Q85 73 95 60');
}
50% {
d: path('M70 75 Q85 77 95 70');
}
}
.tail {
animation: tailWave 3s infinite;
}
@keyframes blink {
0%,
90%,
100% {
transform: scaleY(1);
}
95% {
transform: scaleY(0.1);
}
}
.eye-lid {
animation: blink 4s infinite;
}
@keyframes eyeShine {
0%,
100% {
transform: translate(0, 0);
}
50% {
transform: translate(1px, 1px);
}
}
.eye-shine {
animation: eyeShine 3s infinite;
}
@keyframes pupilDilate {
0%,
100% {
r: 5;
}
50% {
r: 6;
}
}
.pupil {
animation: pupilDilate 4s infinite;
}
@keyframes walk {
0%,
100% {
transform: translateX(0) scaleX(1);
}
25% {
transform: translateX(20px) scaleX(1);
}
50% {
transform: translateX(40px) scaleX(-1);
}
75% {
transform: translateX(20px) scaleX(-1);
}
}
#cat {
animation: walk 4s infinite;
}
.walking {
animation: walk 4s infinite linear;
}
.status-container {
position: fixed;
/* 位置を固定 */
left: 100px;
/* 左側に余白を追加し、進捗バーの下に配置 */
top: 230px;
/* 進捗バーに近い上部の位置に配置 */
width: 210px;
/* 横幅を短く設定 */
height: auto;
/* 高さは内容に応じて自動調整 */
max-height: 90vh;
/* 最大高さをビューポートの90%に設定 */
background: linear-gradient(135deg, #111, #333);
/* 背景にグラデーションを設定 */
border: 15px solid #444;
/* 境界線を設定 */
border-radius: 15px;
/* 境界線の角を丸くする */
box-shadow:
/* 影を設定 */
0 0 0 5px #222,
0 10px 20px rgba(0, 0, 0, 0.5),
inset 0 1px 1px rgba(255, 255, 255, 0.1);
padding: 15px;
/* 内側の余白を設定 */
font-family: 'Roboto', sans-serif;
/* フォントファミリーを設定 */
color: #ecf0f1;
/* 文字色を設定 */
display: flex;
/* Flexbox レイアウトを使用 */
flex-direction: column;
/* 子要素を縦方向に配置 */
z-index: 1002;
/* 最前面に表示 */
margin-bottom: 20px;
/* 下側の余白を追加 */
transform: translateY(0);
/* Y軸の変換をリセット(必要に応じて) */
}
.status-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(to right,
rgba(46, 204, 113, 0),
rgba(46, 204, 113, 1),
rgba(46, 204, 113, 0));
animation: scanline 3s linear infinite;
opacity: 0.5;
}
@keyframes scanline {
0% {
transform: translateY(0);
}
100% {
transform: translateY(280px);
/* .status-containerの高さに合わせて調整 */
}
}
.status-container h3 {
color: #2ecc71;
text-align: center;
margin-top: 5px;
margin-bottom: 15px;
font-size: 18px;
font-weight: 300;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 10px rgba(46, 204, 113, 0.5);
}
.status-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.status-item {
font-size: 12px;
padding: 5px;
display: flex;
align-items: center;
background: rgba(44, 62, 80, 0.6);
border-radius: 8px;
transition: all 0.3s ease;
}
.status-item:hover {
background: rgba(52, 73, 94, 0.8);
transform: translateX(5px);
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.status-text {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-state {
width: 60px;
text-align: center;
font-weight: bold;
border-radius: 4px;
padding: 2px 4px;
margin-left: 5px;
}
.status-running .status-indicator {
background-color: #2ecc71;
box-shadow: 0 0 10px #2ecc71;
}
.status-stopped .status-indicator {
background-color: #e74c3c;
box-shadow: 0 0 10px #e74c3c;
}
.status-running .status-state {
background-color: #2ecc71;
color: #fff;
}
.status-stopped .status-state {
background-color: #e74c3c;
color: #fff;
}
.tv-stand {
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
width: 150px;
height: 20px;
background: #444;
border-radius: 0 0 10px 10px;
}
/* 新しいスタイルを追加 */
#logCheckArea {
margin-top: 20px;
padding: 10px;
background-color: #f0f0f0;
border-radius: 5px;
}
#logTextarea {
width: 100%;
height: 200px;
margin-bottom: 10px;
}
#checkResult {
font-weight: bold;
margin-top: 10px;
}
.code-block {
background-color: #3a3838;
border: 1px solid #ddd;
color: white;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
margin: 10px 0;
position: relative;
}
.code-block pre {
margin: 0;
padding: 15px;
overflow-x: auto;
}
.copy-button {
position: absolute;
top: 5px;
right: 5px;
padding: 5px 10px;
background-color: #d749fa;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* 新しいスタイルを追加 */
@keyframes blinkBorder {
0% {
box-shadow: 0 0 0 0px rgba(255, 0, 0, 0.7);
}
50% {
box-shadow: 0 0 0 5px rgba(255, 0, 0, 0.7);
}
100% {
box-shadow: 0 0 0 0px rgba(255, 0, 0, 0.7);
}
}
.blink-border {
animation: blinkBorder 1s infinite;
}
.logCheckArea {
margin-top: 20px;
padding: 10px;
background-color: #f0f0f0;
border-radius: 5px;
}
.logTextarea {
width: 95%;
height: 200px;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
}
.checkResult {
font-weight: bold;
margin-top: 10px;
padding: 10px;
border-radius: 4px;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #1e90ff;
color: white;
text-decoration: none;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 16px;
}
.button:disabled {
background-color: #cccccc;
}
.button.completed {
background-color: #A100FF;
}
.completion-message {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
perspective: 1000px;
}
.message-content {
background: linear-gradient(45deg, #00a8ff, #7708e5);
color: white;
padding: 40px;
border-radius: 20px;
text-align: center;
transform: rotateX(90deg);
opacity: 0;
transition: all 1s ease-out;
}
.completion-message.show .message-content {
transform: rotateX(0deg);
opacity: 1;
}
.completion-message h1 {
font-size: 4em;
margin-bottom: 20px;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
.completion-message p {
font-size: 1.5em;
margin-bottom: 30px;
}
.completion-message button {
font-size: 1.2em;
padding: 10px 20px;
background: white;
color: #7708e5;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.completion-message button:hover {
background: #7708e5;
color: white;
}
.hidden {
display: none !important;
}
#completion-message {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
/* 透明度を追加 */
z-index: 9997;
display: none;
backdrop-filter: blur(5px);
/* 背景をぼかす効果を追加 */
}
#completion-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
display: none;
}
#particles-js {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9998;
display: none;
}
#message-container {
font-size: 48px;
color: #fff;
text-shadow: 0 0 10px #00a8ff;
}
.letter {
display: inline-block;
opacity: 0;
transform: translateZ(1000px) rotateY(90deg);
transition: all 2s ease-out;
}
.letter.visible {
opacity: 1;
transform: translateZ(0) rotateY(0deg);
}
/* エラーメッセージのスタイルを追加 */
.error-message {
background-color: #ffcccc;
border: 2px solid #ff0000;
padding: 10px;
margin-top: 10px;
border-radius: 5px;
font-weight: bold;
color: #ff0000;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.error-message.visible {
opacity: 1;
}
/* ハイライト用のスタイル */
.highlight {
animation: highlightPulse 2s infinite;
}
@keyframes highlightPulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(255, 0, 0, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 0, 0, 0);
}
}
</style>
</head>
<body>
<div id="completion-message">
<div id="particles-js"></div>
<canvas id="completion-canvas"></canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/particles.js/2.0.0/particles.min.js"></script>
<div class="fixed-header">
<h1>近未来の作業手順書</h1>
<div class="progress-container">
<div class="progress-bar" id="progress"></div>
<div class="progress-text" id="progress-text">0%</div>
</div>
</div>
<div class="game-container">
<div id="cat">
<svg viewBox="0 0 100 100">
<ellipse cx="50" cy="65" rx="30" ry="25" fill="#b0c4de" />
<circle cx="50" cy="40" r="25" fill="#b0c4de" />
<path class="ear-left" d="M35 25 Q25 10 35 15" fill="#b0c4de" />
<path class="ear-right" d="M65 25 Q75 10 65 15" fill="#b0c4de" />
<circle cx="40" cy="35" r="10" fill="#e6f2ff" />
<circle cx="60" cy="35" r="10" fill="#e6f2ff" />
<circle class="pupil" cx="40" cy="35" r="5" fill="#A100FF" />
<circle class="pupil" cx="60" cy="35" r="5" fill="#A100FF" />
<circle class="eye-shine" cx="37" cy="32" r="2" fill="white" />
<circle class="eye-shine" cx="57" cy="32" r="2" fill="white" />
<circle cx="43" cy="38" r="1" fill="white" opacity="0.7" />
<circle cx="63" cy="38" r="1" fill="white" opacity="0.7" />
<path class="eye-lid" d="M30 35 Q40 25 50 35" fill="none" stroke="#b0c4de" stroke-width="2" />
<path class="eye-lid" d="M50 35 Q60 25 70 35" fill="none" stroke="#b0c4de" stroke-width="2" />
<path d="M48 45 Q50 47 52 45" fill="#ffc0cb" />
<path d="M45 50 Q50 52 55 50" fill="none" stroke="#ffc0cb" stroke-width="0.5" />
<line x1="35" y1="47" x2="20" y2="45" stroke="white" stroke-width="0.5" />
<line x1="35" y1="49" x2="20" y2="49" stroke="white" stroke-width="0.5" />
<line x1="35" y1="51" x2="20" y2="53" stroke="white" stroke-width="0.5" />
<line x1="65" y1="47" x2="80" y2="45" stroke="white" stroke-width="0.5" />
<line x1="65" y1="49" x2="80" y2="49" stroke="white" stroke-width="0.5" />
<line x1="65" y1="51" x2="80" y2="53" stroke="white" stroke-width="0.5" />
<path class="tail" d="M70 75 Q85 73 95 60" fill="none" stroke="#b0c4de" stroke-width="8"
stroke-linecap="round" />
</svg>
</div>
<div id="steps-container">
<!-- ステップの内容はJavaScriptで動的に生成します -->
</div>
<div id="statusMonitor" class="status-container" draggable="true">
<h3>サービスステータス</h3>
<div class="status-item status-running">
<span class="status-indicator"></span>
<span class="status-text">OracleServiceXE:</span>
<span id="oracleXEStatus" class="status-state">起動中</span>
</div>
<div class="status-item status-running">
<span class="status-indicator"></span>
<span class="status-text">OracleTNSListener:</span>
<span id="oracleTNSStatus" class="status-state">起動中</span>
</div>
<div class="status-item status-running">
<span class="status-indicator"></span>
<span class="status-text">OracleJobScheduler:</span>
<span id="oracleJobStatus" class="status-state">起動中</span>
</div>
<div class="status-item status-running">
<span class="status-indicator"></span>
<span class="status-text">Tomcat9:</span>
<span id="tomcatStatus" class="status-state">起動中</span>
</div>
<div class="status-item status-running">
<span class="status-indicator"></span>
<span class="status-text">NISMail_MTA:</span>
<span id="nismailMTAStatus" class="status-state">起動中</span>
</div>
<div class="status-item status-running">
<span class="status-indicator"></span>
<span class="status-text">NISMail_POP3:</span>
<span id="nismailPOP3Status" class="status-state">起動中</span>
</div>
<div class="status-item status-running">
<span class="status-indicator"></span>
<span class="status-text">NISMail_IMAP:</span>
<span id="nismailIMAPStatus" class="status-state">起動中</span>
</div>
</div>
</div>
<div id="task-complete" class="hidden">タスクコンプリート!</div>
<script>
const cat = document.getElementById('cat');
const taskComplete = document.getElementById('task-complete');
const stepsContainer = document.getElementById('steps-container');
const progressBar = document.getElementById('progress');
const progressText = document.getElementById('progress-text');
const oracleStatus = document.getElementById('oracleStatus');
const tomcatStatus = document.getElementById('tomcatStatus');
const steps = [
{ title: "準備", description: "管理者権限でコマンドプロンプトを開く", action: "準備完了", command: null },
{ title: "Oracle Job Schedulerの停止", description: "以下のコマンドを実行してOracle Job Schedulerを停止します:", action: "停止実行", command: "net stop OracleJobSchedulerXE" },
{ title: "Oracle Databaseの停止", description: "以下のコマンドを実行してOracle Databaseを停止します:", action: "停止実行", command: "net stop OracleServiceXE" },
{ title: "Oracle Listenerの停止", description: "以下のコマンドを実行してOracle Listenerを停止します:", action: "停止実行", command: "net stop OracleOraDB19Home1TNSListener" },
{ title: "Tomcatの停止", description: "以下のコマンドを実行してTomcatを停止します:", action: "停止実行", command: "net stop Tomcat9" },
{ title: "NISMail IMAPの停止", description: "以下のコマンドを実行してNISMail IMAPを停止します:", action: "停止実行", command: "net stop NISMail_IMAP" },
{ title: "NISMail POP3の停止", description: "以下のコマンドを実行してNISMail POP3を停止します:", action: "停止実行", command: "net stop NISMail_POP3" },
{ title: "NISMail MTAの停止", description: "以下のコマンドを実行してNISMail MTAを停止します:", action: "停止実行", command: "net stop NISMail_MTA" },
{ title: "サービス停止結果の確認", description: "ログを確認", action: "チェック実行", command: null },
{ title: "OS再起動", description: "以下のコマンドを実行してOSを再起動します:", action: "再起動実行", command: "shutdown /r /t 0" },
{ title: "Oracle Listenerの起動", description: "以下のコマンドを実行してOracle Listenerを起動します:", action: "起動実行", command: "net start OracleOraDB19Home1TNSListener" },
{ title: "Oracle Databaseの起動", description: "以下のコマンドを実行してOracle Databaseを起動します:", action: "起動実行", command: "net start OracleServiceXE" },
{ title: "Oracle Job Schedulerの起動", description: "以下のコマンドを実行してOracle Job Schedulerを起動します:", action: "起動実行", command: "net start OracleJobSchedulerXE" },
{ title: "Tomcatの起動", description: "以下のコマンドを実行してTomcatを起動します:", action: "起動実行", command: "net start Tomcat9" },
{ title: "NISMail MTAの起動", description: "以下のコマンドを実行してNISMail MTAを起動します:", action: "起動実行", command: "net start NISMail_MTA" },
{ title: "NISMail POP3の起動", description: "以下のコマンドを実行してNISMail POP3を起動します:", action: "起動実行", command: "net start NISMail_POP3" },
{ title: "NISMail IMAPの起動", description: "以下のコマンドを実行してNISMail IMAPを起動します:", action: "起動実行", command: "net start NISMail_IMAP" },
{ title: "サービス起動結果の確認", description: "ログを確認", action: "チェック実行", command: null }
];
let currentStep = 0;
const totalSteps = steps.length;
function createSteps() {
steps.forEach((step, index) => {
const stepElement = document.createElement('div');
stepElement.className = 'step';
if (index === 8 || index === 17) { // サービス停止・起動結果の確認ステップ
stepElement.innerHTML = `
<h3>${index + 1}. ${step.title}</h3>
<p>${step.description}</p>
<div class="logCheckArea" id="logCheckArea_${index}">
<textarea class="logTextarea" id="logTextarea_${index}" placeholder="ここにログを貼り付けてください"></textarea>
<button class="button" onclick="checkServiceLog(${index})">${step.action}</button>
<div class="checkResult" id="checkResult_${index}"></div>
</div>
`;
} else {
let commandHtml = '';
if (step.command) {
commandHtml = `
<div class="code-block">
<pre><code>${step.command}</code></pre>
<button class="copy-button" onclick="copyToClipboard('${step.command}')">コピー</button>
</div>
`;
}
stepElement.innerHTML = `
<h3>${index + 1}. ${step.title}</h3>
<p>${step.description}</p>
${commandHtml}
<button class="button" onclick="completeStep(${index})" ${index === 0 ? '' : 'disabled'}>${step.action}</button>
`;
}
stepsContainer.appendChild(stepElement);
});
}
function updateProgress() {
const progress = Math.round((currentStep / totalSteps) * 100);
progressBar.style.width = `${progress}%`;
progressText.textContent = `${progress}%`;
}
function updateStatus(step) {
const statusMap = {
1: { id: 'oracleJobStatus', state: '停止中' },
2: { id: 'oracleXEStatus', state: '停止中' },
3: { id: 'oracleTNSStatus', state: '停止中' },
4: { id: 'tomcatStatus', state: '停止中' },
5: { id: 'nismailIMAPStatus', state: '停止中' },
6: { id: 'nismailPOP3Status', state: '停止中' },
7: { id: 'nismailMTAStatus', state: '停止中' },
10: { id: 'oracleTNSStatus', state: '起動中' },
11: { id: 'oracleXEStatus', state: '起動中' },
12: { id: 'oracleJobStatus', state: '起動中' },
13: { id: 'tomcatStatus', state: '起動中' },
14: { id: 'nismailMTAStatus', state: '起動中' },
15: { id: 'nismailPOP3Status', state: '起動中' },
16: { id: 'nismailIMAPStatus', state: '起動中' }
};
if (statusMap[step]) {
const { id, state } = statusMap[step];
const statusElement = document.getElementById(id);
statusElement.textContent = state;
statusElement.className = `status-state status-${state === '起動中' ? 'running' : 'stopped'}`;
statusElement.closest('.status-item').className = `status-item status-${state === '起動中' ? 'running' : 'stopped'}`;
}
}
function checkServiceLog(step) {
const logText = document.getElementById(`logTextarea_${step}`).value;
if (logText.trim() === '') {
alert('ログを張り付けてください。');
return;
}
// サービス停止時のサービスリスト
const stopServices = [
'OracleJobSchedulerXE',
'OracleServiceXE',
'OracleOraDB19Home1TNSListener',
'Tomcat9',
'NISMail_IMAP',
'NISMail_POP3',
'NISMail_MTA'
];
// サービス起動時のサービスリスト
const startServices = [
'OracleOraDB19Home1TNSListener',
'OracleServiceXE',
'OracleJobSchedulerXE',
'Tomcat9',
'NISMail_MTA',
'NISMail_POP3',
'NISMail_IMAP'
];
const isStopCheck = step === 8;
const expectedCommand = isStopCheck ? 'net stop' : 'net start';
const expectedMessage = isStopCheck ? '停止されました' : '開始されました';
const notOkServices = [];
// サービス停止時とサービス起動時で使用するリストを選択
const currentServices = isStopCheck ? stopServices : startServices;
// ここでcurrentServices変数を使用してループを実行
currentServices.forEach((service) => {
const regex = new RegExp(`${expectedCommand} ${service}[\\s\\S]*${service}.*${expectedMessage}`);
if (!regex.test(logText)) {
notOkServices.push(service);
}
});
const resultElement = document.getElementById(`checkResult_${step}`);
if (notOkServices.length === 0) {
resultElement.textContent = `OK: すべてのサービスが正常に${isStopCheck ? '停止' : '起動'}されました。`;
resultElement.style.color = 'green';
completeStep(step);
} else {
const errorMessage = `NG: 以下のサービスが正常に${isStopCheck ? '停止' : '起動'}されていません: ${notOkServices.join(', ')}`;
showErrorMessage(step, errorMessage);
setTimeout(() => {
notOkServices.forEach((service) => {
const index = currentServices.indexOf(service);
let stepIndex = isStopCheck ? index + 1 : index + 10;
updateServiceStatus(service, isStopCheck ? '起動中' : '停止中');
resetStep(stepIndex);
highlightAndScrollToStep(stepIndex);
});
}, 3000); // 3秒後にエラーステップにフォーカス
}
}
function showErrorMessage(step, message) {
const resultElement = document.getElementById(`checkResult_${step}`);
resultElement.innerHTML = `<div class="error-message">${message}</div>`;
resultElement.style.color = 'red';
// エラーメッセージを表示してハイライト
setTimeout(() => {
const errorMessageElement = resultElement.querySelector('.error-message');
errorMessageElement.classList.add('visible');
errorMessageElement.classList.add('highlight');
}, 10);
}
function highlightAndScrollToStep(stepIndex) {
const step = document.querySelectorAll('.step')[stepIndex];
step.classList.add('highlight');
step.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// エラーが解決されたときにハイライトを解除する関数を追加
function removeHighlight(stepIndex) {
const step = document.querySelectorAll('.step')[stepIndex];
step.classList.remove('highlight');
}
function resetStep(stepIndex) {
const steps = document.querySelectorAll('.step');
const buttons = document.querySelectorAll('.step .button');
steps[stepIndex].style.backgroundColor = '';
buttons[stepIndex].classList.remove('completed');
buttons[stepIndex].disabled = false;
steps[stepIndex].classList.remove('blink-border');
currentStep = Math.min(currentStep, stepIndex);
updateProgress();
}
function highlightStep(stepIndex) {
const step = document.querySelectorAll('.step')[stepIndex];
step.classList.add('blink-border');
step.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
let isDragging = false;
let offsetX, offsetY;
cat.addEventListener('mousedown', (e) => {
isDragging = true;
const catRect = cat.getBoundingClientRect();
offsetX = e.clientX - catRect.left;
offsetY = e.clientY - catRect.top;
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
let newX = e.clientX - offsetX;
let newY = e.clientY - offsetY;
newX = Math.max(0, Math.min(newX, window.innerWidth - cat.offsetWidth));
newY = Math.max(0, Math.min(newY, window.innerHeight - cat.offsetHeight));
cat.style.left = `${newX}px`;
cat.style.top = `${newY}px`;
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
function updateCatPosition() {
const catRect = cat.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
let newY = catRect.top;
let newX = catRect.left;
if (newY < 0) newY = 0;
if (newY > viewportHeight - catRect.height) newY = viewportHeight - catRect.height;
if (newX < 0) newX = 0;
if (newX > viewportWidth - catRect.width) newX = viewportWidth - catRect.width;
cat.style.top = `${newY}px`;
cat.style.left = `${newX}px`;
}
window.addEventListener('scroll', updateCatPosition);
window.addEventListener('resize', updateCatPosition);
createSteps();
updateProgress();
cat.style.left = '20px';
cat.style.top = '120px';
function completeStep(step) {
const steps = document.querySelectorAll('.step');
const buttons = document.querySelectorAll('.step .button');
buttons[step].classList.add('completed');
steps[step].style.backgroundColor = '#A9A9A9';
steps[step].classList.remove('blink-border');
// ハイライトを解除
removeHighlight(step);
if (step < totalSteps - 1) {
buttons[step + 1].disabled = false;
}
currentStep = Math.max(currentStep, step + 1);
updateProgress();
updateStatus(step);
if (currentStep === totalSteps) {
showCompletionMessage();
// 最後のステップ完了後、currentStepをリセット
setTimeout(() => {
currentStep = 0;
updateProgress();
// すべてのステップとボタンをリセット
steps.forEach((step, index) => {
step.style.backgroundColor = '';
buttons[index].classList.remove('completed');
buttons[index].disabled = index !== 0;
});
}, 15000); // showCompletionMessageのタイムアウトと同じ時間
}
}
function makeElementDraggable(element) {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
element.addEventListener("mousedown", dragStart);
document.addEventListener("mousemove", drag);
document.addEventListener("mouseup", dragEnd);
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === element) {
isDragging = true;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, element);
}
}
function dragEnd(e) {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
function setTranslate(xPos, yPos, el) {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
}
}
function updateServiceStatus(service, status) {
const statusMap = {
'OracleJobSchedulerXE': 'oracleJobStatus',
'OracleServiceXE': 'oracleXEStatus',
'OracleOraDB19Home1TNSListener': 'oracleTNSStatus',
'Tomcat9': 'tomcatStatus',
'NISMail_IMAP': 'nismailIMAPStatus',
'NISMail_POP3': 'nismailPOP3Status',
'NISMail_MTA': 'nismailMTAStatus'
};
const statusElement = document.getElementById(statusMap[service]);
if (statusElement) {
statusElement.textContent = status;
statusElement.className = `status-state status-${status === '起動中' ? 'running' : 'stopped'}`;
statusElement.closest('.status-item').className = `status-item status-${status === '起動中' ? 'running' : 'stopped'}`;
}
}
// 新しいParticleクラスとアニメーション関連の関数を追加
class Particle {
constructor(x, y, targetX, targetY) {
this.x = Math.random() * window.innerWidth;
this.y = Math.random() * window.innerHeight;
this.size = Math.random() * 1.5 + 0.5; // 粒子のサイズを小さくして、より細かい表現に
this.targetX = targetX;
this.targetY = targetY;
this.speed = Math.random() * 0.04 + 0.02; // スピードを少し上げて、アニメーションを速く
this.velocityX = 0;
this.velocityY = 0;
this.color = `hsl(${Math.random() * 60 + 200}, 100%, 50%)`;
}
update() {
this.velocityX = (this.targetX - this.x) * this.speed;
this.velocityY = (this.targetY - this.y) * this.speed;
this.x += this.velocityX;
this.y += this.velocityY;
}
draw(ctx) {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
function showCompletionMessage() {
const completionMessage = document.getElementById('completion-message');
const canvas = document.getElementById('completion-canvas');
const particlesContainer = document.getElementById('particles-js');
completionMessage.style.display = 'block';
canvas.style.display = 'block';
particlesContainer.style.display = 'block';
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const particles = [];
const fontSize = Math.min(canvas.width / 15, canvas.height / 8);
ctx.font = `bold ${fontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const lines = ['Thank you', 'for your', 'hard work!'];
const lineHeight = fontSize * 1.2;
const colors = ['#FF1493', '#00FFFF', '#FFD700']; // ピンク、シアン、金色
function drawText() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
lines.forEach((line, index) => {
const y = canvas.height / 2 + (index - 1) * lineHeight;
// 外枠を描画
ctx.strokeStyle = 'white';
ctx.lineWidth = fontSize / 15;
ctx.strokeText(line, canvas.width / 2, y);
// グラデーションを作成
const gradient = ctx.createLinearGradient(0, y - fontSize/2, 0, y + fontSize/2);
gradient.addColorStop(0, colors[index]);
gradient.addColorStop(1, 'white');
// テキストを描画
ctx.fillStyle = gradient;
ctx.fillText(line, canvas.width / 2, y);
});
}
// 粒子の目標位置を取得
drawText();
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
class Particle {
constructor(x, y, targetX, targetY, color) {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = Math.random() * 2 + 1;
this.targetX = targetX;
this.targetY = targetY;
this.speed = Math.random() * 0.02 + 0.02;
this.color = color;
this.trail = [];
}
update() {
this.trail.unshift({ x: this.x, y: this.y });
if (this.trail.length > 5) {
this.trail.pop();
}
this.x += (this.targetX - this.x) * this.speed;
this.y += (this.targetY - this.y) * this.speed;
}
draw() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
// 軌跡を描画
ctx.strokeStyle = this.color;
ctx.lineWidth = this.size / 2;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
this.trail.forEach(point => {
ctx.lineTo(point.x, point.y);
});
ctx.stroke();
}
}
// 粒子を生成
for (let y = 0; y < imageData.height; y += 4) {
for (let x = 0; x < imageData.width; x += 4) {
if (imageData.data[(y * imageData.width + x) * 4 + 3] > 128) {
const colorIndex = Math.floor(y / (canvas.height / 3));
particles.push(new Particle(x, y, x, y, colors[colorIndex]));
}
}
}
// 背景の星空エフェクト
const starCanvas = document.createElement('canvas');
starCanvas.style.position = 'absolute';
starCanvas.style.top = '0';
starCanvas.style.left = '0';
starCanvas.width = window.innerWidth;
starCanvas.height = window.innerHeight;
completionMessage.appendChild(starCanvas);
const starCtx = starCanvas.getContext('2d');
class Star {
constructor(x, y, radius, color) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.originalRadius = radius;
this.twinkleSpeed = Math.random() * 0.05 + 0.01;
this.twinklePhase = Math.random() * Math.PI * 2;
this.speed = Math.random() * 0.05 + 0.01; // 星の動きのスピード
}
draw() {
const twinkle = Math.sin(this.twinklePhase) * 0.5 + 0.5;
const currentRadius = this.originalRadius * (0.8 + twinkle * 0.4);
starCtx.beginPath();
starCtx.arc(this.x, this.y, currentRadius, 0, Math.PI * 2);
starCtx.fillStyle = this.color;
starCtx.fill();
// レンズフレア効果
const gradient = starCtx.createRadialGradient(this.x, this.y, 0, this.x, this.y, currentRadius * 4);
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.4)');
gradient.addColorStop(0.1, 'rgba(255, 255, 255, 0.1)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
starCtx.fillStyle = gradient;
starCtx.beginPath();
starCtx.arc(this.x, this.y, currentRadius * 4, 0, Math.PI * 2);
starCtx.fill();
this.twinklePhase += this.twinkleSpeed;
}
update() {
this.y += this.speed; // 星を下に動かす
if (this.y > starCanvas.height) {
this.y = 0; // 画面下に到達したら上に戻す
this.x = Math.random() * starCanvas.width; // x座標をランダムに設定
}
}
}
class ShootingStar {
constructor() {
this.reset();
}
reset() {
this.x = Math.random() * starCanvas.width;
this.y = 0;
this.size = Math.random() * 2 + 1;
this.speed = Math.random() * 5 + 3;
this.lifetime = Math.random() * 1000 + 1000;
this.opacity = 0;
this.trail = [];
}
draw() {
if (this.opacity <= 0) return;
starCtx.fillStyle = `rgba(255, 255, 255, ${this.opacity})`;
for (let i = 0; i < this.trail.length; i++) {
const point = this.trail[i];
starCtx.beginPath();
starCtx.arc(point.x, point.y, this.size * (1 - i / this.trail.length), 0, Math.PI * 2);
starCtx.fill();
}
}
update() {
this.y += this.speed;
this.x += this.speed * 0.3;
this.trail.unshift({ x: this.x, y: this.y });
if (this.trail.length > 5) {
this.trail.pop();
}
if (this.y > starCanvas.height) {
this.reset();
}
this.lifetime -= 16; // 60FPSを想定
if (this.lifetime > 0) {
this.opacity = Math.min(this.opacity + 0.05, 1);
} else {
this.opacity = Math.max(this.opacity - 0.05, 0);
}
}
}
// 太陽光効果
class Sunlight {
constructor() {
this.x = starCanvas.width / 2;
this.y = -starCanvas.height / 4;
this.radius = starCanvas.height;
}
draw() {
const gradient = starCtx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius);
gradient.addColorStop(0, 'rgba(255, 255, 200, 0.3)');
gradient.addColorStop(0.3, 'rgba(255, 255, 200, 0.1)');
gradient.addColorStop(1, 'rgba(255, 255, 200, 0)');
starCtx.fillStyle = gradient;
starCtx.beginPath();
starCtx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
starCtx.fill();
}
}
const stars = Array(200).fill().map(() => new Star(
Math.random() * starCanvas.width,
Math.random() * starCanvas.height,
Math.random() * 2 + 0.5,
`hsl(${Math.random() * 60 + 180}, 100%, ${Math.random() * 20 + 80}%)`
));
const sunlight = new Sunlight();
const shootingStars = Array(3).fill().map(() => new ShootingStar());
function animateStarfield() {
starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
// 背景のグラデーション(夜空の効果)
const skyGradient = starCtx.createLinearGradient(0, 0, 0, starCanvas.height);
skyGradient.addColorStop(0, 'rgba(0, 10, 30, 0.8)');
skyGradient.addColorStop(1, 'rgba(0, 0, 10, 0.8)');
starCtx.fillStyle = skyGradient;
starCtx.fillRect(0, 0, starCanvas.width, starCanvas.height);
sunlight.draw();
stars.forEach(star => {
star.update();
star.draw();
});
shootingStars.forEach(star => {
star.update();
star.draw();
});
requestAnimationFrame(animateStarfield);
}
let animationPhase = 0;
const totalDuration = 7000; // 10秒間のアニメーション
const startTime = Date.now();
function animate() {
const currentTime = Date.now();
const elapsedTime = currentTime - startTime;
animationPhase = Math.min(elapsedTime / totalDuration, 1);
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(particle => {
if (animationPhase < 0.7) { // 最初の70%の時間は粒子が集まる
particle.update();
}
particle.draw();
});
if (animationPhase >= 0.7 && animationPhase < 0.9) { // 70%から90%の間で文字をフェードイン
ctx.globalAlpha = (animationPhase - 0.7) / 0.2;
drawText();
ctx.globalAlpha = 1;
} else if (animationPhase >= 0.9) { // 最後の10%で文字を完全に表示
drawText();
}
if (animationPhase < 1) {
requestAnimationFrame(animate);
}
}
animate();
animateStarfield();
// particles.js の設定(より自然な動きに調整)
particlesJS('particles-js', {
particles: {
number: { value: 50, density: { enable: true, value_area: 800 } },
color: { value: "#ffffff" },
shape: { type: "circle" },
opacity: { value: 0.5, random: true, anim: { enable: true, speed: 0.5, opacity_min: 0.1, sync: false } },
size: { value: 2, random: true, anim: { enable: true, speed: 1, size_min: 0.1, sync: false } },
line_linked: { enable: false },
move: {
enable: true,
speed: 0.2,
direction: "none",
random: true,
straight: false,
out_mode: "out",
bounce: false
}
},
interactivity: {
detect_on: "canvas",
events: {
onhover: { enable: true, mode: "bubble" },
onclick: { enable: true, mode: "repulse" },
resize: true
},
modes: {
bubble: { distance: 100, size: 4, duration: 2, opacity: 0.8, speed: 3 },
repulse: { distance: 100, duration: 0.4 }
}
},
retina_detect: true
});
setTimeout(() => {
completionMessage.style.display = 'none';
canvas.style.display = 'none';
particlesContainer.style.display = 'none';
starCanvas.remove(); // 追加した星のキャンバスも削除
}, totalDuration + 15000); // アニメーション終了後5秒間表示
}
window.addEventListener('resize', () => {
const canvas = document.getElementById('completion-canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
// ステータスモニターを動かせるようにする
const statusMonitor = document.getElementById('statusMonitor');
makeElementDraggable(statusMonitor);
</script>
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('コピーしました!');
});
}
</script>
</body>
</html>
サンプルログ
アプリ内のログチェックで以下のログを適宜張り付けてご利用ください。
サンプルログ
#サービス停止ログ(正常)
C:\Windows\system32>net stop OracleJobSchedulerXE
OracleJobSchedulerXE サービスを停止中です.
OracleJobSchedulerXE サービスは正常に停止されました。
C:\Windows\system32>net stop OracleServiceXE
OracleServiceXE サービスを停止中です.
OracleServiceXE サービスは正常に停止されました。
C:\Windows\system32>net stop OracleOraDB19Home1TNSListener
OracleOraDB19Home1TNSListener サービスを停止中です.
OracleOraDB19Home1TNSListener サービスは正常に停止されました。
C:\Windows\system32>net stop Tomcat9
Tomcat9 サービスを停止中です.
Tomcat9 サービスは正常に停止されました。
C:\Windows\system32>net stop NISMail_IMAP
NISMail_IMAP サービスを停止中です.
NISMail_IMAP サービスは正常に停止されました。
C:\Windows\system32>net stop NISMail_POP3
NISMail_POP3 サービスを停止中です.
NISMail_POP3 サービスは正常に停止されました。
C:\Windows\system32>net stop NISMail_MTA
NISMail_MTA サービスを停止中です.
NISMail_MTA サービスは正常に停止されました。
#サービス停止ログ(異常判定パターン)
C:\Windows\system32>net stop OracleJobSchedulerXE
OracleJobSchedulerXE サービスを停止中です.
OracleJobSchedulerXE サービスは正常に停止されました。
C:\Windows\system32>net stop OracleServiceXE
OracleServiceXE サービスを停止中です.
OracleServiceXE サービスは正常に停止されました。
C:\Windows\system32>net stop OracleOraDB19Home1TNSListener
OracleOraDB19Home1TNSListener サービスを停止中です.
OracleOraDB19Home1TNSListener サービスは正常に停止されました。
C:\Windows\system32>net stop Tomcat9
Tomcat9 サービスを停止中です.
C:\Windows\system32>net stop NISMail_IMAP
NISMail_IMAP サービスを停止中です.
NISMail_IMAP サービスは正常に停止されました。
C:\Windows\system32>net stop NISMail_POP3
NISMail_POP3 サービスを停止中です.
NISMail_POP3 サービスは正常に停止されました。
C:\Windows\system32>net stop NISMail_MTA
NISMail_MTA サービスを停止中です.
NISMail_MTA サービスは正常に停止されました。
#サービス起動ログ(正常)
C:\Windows\system32>net start OracleOraDB19Home1TNSListener
OracleOraDB19Home1TNSListener サービスを開始中です.
OracleOraDB19Home1TNSListener サービスは正常に開始されました。
C:\Windows\system32>net start OracleServiceXE
OracleServiceXE サービスを開始中です.
OracleServiceXE サービスは正常に開始されました。
C:\Windows\system32>net start OracleJobSchedulerXE
OracleJobSchedulerXE サービスを開始中です.
OracleJobSchedulerXE サービスは正常に開始されました。
C:\Windows\system32>net start Tomcat9
Tomcat9 サービスを開始中です.
Tomcat9 サービスは正常に開始されました。
C:\Windows\system32>net start NISMail_MTA
NISMail_MTA サービスを開始中です.
NISMail_MTA サービスは正常に開始されました。
C:\Windows\system32>net start NISMail_POP3
NISMail_POP3 サービスを開始中です.
NISMail_POP3 サービスは正常に開始されました。
C:\Windows\system32>net start NISMail_IMAP
NISMail_IMAP サービスを開始中です.
NISMail_IMAP サービスは正常に開始されました。
おわりに
「近未来の運用ドキュメントアプリ」は、直感的な操作性と臨場感にあふれたエクスペリエンスで、運用手順のあり方を根本から変える可能性を秘めています。ミスの削減や運用品質の向上にも大きく貢献するでしょう。
ぜひ一度お試しいただき、運用ドキュメントの新たな形を体感してみてください!
Discussion