diff --git a/maintenance_service_http_monitoring/README.md b/maintenance_service_http_monitoring/README.md index 63afdf4..3b9bbc7 100644 --- a/maintenance_service_http_monitoring/README.md +++ b/maintenance_service_http_monitoring/README.md @@ -98,10 +98,20 @@ On service instances, you can see: ## Automatic Maintenance Requests When a service fails HTTP checks: -- A corrective maintenance request is created with prefix "[HTTP KO]" -- The request is linked to the equipment -- Only one request per equipment per day is created -- The request description lists all failing services +- A corrective maintenance request is created per failing service, named + ``[HTTP KO] {service_url}`` +- The request description includes the error detail: the HTTP status code, + or a network error label (timeout / DNS / SSL) when no HTTP response was received +- No duplicate is created as long as an open request already exists for that service +- A **double-check** is performed before creating the request: the service is retested + after 2 seconds. A maintenance request is only created if the service fails **both** + checks, reducing noise from transient HTTP errors + +When a service recovers (returns HTTP 200 after having an open request): +- The open maintenance request is automatically moved to the first **done** stage +- A chatter note is posted on the request by OdooBot to record the automatic closure +- The link between the service and the request is cleared, allowing a new request to + be created if the service fails again in the future ## Webhook notifications diff --git a/maintenance_service_http_monitoring/__manifest__.py b/maintenance_service_http_monitoring/__manifest__.py index d0fe809..7d092b8 100644 --- a/maintenance_service_http_monitoring/__manifest__.py +++ b/maintenance_service_http_monitoring/__manifest__.py @@ -5,7 +5,7 @@ "license": "AGPL-3", "category": "Tools", "summary": "Monitor HTTP availability of services", - "depends": ["base", "maintenance", "maintenance_server_data"], + "depends": ["base", "maintenance", "hr_maintenance", "maintenance_server_data"], "external_dependencies": {"python": ["requests"]}, "data": [ "data/ir_config_parameter.xml", diff --git a/maintenance_service_http_monitoring/models/maintenance_equipment.py b/maintenance_service_http_monitoring/models/maintenance_equipment.py index 644ffa5..2a409fa 100644 --- a/maintenance_service_http_monitoring/models/maintenance_equipment.py +++ b/maintenance_service_http_monitoring/models/maintenance_equipment.py @@ -3,7 +3,6 @@ from datetime import timedelta from odoo import api, fields, models - try: import requests as http_requests except ImportError: @@ -13,28 +12,21 @@ _logger = logging.getLogger(__name__) WEBHOOK_TIMEOUT = 10 # seconds + class MaintenanceEquipment(models.Model): _inherit = "maintenance.equipment" maintenance_mode = fields.Boolean( - string="Maintenance Mode", default=False, tracking=True, ) maintenance_mode_start = fields.Datetime( - string="Maintenance Mode Start", readonly=True, ) maintenance_mode_end = fields.Datetime( - string="Maintenance Mode End", readonly=True, help="Computed from start + configured duration", ) - http_maintenance_request = fields.Many2one( - "maintenance.request", - string="HTTP Maintenance Request", - readonly=True, - ) def action_activate_maintenance_mode(self): for rec in self: @@ -75,31 +67,30 @@ class MaintenanceEquipment(models.Model): ) expired.action_deactivate_maintenance_mode() - def create_http_maintenance_request(self, ko_services): + def create_http_maintenance_request(self, ko_service): + """ + Create or return the open maintenance.request for a single KO service. + + Deduplication: if ko_service already has an open (non-done) request, + return it without creating a new one. + """ self.ensure_one() - today = fields.Date.context_today(self) - name = f"[HTTP KO] {self.name}" - domain = [ - ("name", "=", name), - ("equipment_id", "=", self.id), - ("maintenance_type", "=", "corrective"), - ("create_date", ">=", f"{today} 00:00:00"), - ("create_date", "<=", f"{today} 23:59:59"), - ] - existing = self.env["maintenance.request"].search(domain, limit=1) - # Check if a task with same name already exist for the day, if it’s the case : skip - if existing: - self.http_maintenance_request = existing.id + existing = ko_service.http_maintenance_request + if existing and not existing.stage_id.done: return existing - request = self.http_maintenance_request - if request and not request.stage_id.done: - return request + status_code = ko_service.last_http_status_code + if status_code == -1: + error_detail = "Erreur réseau (timeout / DNS / SSL)" + else: + error_detail = f"HTTP {status_code}" + name = f"[HTTP KO] {ko_service.service_url}" + description = f"Service KO: {ko_service.service_url}\n{error_detail}" vals = { "name": name, "equipment_id": self.id, "priority": "2", "maintenance_type": "corrective", - "description": self._build_ko_services_description(ko_services), + "description": description, } if self.employee_id: vals["employee_id"] = self.employee_id.id @@ -112,35 +103,36 @@ class MaintenanceEquipment(models.Model): if team: vals["maintenance_team_id"] = team.id request = self.env["maintenance.request"].create(vals) - self.http_maintenance_request = request.id - self._notify_webhook(request, ko_services) + ko_service.http_maintenance_request = request.id + self._notify_webhook(request, ko_service) return request - def _notify_webhook(self, request, ko_services): - """Send a webhook notification when a new maintenance request is created.""" - ICP = self.env['ir.config_parameter'].sudo() + def _notify_webhook(self, request, ko_service): + """ + Send a webhook notification when a new maintenance request is created. + """ + ICP = self.env["ir.config_parameter"].sudo() webhook_url = ICP.get_param( - 'maintenance_service_http_monitoring.webhook_url', '' + "maintenance_service_http_monitoring.webhook_url", "" ) if not webhook_url: return webhook_user = ICP.get_param( - 'maintenance_service_http_monitoring.webhook_user', '' + "maintenance_service_http_monitoring.webhook_user", "" ) webhook_password = ICP.get_param( - 'maintenance_service_http_monitoring.webhook_password', '' + "maintenance_service_http_monitoring.webhook_password", "" ) - base_url = ICP.get_param('web.base.url', '') + base_url = ICP.get_param("web.base.url", "") link = ( - f"{base_url}/web#id={request.id}" - f"&model=maintenance.request&view_type=form" + f"{base_url}/web#id={request.id}&model=maintenance.request&view_type=form" ) payload = { - 'id': request.id, - 'name': request.name, - 'description': request.description or '', - 'equipment': self.name, - 'link': link, + "id": request.id, + "name": request.name, + "description": request.description or "", + "equipment": self.name, + "link": link, } auth = None if webhook_user and webhook_password: @@ -155,9 +147,6 @@ class MaintenanceEquipment(models.Model): except Exception as e: _logger.warning( "Webhook notification failed for maintenance request %s: %s", - request.id, e, + request.id, + e, ) - - def _build_ko_services_description(self, ko_services): - lines = [f"Service KO: {s.service_url or s.name}" for s in ko_services] - return "\n".join(lines) diff --git a/maintenance_service_http_monitoring/models/service_instance.py b/maintenance_service_http_monitoring/models/service_instance.py index 3024801..3c7cc66 100644 --- a/maintenance_service_http_monitoring/models/service_instance.py +++ b/maintenance_service_http_monitoring/models/service_instance.py @@ -1,4 +1,5 @@ import logging +import time from odoo import api, fields, models @@ -10,6 +11,7 @@ except ImportError: _logger = logging.getLogger(__name__) HTTP_CHECK_TIMEOUT = 10 # seconds +HTTP_RETRY_DELAY = 2 # seconds between pass 1 and pass 2 class ServiceInstance(models.Model): @@ -29,13 +31,25 @@ class ServiceInstance(models.Model): readonly=True, default=True, ) + http_maintenance_request = fields.Many2one( + "maintenance.request", + string="HTTP Maintenance Request", + readonly=True, + ) def check_http_status(self): + """ + Perform HTTP check for each record and return the KO recordset. + + Writes last_http_status_code, last_http_check_date and http_status_ok on every + checked record. Does NOT create maintenance.request — that decision belongs to + the caller (cron) after optional retry logic. + """ + ko_records = self.browse() for rec in self: if not rec.service_url or not rec.equipment_id: continue - equipment = rec.equipment_id - if getattr(equipment, "maintenance_mode", False): + if rec.equipment_id.maintenance_mode: continue status_ok = False status_code = -1 @@ -57,20 +71,77 @@ class ServiceInstance(models.Model): } ) if not status_ok: - # Delegate maintenance.request creation to equipment - if hasattr(equipment, "create_http_maintenance_request"): - equipment.create_http_maintenance_request([rec]) + ko_records |= rec + return ko_records + + def _close_http_maintenance_request(self): + """ + Close the open maintenance.request for each recovered service. + + Moves the request to the first done stage, posts a chatter note as OdooBot, and + clears http_maintenance_request on the service instance. + """ + done_stage = self.env["maintenance.stage"].search( + [("done", "=", True)], limit=1 + ) + if not done_stage: + return + odoobot = self.env.ref("base.partner_root") + for rec in self: + request = rec.http_maintenance_request + if not request or request.stage_id.done: + continue + request.sudo().write({"stage_id": done_stage.id}) + request.sudo().message_post( + body=( + f"Service {rec.service_url} is back online. " + "This request has been automatically closed by the monitoring cron." + ), + author_id=odoobot.id, + message_type="comment", + subtype_xmlid="mail.mt_note", + ) + rec.http_maintenance_request = False @api.model def cron_check_http_services(self): + """ + Check all active services with a URL, with one retry on failure. + + Pass 1: test every eligible service. + - Services that had an open request and are now OK are auto-resolved. + - Services still KO after pass 1 are retested after HTTP_RETRY_DELAY seconds. + maintenance.request is created only for services that fail both passes, + reducing noise from transient HTTP errors. + """ domain = [ ("active", "=", True), ("service_url", "!=", False), ("equipment_id", "!=", False), ] - services = self.search(domain) - for service in services: - equipment = service.equipment_id - if getattr(equipment, "maintenance_mode", False): - continue - service.check_http_status() + services = self.search(domain).filtered( + lambda s: not s.equipment_id.maintenance_mode + ) + + # Snapshot services that currently have an open request before pass 1 + services_with_open_request = services.filtered( + lambda s: s.http_maintenance_request + and not s.http_maintenance_request.stage_id.done + ) + + ko_after_pass1 = services.check_http_status() + + # Auto-resolve services that recovered during pass 1 + recovered = services_with_open_request.filtered(lambda s: s.http_status_ok) + if recovered: + recovered._close_http_maintenance_request() + + if not ko_after_pass1: + return + + time.sleep(HTTP_RETRY_DELAY) + + ko_confirmed = ko_after_pass1.check_http_status() + + for service in ko_confirmed: + service.equipment_id.create_http_maintenance_request(service) diff --git a/maintenance_service_http_monitoring/tests/__init__.py b/maintenance_service_http_monitoring/tests/__init__.py new file mode 100644 index 0000000..ab5c71a --- /dev/null +++ b/maintenance_service_http_monitoring/tests/__init__.py @@ -0,0 +1 @@ +from . import test_http_monitoring diff --git a/maintenance_service_http_monitoring/tests/test_http_monitoring.py b/maintenance_service_http_monitoring/tests/test_http_monitoring.py new file mode 100644 index 0000000..7de03d8 --- /dev/null +++ b/maintenance_service_http_monitoring/tests/test_http_monitoring.py @@ -0,0 +1,360 @@ +from datetime import timedelta +from unittest.mock import MagicMock, patch + +from odoo import fields +from odoo.tests.common import TransactionCase + +SERVICE_INSTANCE_REQUESTS = ( + "odoo.addons.maintenance_service_http_monitoring.models.service_instance.requests" +) +EQUIPMENT_HTTP_REQUESTS = ( + "odoo.addons.maintenance_service_http_monitoring" + ".models.maintenance_equipment.http_requests" +) +SERVICE_INSTANCE_SLEEP = ( + "odoo.addons.maintenance_service_http_monitoring.models.service_instance.time.sleep" +) + + +def _mock_response(status_code): + response = MagicMock() + response.status_code = status_code + return response + + +class TestHttpMonitoring(TransactionCase): + def setUp(self): + super().setUp() + team = self.env["maintenance.team"].search([], limit=1) + self.equipment = self.env["maintenance.equipment"].create( + { + "name": "Test Server", + "maintenance_team_id": team.id if team else False, + } + ) + self.service = self.env["service"].create({"name": "Test Service"}) + self.service_instance = self.env["service.instance"].create( + { + "equipment_id": self.equipment.id, + "service_id": self.service.id, + "service_url": "https://example.com", + } + ) + + # ------------------------------------------------------------------ + # Test 1 -- HTTP 200 -> service marked OK + # ------------------------------------------------------------------ + def test_http_200_sets_status_ok(self): + with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests: + mock_requests.get.return_value = _mock_response(200) + mock_requests.exceptions.RequestException = Exception + self.service_instance.check_http_status() + + self.assertTrue(self.service_instance.http_status_ok) + self.assertEqual(self.service_instance.last_http_status_code, 200) + self.assertIsNotNone(self.service_instance.last_http_check_date) + + # ------------------------------------------------------------------ + # Test 2 -- Two KO passes -> maintenance.request created on the service + # ------------------------------------------------------------------ + def test_http_500_creates_maintenance_request(self): + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + ): + mock_requests.get.return_value = _mock_response(500) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + self.assertFalse(self.service_instance.http_status_ok) + self.assertEqual(self.service_instance.last_http_status_code, 500) + + request = self.service_instance.http_maintenance_request + self.assertTrue(request) + self.assertEqual(request.name, f"[HTTP KO] {self.service_instance.service_url}") + self.assertEqual(request.priority, "2") + self.assertEqual(request.maintenance_type, "corrective") + self.assertIn("HTTP 500", request.description) + self.assertIn(self.service_instance.service_url, request.description) + + # ------------------------------------------------------------------ + # Test 3 -- Network error -> KO with code -1 + # ------------------------------------------------------------------ + def test_network_error_sets_status_ko_and_minus_one(self): + with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests: + mock_requests.get.side_effect = Exception("connection refused") + mock_requests.exceptions.RequestException = Exception + self.service_instance.check_http_status() + + self.assertFalse(self.service_instance.http_status_ok) + self.assertEqual(self.service_instance.last_http_status_code, -1) + + # ------------------------------------------------------------------ + # Test 4 -- Two consecutive cron runs KO -> no duplicate request + # ------------------------------------------------------------------ + def test_no_duplicate_request_on_repeated_failure(self): + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + ): + mock_requests.get.return_value = _mock_response(500) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + request_1 = self.service_instance.http_maintenance_request + self.assertTrue(request_1) + + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + ): + mock_requests.get.return_value = _mock_response(500) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + self.assertEqual(self.service_instance.http_maintenance_request, request_1) + self.assertEqual( + self.env["maintenance.request"].search_count( + [("equipment_id", "=", self.equipment.id)] + ), + 1, + ) + + # ------------------------------------------------------------------ + # Test 5 -- Equipment in maintenance mode -> cron skips it + # ------------------------------------------------------------------ + def test_maintenance_mode_skips_http_check(self): + self.equipment.write({"maintenance_mode": True}) + + with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests: + mock_requests.get.return_value = _mock_response(500) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + mock_requests.get.assert_not_called() + self.assertFalse(self.service_instance.last_http_check_date) + + # ------------------------------------------------------------------ + # Test 6 -- Expired maintenance mode -> cron deactivates it + # ------------------------------------------------------------------ + def test_maintenance_mode_auto_expiry(self): + past = fields.Datetime.now() - timedelta(hours=1) + self.equipment.write( + { + "maintenance_mode": True, + "maintenance_mode_start": past - timedelta(hours=4), + "maintenance_mode_end": past, + } + ) + self.assertTrue(self.equipment.maintenance_mode) + + self.env["maintenance.equipment"].cron_deactivate_expired_maintenance_mode() + + self.assertFalse(self.equipment.maintenance_mode) + self.assertFalse(self.equipment.maintenance_mode_start) + self.assertFalse(self.equipment.maintenance_mode_end) + + # ------------------------------------------------------------------ + # Test 7 -- Service without URL -> ignored by cron + # ------------------------------------------------------------------ + def test_service_without_url_is_ignored(self): + self.service_instance.write({"service_url": False}) + + with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests: + mock_requests.get.return_value = _mock_response(200) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + mock_requests.get.assert_not_called() + self.assertFalse(self.service_instance.last_http_check_date) + + # ------------------------------------------------------------------ + # Test 8 -- HTTP 404 (non-exception) -> KO with correct code + # ------------------------------------------------------------------ + def test_http_non_200_non_exception(self): + with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests: + mock_requests.get.return_value = _mock_response(404) + mock_requests.exceptions.RequestException = Exception + self.service_instance.check_http_status() + + self.assertFalse(self.service_instance.http_status_ok) + self.assertEqual(self.service_instance.last_http_status_code, 404) + + # ------------------------------------------------------------------ + # Test 9 -- Webhook called when a new maintenance.request is created + # ------------------------------------------------------------------ + def test_webhook_called_on_new_request(self): + self.env["ir.config_parameter"].sudo().set_param( + "maintenance_service_http_monitoring.webhook_url", + "https://webhook.example.com/hook", + ) + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + patch(EQUIPMENT_HTTP_REQUESTS) as mock_http, + ): + mock_requests.get.return_value = _mock_response(500) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + mock_http.post.assert_called_once() + call_kwargs = mock_http.post.call_args + payload = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + self.assertEqual(payload["equipment"], self.equipment.name) + + # ------------------------------------------------------------------ + # Test 10 -- Webhook skipped when no URL configured + # ------------------------------------------------------------------ + def test_webhook_skipped_when_no_url(self): + self.env["ir.config_parameter"].sudo().set_param( + "maintenance_service_http_monitoring.webhook_url", "" + ) + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + patch(EQUIPMENT_HTTP_REQUESTS) as mock_http, + ): + mock_requests.get.return_value = _mock_response(500) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + mock_http.post.assert_not_called() + + # ------------------------------------------------------------------ + # Test 11 -- Service without equipment -> check_http_status skips it + # ------------------------------------------------------------------ + def test_service_without_equipment_is_ignored(self): + self.service_instance.write({"equipment_id": False}) + + with patch(SERVICE_INSTANCE_REQUESTS) as mock_requests: + mock_requests.get.return_value = _mock_response(200) + mock_requests.exceptions.RequestException = Exception + self.service_instance.check_http_status() + + mock_requests.get.assert_not_called() + self.assertFalse(self.service_instance.last_http_check_date) + + # ------------------------------------------------------------------ + # Test 12 -- Transient failure (KO pass 1, OK pass 2) -> no request created + # ------------------------------------------------------------------ + def test_transient_failure_no_request_created(self): + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + ): + mock_requests.get.side_effect = [ + _mock_response(500), # pass 1: KO + _mock_response(200), # pass 2 (retry): OK + ] + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + self.assertTrue(self.service_instance.http_status_ok) + self.assertEqual(self.service_instance.last_http_status_code, 200) + self.assertFalse(self.service_instance.http_maintenance_request) + self.assertEqual( + self.env["maintenance.request"].search_count( + [("equipment_id", "=", self.equipment.id)] + ), + 0, + ) + + # ------------------------------------------------------------------ + # Test 13 -- Confirmed failure (KO pass 1 and 2) -> request created + # ------------------------------------------------------------------ + def test_confirmed_failure_creates_request(self): + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP) as mock_sleep, + ): + mock_requests.get.return_value = _mock_response(503) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + mock_sleep.assert_called_once_with(2) + self.assertEqual(mock_requests.get.call_count, 2) + self.assertFalse(self.service_instance.http_status_ok) + self.assertEqual(self.service_instance.last_http_status_code, 503) + self.assertTrue(self.service_instance.http_maintenance_request) + + # ------------------------------------------------------------------ + # Test 14 -- 2 KO services on same equipment -> 2 distinct requests + # ------------------------------------------------------------------ + def test_two_ko_services_same_equipment_create_two_requests(self): + service2 = self.env["service"].create({"name": "Test Service 2"}) + service_instance2 = self.env["service.instance"].create( + { + "equipment_id": self.equipment.id, + "service_id": service2.id, + "service_url": "https://other.example.com", + } + ) + + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + ): + mock_requests.get.return_value = _mock_response(500) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + req1 = self.service_instance.http_maintenance_request + req2 = service_instance2.http_maintenance_request + + self.assertTrue(req1) + self.assertTrue(req2) + self.assertNotEqual(req1, req2) + self.assertEqual(req1.name, f"[HTTP KO] {self.service_instance.service_url}") + self.assertEqual(req2.name, f"[HTTP KO] {service_instance2.service_url}") + self.assertEqual( + self.env["maintenance.request"].search_count( + [("equipment_id", "=", self.equipment.id)] + ), + 2, + ) + + # ------------------------------------------------------------------ + # Test 15 -- Service recovery closes the open request and posts a note + # ------------------------------------------------------------------ + def test_service_recovery_closes_request(self): + # First cron run: service is KO -> request created + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + ): + mock_requests.get.return_value = _mock_response(500) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + request = self.service_instance.http_maintenance_request + self.assertTrue(request) + self.assertFalse(request.stage_id.done) + + # Second cron run: service is back OK -> request auto-closed + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + ): + mock_requests.get.return_value = _mock_response(200) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + # Request must be in a done stage + self.assertTrue(request.stage_id.done) + # http_maintenance_request must be cleared on the service instance + self.assertFalse(self.service_instance.http_maintenance_request) + # A chatter note must have been posted mentioning the service URL + notes = request.message_ids.filtered( + lambda m: self.service_instance.service_url in (m.body or "") + ) + self.assertTrue(notes) + + # ------------------------------------------------------------------ + # Test 16 -- No open request -> _close_http_maintenance_request is a no-op + # ------------------------------------------------------------------ + def test_no_close_when_no_open_request(self): + # Service is OK from the start, no request exists + self.assertFalse(self.service_instance.http_maintenance_request) + # Calling close directly must not raise + self.service_instance._close_http_maintenance_request() + self.assertFalse(self.service_instance.http_maintenance_request) diff --git a/maintenance_service_http_monitoring/views/maintenance_equipment_views.xml b/maintenance_service_http_monitoring/views/maintenance_equipment_views.xml index 9031d62..16a49eb 100644 --- a/maintenance_service_http_monitoring/views/maintenance_equipment_views.xml +++ b/maintenance_service_http_monitoring/views/maintenance_equipment_views.xml @@ -36,7 +36,6 @@ - diff --git a/maintenance_service_http_monitoring/views/service_instance_views.xml b/maintenance_service_http_monitoring/views/service_instance_views.xml index 9b89a90..4536ee0 100644 --- a/maintenance_service_http_monitoring/views/service_instance_views.xml +++ b/maintenance_service_http_monitoring/views/service_instance_views.xml @@ -17,6 +17,7 @@ +