halyn 0.3.4__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {halyn-0.3.4/src/halyn.egg-info → halyn-0.4.0}/PKG-INFO +1 -1
- {halyn-0.3.4 → halyn-0.4.0}/pyproject.toml +1 -1
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/__init__.py +1 -1
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/control_plane.py +2 -0
- halyn-0.4.0/src/halyn/dashboard.py +206 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/mcp_serve.py +9 -1
- {halyn-0.3.4 → halyn-0.4.0/src/halyn.egg-info}/PKG-INFO +1 -1
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn.egg-info/SOURCES.txt +1 -0
- {halyn-0.3.4 → halyn-0.4.0}/LICENSE +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/README.md +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/setup.cfg +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/__main__.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/audit.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/auth.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/autonomy.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/cli.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/config.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/consent.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/discovery.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/__init__.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/browser.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/dds.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/docker.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/http_auto.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/mqtt.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/opcua.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/ros2.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/serial.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/socket_raw.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/ssh.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/unitree.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/drivers/websocket.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/engine.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/integrations/__init__.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/integrations/telegram.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/intent.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/llm.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/mcp.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/memory/__init__.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/memory/store.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/nrp_bridge.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/py.typed +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/sanitizer.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/server.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/types.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn/watchdog.py +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn.egg-info/dependency_links.txt +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn.egg-info/entry_points.txt +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn.egg-info/requires.txt +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/src/halyn.egg-info/top_level.txt +0 -0
- {halyn-0.3.4 → halyn-0.4.0}/tests/test_halyn.py +0 -0
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
15
|
import logging
|
|
16
|
+
import os
|
|
16
17
|
import time
|
|
17
18
|
from dataclasses import dataclass, field
|
|
18
19
|
from typing import Any
|
|
@@ -53,6 +54,7 @@ class ControlPlane:
|
|
|
53
54
|
|
|
54
55
|
# Safety & Control
|
|
55
56
|
self.autonomy = AutonomyController(default_level=Level.SUPERVISED)
|
|
57
|
+
os.makedirs(self.config.data_dir, exist_ok=True)
|
|
56
58
|
self.audit = AuditStore(f"{self.config.data_dir}/audit.db")
|
|
57
59
|
self.consent = ConsentStore(f"{self.config.data_dir}/consent.db")
|
|
58
60
|
self.intents = IntentStore(f"{self.config.data_dir}/intent.db")
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Copyright (c) 2026 Elmadani SALKA. MIT License.
|
|
2
|
+
"""
|
|
3
|
+
Halyn Dashboard — Built-in web UI for the MCP Server.
|
|
4
|
+
|
|
5
|
+
When running halyn-mcp, opening http://localhost:8935 shows this dashboard.
|
|
6
|
+
Real-time device status, shield rules, audit chain — all visual.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
DASHBOARD_HTML = '''<!DOCTYPE html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="UTF-8">
|
|
13
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
14
|
+
<title>Halyn Dashboard</title>
|
|
15
|
+
<style>
|
|
16
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
17
|
+
:root{--bg:#0b0d10;--bg2:#13161b;--fg:#e1e4ea;--fg2:#8b919e;--fg3:#5c6370;--accent:#0a6e3f;--accent2:#3ddc84;--red:#ef4444;--border:#1e2330;--font:system-ui,sans-serif;--mono:'Courier New',monospace}
|
|
18
|
+
body{background:var(--bg);color:var(--fg);font-family:var(--font);min-height:100vh}
|
|
19
|
+
.top{background:var(--bg2);border-bottom:1px solid var(--border);padding:.75rem 1.5rem;display:flex;justify-content:space-between;align-items:center}
|
|
20
|
+
.top h1{font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem}
|
|
21
|
+
.top h1 span{color:var(--accent2)}
|
|
22
|
+
.top .status{font-size:.75rem;color:var(--accent2);font-family:var(--mono);display:flex;align-items:center;gap:.4rem}
|
|
23
|
+
.top .dot{width:8px;height:8px;border-radius:50%;background:var(--accent2);animation:pulse 2s infinite}
|
|
24
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
25
|
+
.grid{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--border);margin:0;height:calc(100vh - 49px)}
|
|
26
|
+
.panel{background:var(--bg);padding:1rem;overflow-y:auto}
|
|
27
|
+
.panel h2{font-size:.8rem;font-weight:600;color:var(--fg2);text-transform:uppercase;letter-spacing:1px;margin-bottom:.75rem;display:flex;align-items:center;gap:.5rem}
|
|
28
|
+
.panel h2 .count{background:var(--bg2);color:var(--fg3);font-size:.65rem;padding:2px 6px;border-radius:8px}
|
|
29
|
+
|
|
30
|
+
/* Chat / Command */
|
|
31
|
+
.chat{display:flex;flex-direction:column;height:100%}
|
|
32
|
+
.messages{flex:1;overflow-y:auto;padding-bottom:.5rem}
|
|
33
|
+
.msg{margin-bottom:.5rem;padding:.5rem .75rem;border-radius:8px;font-size:.82rem;line-height:1.5;max-width:90%}
|
|
34
|
+
.msg.user{background:var(--accent);color:#fff;margin-left:auto;border-bottom-right-radius:2px}
|
|
35
|
+
.msg.sys{background:var(--bg2);color:var(--fg2);border-bottom-left-radius:2px}
|
|
36
|
+
.msg.blocked{background:#2a1215;color:var(--red);border:1px solid #4a1c20}
|
|
37
|
+
.msg.ok{background:#0f2318;color:var(--accent2);border:1px solid #1a3a28}
|
|
38
|
+
.input-row{display:flex;gap:.5rem;padding-top:.5rem;border-top:1px solid var(--border)}
|
|
39
|
+
.input-row input{flex:1;background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:.6rem .75rem;border-radius:6px;font-size:.82rem;font-family:var(--font);outline:none}
|
|
40
|
+
.input-row input:focus{border-color:var(--accent)}
|
|
41
|
+
.input-row button{background:var(--accent);color:#fff;border:none;padding:.6rem 1rem;border-radius:6px;font-size:.82rem;cursor:pointer;font-weight:500}
|
|
42
|
+
.input-row button:hover{background:var(--accent2);color:#000}
|
|
43
|
+
|
|
44
|
+
/* Shields */
|
|
45
|
+
.shield{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:.5rem .75rem;margin-bottom:.4rem;font-family:var(--mono);font-size:.75rem;color:var(--fg2);display:flex;align-items:center;gap:.5rem}
|
|
46
|
+
.shield::before{content:"🛡️";font-size:.9rem}
|
|
47
|
+
|
|
48
|
+
/* Audit */
|
|
49
|
+
.audit-entry{display:grid;grid-template-columns:auto 1fr auto auto;gap:.5rem;padding:.4rem 0;border-bottom:1px solid var(--border);font-size:.72rem;font-family:var(--mono);align-items:center}
|
|
50
|
+
.audit-entry .time{color:var(--fg3)}
|
|
51
|
+
.audit-entry .tool{color:var(--fg)}
|
|
52
|
+
.audit-entry .hash{color:var(--fg3);font-size:.65rem}
|
|
53
|
+
.audit-entry .badge{padding:1px 6px;border-radius:4px;font-size:.6rem;font-weight:600}
|
|
54
|
+
.badge.ok{background:#0f2318;color:var(--accent2)}
|
|
55
|
+
.badge.blocked{background:#2a1215;color:var(--red)}
|
|
56
|
+
|
|
57
|
+
/* Stats */
|
|
58
|
+
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;margin-bottom:1rem}
|
|
59
|
+
.stat{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:.75rem;text-align:center}
|
|
60
|
+
.stat .v{font-family:var(--mono);font-size:1.5rem;font-weight:700;color:var(--accent2)}
|
|
61
|
+
.stat .l{font-size:.65rem;color:var(--fg3);margin-top:.2rem}
|
|
62
|
+
|
|
63
|
+
@media(max-width:768px){.grid{grid-template-columns:1fr;height:auto}.panel{min-height:50vh}.stats{grid-template-columns:repeat(2,1fr)}}
|
|
64
|
+
</style>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<div class="top">
|
|
68
|
+
<h1>⬡ <span>Halyn</span> Dashboard</h1>
|
|
69
|
+
<div class="status"><div class="dot"></div> <span id="uptime">connecting...</span></div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="grid">
|
|
72
|
+
<!-- LEFT: Chat + Command -->
|
|
73
|
+
<div class="panel">
|
|
74
|
+
<div class="chat">
|
|
75
|
+
<div class="messages" id="msgs">
|
|
76
|
+
<div class="msg sys">Welcome to Halyn. Type a command or talk naturally.<br><br>Try: <code>observe all</code> · <code>shield deny * delete *</code> · <code>status</code> · <code>audit</code></div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="input-row">
|
|
79
|
+
<input type="text" id="cmd" placeholder="Type a command... (e.g. 'restart nginx on server-01')" autofocus>
|
|
80
|
+
<button onclick="send()">Send</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<!-- RIGHT: Status + Shields + Audit -->
|
|
85
|
+
<div class="panel">
|
|
86
|
+
<div class="stats" id="stats">
|
|
87
|
+
<div class="stat"><div class="v" id="s-nodes">0</div><div class="l">Nodes</div></div>
|
|
88
|
+
<div class="stat"><div class="v" id="s-shields">0</div><div class="l">Shields</div></div>
|
|
89
|
+
<div class="stat"><div class="v" id="s-audit">0</div><div class="l">Audit</div></div>
|
|
90
|
+
<div class="stat"><div class="v" id="s-uptime">0s</div><div class="l">Uptime</div></div>
|
|
91
|
+
</div>
|
|
92
|
+
<h2>🛡️ Shield Rules <span class="count" id="shield-count">0</span></h2>
|
|
93
|
+
<div id="shields"></div>
|
|
94
|
+
<h2 style="margin-top:1rem">📜 Audit Chain <span class="count" id="audit-count">0</span></h2>
|
|
95
|
+
<div id="audit"></div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<script>
|
|
99
|
+
const $ = s => document.getElementById(s);
|
|
100
|
+
|
|
101
|
+
async function mcp(name, args={}) {
|
|
102
|
+
const r = await fetch('/mcp', {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {'Content-Type': 'application/json'},
|
|
105
|
+
body: JSON.stringify({jsonrpc:'2.0',id:Date.now(),method:'tools/call',params:{name,arguments:args}})
|
|
106
|
+
});
|
|
107
|
+
const d = await r.json();
|
|
108
|
+
return JSON.parse(d.result.content[0].text);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function addMsg(text, cls='sys') {
|
|
112
|
+
const d = document.createElement('div');
|
|
113
|
+
d.className = 'msg ' + cls;
|
|
114
|
+
d.innerHTML = text;
|
|
115
|
+
$('msgs').appendChild(d);
|
|
116
|
+
$('msgs').scrollTop = $('msgs').scrollHeight;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function refresh() {
|
|
120
|
+
try {
|
|
121
|
+
const s = await mcp('halyn_status');
|
|
122
|
+
$('s-nodes').textContent = s.nodes;
|
|
123
|
+
$('s-shields').textContent = s.shields;
|
|
124
|
+
$('s-audit').textContent = s.audit_entries;
|
|
125
|
+
$('s-uptime').textContent = s.uptime_seconds + 's';
|
|
126
|
+
$('uptime').textContent = 'v' + s.version + ' · ' + s.uptime_seconds + 's uptime';
|
|
127
|
+
|
|
128
|
+
const sh = await mcp('halyn_shield_list');
|
|
129
|
+
$('shield-count').textContent = sh.count;
|
|
130
|
+
$('shields').innerHTML = sh.shields.map(r => '<div class="shield">' + r + '</div>').join('');
|
|
131
|
+
|
|
132
|
+
const au = await mcp('halyn_audit', {limit: 20});
|
|
133
|
+
$('audit-count').textContent = au.total;
|
|
134
|
+
$('audit').innerHTML = au.entries.reverse().map(e => {
|
|
135
|
+
const badge = e.result_ok ? '<span class="badge ok">OK</span>' : '<span class="badge blocked">BLOCKED</span>';
|
|
136
|
+
return '<div class="audit-entry"><span class="time">' + e.timestamp.split('T')[1].replace('Z','') + '</span><span class="tool">' + e.tool + '</span>' + badge + '<span class="hash">' + e.hash + '</span></div>';
|
|
137
|
+
}).join('');
|
|
138
|
+
} catch(e) {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function send() {
|
|
142
|
+
const input = $('cmd');
|
|
143
|
+
const cmd = input.value.trim();
|
|
144
|
+
if (!cmd) return;
|
|
145
|
+
input.value = '';
|
|
146
|
+
|
|
147
|
+
addMsg(cmd, 'user');
|
|
148
|
+
|
|
149
|
+
const lower = cmd.toLowerCase();
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
if (lower === 'status' || lower === 'state') {
|
|
153
|
+
const s = await mcp('halyn_status');
|
|
154
|
+
addMsg('Nodes: ' + s.nodes + ' · Shields: ' + s.shields + ' · Audit: ' + s.audit_entries + ' · Uptime: ' + s.uptime_seconds + 's', 'ok');
|
|
155
|
+
} else if (lower.startsWith('shield ') || lower.startsWith('deny ') || lower.startsWith('limit ')) {
|
|
156
|
+
const rule = lower.startsWith('shield ') ? cmd.slice(7) : cmd;
|
|
157
|
+
const r = await mcp('halyn_shield_add', {rule});
|
|
158
|
+
if (r.error) { addMsg('Error: ' + r.error, 'blocked'); }
|
|
159
|
+
else { addMsg('Shield added: ' + r.added + ' (total: ' + r.total_shields + ')', 'ok'); }
|
|
160
|
+
} else if (lower === 'shields' || lower === 'rules') {
|
|
161
|
+
const r = await mcp('halyn_shield_list');
|
|
162
|
+
addMsg(r.shields.length ? r.shields.map(s => '🛡️ ' + s).join('<br>') : 'No shields active.', 'sys');
|
|
163
|
+
} else if (lower === 'audit' || lower === 'log' || lower === 'history') {
|
|
164
|
+
const r = await mcp('halyn_audit', {limit: 10});
|
|
165
|
+
const lines = r.entries.map(e => {
|
|
166
|
+
const s = e.result_ok ? '✓' : '✗';
|
|
167
|
+
return s + ' ' + e.timestamp.split('T')[1].replace('Z','') + ' ' + e.tool;
|
|
168
|
+
});
|
|
169
|
+
addMsg(lines.join('<br>') || 'No audit entries yet.', 'sys');
|
|
170
|
+
} else if (lower.startsWith('observe') || lower.startsWith('watch') || lower.startsWith('read')) {
|
|
171
|
+
const node = cmd.split(' ').slice(1).join(' ') || 'all';
|
|
172
|
+
const r = await mcp('halyn_observe', {node});
|
|
173
|
+
addMsg('<pre>' + JSON.stringify(r, null, 2) + '</pre>', 'sys');
|
|
174
|
+
} else if (lower === 'stop' || lower === 'emergency') {
|
|
175
|
+
const r = await mcp('halyn_emergency_stop');
|
|
176
|
+
addMsg('⚠️ ' + r.status, 'blocked');
|
|
177
|
+
} else if (lower === 'nodes' || lower === 'devices') {
|
|
178
|
+
const r = await mcp('halyn_nodes');
|
|
179
|
+
addMsg('<pre>' + JSON.stringify(r, null, 2) + '</pre>', 'sys');
|
|
180
|
+
} else if (lower === 'help') {
|
|
181
|
+
addMsg('Commands:<br>• <b>status</b> — system overview<br>• <b>observe [node]</b> — read device state<br>• <b>shield deny * delete *</b> — add safety rule<br>• <b>shields</b> — list active rules<br>• <b>audit</b> — view action log<br>• <b>nodes</b> — list devices<br>• <b>stop</b> — emergency stop<br>• Or type any action: <i>restart nginx on server-01</i>', 'sys');
|
|
182
|
+
} else {
|
|
183
|
+
// Treat as action on default node
|
|
184
|
+
const parts = cmd.match(/(.+?)\\s+on\\s+(.+)/i);
|
|
185
|
+
const node = parts ? parts[2] : 'default';
|
|
186
|
+
const command = parts ? parts[1] : cmd;
|
|
187
|
+
const r = await mcp('halyn_act', {node, command});
|
|
188
|
+
if (r.blocked) {
|
|
189
|
+
addMsg('🛡️ BLOCKED: ' + r.reason, 'blocked');
|
|
190
|
+
} else {
|
|
191
|
+
addMsg('✓ Executed: ' + command + (node !== 'default' ? ' on ' + node : ''), 'ok');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch(e) {
|
|
195
|
+
addMsg('Error: ' + e.message, 'blocked');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
refresh();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
$('cmd').addEventListener('keydown', e => { if (e.key === 'Enter') send(); });
|
|
202
|
+
refresh();
|
|
203
|
+
setInterval(refresh, 5000);
|
|
204
|
+
</script>
|
|
205
|
+
</body>
|
|
206
|
+
</html>'''
|
|
@@ -30,6 +30,10 @@ log = logging.getLogger("halyn.mcp_serve")
|
|
|
30
30
|
|
|
31
31
|
try:
|
|
32
32
|
from aiohttp import web
|
|
33
|
+
try:
|
|
34
|
+
from halyn.dashboard import DASHBOARD_HTML
|
|
35
|
+
except ImportError:
|
|
36
|
+
DASHBOARD_HTML = "<h1>Halyn MCP Server</h1><p>Dashboard not available.</p>"
|
|
33
37
|
from aiohttp.web import middleware
|
|
34
38
|
except ImportError:
|
|
35
39
|
print("pip install aiohttp # required for MCP server")
|
|
@@ -323,9 +327,13 @@ def create_app(config_path: str | None = None) -> web.Application:
|
|
|
323
327
|
async def handle_health(request: web.Request) -> web.Response:
|
|
324
328
|
return web.json_response({"status": "ok", "server": "halyn-mcp", "version": SERVER_INFO["version"]})
|
|
325
329
|
|
|
330
|
+
async def handle_dashboard(request: web.Request) -> web.Response:
|
|
331
|
+
return web.Response(text=DASHBOARD_HTML, content_type="text/html")
|
|
332
|
+
|
|
326
333
|
app.router.add_post("/mcp", handle_mcp)
|
|
327
334
|
app.router.add_get("/health", handle_health)
|
|
328
|
-
app.router.add_get("/",
|
|
335
|
+
app.router.add_get("/", handle_dashboard)
|
|
336
|
+
app.router.add_get("/api/health", handle_health)
|
|
329
337
|
|
|
330
338
|
return app
|
|
331
339
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|