משתמש:מוטי בוט/נתוני ניטור2.js
מראה
לתשומת ליבך: לאחר הפרסום, ייתכן שיהיה צורך לנקות את זיכרון המטמון (cache) של הדפדפן כדי להבחין בשינויים.
- פיירפוקס / ספארי: להחזיק את המקש Shift בעת לחיצה על טעינה מחדש (Reload) או ללחוץ על צירוף המקשים Ctrl-F5 או Ctrl-R (במחשב מק: ⌘-R).
- גוגל כרום: ללחוץ על צירוף המקשים Ctrl-Shift-R (במחשב מק: ⌘-Shift-R).
- אדג': להחזיק את המקש Ctrl בעת לחיצה על רענן (Refresh) או ללחוץ על צירוף המקשים Ctrl-F5.
//לקבלת כמות ניטורים פר משתמש בזמן נתון - גרסה מורחבת עם ויזואליזציה וחלונות רחבים
mw.loader.using(["mediawiki.api", "oojs-ui-windows"]).then(() => {
// פונקציה להצגת הודעות בתוך הדיאלוג באמצעות OOUI
function showInDialogMessage(message, type = 'success') {
// יצירת הודעה עם סגנון OOUI
const messageWidget = new OO.ui.MessageWidget({
type: type, // 'success', 'error', 'warning', 'notice'
label: message,
closable: true
});
// הוספת הודעה לראש הדיאלוג
const dialogContent = document.querySelector('.oo-ui-dialog .oo-ui-window-body');
if (dialogContent) {
const messageContainer = messageWidget.$element[0];
messageContainer.style.cssText = `
margin: 10px;
animation: slideDown 0.3s ease;
`;
// הוספת אנימציה
const style = document.createElement('style');
if (!document.getElementById('message-animation-style')) {
style.id = 'message-animation-style';
style.textContent = `
@keyframes slideDown {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
`;
document.head.appendChild(style);
}
dialogContent.insertBefore(messageContainer, dialogContent.firstChild);
// הסרה אוטומטית אחרי 4 שניות
setTimeout(() => {
if (messageContainer.parentNode) {
messageContainer.style.opacity = '0';
setTimeout(() => {
if (messageContainer.parentNode) {
messageContainer.remove();
}
}, 300);
}
}, 4000);
}
// fallback לתצוגה בקונסולה
console.log(`[${type.toUpperCase()}] ${message}`);
}
function createProgressDialog() {
// יצירת דיאלוג תהליך מלא עם יכולת הגדרת רוחב
class ProgressDialog extends OO.ui.ProcessDialog {
constructor(config) {
super(config);
this.size = 'medium';
this.message = '';
this.progress = null;
}
static static = {
name: 'progressDialog',
title: 'מעבד נתוני ניטור',
actions: []
};
initialize() {
super.initialize.call(this);
this.content = new OO.ui.PanelLayout({
padded: true,
expanded: false
});
this.$body.append(this.content.$element);
}
getSetupProcess(data) {
return super.getSetupProcess.call(this, data)
.next(() => {
this.updateContent(data.message || '', data.progress);
});
}
updateContent(message, progress = null) {
this.message = message;
this.progress = progress;
const progressBar = progress !== null ?
`<div style="background: #eee; border-radius: 4px; margin: 10px 0;">
<div style="background: #36c; height: 20px; border-radius: 4px; width: ${progress}%; transition: width 0.3s;"></div>
</div>` : '';
this.content.$element.html(`
<div style="text-align: center; padding: 20px; min-width: 400px;">
${message}
${progressBar}
</div>
`);
}
}
const dialog = new ProgressDialog();
const windowManager = new OO.ui.WindowManager();
$('body').append(windowManager.$element);
windowManager.addWindows([dialog]);
return { dialog, windowManager };
}
function updateProgress(windowManager, dialog, message, progress = null) {
if (dialog.updateContent) {
dialog.updateContent(message, progress);
} else {
// fallback לדיאלוג ישן במקרה של צורך
const progressBar = progress !== null ?
`<div style="background: #eee; border-radius: 4px; margin: 10px 0;">
<div style="background: #36c; height: 20px; border-radius: 4px; width: ${progress}%; transition: width 0.3s;"></div>
</div>` : '';
windowManager.openWindow(dialog, {
title: 'מעבד נתוני ניטור',
message: new OO.ui.HtmlSnippet(`${message}${progressBar}`)
});
}
}
function getTime(num) {
if (!num) return;
let numberInput = Number(num);
if (isNaN(numberInput) || numberInput <= 0 || numberInput > 365) {
showInDialogMessage('יש להזין מספר ימים תקין (1-365)', 'error');
return;
}
const calendar = new Date(new Date() - numberInput * 1000 * 60 * 60 * 24).toISOString();
getlogs(calendar, numberInput);
}
async function getlogs(lestart, days) {
const { dialog, windowManager } = createProgressDialog();
const api = new mw.Api();
const logs = [];
let lecontinue = null;
let totalRequests = 0;
const params = {
list: "logevents",
formatversion: "2",
leprop: "user|timestamp|title",
leaction: "patrol/patrol",
lestart,
ledir: "newer",
lenamespace: "0",
lelimit: "max",
};
try {
windowManager.openWindow(dialog, {
message: 'מתחיל איסוף נתונים...',
progress: 0
});
while (true) {
totalRequests++;
updateProgress(windowManager, dialog,
`מבצע בקשה ${totalRequests}... (נאספו ${logs.length} פעולות)`,
Math.min((logs.length / 1000) * 100, 95)
);
const response = await api.get(params);
lecontinue = response.continue?.lecontinue;
logs.push(...response.query.logevents);
if (lecontinue) {
params.lecontinue = lecontinue;
} else {
updateProgress(windowManager, dialog, 'מעבד נתונים...', 100);
setTimeout(() => {
windowManager.closeWindow(dialog);
parserData(logs, days);
}, 500);
break;
}
}
} catch (error) {
windowManager.closeWindow(dialog);
console.error(error);
showInDialogMessage('אירעה שגיאה באיסוף הנתונים', 'error');
}
}
function parserData(data, days) {
const users = new Map();
const dailyStats = new Map();
data.forEach((log) => {
const user = log.user.replace(/בוט/g, "").trim();
const date = log.timestamp.substring(0, 10);
users.set(user, (users.get(user) || 0) + 1);
if (!dailyStats.has(date)) {
dailyStats.set(date, new Set());
}
dailyStats.get(date).add(user);
});
const sortedUsers = Array.from(users.entries()).sort((a, b) => b[1] - a[1]);
const topUsers = sortedUsers.slice(0, 20);
// סטטיסטיקות בסיסיות
const totalUsers = users.size;
const totalActions = data.length;
const avgActionsPerUser = Math.round(totalActions / totalUsers * 100) / 100;
const avgActionsPerDay = Math.round(totalActions / days * 100) / 100;
const activeUsersPerDay = Array.from(dailyStats.values()).reduce((sum, users) => sum + users.size, 0) / dailyStats.size;
createVisualizationWindow(topUsers, {
totalUsers,
totalActions,
avgActionsPerUser,
avgActionsPerDay,
activeUsersPerDay: Math.round(activeUsersPerDay * 100) / 100,
days,
dailyStats: Array.from(dailyStats.entries()).map(([date, users]) => ({
date,
actions: data.filter(log => log.timestamp.startsWith(date)).length,
activeUsers: users.size
}))
});
}
function createVisualizationWindow(userData, stats) {
console.log('נתוני משתמשים:', userData);
console.log('סטטיסטיקות כלליות:', stats);
// יצירת HTML לויזואליזציה
const maxActions = userData[0] ? userData[0][1] : 1;
const chartHtml = userData.map(([user, actions], index) => {
const percentage = (actions / maxActions) * 100;
const color = `hsl(${210 + index * 15}, 70%, ${60 - index * 2}%)`;
return `
<div style="margin: 8px 0; padding: 8px; background: #f8f9fa; border-radius: 4px; border-right: 4px solid ${color};">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<strong>${user}</strong>
<span style="font-weight: bold; color: ${color};">${actions}</span>
</div>
<div style="background: #e1e5e9; border-radius: 2px; height: 8px;">
<div style="background: ${color}; height: 100%; border-radius: 2px; width: ${percentage}%; transition: width 0.5s ease;"></div>
</div>
</div>
`;
}).join('');
// גרף פעילות יומית פשוט
const dailyChart = stats.dailyStats.slice(-14).map(day => {
const maxDaily = Math.max(...stats.dailyStats.map(d => d.actions));
const height = (day.actions / maxDaily) * 100;
return `
<div style="display: inline-block; margin: 2px; text-align: center; vertical-align: bottom;">
<div title="${day.actions} פעולות" style="width: 20px; background: #36c; height: ${height}px; max-height: 80px; margin-bottom: 4px; border-radius: 2px 2px 0 0;"></div>
<div style="font-size: 10px; writing-mode: vertical-rl; text-orientation: mixed;">${day.date.substring(5)}</div>
</div>
`;
}).join('');
const windowHtml = `
<div style="max-height: 80vh; overflow-y: auto; direction: rtl;">
<!-- סטטיסטיקות כלליות -->
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; margin: -20px -20px 20px -20px; border-radius: 8px 8px 0 0;">
<h2 style="margin: 0 0 15px 0; text-align: center;">ניתוח נתוני ניטור - ${stats.days} ימים</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 15px;">
<div style="text-align: center; background: rgba(255,255,255,0.1); padding: 10px; border-radius: 6px;">
<div style="font-size: 24px; font-weight: bold;">${stats.totalActions.toLocaleString()}</div>
<div style="opacity: 0.9;">סך פעולות</div>
</div>
<div style="text-align: center; background: rgba(255,255,255,0.1); padding: 10px; border-radius: 6px;">
<div style="font-size: 24px; font-weight: bold;">${stats.totalUsers}</div>
<div style="opacity: 0.9;">משתמשים פעילים</div>
</div>
<div style="text-align: center; background: rgba(255,255,255,0.1); padding: 10px; border-radius: 6px;">
<div style="font-size: 24px; font-weight: bold;">${stats.avgActionsPerUser}</div>
<div style="opacity: 0.9;">ממוצע למשתמש</div>
</div>
<div style="text-align: center; background: rgba(255,255,255,0.1); padding: 10px; border-radius: 6px;">
<div style="font-size: 24px; font-weight: bold;">${stats.avgActionsPerDay}</div>
<div style="opacity: 0.9;">ממוצע ליום</div>
</div>
</div>
</div>
<!-- פעילות יומית -->
<div style="margin-bottom: 25px;">
<h3 style="margin: 0 0 10px 0; color: #333;">פעילות יומית (14 הימים האחרונים)</h3>
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; text-align: center; overflow-x: auto;">
${dailyChart}
</div>
</div>
<!-- טופ 20 משתמשים -->
<div>
<h3 style="margin: 0 0 15px 0; color: #333;">20 המשתמשים המובילים</h3>
${chartHtml}
</div>
<!-- כפתורי פעולה
<div style="margin-top: 20px; text-align: center; padding-top: 15px; border-top: 1px solid #eee;">
<button id="consoleBtn" style="background: #36c; color: white; border: none; padding: 8px 16px; border-radius: 4px; margin: 0 5px; cursor: pointer;">
הצג בקונסולה
</button>
<button id="exportBtn" style="background: #28a745; color: white; border: none; padding: 8px 16px; border-radius: 4px; margin: 0 5px; cursor: pointer;">
ייצא ל-CSV
</button>
<button id="copyBtn" style="background: #17a2b8; color: white; border: none; padding: 8px 16px; border-radius: 4px; margin: 0 5px; cursor: pointer;">
העתק כתמונה
</button>
</div>-->
</div>
`;
// יצירת דיאלוג תוצאות מלא עם רוחב מותאם
class ResultDialog extends OO.ui.ProcessDialog {
constructor(config) {
super(config);
this.size = 'larger'; // גודל גדול יותר לתצוגה נוחה
}
static static = {
name: 'resultDialog',
title: 'תוצאות ניתוח נתוני ניטור',
actions: [
{
action: 'close',
label: 'סגור',
flags: ['safe', 'close']
}
]
};
initialize() {
super.initialize.call(this);
this.content = new OO.ui.PanelLayout({
padded: true,
expanded: false
});
this.$body.append(this.content.$element);
// הגדרת רוחב מותאם אישית
this.$element.css({
'max-width': '95vw',
'width': '1200px'
});
}
getSetupProcess(data) {
return super.getSetupProcess.call(this, data)
.next(() => {
this.content.$element.html(data.content);
});
}
getActionProcess(action) {
if (action === 'close') {
return new OO.ui.Process(() => {
this.close();
});
}
return super.getActionProcess.call(this, action);
}
}
const resultDialog = new ResultDialog();
const windowManager = new OO.ui.WindowManager({
modal: true
});
$('body').append(windowManager.$element);
windowManager.addWindows([resultDialog]);
// פתיחת החלון
windowManager.openWindow(resultDialog, {
content: windowHtml
}).then(() => {
// טיפול בסגירת החלון
resultDialog.on('close', () => {
windowManager.destroy();
});
});
// הוספת event listeners לכפתורים
setTimeout(() => {
const consoleBtn = document.getElementById('consoleBtn');
const exportBtn = document.getElementById('exportBtn');
const copyBtn = document.getElementById('copyBtn');
if (consoleBtn) {
consoleBtn.addEventListener('click', () => {
console.log('📊 סטטיסטיקות ניטור:');
console.log('====================');
console.log(`תקופה: ${stats.days} ימים`);
console.log(`סך פעולות: ${stats.totalActions.toLocaleString()}`);
console.log(`משתמשים פעילים: ${stats.totalUsers}`);
console.log(`ממוצע למשתמש: ${stats.avgActionsPerUser}`);
console.log(`ממוצע ליום: ${stats.avgActionsPerDay}`);
console.log('====================');
console.table(Object.fromEntries(userData));
console.log('נתונים מלאים:', stats);
showInDialogMessage('נתונים הודפסו בקונסולה! פתח את F12 לצפייה', 'notice');
});
}
if (exportBtn) {
exportBtn.addEventListener('click', () => {
try {
// יצירת CSV עם BOM לתמיכה בעברית
const BOM = '\uFEFF';
let csv = BOM + 'משתמש,כמות פעולות\n';
userData.forEach(([user, actions]) => {
csv += `"${user}",${actions}\n`;
});
// הוספת סטטיסטיקות נוספות
csv += '\n--- סטטיסטיקות כלליות ---\n';
csv += `"תקופה (ימים)",${stats.days}\n`;
csv += `"סך פעולות",${stats.totalActions}\n`;
csv += `"משתמשים פעילים",${stats.totalUsers}\n`;
csv += `"ממוצע למשתמש",${stats.avgActionsPerUser}\n`;
csv += `"ממוצע ליום",${stats.avgActionsPerDay}\n`;
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `patrol-stats-${stats.days}d-${new Date().toISOString().substring(0, 10)}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showInDialogMessage('קובץ CSV נשמר בהצלחה!', 'success');
} catch (error) {
console.error('שגיאה בייצוא:', error);
showInDialogMessage('אירעה שגיאה בייצוא הקובץ', 'error');
}
});
}
if (copyBtn) {
copyBtn.addEventListener('click', async () => {
try {
showInDialogMessage('מכין תמונה להעתקה...', 'notice');
// יצירת SVG מהתוכן
const dialogContent = document.querySelector('.oo-ui-dialog .oo-ui-window-body .oo-ui-panelLayout-padded');
if (!dialogContent) {
throw new Error('לא נמצא תוכן הדיאלוג');
}
// שמירת HTML כקובץ תמונה באמצעות SVG
const rect = dialogContent.getBoundingClientRect();
const htmlContent = dialogContent.innerHTML;
// יצירת SVG עם HTML מוטמע
const svgData = `
<svg xmlns="http://www.w3.org/2000/svg" width="${rect.width}" height="${rect.height}">
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml" style="
font-family: Arial, sans-serif;
background: white;
padding: 20px;
direction: rtl;
width: ${rect.width - 40}px;
height: ${rect.height - 40}px;
overflow: hidden;
">
${htmlContent}
</div>
</foreignObject>
</svg>
`;
// המרה ל-canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = rect.width * 2; // רזולוציה גבוהה
canvas.height = rect.height * 2;
const img = new Image();
img.onload = async () => {
ctx.scale(2, 2);
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, rect.width, rect.height);
ctx.drawImage(img, 0, 0);
// העתקה ללוח
try {
canvas.toBlob(async (blob) => {
if (navigator.clipboard && navigator.clipboard.write) {
const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]);
showInDialogMessage('תמונה הועתקה ללוח בהצלחה!', 'success');
} else {
throw new Error('הדפדפן לא תומך בהעתקה ללוח');
}
}, 'image/png');
} catch (clipboardError) {
// fallback - הורדת קובץ
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `patrol-stats-${stats.days}d-${new Date().toISOString().substring(0, 10)}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showInDialogMessage('תמונה נשמרה כקובץ', 'warning');
}, 'image/png');
}
};
img.onerror = () => {
// פתרון חלופי - יצירת תמונה פשוטה עם הנתונים
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, rect.width, rect.height);
// כותרת
ctx.fillStyle = '#333';
ctx.font = 'bold 24px Arial';
ctx.textAlign = 'center';
ctx.fillText(`ניתוח נתוני ניטור - ${stats.days} ימים`, rect.width / 2, 40);
// סטטיסטיקות
ctx.font = '16px Arial';
ctx.textAlign = 'right';
let y = 80;
ctx.fillText(`סך פעולות: ${stats.totalActions.toLocaleString()}`, rect.width - 20, y += 30);
ctx.fillText(`משתמשים פעילים: ${stats.totalUsers}`, rect.width - 20, y += 25);
ctx.fillText(`ממוצע למשתמש: ${stats.avgActionsPerUser}`, rect.width - 20, y += 25);
ctx.fillText(`ממוצע ליום: ${stats.avgActionsPerDay}`, rect.width - 20, y += 25);
// רשימת משתמשים מובילים
y += 40;
ctx.fillText('משתמשים מובילים:', rect.width - 20, y);
userData.slice(0, 10).forEach(([user, actions], index) => {
ctx.fillText(`${index + 1}. ${user}: ${actions}`, rect.width - 20, y += 25);
});
// העתקה
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `patrol-stats-${stats.days}d-${new Date().toISOString().substring(0, 10)}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showInDialogMessage('תמונה נשמרה כקובץ', 'success');
}, 'image/png');
};
const svgBlob = new Blob([svgData], { type: 'image/svg+xml' });
img.src = URL.createObjectURL(svgBlob);
} catch (error) {
console.error('שגיאה ביצירת תמונה:', error);
showInDialogMessage('אירעה שגיאה ביצירת התמונה', 'error');
}
});
}
}, 500);
// הוספת אנימציה לגרפים
setTimeout(() => {
const bars = document.querySelectorAll('[style*="transition: width"]');
bars.forEach((bar, index) => {
setTimeout(() => {
bar.style.width = bar.style.width;
}, index * 50);
});
}, 100);
}
$(mw.util.addPortletLink("p-tb", "#", "הצג נתוני ניטור", "statusPatrol")).click(() => {
// הפעלת הסקריפט
OO.ui.prompt("הזן מספר ימים לניתוח:", { textInput: { value: "7" } })
.then((result) => {
if (result !== null) {
getTime(result);
}
});
});
});