diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2b30133..3aa9983 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -69,6 +69,11 @@ const routes: Routes = [ loadChildren: () => import('./views/syslog/syslog.module').then((m) => m.SyslogModule) }, + { + path: 'syslog-regex', + loadChildren: () => + import('./views/syslog-regex/syslog-regex.module').then((m) => m.SyslogRegexModule) + }, { path: 'backups', loadChildren: () => @@ -99,6 +104,11 @@ const routes: Routes = [ loadChildren: () => import('./views/snippets/snippets.module').then((m) => m.SnippetsModule) }, + { + path: 'sequences', + loadChildren: () => + import('./views/sequences/sequences.module').then((m) => m.SequencesModule) + }, { path: 'user_manager', loadChildren: () => @@ -109,6 +119,11 @@ const routes: Routes = [ loadChildren: () => import('./views/permissions/permissions.module').then((m) => m.PermissionsModule) }, + { + path: 'vpn', + loadChildren: () => + import('./views/vpn/vpn.module').then((m) => m.VpnModule) + }, { path: 'pages', loadChildren: () => @@ -137,7 +152,7 @@ const routes: Routes = [ title: 'Login Page' } }, - {path: '**', redirectTo: 'dashboard'} + { path: '**', redirectTo: 'dashboard' } ]; @NgModule({ diff --git a/src/app/containers/default-layout/_nav.ts b/src/app/containers/default-layout/_nav.ts index 00c76ce..830b492 100644 --- a/src/app/containers/default-layout/_nav.ts +++ b/src/app/containers/default-layout/_nav.ts @@ -5,18 +5,23 @@ export const navItems: INavData[] = [ name: 'Dashboard', url: '/dashboard', iconComponent: { name: 'cil-speedometer' }, - + }, { name: 'Monitoring Wall', url: '/monitoring', - icon:'fa-solid fa-tv', - attributes: { 'pro':true } + icon: 'fa-solid fa-tv', + attributes: { 'pro': true } }, { title: true, name: 'Device Managment' }, + { + name: 'VPN Server', + url: '/vpn', + icon: 'fa-solid fa-network-wired' + }, { name: 'Devices', url: '/devices', @@ -31,8 +36,8 @@ export const navItems: INavData[] = [ { name: 'Network Maps', url: '/maps', - icon:'fa-solid fa-map', - attributes: { 'pro':true } + icon: 'fa-solid fa-map', + attributes: { 'pro': true } }, // { // name: 'Tools', @@ -70,18 +75,24 @@ export const navItems: INavData[] = [ url: '/snippets', icon: 'fa-solid fa-code' }, + { + name: 'Sequences', + url: '/sequences', + icon: 'fa-solid fa-code-branch', + attributes: { 'pro': true } + }, { name: 'Sync and Cloner', url: '/cloner', icon: 'fa-solid fa-rotate', - attributes: { 'pro':true } + attributes: { 'pro': true } }, { name: 'Password Vault', url: '/vault', - icon:'fa-solid fa-vault', - attributes: { 'pro':true } + icon: 'fa-solid fa-vault', + attributes: { 'pro': true } }, // { // name: 'Tools', @@ -133,6 +144,12 @@ export const navItems: INavData[] = [ icon: 'fa-solid fa-person-circle-question', }, + { + name: 'Syslog Custom Regex', + url: '/syslog-regex', + icon: 'fa-solid fa-code-commit', + attributes: { 'pro': true } + }, { title: true, name: 'Users' @@ -140,12 +157,12 @@ export const navItems: INavData[] = [ { name: 'Users Management', url: '/user_manager', - icon: 'fa-solid fa-user-gear' , + icon: 'fa-solid fa-user-gear', }, { name: 'Permissions', url: '/permissions', - icon: 'fa-solid fa-users' , + icon: 'fa-solid fa-users', }, { title: true, @@ -155,7 +172,7 @@ export const navItems: INavData[] = [ { name: 'Settings', url: '/settings', - icon: 'fa-solid fa-gear' , + icon: 'fa-solid fa-gear', }, // { // name: 'Backup', @@ -177,7 +194,7 @@ export const navItems: INavData[] = [ { name: 'Buy Pro', url: 'https://mikrowizard.com/pricing/', - icon:'fa-solid fa-money-check-dollar', - attributes: { 'free':true,target: '_blank' } + icon: 'fa-solid fa-money-check-dollar', + attributes: { 'free': true, target: '_blank' } } ]; diff --git a/src/app/providers/mikrowizard/data.ts b/src/app/providers/mikrowizard/data.ts index 6bc40f6..e7b5d20 100644 --- a/src/app/providers/mikrowizard/data.ts +++ b/src/app/providers/mikrowizard/data.ts @@ -8,7 +8,7 @@ import { User } from './user'; @Injectable() export class dataProvider { - + // public serverUrl: string = "/api"; public serverUrl: string = ""; private db: string = "NothingImportant"; @@ -60,60 +60,60 @@ export class dataProvider { //// //// MikroWizard API //// - get_front_version(){ + get_front_version() { return this.MikroWizardRPC.sendHttpGetRequest("/api/frontver/"); } - change_password(oldpass:string,newpass:string){ - var data={ - 'oldpass':oldpass, - 'newpass':newpass + change_password(oldpass: string, newpass: string) { + var data = { + 'oldpass': oldpass, + 'newpass': newpass } return this.MikroWizardRPC.sendJsonRequest("/api/user/change_password", data); } - dashboard_stats(versioncheck:boolean,front_version:string){ - var data={ - 'versioncheck':versioncheck, - 'front_version':front_version + dashboard_stats(versioncheck: boolean, front_version: string) { + var data = { + 'versioncheck': versioncheck, + 'front_version': front_version } return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/stats", data); } - monitoring_devices_events(page:number,textfilter:string=''){ - var data={ - 'page':page, - 'textfilter':textfilter + monitoring_devices_events(page: number, textfilter: string = '') { + var data = { + 'page': page, + 'textfilter': textfilter } - + return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/devs/get", data); } - - monitoring_events_fix(event_id:number){ - var data={ - 'event_id':event_id + + monitoring_events_fix(event_id: number) { + var data = { + 'event_id': event_id } return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/fix", data); } - - monitoring_all_events(devid:number,page:number){ - var data={ - 'devid':devid, - 'page':page + + monitoring_all_events(devid: number, page: number) { + var data = { + 'devid': devid, + 'page': page } return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/events/get", data); } - monitoring_unfixed_events(devid:number){ - var data={ - 'devid':devid + monitoring_unfixed_events(devid: number) { + var data = { + 'devid': devid } return this.MikroWizardRPC.sendJsonRequest("/api/monitoring/eventunfixed/get", data); } - dashboard_traffic(delta:string){ - var data={ - 'delta':delta + dashboard_traffic(delta: string) { + var data = { + 'delta': delta } return this.MikroWizardRPC.sendJsonRequest("/api/dashboard/traffic", data); } - get_dev_list(data:any) { + get_dev_list(data: any) { return this.MikroWizardRPC.sendJsonRequest("/api/dev/list", data); } @@ -121,442 +121,502 @@ export class dataProvider { return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/list", {}); } - get_devgroup_members(gid:number) { - var data={ - 'gid':gid + get_devgroup_members(gid: number) { + var data = { + 'gid': gid } return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/members", data); } - delete_group(id:number){ - var data={ - 'gid':id + delete_group(id: number) { + var data = { + 'gid': id } return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/delete", data); } - delete_devices(devids:any){ + delete_devices(devids: any) { var data = { - 'devids':devids + 'devids': devids } return this.MikroWizardRPC.sendJsonRequest("/api/dev/delete", data); } get_dev_info(id: number) { - var data={ - 'devid':id + var data = { + 'devid': id } return this.MikroWizardRPC.sendJsonRequest("/api/dev/info", data); } get_editform(id: number) { - var data={ - 'devid':id + var data = { + 'devid': id } return this.MikroWizardRPC.sendJsonRequest("/api/dev/get_editform", data); } - save_editform(data:any){ + save_editform(data: any) { return this.MikroWizardRPC.sendJsonRequest("/api/dev/save_editform", data); } - get_dev_sensors(id: number,delta:string="5m",total_type:string="bps") { - var data={ - 'devid':id, - 'delta':delta, - 'total':total_type + get_dev_sensors(id: number, delta: string = "5m", total_type: string = "bps") { + var data = { + 'devid': id, + 'delta': delta, + 'total': total_type } return this.MikroWizardRPC.sendJsonRequest("/api/dev/sensors", data); } - get_dev_radio_sensors(id: number, delta:string="5m"){ - var data={ - 'devid':id, - 'delta':delta + get_dev_radio_sensors(id: number, delta: string = "5m") { + var data = { + 'devid': id, + 'delta': delta } return this.MikroWizardRPC.sendJsonRequest("/api/dev/radio/sensors", data); } - get_dev_dhcp_info(id: number){ - var data={ - 'devid':id, + get_dev_dhcp_info(id: number) { + var data = { + 'devid': id, } return this.MikroWizardRPC.sendJsonRequest("/api/dev/dhcp-server/get", data); } - get_dev_ifstat(id: number,delta:string="5m",iface:string="ether1",type:string="bps") { - var data={ - 'devid':id, - 'delta':delta, - 'type':type, - 'interface':iface + get_dev_ifstat(id: number, delta: string = "5m", iface: string = "ether1", type: string = "bps") { + var data = { + 'devid': id, + 'delta': delta, + 'type': type, + 'interface': iface } return this.MikroWizardRPC.sendJsonRequest("/api/dev/ifstat", data); } - totp(action:string,userid:string){ - var data={ - 'userid':userid, - 'action':action + totp(action: string, userid: string) { + var data = { + 'userid': userid, + 'action': action } return this.MikroWizardRPC.sendJsonRequest("/api/user/totp", data); } - - get_user_restrictions(uid:string){ - var data={ - 'uid':uid + + get_user_restrictions(uid: string) { + var data = { + 'uid': uid } return this.MikroWizardRPC.sendJsonRequest("/api/user/restrictions", data); } - save_user_restrictions(uid:string,restrictions:any){ - var data={ - 'uid':uid, - 'restrictions':restrictions + save_user_restrictions(uid: string, restrictions: any) { + var data = { + 'uid': uid, + 'restrictions': restrictions } return this.MikroWizardRPC.sendJsonRequest("/api/user/save_restrictions", data); } - mytotp(action:string,otp:any=false){ - var data={ - 'action':action, - 'otp':otp + mytotp(action: string, otp: any = false) { + var data = { + 'action': action, + 'otp': otp } return this.MikroWizardRPC.sendJsonRequest("/api/user/mytotp", data); } - get_auth_logs(filters:any) { - var data=filters; + get_auth_logs(filters: any) { + var data = filters; return this.MikroWizardRPC.sendJsonRequest("/api/auth/list", data); } - get_account_logs(filters:any) { - var data=filters; + get_account_logs(filters: any) { + var data = filters; return this.MikroWizardRPC.sendJsonRequest("/api/account/list", data); } - get_dev_logs(filters:any) { - var data=filters; + get_dev_logs(filters: any) { + var data = filters; return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/list", data); } - get_syslog(filters:any) { - var data=filters; + get_syslog(filters: any) { + var data = filters; return this.MikroWizardRPC.sendJsonRequest("/api/syslog/list", data); } - get_details_grouped(devid:number=0){ - var data={ - 'devid':devid + get_details_grouped(devid: number = 0) { + var data = { + 'devid': devid } return this.MikroWizardRPC.sendJsonRequest("/api/devlogs/details/list", data); } - scan_devs(type:string,info:any){ - var data: any={ - 'type':type + scan_devs(type: string, info: any) { + var data: any = { + 'type': type } - if(type=="ip"){ + if (type == "ip") { data = Object.assign(data, info); } return this.MikroWizardRPC.sendJsonRequest("/api/scanner/scan", data); } - scan_results(){ + scan_results() { return this.MikroWizardRPC.sendJsonRequest("/api/scanner/results", {}); } - get_groups(searchstr:string=""){ - var data={ - 'searchstr':searchstr + get_groups(searchstr: string = "") { + var data = { + 'searchstr': searchstr } return this.MikroWizardRPC.sendJsonRequest("/api/search/groups", data); - } + } - get_devices(searchstr:string=""){ - var data={ - 'searchstr':searchstr + get_devices(searchstr: string = "") { + var data = { + 'searchstr': searchstr } return this.MikroWizardRPC.sendJsonRequest("/api/search/devices", data); - } + } - update_save_group(group:any){ - var data={ + update_save_group(group: any) { + var data = { ...group } return this.MikroWizardRPC.sendJsonRequest("/api/devgroup/update_save_group", data); } - get_snippets(name:string,desc:string,content:string,page:number=0,size:number=1000,limit:any=false){ - var data={ - 'name':name, - 'description':desc, - 'content':content, - 'page':page, - 'size':size, - 'limit':limit + get_snippets(name: string, desc: string, content: string, page: number = 0, size: number = 1000, limit: any = false) { + var data = { + 'name': name, + 'description': desc, + 'content': content, + 'page': page, + 'size': size, + 'limit': limit } return this.MikroWizardRPC.sendJsonRequest("/api/snippet/list", data); } - save_snippet(data:any){ - return this.MikroWizardRPC.sendJsonRequest("/api/snippet/save", {...data}); + save_snippet(data: any) { + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/save", { ...data }); } - Exec_snipet(data:any,members:any) { - data['members']=members; + Exec_snipet(data: any, members: any) { + data['members'] = members; return this.MikroWizardRPC.sendJsonRequest("/api/snippet/exec", data); } - delete_snippet(id:number){ - var data={ - 'id':id + delete_snippet(id: number) { + var data = { + 'id': id } return this.MikroWizardRPC.sendJsonRequest("/api/snippet/delete", data); } - get_executed_snipet(id:number){ - var data={ - 'id':id + get_executed_snipet(id: number) { + var data = { + 'id': id } return this.MikroWizardRPC.sendJsonRequest("/api/snippet/executed", data); } + get_sequences() { + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/list", {}); + } + + save_sequence(data: any) { + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/save", { ...data }); + } + + delete_sequence(id: number) { + var data = { + 'id': id + } + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/delete", data); + } + + get_sequence_history(id: number) { + var data = { + 'id': id + } + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/history", data); + } + + exec_sequence(data: any) { + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/sequence/exec", data); + } + + get_syslog_regexes() { + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/list", {}); + } + + save_syslog_regex(data: any) { + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/save", { ...data }); + } + + delete_syslog_regex(id: number) { + var data = { + 'id': id + } + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/delete", data); + } + + get_syslogregex_samples() { + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/syslogregex/samples", {}); + } + + get_alerts() { + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/alert/list", {}); + } + + save_alert(data: any) { + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/alert/save", { ...data }); + } + + delete_alert(id: number) { + var data = { + 'id': id + } + return this.MikroWizardRPC.sendJsonRequest("/api/snippet/alert/delete", data); + } + get_user_task_list() { return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/list", {}); } - Add_task(data:any,members:any) { - data['members']=members; + Add_task(data: any, members: any) { + data['members'] = members; return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/create", data); } - Delete_task(taskid:Number) { - var data={ - 'taskid':taskid, + Delete_task(taskid: Number) { + var data = { + 'taskid': taskid, } return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/delete", data); } - Edit_task(data:any,members:any) { - data['members']=members; + Edit_task(data: any, members: any) { + data['members'] = members; return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/edit", data); } - get_task_members(taskid:Number) { - var data={ - 'taskid':taskid, + get_task_members(taskid: Number) { + var data = { + 'taskid': taskid, } return this.MikroWizardRPC.sendJsonRequest("/api/taskmember/details", data); } - get_users(page:Number,size:Number,search:string) { - var data={ - 'page':page, - 'size':size, - 'search':search + get_users(page: Number, size: Number, search: string) { + var data = { + 'page': page, + 'size': size, + 'search': search } return this.MikroWizardRPC.sendJsonRequest("/api/users/list", data); } - get_perms(page:Number,size:Number,search:string) { - var data={ - 'page':page, - 'size':size, - 'search':search + get_perms(page: Number, size: Number, search: string) { + var data = { + 'page': page, + 'size': size, + 'search': search } return this.MikroWizardRPC.sendJsonRequest("/api/perms/list", data); } - - create_perm(name:string,perms:any) { - var data={ - 'name':name, - 'perms':perms + + create_perm(name: string, perms: any) { + var data = { + 'name': name, + 'perms': perms } return this.MikroWizardRPC.sendJsonRequest("/api/perms/create", data); } - edit_perm(id:Number,name:string,perms:any) { - + edit_perm(id: Number, name: string, perms: any) { + var data = { - 'id':id, - 'name':name, - 'perms':perms + 'id': id, + 'name': name, + 'perms': perms } return this.MikroWizardRPC.sendJsonRequest("/api/perms/edit", data); } - - delete_perm(id:number){ - var data={ - 'id':id + + delete_perm(id: number) { + var data = { + 'id': id } return this.MikroWizardRPC.sendJsonRequest("/api/perms/delete", data); } - get_vault_setting(){ + get_vault_setting() { return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get", {}); } - vault_task(data:any){ + vault_task(data: any) { return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/task", data); } - vault_history(){ + vault_history() { return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/history", {}); } - exec_vault(){ + exec_vault() { return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/execute", {}); } - reveal_password(devid:number,username:string){ - var data={ - 'devid':devid, - 'username':username + reveal_password(devid: number, username: string) { + var data = { + 'devid': devid, + 'username': username } return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/reveal", data); } - get_passwords(data:any){ + get_passwords(data: any) { return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_passwords", data); } - get_device_pass(devid:number){ - var data={ - 'devid':devid + get_device_pass(devid: number) { + var data = { + 'devid': devid } return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_device_pass", data); } - user_perms(uid:string) { - + user_perms(uid: string) { + var data = { - 'uid':uid, + 'uid': uid, } return this.MikroWizardRPC.sendJsonRequest("/api/userperms/list", data); } - Add_user_perm(uid:Number,permid:Number,devgroupid:Number){ + Add_user_perm(uid: Number, permid: Number, devgroupid: Number) { var data = { - 'uid':uid, - 'pid':permid, - 'gid':devgroupid + 'uid': uid, + 'pid': permid, + 'gid': devgroupid } return this.MikroWizardRPC.sendJsonRequest("/api/userperms/create", data); } - Delete_user_perm(id:number){ - var data={ - 'id':id + Delete_user_perm(id: number) { + var data = { + 'id': id } return this.MikroWizardRPC.sendJsonRequest("/api/userperms/delete", data); } - edit_user(data:any) { + edit_user(data: any) { - return this.MikroWizardRPC.sendJsonRequest("/api/user/edit", data); + return this.MikroWizardRPC.sendJsonRequest("/api/user/edit", data); } - create_user(data:any) { + create_user(data: any) { return this.MikroWizardRPC.sendJsonRequest("/api/user/create", data); } - delete_user(id:number){ - var data={ - 'uid':id + delete_user(id: number) { + var data = { + 'uid': id } return this.MikroWizardRPC.sendJsonRequest("/api/user/delete", data); } - check_firmware(devids:any) { + check_firmware(devids: any) { var data = { - 'devids':devids + 'devids': devids } return this.MikroWizardRPC.sendJsonRequest("/api/firmware/check_firmware_update", data); } - get_firms(page:Number,size:Number,search:any) { + get_firms(page: Number, size: Number, search: any) { var data = { - 'page':page, - 'size':size, - 'search':search + 'page': page, + 'size': size, + 'search': search } return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_firms", data); } - delete_firm(id:number){ - var data={ - 'id':id + delete_firm(id: number) { + var data = { + 'id': id } return this.MikroWizardRPC.sendJsonRequest("/api/firmware/delete_from_repository", data); } - - get_backups(data:any) { + + get_backups(data: any) { return this.MikroWizardRPC.sendJsonRequest("/api/backup/list", data); } - - get_backup(id:number){ + + get_backup(id: number) { var data = { - 'id':id + 'id': id } return this.MikroWizardRPC.sendJsonRequest("/api/backup/get", data); } - restore_backup(id:number){ + restore_backup(id: number) { var data = { - 'backupid':id + 'backupid': id } return this.MikroWizardRPC.sendJsonRequest("/api/backup/restore", data); } get_downloadable_firms() { - + return this.MikroWizardRPC.sendJsonRequest("/api/firmware/get_downloadable_firms", {}); } - download_firmware_to_repository(version:string){ + download_firmware_to_repository(version: string) { var data = { - 'version':version + 'version': version } return this.MikroWizardRPC.sendJsonRequest("/api/firmware/download_firmware_to_repository", data); } - save_firmware_setting(updatebehavior:string,firmwaretoinstall:string,firmwaretoinstallv6:string){ + save_firmware_setting(updatebehavior: string, firmwaretoinstall: string, firmwaretoinstallv6: string) { var data = { - 'updatebehavior':updatebehavior, - 'firmwaretoinstall':firmwaretoinstall, - 'firmwaretoinstallv6':firmwaretoinstallv6 + 'updatebehavior': updatebehavior, + 'firmwaretoinstall': firmwaretoinstall, + 'firmwaretoinstallv6': firmwaretoinstallv6 } return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware_settings", data); } - update_firmware(devids:string){ + update_firmware(devids: string) { var data = { - 'devids':devids + 'devids': devids } return this.MikroWizardRPC.sendJsonRequest("/api/firmware/update_firmware", data); } - upgrade_firmware(devids:string){ + upgrade_firmware(devids: string) { var data = { - 'devids':devids + 'devids': devids } - return this.MikroWizardRPC.sendJsonRequest("/api/firmware/upgrade_firmware", data); + return this.MikroWizardRPC.sendJsonRequest("/api/firmware/upgrade_firmware", data); } - reboot_devices(devids:string){ + reboot_devices(devids: string) { var data = { - 'devids':devids + 'devids': devids } - return this.MikroWizardRPC.sendJsonRequest("/api/firmware/reboot_devices", data); + return this.MikroWizardRPC.sendJsonRequest("/api/firmware/reboot_devices", data); } - get_settings(){ + get_settings() { return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/get_all", {}); } - save_sys_setting(data:any){ + save_sys_setting(data: any) { return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/save_all", data); } - - get_running_tasks(){ + + get_running_tasks() { return this.MikroWizardRPC.sendJsonRequest("/api/tasks/list", {}); } - stop_task(signal:number){ - var data={ - 'signal':signal + stop_task(signal: number) { + var data = { + 'signal': signal } return this.MikroWizardRPC.sendJsonRequest("/api/tasks/stop", data); } - apply_update(action:string){ - var data={ - 'action':action + apply_update(action: string) { + var data = { + 'action': action } return this.MikroWizardRPC.sendJsonRequest("/api/sysconfig/apply_update", data); } @@ -566,62 +626,62 @@ export class dataProvider { return this.MikroWizardRPC.sendJsonRequest("/api/cloner/list", {}); } - Add_cloner(data:any,members:any) { - data['members']=members; + Add_cloner(data: any, members: any) { + data['members'] = members; return this.MikroWizardRPC.sendJsonRequest("/api/cloner/create", data); } - Delete_cloner(clonerid:number) { - var data={ - 'clonerid':clonerid, + Delete_cloner(clonerid: number) { + var data = { + 'clonerid': clonerid, } return this.MikroWizardRPC.sendJsonRequest("/api/cloner/delete", data); } - Edit_cloner(data:any,members:any) { - data['members']=members; + Edit_cloner(data: any, members: any) { + data['members'] = members; return this.MikroWizardRPC.sendJsonRequest("/api/cloner/edit", data); } - get_cloner_members(clonerid:number) { - var data={ - 'clonerid':clonerid, + get_cloner_members(clonerid: number) { + var data = { + 'clonerid': clonerid, } return this.MikroWizardRPC.sendJsonRequest("/api/cloner/memberdetails", data); } - killSession(devid:number,item:any){ - var data={ - 'devid':devid, - 'item':item + killSession(devid: number, item: any) { + var data = { + 'devid': devid, + 'item': item } return this.MikroWizardRPC.sendJsonRequest("/api/dev/kill_session", data); } - getDhcpHistory(item:any){ - var data={ - 'item':item + getDhcpHistory(item: any) { + var data = { + 'item': item } return this.MikroWizardRPC.sendJsonRequest("/api/dhcp-history/get", data); } - getNetworkMap(){ + getNetworkMap() { return this.MikroWizardRPC.sendJsonRequest("/api/networkmap/get", {}); } - bulk_add_devices(devices: any[]){ + bulk_add_devices(devices: any[]) { var data = { 'devices': devices } return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add", data); } - bulk_add_status(taskId: string){ + bulk_add_status(taskId: string) { var data = { 'taskId': taskId } return this.MikroWizardRPC.sendJsonRequest("/api/dev/bulk_add_status", data); } - group_firmware_action(groupId: number, action: string){ + group_firmware_action(groupId: number, action: string) { var data = { 'groupId': groupId, 'action': action @@ -637,7 +697,7 @@ export class dataProvider { this.MikroWizardRPC.clearCookeis(); this.MikroWizardRPC.setNewSession(context, session); } - + checkSessionExpired(error: any) { console.log(error); if ('title' in error && error.title == "session_expired") diff --git a/src/app/providers/mikrowizard/vpn.service.ts b/src/app/providers/mikrowizard/vpn.service.ts new file mode 100644 index 0000000..8a2040b --- /dev/null +++ b/src/app/providers/mikrowizard/vpn.service.ts @@ -0,0 +1,166 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable, interval, Subject, catchError, throwError, map, switchMap } from 'rxjs'; + +export interface VpnStats { + last_handshake: number; + rx_bytes: number; + tx_bytes: number; + rx_speed?: number; + tx_speed?: number; + nat_mode: string; + enabled: boolean; + allowed_ips: string; +} + +export interface VpnPeer { + id: number; + public_key: string; + assigned_ip: string; + nat_mode: string; + split_targets: string[]; + dns_server: string | null; + persistent_keepalive: number; + custom_interface: string | null; + linked_device_id: number | null; + is_enabled: boolean; + created_at: string; + stats?: VpnStats; // Joined from the /status API + is_managed?: boolean; + status?: 'online' | 'offline' | 'unreachable'; + name?: string; + description?: string; + scan_status?: 'starting' | 'running' | 'completed' | 'failed' | null; + mt_user: string | null; + mt_pass: string | null; + mt_port: number | null; +} + +export interface VpnServerConfig { + id?: number; + api_endpoint: string; + api_token: string | null; + vpn_subnet: string; + public_server_ip: string | null; +} + +export interface VpnStatusResponse { + status: 'running' | 'setup_required' | 'error'; + peers?: VpnPeer[]; + server_config?: VpnServerConfig; + message?: string; +} + +export interface VpnLiveStatusResponse { + server: { + rx_bytes: number; + tx_bytes: number; + rx_speed: number; + tx_speed: number; + }; + peers: any[]; // The live endpoint returns an array of peer objects with full stats, same as /status +} + +@Injectable({ + providedIn: 'root' +}) +export class VpnService { + private apiUrl = '/api/vpn'; + + constructor(private http: HttpClient) { } + + // System Endpoints + getStatus(): Observable { + return this.http.get<{ result: VpnStatusResponse }>(`${this.apiUrl}/status`).pipe(map(r => r.result)); + } + + getLiveStatus(): Observable { + return this.http.get<{ result: VpnLiveStatusResponse }>(`${this.apiUrl}/status/live`).pipe( + map(res => res.result), + catchError(err => throwError(() => err)) + ); + } + + resetServerCounters(): Observable { + return this.http.post(`${this.apiUrl}/server/reset-counters`, {}).pipe( + catchError(err => throwError(() => err)) + ); + } + + getSystemConfig(): Observable<{ status: string, config: VpnServerConfig }> { + return this.http.get<{ result: { status: string, config: VpnServerConfig } }>(`${this.apiUrl}/system/config`).pipe(map(r => r.result)); + } + + updateSystemConfig(config: Partial): Observable { + return this.http.post<{ result: any }>(`${this.apiUrl}/system/config`, config).pipe(map(r => r.result)); + } + + flushSystem(wipe_database: boolean): Observable { + return this.http.post<{ result: any }>(`${this.apiUrl}/system/flush`, { wipe_database }).pipe(map(r => r.result)); + } + + // Peer Endpoints + addPeer(peerData: { + pubkey?: string, + custom_ip?: string, + nat_mode: 'full' | 'split' | 'off', + split_targets: string[], + persistent_keepalive: number, + custom_interface?: string, + name?: string, + description?: string, + mt_user?: string | null, + mt_pass?: string | null, + mt_port?: number | null + }): Observable<{ status: string, peer: VpnPeer }> { + return this.http.post<{ result: { status: string, peer: VpnPeer } }>(`${this.apiUrl}/peers/add`, peerData).pipe(map(r => r.result)); + } + + editPeer(peerData: { + pubkey: string, + custom_ip?: string, + nat_mode?: string, + split_targets?: string[], + persistent_keepalive?: number, + custom_interface?: string, + name?: string, + description?: string, + mt_user?: string | null, + mt_pass?: string | null, + mt_port?: number | null + }): Observable { + return this.http.post<{ result: any }>(`${this.apiUrl}/peers/edit`, peerData).pipe(map(r => r.result)); + } + + togglePeer(pubkey: string, enabled: boolean): Observable { + return this.http.post<{ result: any }>(`${this.apiUrl}/peers/toggle`, { pubkey, enabled }).pipe(map(r => r.result)); + } + + deletePeer(pubkey: string): Observable { + return this.http.post<{ result: any }>(`${this.apiUrl}/peers/delete`, { pubkey }).pipe(map(r => r.result)); + } + + getPeerConfig(pubkey: string): Observable<{ status: string, config: string }> { + return this.http.post<{ result: { status: string, config: string } }>(`${this.apiUrl}/peers/config`, { pubkey }).pipe(map(r => r.result)); + } + + getPeerMikrotikScript(pubkey: string): Observable<{ status: string, script: string }> { + return this.http.post<{ result: { status: string, script: string } }>(`${this.apiUrl}/peers/mikrotik-script`, { pubkey }).pipe(map(r => r.result)); + } + + scanLinkedDevice(pubkey: string): Observable { + return this.http.post<{ result: any }>(`${this.apiUrl}/peers/scan`, { pubkey }).pipe(map(r => r.result)); + } + + getPeerQrCode(pubkey: string): Observable { + return this.http.post(`${this.apiUrl}/peers/qrcode`, { pubkey }, { responseType: 'blob' }); + } + + resetPeerCounters(pubkey: string): Observable { + // pubkey must be url-encoded to safely pass base64 across URL path + return this.http.post(`${this.apiUrl}/peer/${encodeURIComponent(pubkey)}/reset-counters`, {}).pipe( + catchError(err => throwError(() => err)) + ); + } + +} diff --git a/src/app/views/sequences/sequences-routing.module.ts b/src/app/views/sequences/sequences-routing.module.ts new file mode 100644 index 0000000..aad506d --- /dev/null +++ b/src/app/views/sequences/sequences-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { SequencesComponent } from './sequences.component'; + +const routes: Routes = [ + { + path: '', + component: SequencesComponent, + data: { + title: 'Sequences' + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class SequencesRoutingModule { +} diff --git a/src/app/views/sequences/sequences.component.html b/src/app/views/sequences/sequences.component.html new file mode 100644 index 0000000..8b29a65 --- /dev/null +++ b/src/app/views/sequences/sequences.component.html @@ -0,0 +1,590 @@ + + + + + + + Alert Sequences (Pro) + + +
+ + +
+
+
+
+ + + + + {{value}} + + + + + Snippet #{{value}} + + + + + + + + + + +
{{value}}
+
+
+ + + + + + + + +
+
+
+
+
+ + + +
+ Edit Sequence: {{current_sequence['name']}} +
+
+ Create New Sequence +
+ +
+ + + + +
Sequence Details
+
+ + + + + + + + + + + +
+ + + + + + + + +
+
+
+ + + + +
Condition Flowchart
+ +
+ + +
+ +
No conditions defined
+

The source snippet will execute unconditionally.
Add a condition below to + build branching logic based on the snippet's output.

+
+ +
+ +
+
Source Snippet Output
+
+
+ + + +
+ +
+
+
+ + + + +
+ + + +
+
+ + +
+
+ Condition + +
+
+ + + + + +
+
+ + Regex +
+ +
+
+
+ +
+
+ + +
+ + +
+
+ +
+
+ Action + +
+
+ + + +
+ +
+ + +
+ + + +
+ +
+
+
+
+ + +
+
+ +
+ +
+
+ +
+
+
+ + + + +
Manage Alert Definitions
+ +
+ + + + + + +
{{editingAlert.id === 0 ? 'Create Alert' : 'Edit Alert'}}
+
+ +
+ + +
+
+ + +
+ + +
+
+
+ + + + + +
Existing Alerts
+
+ +
+ + + + + + + + + + + + + + + + + + +
NameLevelActions
{{alert.name}} + + {{alert.level | uppercase}} + + + + +
No alerts defined.
+
+
+
+
+
+
+
+ + + + +
Execution History: {{viewing_sequence_name}} +
+ +
+ + + +
+
+
+ + + + + + {{run.created}} + Exec ID: {{run.exec_id | slice:0:8}}... + +
+ + {{run.successCount}}/{{run.totalCount}} Success + +
+
+
+ + + + +
+ Device #{{item.device_id}} +
+
+
+ + + + {{item.status | uppercase}} + + + + + + + + +
+
+
+
+
+
+
+
+ +
No History Found
+

This sequence hasn't been executed yet or history tracking is disabled.

+
+
+
+
+
+ + + + +
Execution Trace: Device #{{viewing_device_id}}
+ +
+ + + + +
+
+
{{ + current_trace.parsedLog?.ssh_output || current_trace.task_log }}
+
+
+
+ +
+ +
+ +
+ + +
{{current_trace.parsedLog.evaluation}}
+
+
+
+ No evaluation details found for this run. +
+
+
+
+ + + +
+ + + +
+
+
+ + + + Rule [{{rule.type}}]: '{{rule.pattern}}' → + {{rule.matched ? 'MATCHED' : 'NO MATCH'}} + +
+
+
+ + ACTION: {{act.action_type}} + (Alert ID: {{act.alert_id}}) + (Snippet: {{act.snippet_id}}) + + +
+ +
+
+
+
+
+
+ + + + +
Exec Sequence
+ +
+ +
+ + +
+ +
+ + +
+ + + + + + +
Members :
+ + + +   {{value}} + + + + {{value}} + + + + + + + + +
+ + +
+ + + + +
+ + + + +
Editing Group
+ +
+ + +
Group Members :
+ + + +   {{value}} + + + + {{value}} + + + + + {{value}} + + + +
+
+
+
+ + + + +
\ No newline at end of file diff --git a/src/app/views/sequences/sequences.component.scss b/src/app/views/sequences/sequences.component.scss new file mode 100644 index 0000000..da9a69d --- /dev/null +++ b/src/app/views/sequences/sequences.component.scss @@ -0,0 +1,257 @@ +/* Flowchart Builder Styling */ +.bg-grid-pattern { + background-image: linear-gradient(to right, #e8ecef 1px, transparent 1px), linear-gradient(to bottom, #e8ecef 1px, transparent 1px); + background-size: 20px 20px; +} + +.flow-tree { + display: flex; + flex-direction: column; + align-items: center; + padding: 1rem 0; +} + +.flow-entry-node { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 0; +} + +.entry-bubble { + background: #321fdb; + color: white; + padding: 0.5rem 1.5rem; + border-radius: 50px; + font-weight: 600; + box-shadow: 0 4px 6px rgba(50, 31, 219, 0.2); + z-index: 2; +} + +/* Connectors */ +.flow-connector-down { + width: 2px; + height: 30px; + background-color: #9da5b1; + position: relative; + z-index: 1; +} + +/* Groups and Branches */ +/* Groups and Branches */ +.condition-group, +.action-group { + display: flex; + justify-content: center; + gap: 0; + position: relative; + /* No padding-top here, used on branches instead */ + flex-wrap: nowrap; + min-width: max-content; +} + +/* Horizontal connectors refined */ +.condition-branch::after, +.action-branch::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background-color: #9da5b1; + z-index: 1; +} + +.condition-branch:first-child::after, +.action-branch:first-child::after { + left: 50%; +} + +.condition-branch:last-child::after, +.action-branch:last-child::after { + right: 50%; +} + +.condition-branch:only-child::after, +.action-branch:only-child::after { + display: none; +} + +.condition-branch, +.action-branch { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + padding: 30px 1rem 0 1rem; /* 30px top padding for connectors */ + flex: 1 1 0px; /* Equal width branches */ + min-width: 240px; /* Minimum width to keep content readable */ + max-width: 400px; /* Reasonable expansion limit */ +} + +/* Horizontal connectors spanning between centers of nodes */ +.condition-branch::after, +.action-branch::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background-color: #9da5b1; + z-index: 1; +} + +.condition-branch:first-child::after, +.action-branch:first-child::after { + left: 50%; +} + +.condition-branch:last-child::after, +.action-branch:last-child::after { + right: 50%; +} + +.condition-branch:only-child::after, +.action-branch:only-child::after { + display: none; +} + +/* Vertical connector from horizontal bar down to card */ +.condition-branch::before, +.action-branch::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + width: 2px; + height: 30px; + background-color: #9da5b1; + z-index: 1; +} + +/* Connector from parent node down to the group's horizontal bar */ +.flow-connector-down { + width: 2px; + height: 30px; + background-color: #9da5b1; + margin: 0 auto; + position: relative; + z-index: 1; +} + +/* Cards with Dynamic Widths based on branch width */ +.node-card { + width: 100%; /* Fill the branch */ + background: #fff; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); + border: 1px solid #d8dbe0; + overflow: hidden; + position: relative; + z-index: 2; + transition: all 0.2s ease; +} + +.node-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); +} + +.node-header { + padding: 0.5rem 1rem; + font-weight: 600; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.node-body { + padding: 1rem; +} + +.condition-node { + border-top: 4px solid #f9b115; +} + +.action-node { + border-top: 4px solid #2eb85c; +} + +/* Modal Wide Override */ +::ng-deep .builder-modal .modal-xl { + max-width: 95vw !important; +} + +.flowchart-container { + min-height: 60vh; +} + +.bg-dark { + background-color: #1a1d21 !important; +} + +.terminal-container { + background-color: #0d1117; + border-radius: 6px; + padding: 1rem; + max-height: 60vh; + overflow-y: auto; + border: 1px solid #30363d; + + .terminal-content { + background-color: transparent; + + ::ng-deep pre { + display: block !important; + white-space: pre-wrap !important; + word-break: break-all !important; + min-height: 50vh !important; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; + font-size: 0.9rem !important; + color: #e6edf3 !important; + padding: 0 !important; + margin: 0 !important; + background: transparent !important; + } + + ::ng-deep .hljs { + background: transparent !important; + padding: 0 !important; + font-family: inherit !important; + font-size: inherit !important; + } + } +} + +.cursor-pointer { + cursor: pointer; +} + +.hover-shadow:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +::ng-deep .nav-tabs .nav-link { + cursor: pointer; +} + +.border-secondary { + border-color: #30363d !important; +} + +::ng-deep .nav-tabs .nav-link.active { + background-color: #0d1117 !important; + border-color: #30363d #30363d transparent !important; + color: #fff !important; +} + +::ng-deep .nav-tabs .nav-link { + border: 1px solid transparent; +} + +::ng-deep .nav-tabs { + border-bottom: 1px solid #30363d; +} \ No newline at end of file diff --git a/src/app/views/sequences/sequences.component.ts b/src/app/views/sequences/sequences.component.ts new file mode 100644 index 0000000..d681883 --- /dev/null +++ b/src/app/views/sequences/sequences.component.ts @@ -0,0 +1,429 @@ +import { Component, OnInit } from '@angular/core'; +import { dataProvider } from "../../providers/mikrowizard/data"; +import { ToastComponent } from '@coreui/angular'; +import { NgxSuperSelectOptions } from "ngx-super-select"; + +@Component({ + selector: 'app-sequences', + templateUrl: './sequences.component.html', + styleUrls: ['./sequences.component.scss'] +}) +export class SequencesComponent implements OnInit { + + public sequences: any[] = []; + + // Grid config + source: Array = []; + searching = { + enabled: true, + placeholder: 'Search sequences...' + }; + paging = { + enabled: true, + pageSize: 10, + pageSizes: [10, 25, 50] + }; + columnMenu = { enabled: false }; + sorting = { enabled: true }; + infoPanel = { enabled: true }; + rowSelection: any = { + enabled: true, + type: 'checkbox', + mode: 'multiple', + }; + + public EditSequenceModalVisible: boolean = false; + public current_sequence: any = { + id: 0, + name: '', + source_snippet_id: null, + store_all_history: false, + is_active: true, + conditions_json: [] + }; + + public ManageAlertsModalVisible: boolean = false; + public editingAlert: any = { id: 0, name: '', level: 'Info' }; + + public Snippets: any = []; + public Alerts: any = []; + public ModalAction: string = "add"; + public ExecSequenceModalVisible: boolean = false; + public SelectedSequence: any = {}; + public SelectedMembers: any[] = []; + public SelectedTaskItems: any[] = []; + public availbleMembers: any[] = []; + public NewMemberModalVisible: boolean = false; + public SelectedNewMemberRows: any[] = []; + public NewMemberRows: any[] = []; + + public searchingMembers = { + enabled: true, + placeholder: 'Search members...' + }; + + // Device History Grid Config + searchingHistory = { + enabled: true, + placeholder: 'Search devices in this run...' + }; + pagingHistory = { + enabled: true, + pageSize: 5, + pageSizes: [5, 10, 25] + }; + + // super select options + snippetOptions: Partial = { + selectionMode: "single", + actionsEnabled: false, + displayExpr: "name", + valueExpr: "id", + placeholder: "Select Snippet", + searchEnabled: true, + }; + + alertOptions: Partial = { + selectionMode: "single", + actionsEnabled: false, + displayExpr: "name", + valueExpr: "id", + placeholder: "Select Alert", + searchEnabled: true, + }; + + constructor(public MikroWizardRPC: dataProvider) { } + + ngOnInit(): void { + this.loadSequences(); + this.loadSnippets(); + this.loadAlerts(); + } + + loadSequences() { + this.MikroWizardRPC.get_sequences().then( + (res: any) => { + if (res) { + this.source = res.map((seq: any) => { + if (typeof seq.conditions_json === 'string') { + try { + seq.conditions_json = JSON.parse(seq.conditions_json); + } catch (e) { + seq.conditions_json = []; + } + } + if (!seq.conditions_json) seq.conditions_json = []; + return seq; + }); + } + }, + (error: any) => { + // Handle error visually if needed + } + ); + } + + loadSnippets(): void { + this.MikroWizardRPC.get_snippets("", "", "", 0, 1000, false).then((res: any) => { + this.Snippets = res.map((x: any) => ({ id: x.id, name: x.name })); + }); + } + + loadAlerts(): void { + this.MikroWizardRPC.get_alerts().then((res: any) => { + this.Alerts = res.map((x: any) => ({ id: x.id, name: x.name, level: x.level })); + }); + } + + addCondition(targetArray: any[]) { + targetArray.push({ + type: 'contains', + is_regex: false, + pattern: '', + actions: [] + }); + } + + addAction(condition: any) { + if (!condition.actions) condition.actions = []; + condition.actions.push({ + action_type: 'alert_set', + alert_id: null, + snippet_id: null, + conditions: [] + }); + } + + removeNode(array: any[], index: number) { + if (array && array.length > index) { + array.splice(index, 1); + } + } + + Edit_Sequence(item: any, mode: string) { + if (mode === 'add') { + this.current_sequence = { + id: 0, + name: '', + source_snippet_id: null, + store_all_history: false, + is_active: true, + conditions_json: [] + }; + this.ModalAction = 'add'; + } else { + // deep copy + this.current_sequence = JSON.parse(JSON.stringify(item)); + if (!this.current_sequence.conditions_json) this.current_sequence.conditions_json = []; + this.ModalAction = 'edit'; + } + this.EditSequenceModalVisible = true; + } + + save_sequence() { + let payload = { ...this.current_sequence }; + if (payload.conditions_json && typeof payload.conditions_json !== 'string') { + payload.conditions_json = JSON.stringify(payload.conditions_json); + } + this.MikroWizardRPC.save_sequence(payload).then(() => { + this.EditSequenceModalVisible = false; + this.loadSequences(); + }); + } + + onSelectSourceSnippet($event: any) { + this.current_sequence.source_snippet_id = $event; + } + + // --- Alert Management --- + openManageAlerts() { + this.ManageAlertsModalVisible = true; + this.editingAlert = { id: 0, name: '', level: 'Info', description: '' }; + } + + editAlert(item: any) { + this.editingAlert = { ...item }; + } + + saveAlert() { + this.MikroWizardRPC.save_alert(this.editingAlert).then(() => { + this.loadAlerts(); + this.editingAlert = { id: 0, name: '', level: 'Info', description: '' }; + }); + } + + deleteAlert(id: number) { + if (confirm("Are you sure you want to delete this alert?")) { + this.MikroWizardRPC.delete_alert(id).then(() => { + this.loadAlerts(); + }); + } + } + + confirm_delete(item: any, confirm: boolean) { + if (!confirm) { + if (window.confirm("Are you sure you want to delete sequence: " + item.name + "?")) { + this.MikroWizardRPC.delete_sequence(item.id).then(() => { + this.loadSequences(); + }); + } + } + } + public HistoryModalVisible: boolean = false; + public sequence_history: any[] = []; + public viewing_sequence_name: string = ''; + + public TraceModalVisible: boolean = false; + public current_trace: any = {}; + public viewing_device_id: number = 0; + + showTrace(dev: any) { + this.current_trace = dev; + this.viewing_device_id = dev.device_id; + this.TraceModalVisible = true; + } + + show_history(item: any) { + this.viewing_sequence_name = item.name; + this.sequence_history = []; + this.HistoryModalVisible = true; + this.MikroWizardRPC.get_sequence_history(item.id).then((res: any) => { + // Process the nested history response + this.sequence_history = res.map((run: any) => { + const total = run.devices.length; + const success = run.devices.filter((d: any) => d.status === 'success').length; + return { + ...run, + successCount: success, + totalCount: total, + visible: false, + devices: run.devices.map((dev: any) => ({ + ...dev, + parsedLog: this.parseLog(dev.task_log) + })) + }; + }); + }); + } + + parseLog(logJson: any) { + if (!logJson) return { ssh_output: '', evaluation: '' }; + + let processed = logJson; + // Normalize any escaped newlines if it's a string + if (typeof logJson === 'string') { + processed = logJson.split('\\n').join('\n').replace(/\r\n/g, '\n'); + } + + // If it's already an object, use it directly (but normalize ssh_output) + if (typeof processed === 'object' && processed !== null) { + if (processed.ssh_output) { + processed.ssh_output = processed.ssh_output.split('\\n').join('\n').replace(/\r\n/g, '\n'); + } + return processed; + } + + // Try to parse as JSON first (new format) + try { + if (typeof processed === 'string' && processed.trim().startsWith('{')) { + const parsed = JSON.parse(processed); + if (parsed.ssh_output) { + parsed.ssh_output = parsed.ssh_output.split('\\n').join('\n').replace(/\r\n/g, '\n'); + } + return parsed; + } + } catch (e) { + // Not JSON, continue to legacy parsing + } + + // Legacy Interleaved Log parsing + if (typeof processed !== 'string') return { ssh_output: processed, evaluation: '' }; + + const lines = processed.split('\n'); + let outputSections: string[] = []; + let evalSections: string[] = []; + + let currentSection: string[] = []; + let isCurrentOutput = true; + + for (let line of lines) { + const lowerLine = line.toLowerCase(); + const startsNewOutput = lowerLine.includes('output (') || (lowerLine.includes('ssh output') && !lowerLine.includes('failed')); + const startsNewEval = lowerLine.includes('evaluation'); + + if (startsNewOutput || startsNewEval) { + // Save previous section + if (currentSection.length > 0) { + const block = currentSection.join('\n').trim(); + if (isCurrentOutput) outputSections.push(block); + else evalSections.push(block); + } + // Start new section + currentSection = [line]; + isCurrentOutput = startsNewOutput; + } else { + currentSection.push(line); + } + } + + // Final section + if (currentSection.length > 0) { + const block = currentSection.join('\n').trim(); + if (isCurrentOutput) outputSections.push(block); + else evalSections.push(block); + } + + return { + ssh_output: outputSections.join('\n\n').trim(), + evaluation: evalSections.join('\n\n').trim() + }; + } + + // --- Manual Execution --- + Run_Sequence(item: any) { + this.SelectedSequence = item; + this.current_sequence = { ...item }; + this.current_sequence["selection_type"] = "devices"; + this.SelectedMembers = []; + this.SelectedTaskItems = []; + this.ExecSequenceModalVisible = true; + } + + form_changed() { + this.SelectedMembers = []; + this.SelectedTaskItems = []; + } + + remove_member(item: any) { + this.SelectedMembers = this.SelectedMembers.filter( + (x: any) => x.id != item.id + ); + this.SelectedTaskItems = this.SelectedMembers.map((x: any) => { + return x.id; + }); + } + + show_new_member_form() { + this.NewMemberModalVisible = true; + this.availbleMembers = []; + this.SelectedNewMemberRows = []; + this.NewMemberRows = []; + + var data = { + group_id: false, + search: false, + page: false, + size: 10000, + }; + + if (this.current_sequence["selection_type"] == "devices") + this.MikroWizardRPC.get_dev_list(data).then((res: any) => { + this.availbleMembers = res.filter( + (x: any) => !this.SelectedTaskItems.includes(x.id) + ); + }); + else + this.MikroWizardRPC.get_devgroup_list().then((res: any) => { + this.availbleMembers = res.filter( + (x: any) => !this.SelectedTaskItems.includes(x.id) + ); + }); + } + + onSelectedRowsNewMembers(rows: any): void { + this.NewMemberRows = rows; + this.SelectedNewMemberRows = rows.map((m: any) => m.source); + } + + isObject(val: any): boolean { + return typeof val === 'object' && val !== null && !Array.isArray(val); + } + + isArray(val: any): boolean { + return Array.isArray(val); + } + + add_new_members() { + this.SelectedMembers = [ + ...new Set(this.SelectedMembers.concat(this.SelectedNewMemberRows)), + ]; + + this.SelectedTaskItems = this.SelectedMembers.map((x: any) => { + return x.id; + }); + + this.NewMemberModalVisible = false; + } + + submit_exec() { + const payload = { + sequence_id: this.SelectedSequence.id, + selection_type: this.current_sequence.selection_type, + members: this.SelectedTaskItems + }; + this.MikroWizardRPC.exec_sequence(payload).then((res: any) => { + this.ExecSequenceModalVisible = false; + // Optionally show success toast or refresh history + }); + } +} diff --git a/src/app/views/sequences/sequences.module.ts b/src/app/views/sequences/sequences.module.ts new file mode 100644 index 0000000..83f7ad8 --- /dev/null +++ b/src/app/views/sequences/sequences.module.ts @@ -0,0 +1,50 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule } from "@angular/forms"; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ToastModule, + ModalModule, + BadgeModule, + AccordionModule, + NavModule, + TabsModule, + AlertModule, + CollapseModule +} from "@coreui/angular"; +import { HighlightJsModule } from 'ngx-highlight-js'; +import { SequencesRoutingModule } from "./sequences-routing.module"; +import { SequencesComponent } from "./sequences.component"; +import { GuiGridModule } from "@generic-ui/ngx-grid"; +import { NgxSuperSelectModule } from "ngx-super-select"; + +@NgModule({ + imports: [ + SequencesRoutingModule, + CardModule, + CommonModule, + GridModule, + FormModule, + ButtonModule, + ButtonGroupModule, + GuiGridModule, + ModalModule, + ToastModule, + FormsModule, + BadgeModule, + NgxSuperSelectModule, + AccordionModule, + NavModule, + TabsModule, + AlertModule, + HighlightJsModule, + CollapseModule + ], + declarations: [SequencesComponent], +}) +export class SequencesModule { } diff --git a/src/app/views/snippets/snippets.component.ts b/src/app/views/snippets/snippets.component.ts index 174a3e9..58d0674 100644 --- a/src/app/views/snippets/snippets.component.ts +++ b/src/app/views/snippets/snippets.component.ts @@ -38,6 +38,7 @@ export class SnippetsComponent implements OnInit, OnDestroy { public uid: number; public uname: string; public tz: string; + public ispro: boolean = false; constructor( private data_provider: dataProvider, @@ -56,7 +57,8 @@ export class SnippetsComponent implements OnInit, OnDestroy { // console.dir("res",res) _self.uid = res.uid; _self.uname = res.name; - _self.tz = res.tz; + _self.tz = res.tz; + _self.ispro = res.ispro; // console.dir("role",res.role); const userId = _self.uid; @@ -182,31 +184,31 @@ export class SnippetsComponent implements OnInit, OnDestroy { this.ModalAction = "edit"; } } - show_exec(item:any){ - var _self=this; - this.SelectedSnippet = item; - this.ExecutedDataModalVisible = true; - this.data_provider - .get_executed_snipet(_self.SelectedSnippet["id"]) - .then((res) => { - let index = 1; - _self.ExecutedData= res.map((d: any) => { - d.index = index; - d.ended = formatInTimeZone( - d.created.split(".")[0] + ".000Z", - _self.tz, - "yyyy-MM-dd HH:mm:ss XXX" - ); - d.started = formatInTimeZone( - d.info.created.split(".")[0] + ".000Z", - _self.tz, - "yyyy-MM-dd HH:mm:ss XXX" - ); - index += 1; - return d; - }); - _self.DeleteConfirmModalVisible = false; - }); + show_exec(item: any) { + var _self = this; + this.SelectedSnippet = item; + this.ExecutedDataModalVisible = true; + this.data_provider + .get_executed_snipet(_self.SelectedSnippet["id"]) + .then((res) => { + let index = 1; + _self.ExecutedData = res.map((d: any) => { + d.index = index; + d.ended = formatInTimeZone( + d.created.split(".")[0] + ".000Z", + _self.tz, + "yyyy-MM-dd HH:mm:ss XXX" + ); + d.started = formatInTimeZone( + d.info.created.split(".")[0] + ".000Z", + _self.tz, + "yyyy-MM-dd HH:mm:ss XXX" + ); + index += 1; + return d; + }); + _self.DeleteConfirmModalVisible = false; + }); } form_changed() { @@ -258,7 +260,7 @@ export class SnippetsComponent implements OnInit, OnDestroy { this.NewMemberRows = rows; this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => m.source); } - + add_new_members() { var _self = this; _self.SelectedMembers = [ @@ -274,12 +276,12 @@ export class SnippetsComponent implements OnInit, OnDestroy { submit(action: string) { var _self = this; - this.data_provider - .Exec_snipet(_self.current_snippet, _self.SelectedTaskItems) - .then((res) => { - _self.initGridTable(); - }); - + this.data_provider + .Exec_snipet(_self.current_snippet, _self.SelectedTaskItems) + .then((res) => { + _self.initGridTable(); + }); + this.ExecSnipetModalVisible = false; } @@ -287,7 +289,7 @@ export class SnippetsComponent implements OnInit, OnDestroy { this.current_snippet = item; this.current_snippet["task_type"] = "snipet_exec"; this.current_snippet["selection_type"] = "devices"; - this.form_changed(); + this.form_changed(); this.ExecSnipetModalVisible = true; this.ModalAction = "exec"; } @@ -318,7 +320,7 @@ export class SnippetsComponent implements OnInit, OnDestroy { initGridTable(): void { var _self = this; - _self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => { + _self.data_provider.get_snippets("", "", "", 0, 1000, false).then((res) => { _self.source = res.map((x: any) => { x.created = [ x.created.split("T")[0], @@ -330,18 +332,18 @@ export class SnippetsComponent implements OnInit, OnDestroy { }); } - sanitizeString(desc:string) { - var itemDesc:string=''; + sanitizeString(desc: string) { + var itemDesc: string = ''; if (desc) { - itemDesc = desc.toString().replace(/"/g, '\"'); - itemDesc = itemDesc.replace(/'/g, '\''); + itemDesc = desc.toString().replace(/"/g, '\"'); + itemDesc = itemDesc.replace(/'/g, '\''); } else { - itemDesc = ''; + itemDesc = ''; } return itemDesc; } - exportToCsv(jsonResponse:any) { + exportToCsv(jsonResponse: any) { const data = jsonResponse; const columns = this.getColumns(data); const csvData = this.convertToCsv(data, columns); @@ -349,7 +351,7 @@ export class SnippetsComponent implements OnInit, OnDestroy { } getColumns(data: any[]): string[] { - const columns : any = []; + const columns: any = []; data.forEach(row => { Object.keys(row).forEach((col) => { if (!columns.includes(col)) { @@ -361,13 +363,13 @@ export class SnippetsComponent implements OnInit, OnDestroy { } convertToCsv(data: any[], columns: string[]): string { - var _self=this; + var _self = this; let csv = ''; csv += columns.join(',') + '\n'; data.forEach(row => { - const values : any = []; - columns.forEach((col:any) => { - values.push('"'+_self.sanitizeString(row[col])+'"'); + const values: any = []; + columns.forEach((col: any) => { + values.push('"' + _self.sanitizeString(row[col]) + '"'); }); csv += values.join(',') + '\n'; }); @@ -376,10 +378,10 @@ export class SnippetsComponent implements OnInit, OnDestroy { downloadFile(data: string, filename: string, type: string) { const blob = new Blob([data], { type: type }); - const nav = (window.navigator as any); + const nav = (window.navigator as any); if (nav.msSaveOrOpenBlob) { - nav.msSaveBlob(blob, filename); + nav.msSaveBlob(blob, filename); } else { const link = document.createElement('a'); link.setAttribute('href', URL.createObjectURL(blob)); @@ -405,5 +407,5 @@ export class SnippetsComponent implements OnInit, OnDestroy { - ngOnDestroy(): void {} + ngOnDestroy(): void { } } diff --git a/src/app/views/syslog-regex/syslog-regex-routing.module.ts b/src/app/views/syslog-regex/syslog-regex-routing.module.ts new file mode 100644 index 0000000..7d4a256 --- /dev/null +++ b/src/app/views/syslog-regex/syslog-regex-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { SyslogRegexComponent } from './syslog-regex.component'; + +const routes: Routes = [ + { + path: '', + component: SyslogRegexComponent, + data: { + title: 'Syslog Custom Regex' + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class SyslogRegexRoutingModule { +} diff --git a/src/app/views/syslog-regex/syslog-regex.component.html b/src/app/views/syslog-regex/syslog-regex.component.html new file mode 100644 index 0000000..1a1edfd --- /dev/null +++ b/src/app/views/syslog-regex/syslog-regex.component.html @@ -0,0 +1,537 @@ + + + + + + + Syslog Custom Regex + + +
+ + +
+
+
+
+ + + + + {{value}} + + + + + {{value}} + + + + +
+ {{ getAlertName(value) }} + {{ + getAlertLevel(value)}} +
+
+
+ + + + + Global + String + Match + + + + + + {{value}} + + + + +
{{value}}
+
+
+ + + + + + +
+
+
+
+
+ + + +
+ Edit Regex: {{current_regex['name']}} +
+
+ Add New Regex +
+ +
+ + + +
Configuration Details
+
+ +
+ + +
+ +
+
1. General Information
+ +
+ + +
+ + +

Provide a real log message to build and test your regex + against.

+
+
+ +
+
+ +
+
+
+
+ + +
+
2. + Event Handling & Alerts
+

What happens when this regex matches a log? The event will + inherit the title and severity from the chosen Alert.

+ +
+
+ + + + + +
+
+
+ +
+ + +
+
+ +
+ + +
+ +
+
+
+
+ +
+ Regex matching will + occur, but no alert,soring in db or action will be + applied. +
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+ + +
+
3. + Pattern Extraction
+ + + + +
+
+ + +
+ {{rawRegexError}} + Only the + (?P<comment>...) group will modify the stored + event + message. +
+ + +
+ +
+ Add segments to match the log + structurally: +
+ + +
+
+ +
+
+ + +
+ + {{i+1}} + +
+ +
+ +
+
+ Static Match +
+
+ +
+
+ + +
+
+ Extract + Data + +
+
+ Map to + Field + + +
+
+
+ + +
+
No + extraction blocks yet.
+
+
+
+ +
+
4. + Live Validation Output
+ + +
+ Engine Pattern + {{generatedRegex || '(empty)'}} +
+ +
+
+
+ {{testFeedback || 'Awaiting valid configuration...'}} +
+ +
+
+ + + + + + + + + + + + + + + + + + + +
CategoryEventLevelDetailStatus
{{simulatedDbRow.eventtype + || 'null'}}{{simulatedDbRow.detail || 'null'}} + + {{simulatedDbRow.level || 'null'}} + + {{simulatedDbRow.comment}} + {{simulatedDbRow.status == '1' ? 'Fixed' : 'Not + Fixed'}} +
+
+
+
+ +
+
+ +
+
+
+ + + + +
+ + + + +
Manage Alert Definitions
+ +
+ + + + + + +
{{editingAlert.id === 0 ? 'Create Alert' : 'Edit Alert'}}
+
+ +
+ + +
+
+ + +
+ + +
+
+
+ + + + + +
Existing Alerts
+
+ +
+ + + + + + + + + + + + + + + + + + +
NameLevelActions
{{alert.name}} + + {{alert.level | uppercase}} + + + + +
No alerts defined.
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/views/syslog-regex/syslog-regex.component.scss b/src/app/views/syslog-regex/syslog-regex.component.scss new file mode 100644 index 0000000..e8c18dd --- /dev/null +++ b/src/app/views/syslog-regex/syslog-regex.component.scss @@ -0,0 +1,114 @@ +.alert-config-box { + transition: all 0.3s ease; + border-color: #e2e8f0 !important; +} + +code { + background-color: #f8f9fa; + padding: 0.2rem 0.4rem; + border-radius: 4px; + color: #d63384; + font-size: 0.875em; +} + +/* Regex Builder Styles */ +.cursor-pointer { + cursor: pointer; +} + +.test-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; +} + +.step-title { + font-size: 0.95rem; + font-weight: 600; +} + +/* Highlighter Textarea Overlay Trick */ +.position-relative { + position: relative; +} + +textarea.form-control { + position: relative; + z-index: 2; + background: transparent !important; + color: transparent !important; /* Hide real text to only show highlights behind it, but keep caret visible */ + caret-color: #000; +} + +.backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + padding: 0.375rem 0.75rem; /* Match textarea padding */ + font-size: 0.8rem; /* Match small text if needed */ + line-height: 1.5; + color: #495057; + background-color: #fff; + white-space: pre-wrap; + word-wrap: break-word; + border: 1px solid transparent; + pointer-events: none; + border-radius: 0.375rem; + overflow-y: auto; +} + +.segment-row { + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid #edf2f7 !important; + + &:hover { + background-color: #ffffff !important; + border-color: #90cdf4 !important; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + transform: translateY(-2px); + } + + .btn-link { + opacity: 0.3; + transition: opacity 0.2s; + } + + &:hover .btn-link { + opacity: 1; + } +} + +.test-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + box-shadow: 0 0 5px rgba(0,0,0,0.1); +} + +.step-card { + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1); + &:hover { + transform: translateY(-2px); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important; + } +} + +.table-dark { + --cui-table-bg: transparent; + --cui-table-color: #fff; +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-5px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-fade-in { + animation: fadeIn 0.4s ease-out forwards; +} diff --git a/src/app/views/syslog-regex/syslog-regex.component.ts b/src/app/views/syslog-regex/syslog-regex.component.ts new file mode 100644 index 0000000..93accfd --- /dev/null +++ b/src/app/views/syslog-regex/syslog-regex.component.ts @@ -0,0 +1,584 @@ +import { Component, OnInit } from '@angular/core'; +import { dataProvider } from "../../providers/mikrowizard/data"; +import { NgxSuperSelectOptions } from "ngx-super-select"; +import { ToastComponent } from '@coreui/angular'; + +interface RegexSegment { + type: 'static' | 'dynamic'; + value: string; // Used for static text + captureType?: string; // Used for dynamic: everything, word, ip, mac + targetField?: string; // Mapping: src, detail, level, status, comment +} + +@Component({ + selector: 'app-syslog-regex', + templateUrl: './syslog-regex.component.html', + styleUrls: ['./syslog-regex.component.scss'] +}) +export class SyslogRegexComponent implements OnInit { + + public syslogRegexes: any[] = []; + + // Grid config + source: Array = []; + searching = { + enabled: true, + placeholder: 'Search regexes...' + }; + paging = { + enabled: true, + pageSize: 10, + pageSizes: [10, 25, 50] + }; + columnMenu = { enabled: false }; + sorting = { enabled: true }; + infoPanel = { enabled: true }; + rowSelection: any = { + enabled: true, + type: 'checkbox', + mode: 'multiple', + }; + + public EditRegexModalVisible: boolean = false; + public current_regex: any = { + id: 0, + name: '', + regex_pattern: '', + alert_enabled: false, + alert_id: null, + global_alert: true, + match_string: '', + alert_mode: 'global', + default_eventtype: '', + default_status: 0 + }; + + // Builder vs Raw Mode + public inputMode: 'raw' | 'builder' = 'raw'; + public rawRegexError: string = ''; + public sampleLog: string = ''; + + // Segment-based Builder State + public segments: RegexSegment[] = []; + + public generatedRegex: string = ''; + public testFeedback: string = ''; + public testResults: { field: string, value: string }[] = []; + public simulatedDbRow: { eventtype: string, src: string, detail: string, level: string, status: string, comment: string } | null = null; + public isTestValid: boolean = false; + public extractedValue: string = ''; + public syslogSamples: string[] = []; + public selectedSampleIndex: number | null = null; + + public ManageAlertsModalVisible: boolean = false; + public editingAlert: any = { id: 0, name: '', level: 'Info' }; + + public Alerts: any = []; + public ModalAction: string = "add"; + + // super select options + alertOptions: Partial = { + selectionMode: "single", + actionsEnabled: false, + displayExpr: "name", + valueExpr: "id", + placeholder: "Select Alert", + searchEnabled: true, + }; + + constructor(public MikroWizardRPC: dataProvider) { } + + ngOnInit(): void { + this.loadRegexes(); + this.loadAlerts(); + } + + loadRegexes() { + this.MikroWizardRPC.get_syslog_regexes().then( + (res: any) => { + if (res) { + this.source = res; + } + }, + (error: any) => { + // Handle error visually if needed + } + ); + } + + loadAlerts(): void { + this.MikroWizardRPC.get_alerts().then((res: any) => { + this.Alerts = res.map((x: any) => ({ id: x.id, name: x.name, level: x.level })); + }); + } + + resetBuilder() { + this.segments = []; + this.generatedRegex = ''; + this.testFeedback = ''; + this.testResults = []; + this.simulatedDbRow = null; + this.extractedValue = ''; + this.selectedSampleIndex = null; + this.loadSyslogSamples(); + } + + Edit_Regex(item: any, mode: string) { + // Reset builder state + this.inputMode = 'raw'; + this.rawRegexError = ''; + this.resetBuilder(); + + if (mode === 'add') { + this.current_regex = { + id: 0, + name: '', + regex_pattern: '', + alert_enabled: false, + alert_id: null, + global_alert: true, + match_string: '', + alert_mode: 'global', + eventtype: '', + status: 0 + }; + this.ModalAction = 'add'; + } else { + // deep copy + this.current_regex = JSON.parse(JSON.stringify(item)); + this.current_regex.alert_mode = this.current_regex.global_alert ? 'global' : 'conditional'; + + // Try to deconstruct the regex into builder segments + if (this.current_regex.regex_pattern) { + const wasDeconstructed = this.deconstructRegex(this.current_regex.regex_pattern); + if (wasDeconstructed) { + this.inputMode = 'builder'; + } + } + + this.ModalAction = 'edit'; + } + + // Initial validation + this.validateRawRegex(); + + this.EditRegexModalVisible = true; + } + + save_regex() { + if (this.inputMode === 'builder') { + this.current_regex.regex_pattern = this.generatedRegex; + } + + // Final validation check + this.validateRawRegex(); + if (this.rawRegexError && this.inputMode === 'raw') { + return; // Prevent save if invalid + } + if (!this.isTestValid && this.inputMode === 'builder') { + if (!this.generatedRegex) return; + } + + let payload = { ...this.current_regex }; + // Ensure alert_id is null if not alert_enabled + if (!payload.alert_enabled) { + payload.alert_id = null; + payload.global_alert = false; + payload.match_string = ''; + } else { + if (payload.alert_mode === 'global') { + payload.global_alert = true; + payload.match_string = ''; + } else { + payload.global_alert = false; + if (!payload.match_string) { + alert("Please provide a Match String for conditional alert storage."); + return; + } + } + } + + // Remove internal properties and level (now inherited from alert) + delete payload.alert_mode; + delete payload.level; + + // Ensure alert_id is mandatory for save (as per v3.2) + if (payload.alert_enabled && !payload.alert_id) { + alert("Please select an Alert Definition."); + return; + } + + this.MikroWizardRPC.save_syslog_regex(payload).then(() => { + this.EditRegexModalVisible = false; + this.loadRegexes(); + }); + } + + confirm_delete(item: any, confirm: boolean) { + if (!confirm) { + if (window.confirm("Are you sure you want to delete custom regex: " + item.name + "?")) { + this.MikroWizardRPC.delete_syslog_regex(item.id).then(() => { + this.loadRegexes(); + }); + } + } + } + + // --- Alert Management --- + openManageAlerts() { + this.ManageAlertsModalVisible = true; + this.editingAlert = { id: 0, name: '', level: 'Info', description: '' }; + } + + editAlert(item: any) { + this.editingAlert = { ...item }; + } + + saveAlert() { + this.MikroWizardRPC.save_alert(this.editingAlert).then(() => { + this.loadAlerts(); + this.editingAlert = { id: 0, name: '', level: 'Info', description: '' }; + this.loadRegexes(); + }); + } + + deleteAlert(id: number) { + if (confirm("Are you sure you want to delete this alert?")) { + this.MikroWizardRPC.delete_alert(id).then(() => { + this.loadAlerts(); + }); + } + } + + // --- Syslog Samples Management --- + loadSyslogSamples() { + this.MikroWizardRPC.get_syslogregex_samples().then( + (res: any) => { + if (res && Array.isArray(res)) { + this.syslogSamples = res; + } + }, + (error: any) => { + console.error("Failed to load syslog samples", error); + } + ); + } + + onSampleSelected() { + if (this.selectedSampleIndex !== null && this.syslogSamples[this.selectedSampleIndex]) { + this.sampleLog = this.syslogSamples[this.selectedSampleIndex]; + this.updateBuilder(); + } + } + + getAlertName(id: any): string { + const alert = this.Alerts.find((a: any) => Number(a.id) === Number(id)); + return alert ? alert.name : 'No Alert'; + } + + getAlertLevel(id: any): string { + const alert = this.Alerts.find((a: any) => Number(a.id) === Number(id)); + return alert ? alert.level : 'N/A'; + } + + openAIHelp() { + if (!this.sampleLog) return; + + const currentRegexText = this.current_regex.regex_pattern ? `\nOptional context - my current attempt is:\n"${this.current_regex.regex_pattern}"` : ""; + + const prompt = `I need help creating a Python regex pattern (re module syntax) for a specific syslog message. + +### My Syslog Sample: +"${this.sampleLog}" + +### Requirement: +Create a regex that extracts the most important information from this log into a named capture group called "comment" using the syntax: (?P...) +${currentRegexText} + +### Example Format: +If the log was "System error: Disk full", the regex should be "System error: (?P.*)". + +Please provide the completed Python regex for my sample log.`; + + const encodedPrompt = encodeURIComponent(prompt); + window.open(`https://chatgpt.com/?q=${encodedPrompt}`, '_blank'); + } + + // --- Segment Management --- + addSegment(type: 'static' | 'dynamic') { + if (type === 'static') { + this.segments.push({ type: 'static', value: '' }); + } else { + this.segments.push({ type: 'dynamic', value: '', captureType: 'everything' }); + } + this.updateBuilder(); + } + + removeSegment(index: number) { + this.segments.splice(index, 1); + this.updateBuilder(); + } + + moveSegmentUp(index: number) { + if (index > 0) { + const temp = this.segments[index - 1]; + this.segments[index - 1] = this.segments[index]; + this.segments[index] = temp; + this.updateBuilder(); + } + } + + moveSegmentDown(index: number) { + if (index < this.segments.length - 1) { + const temp = this.segments[index + 1]; + this.segments[index + 1] = this.segments[index]; + this.segments[index] = temp; + this.updateBuilder(); + } + } + + // --- Regex Builder Deconstruction --- + deconstructRegex(pattern: string): boolean { + // This is a specialized parser to turn regex strings back into segments + if (!pattern) return false; + + // Common patterns we use + const capturePatterns: { [key: string]: string } = { + 'word': '\\S+', + 'ip': '\\d{1,3}(?:\\.\\d{1,3}){3}', + 'mac': '[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5}', + 'number': '\\d+', + 'everything': '.*' + }; + + try { + // Split by the delimiter we use in updateBuilder: "\\s*" + const parts = pattern.split('\\s*'); + const newSegments: any[] = []; + + for (const part of parts) { + if (!part) continue; + + // Match a named capture group: (?Ppattern) + const namedMatch = part.match(/^\(\?P<([^>]+)>(.+)\)$/); + if (namedMatch) { + const field = namedMatch[1]; + const innerPattern = namedMatch[2]; + + // Try to identify the capture type + let cType = 'custom'; + let cValue = innerPattern; + + for (const [type, p] of Object.entries(capturePatterns)) { + if (innerPattern === p) { + cType = type; + cValue = ''; + break; + } + } + + newSegments.push({ + type: 'dynamic', + captureType: cType, + targetField: field, + value: cType === 'custom' ? cValue : '' + }); + continue; + } + + // Match a positional capture group: (pattern) + const groupMatch = part.match(/^\((.+)\)$/); + if (groupMatch) { + const innerPattern = groupMatch[1]; + + let cType = 'custom'; + let cValue = innerPattern; + + for (const [type, p] of Object.entries(capturePatterns)) { + if (innerPattern === p) { + cType = type; + cValue = ''; + break; + } + } + + newSegments.push({ + type: 'dynamic', + captureType: cType, + targetField: undefined, + value: cType === 'custom' ? cValue : '' + }); + continue; + } + + // Otherwise, it's static (or a complex regex part we treat as static) + // We unescape things that we know we escape + let val = part.replace(/\\([.*+?^${}()|[\]\\])/g, '$1'); + newSegments.push({ type: 'static', value: val }); + } + + if (newSegments.length > 0) { + this.segments = newSegments; + this.updateBuilder(); // Refresh generatedRegex and simulations + return true; + } + } catch (e) { + console.error("Failed to deconstruct regex:", e); + } + return false; + } + + // --- Regex Builder & Validation Methods --- + + validateRawRegex() { + this.rawRegexError = ''; + if (!this.current_regex.regex_pattern) { + this.rawRegexError = 'Regex pattern is required.'; + return; + } + + try { + new RegExp(this.current_regex.regex_pattern); + if (!/\(.*\)/.test(this.current_regex.regex_pattern)) { + this.rawRegexError = 'Must contain at least one capturing group ().'; + } + } catch (e) { + this.rawRegexError = 'Invalid regular expression syntax.'; + } + } + + onRawRegexChange() { + this.validateRawRegex(); + if (this.inputMode === 'raw') { + this.evaluateRegexAgainstSample(); + } + } + + setMode(mode: 'raw' | 'builder') { + this.inputMode = mode; + if (mode === 'builder') { + this.updateBuilder(); + } else { + if (this.generatedRegex && this.isTestValid) { + this.current_regex.regex_pattern = this.generatedRegex; + } + this.validateRawRegex(); + this.evaluateRegexAgainstSample(); + } + } + + escapeRegex(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + updateBuilder() { + let pattern = ''; + this.segments.forEach((seg, index) => { + if (seg.type === 'static') { + if (seg.value) { + let escaped = this.escapeRegex(seg.value); + pattern += escaped.trim(); + } + } else { + let capturePattern = '(.*)'; + if (seg.captureType === 'word') capturePattern = '(\\S+)'; + if (seg.captureType === 'ip') capturePattern = '(\\d{1,3}(?:\\.\\d{1,3}){3})'; + if (seg.captureType === 'mac') capturePattern = '([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})'; + if (seg.captureType === 'number') capturePattern = '(\\d+)'; + if (seg.captureType === 'custom' && seg.value) capturePattern = `(${seg.value})`; + + if (seg.targetField) { + pattern += `(?P<${seg.targetField}>${capturePattern.slice(1, -1)})`; + } else { + pattern += capturePattern; + } + } + + if (index < this.segments.length - 1) { + pattern += '\\s*'; + } + }); + + this.generatedRegex = pattern; + this.evaluateRegexAgainstSample(); + } + + evaluateRegexAgainstSample() { + this.testFeedback = ''; + this.isTestValid = false; + this.testResults = []; + this.simulatedDbRow = null; + + let regexToTest = this.inputMode === 'raw' ? this.current_regex.regex_pattern : this.generatedRegex; + + if (!this.sampleLog || !regexToTest) { + return; + } + + try { + let jsPattern = regexToTest.replace(/\(\?P Number(a.id) === alertIdToMatch); + + let detailValue = '(Inherited from Alert)'; + let levelValue = '(Inherited from Alert)'; + + if (this.current_regex.alert_enabled && selectedAlert) { + detailValue = selectedAlert.name; + levelValue = selectedAlert.level; + } else if (!this.current_regex.alert_enabled) { + detailValue = '(No Alert Linked)'; + levelValue = '(No Alert Linked)'; + } + + let dbRow = { + eventtype: this.current_regex.eventtype || '', + src: 'custom regex', + detail: detailValue, + level: levelValue, + status: this.current_regex.status !== undefined ? this.current_regex.status.toString() : '0', + comment: this.sampleLog + }; + + if (match.groups) { + Object.keys(match.groups).forEach(key => { + this.testResults.push({ field: `[Mapped: ${key}]`, value: match.groups![key] }); + if (key === 'comment') { + (dbRow as any)[key] = match.groups![key]; + } + }); + } else if (match.length > 1) { + for (let i = 1; i < match.length; i++) { + this.testResults.push({ field: `Capture ${i}`, value: match[i] }); + } + } + + this.simulatedDbRow = dbRow; + } else { + this.testFeedback = "Pattern doesn't match the sample log."; + } + } catch (e: any) { + this.testFeedback = 'Regex Error: ' + e.message; + } + } + + getHighlightedSampleBoxHtml(): string { + if (!this.sampleLog || !this.generatedRegex || !this.isTestValid) return this.sampleLog || ''; + try { + let jsPattern = this.generatedRegex.replace(/\(\?P$1'); + } catch (e) { + return this.sampleLog; + } + } +} diff --git a/src/app/views/syslog-regex/syslog-regex.module.ts b/src/app/views/syslog-regex/syslog-regex.module.ts new file mode 100644 index 0000000..41dbf14 --- /dev/null +++ b/src/app/views/syslog-regex/syslog-regex.module.ts @@ -0,0 +1,50 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule } from "@angular/forms"; + +import { + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule, + ToastModule, + ModalModule, + BadgeModule, + AccordionModule, + NavModule, + TabsModule, + AlertModule, + CollapseModule +} from "@coreui/angular"; +import { HighlightJsModule } from 'ngx-highlight-js'; +import { SyslogRegexRoutingModule } from "./syslog-regex-routing.module"; +import { SyslogRegexComponent } from "./syslog-regex.component"; +import { GuiGridModule } from "@generic-ui/ngx-grid"; +import { NgxSuperSelectModule } from "ngx-super-select"; + +@NgModule({ + imports: [ + SyslogRegexRoutingModule, + CardModule, + CommonModule, + GridModule, + FormModule, + ButtonModule, + ButtonGroupModule, + GuiGridModule, + ModalModule, + ToastModule, + FormsModule, + BadgeModule, + NgxSuperSelectModule, + AccordionModule, + NavModule, + TabsModule, + AlertModule, + HighlightJsModule, + CollapseModule + ], + declarations: [SyslogRegexComponent], +}) +export class SyslogRegexModule { } diff --git a/src/app/views/user_tasks/user_tasks.component.html b/src/app/views/user_tasks/user_tasks.component.html index f37fd7c..e0d5b59 100644 --- a/src/app/views/user_tasks/user_tasks.component.html +++ b/src/app/views/user_tasks/user_tasks.component.html @@ -19,8 +19,9 @@ - - + + +   {{value}} @@ -56,257 +57,278 @@ - - -
- Edit Task: {{SelectedTask['name']}} -
-
- Create New Task -
- -
- - -
-
-
Basic Information
- Define the task name, description and type -
- - - - - - - - - - - + + +
+ Edit Task: {{SelectedTask['name']}} +
+
+ Create New Task +
+ +
+ + +
+
+
Basic Information
+ Define the task name, description and type
- -
-
-
Task Configuration
- Configure task-specific settings and parameters -
- - -
- - -
Backup Configuration
-
- -
- - This task will create configuration backups of selected devices. Backups are stored securely and can be restored later. -
-
-
-
- - - - -
Firmware Update Strategy
+ + + + + + + + + + + +
+ +
+
+
Task Configuration
+ Configure task-specific settings and parameters +
+ + +
+ + +
Backup Configuration
-
- - - - - -
- -
+
- Uses global MikroWizard update strategy settings. Check Settings page for configuration. -
- -
- - Downloads latest firmware from mikrotik.com. Server needs internet access. -
- -
- - - - - - - - - - + This task will create configuration backups of selected devices. Backups are stored securely and can be + restored later.
- - -
- - -
Script/Snippet Configuration
-
- - - - - - The selected script will be executed on all target devices when this task runs. - - -
-
- - -
-
-
Schedule Configuration
- Set when this task should run automatically -
-
- -
-
- - -
-
-
-
{{cron.label}}
-
{{cron.value}}
-
{{cron.description}}
-
-
-
- No matching cron presets found -
-
- - {{getCronDescription()}} - -
- - Quick Examples: - * * * * * = every minute | - 0 2 * * * = daily at 2 AM | - 0 */6 * * * = every 6 hours - -
-
-
- - -
-
-
Target Selection
- Choose which devices or groups this task will affect -
-
- - - - -
- - - -
- Selected {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} -
-
- {{SelectedMembers.length}} selected - -
+ + + +
+ +
+ + Uses global MikroWizard update strategy settings. Check Settings page for configuration. +
+ +
+ + Downloads latest firmware from mikrotik.com. Server needs internet access. +
+ +
+ + + + + + + + + + +
+
+
+ + +
+ + +
Script/Snippet Configuration
- -
- -
No {{SelectedTask['selection_type']}} selected
-

Click "Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}" to select targets for this task

-
-
- - - -
- - {{value}} -
-
-
- - - {{value}} - - - - - - - -
-
+ + + + + + The selected script will be executed on all target devices when this task runs. +
- +
+ + +
Sequence Configuration
+
+ + + + + + The selected alert sequence will be executed on all target devices when this task runs. + + +
+
+
+ + +
+
+
Schedule Configuration
+ Set when this task should run automatically +
+
+ +
+
+ + +
+
+
+
{{cron.label}}
+
{{cron.value}}
+
{{cron.description}}
+
+
+
+ No matching cron presets found +
+
+ + {{getCronDescription()}} + +
+ + Quick Examples: + * * * * * = every minute | + 0 2 * * * = daily at 2 AM | + 0 */6 * * * = every 6 hours + +
+
+
+ + +
+
+
Target Selection
+ Choose which devices or groups this task will affect +
+
+ + + + +
+ + + + +
+ Selected {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : + 'Groups'}} +
+
+ {{SelectedMembers.length}} selected + +
+
+ +
+ +
No {{SelectedTask['selection_type']}} selected
+

Click "Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}" to + select targets for this task

+
+
+ + + +
+ + {{value}} +
+
+
+ + + {{value}} + + + + + + + +
+
+
+
+
+ - - - - -
- All fields marked with * are required -
-
- - - -
-
- - - - - -
- Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} to Task -
- -
- - -
- - - {{NewMemberRows.length}} {{SelectedTask['selection_type']}}(s) selected for addition to this task - -
- - - - -
- - Available {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} ({{availbleMembers.length}} total) -
-
- -
- -
All {{SelectedTask['selection_type']}} are already assigned
-

No available {{SelectedTask['selection_type']}} to add to this task

-
-
- - - -
- - {{value}} -
-
-
- - - {{value}} - - - - - {{value}} - - -
-
-
-
-
- - -
- Select {{SelectedTask['selection_type']}} from the list above to add them to this task -
-
- - -
-
-
- - - - -
Confirm delete {{ SelectedTask['name'] }}
- -
- - Are you sure that You want to delete following task ? -
-
- - - - - - - - - - - - - -
Taks name : {{ SelectedTask['name'] }}
Description : {{ SelectedTask['description'] }}
Cron exec : {{ SelectedTask['desc_cron'] }}
-
- - - - -
+ +
+ + + + + + +
+ Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : + 'Groups'}} to Task +
+ +
+ + +
+ + + {{NewMemberRows.length}} {{SelectedTask['selection_type']}}(s) selected for addition to + this task + +
+ + + + +
+ + Available {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} ({{availbleMembers.length}} + total) +
+
+ +
+ +
All {{SelectedTask['selection_type']}} are already assigned
+

No available {{SelectedTask['selection_type']}} to add to this task

+
+
+ + + +
+ + {{value}} +
+
+
+ + + {{value}} + + + + + {{value}} + + +
+
+
+
+
+ + +
+ Select {{SelectedTask['selection_type']}} from the list above to add them to this + task +
+
+ + +
+
+
+ + + + +
Confirm delete {{ SelectedTask['name'] }}
+ +
+ + Are you sure that You want to delete following task ? +
+
+ + + + + + + + + + + + + +
Taks name : {{ SelectedTask['name'] }}
Description : {{ SelectedTask['description'] }}
Cron exec : {{ SelectedTask['desc_cron'] }}
+
+ + + + +
- - -
Confirm RUN {{ SelectedTask['name'] }}
- -
- - Are you sure that You want to run following task ? -
-
- - - - - - - - - - - - - -
Taks name : {{ SelectedTask['name'] }}
Description : {{ SelectedTask['description'] }}
Cron exec : {{ SelectedTask['desc_cron'] }}
-
- - - - -
- + + +
Confirm RUN {{ SelectedTask['name'] }}
+ +
+ + Are you sure that You want to run following task ? +
+
+ + + + + + + + + + + + + +
Taks name : {{ SelectedTask['name'] }}
Description : {{ SelectedTask['description'] }}
Cron exec : {{ SelectedTask['desc_cron'] }}
+
+ + + + +
+ \ No newline at end of file diff --git a/src/app/views/user_tasks/user_tasks.component.ts b/src/app/views/user_tasks/user_tasks.component.ts index b19e2e6..f9ef4c4 100644 --- a/src/app/views/user_tasks/user_tasks.component.ts +++ b/src/app/views/user_tasks/user_tasks.component.ts @@ -77,6 +77,7 @@ export class UserTasksComponent implements OnInit { public DeleteConfirmModalVisible: boolean = false; public Members: any = ""; public Snippets: any; + public Sequences: any = []; public SelectedMembers: any = []; public NewMemberModalVisible: boolean = false; public availbleMembers: any = []; @@ -96,7 +97,7 @@ export class UserTasksComponent implements OnInit { { label: 'Every 10 minutes', value: '*/10 * * * *', description: 'Regular monitoring checks' }, { label: 'Every 15 minutes', value: '*/15 * * * *', description: 'Moderate monitoring frequency' }, { label: 'Every 30 minutes', value: '*/30 * * * *', description: 'Low frequency monitoring' }, - + // Hourly Operations { label: 'Every hour', value: '0 * * * *', description: 'Hourly network checks' }, { label: 'Every 2 hours', value: '0 */2 * * *', description: 'Bi-hourly operations' }, @@ -104,7 +105,7 @@ export class UserTasksComponent implements OnInit { { label: 'Every 6 hours', value: '0 */6 * * *', description: 'Four times daily' }, { label: 'Every 8 hours', value: '0 */8 * * *', description: 'Three times daily' }, { label: 'Every 12 hours', value: '0 */12 * * *', description: 'Twice daily operations' }, - + // Daily Maintenance { label: 'Daily at midnight', value: '0 0 * * *', description: 'Daily maintenance at 00:00' }, { label: 'Daily at 1 AM', value: '0 1 * * *', description: 'Daily backup at 01:00' }, @@ -113,20 +114,20 @@ export class UserTasksComponent implements OnInit { { label: 'Daily at 6 AM', value: '0 6 * * *', description: 'Pre-business hours check' }, { label: 'Daily at 6 PM', value: '0 18 * * *', description: 'End of business day backup' }, { label: 'Daily at 10 PM', value: '0 22 * * *', description: 'Evening maintenance at 22:00' }, - + // Business Hours { label: 'Workdays at 8 AM', value: '0 8 * * 1-5', description: 'Start of business day - Mon to Fri' }, { label: 'Workdays at 9 AM', value: '0 9 * * 1-5', description: 'Business hours start check' }, { label: 'Workdays at 12 PM', value: '0 12 * * 1-5', description: 'Midday check - Mon to Fri' }, { label: 'Workdays at 5 PM', value: '0 17 * * 1-5', description: 'End of business day - Mon to Fri' }, { label: 'Workdays at 6 PM', value: '0 18 * * 1-5', description: 'After hours backup - Mon to Fri' }, - + // Weekly Operations { label: 'Weekly (Sunday midnight)', value: '0 0 * * 0', description: 'Weekly maintenance - Sunday 00:00' }, { label: 'Weekly (Monday midnight)', value: '0 0 * * 1', description: 'Weekly start - Monday 00:00' }, { label: 'Weekly (Friday 6 PM)', value: '0 18 * * 5', description: 'End of week backup - Friday 18:00' }, { label: 'Weekly (Saturday 2 AM)', value: '0 2 * * 6', description: 'Weekend maintenance - Saturday 02:00' }, - + // Monthly Operations { label: 'Monthly (1st at midnight)', value: '0 0 1 * *', description: 'Monthly maintenance - 1st of month' }, { label: 'Monthly (1st at 2 AM)', value: '0 2 1 * *', description: 'Monthly backup - 1st at 02:00' }, @@ -156,6 +157,16 @@ export class UserTasksComponent implements OnInit { enableDarkMode: false, }; + seqOptions: Partial = { + selectionMode: "single", + actionsEnabled: false, + displayExpr: "name", + valueExpr: "id", + placeholder: "Sequence", + searchEnabled: true, + enableDarkMode: false, + }; + public paging: GuiPaging = { enabled: true, page: 1, @@ -299,15 +310,17 @@ export class UserTasksComponent implements OnInit { var _self = this; this.SelectedTask = { ...item }; - + // Initialize cron search and preset tracking this.cronSearch = ''; const currentCron = this.SelectedTask['cron']; this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null; - - if (this.SelectedTask['task_type'] == 'firmware' && 'data' in this.SelectedTask && this.SelectedTask['data']) { - this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']); - if (this.SelectedTask['data']['strategy'] == 'defined') { + + if ((this.SelectedTask['task_type'] == 'firmware' || this.SelectedTask['task_type'] == 'sequence') && 'data' in this.SelectedTask && this.SelectedTask['data']) { + if (typeof this.SelectedTask['data'] === 'string') { + this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']); + } + if (this.SelectedTask['task_type'] == 'firmware' && this.SelectedTask['data']['strategy'] == 'defined') { this.data_provider.get_firms(0, 10000, false).then((res) => { let index = 1; _self.available_firmwares = [ @@ -328,16 +341,23 @@ export class UserTasksComponent implements OnInit { _self.firms_loaded = true; }); } - else{ + else { _self.firms_loaded = true; } } - _self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => { + _self.data_provider.get_snippets("", "", "", 0, 1000, false).then((res) => { _self.Snippets = res.map((x: any) => { return { id: x.id, name: x.name }; }); }); + if (_self.ispro) { + _self.data_provider.get_sequences().then((res: any) => { + _self.Sequences = res.map((x: any) => { + return { id: x.id, name: x.name }; + }); + }); + } if (action != "select_change") { this.SelectedTask["action"] = "edit"; this.data_provider.get_task_members(_self.SelectedTask.id).then((res) => { @@ -354,7 +374,7 @@ export class UserTasksComponent implements OnInit { } - + firmware_type_changed(type: any) { this.SelectedTask['data']['strategy'] = type; if (type == 'system') { @@ -405,7 +425,7 @@ export class UserTasksComponent implements OnInit { onSnippetsValueChanged(v: any) { var _self = this; if (v == "" || v.length < 3) return; - _self.data_provider.get_snippets(v, "", "", 0, 1000,false).then((res) => { + _self.data_provider.get_snippets(v, "", "", 0, 1000, false).then((res) => { _self.Snippets = res.map((x: any) => { return { id: String(x.id), name: x.name }; }); @@ -470,10 +490,10 @@ export class UserTasksComponent implements OnInit { const searchTerm = event.target.value.toLowerCase(); this.cronSearch = searchTerm; this.selectedCronPreset = null; - + if (searchTerm.length > 0) { - this.filteredCrons = this.predefinedCrons.filter(cron => - cron.label.toLowerCase().includes(searchTerm) || + this.filteredCrons = this.predefinedCrons.filter(cron => + cron.label.toLowerCase().includes(searchTerm) || cron.description.toLowerCase().includes(searchTerm) || cron.value.includes(searchTerm) ); @@ -491,7 +511,7 @@ export class UserTasksComponent implements OnInit { onCronInputFocus(): void { this.filteredCrons = this.predefinedCrons; this.showCronDropdown = true; - + // If current cron matches a preset, highlight it const currentCron = this.SelectedTask['cron']; this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null; @@ -500,7 +520,7 @@ export class UserTasksComponent implements OnInit { onCronInputChange(event: any): void { this.SelectedTask['cron'] = event.target.value; this.selectedCronPreset = null; - + // Check if the entered value matches any preset const enteredValue = event.target.value; const matchingPreset = this.predefinedCrons.find(cron => cron.value === enteredValue); @@ -513,7 +533,7 @@ export class UserTasksComponent implements OnInit { if (this.selectedCronPreset) { return this.selectedCronPreset.description; } - + const currentCron = this.SelectedTask['cron']; const matchingPreset = this.predefinedCrons.find(cron => cron.value === currentCron); return matchingPreset ? matchingPreset.description : 'Custom cron expression'; @@ -522,6 +542,8 @@ export class UserTasksComponent implements OnInit { onTaskTypeChange(): void { if (this.SelectedTask['task_type'] === 'snippet') { this.loadSnippets(); + } else if (this.SelectedTask['task_type'] === 'sequence') { + this.loadSequences(); } } @@ -533,4 +555,22 @@ export class UserTasksComponent implements OnInit { }); }); } + + loadSequences(): void { + var _self = this; + _self.data_provider.get_sequences().then((res: any) => { + _self.Sequences = res.map((x: any) => { + return { id: x.id, name: x.name }; + }); + }); + } + + onSequenceSelected($event: any) { + if (!this.SelectedTask['data']) this.SelectedTask['data'] = {}; + this.SelectedTask['data']['sequence_id'] = $event; + } + + onSequencesSearchChanged(v: any) { + // Sequences are fully loaded on open, client side filtering can be used or ignored for now + } } diff --git a/src/app/views/vpn/vpn-routing.module.ts b/src/app/views/vpn/vpn-routing.module.ts new file mode 100644 index 0000000..bc161c3 --- /dev/null +++ b/src/app/views/vpn/vpn-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { VpnComponent } from './vpn.component'; + +const routes: Routes = [ + { + path: '', + component: VpnComponent, + data: { + title: 'VPN Server' + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class VpnRoutingModule { +} diff --git a/src/app/views/vpn/vpn.component.html b/src/app/views/vpn/vpn.component.html new file mode 100644 index 0000000..390aa81 --- /dev/null +++ b/src/app/views/vpn/vpn.component.html @@ -0,0 +1,621 @@ +
+ + + + + + +
+ +
+
+
+ Server Status
+
+ {{status?.status === 'running' ? 'Running' : (status?.status === 'setup_required' ? 'Setup Required' : + 'Error/Offline')}} +
+
+
+
+
+ + + + + +
+ +
+
+
+ Connected Peers
+
+ {{ source.length }} Active +
+
+
+
+
+ + + + + + +
+ +
+
+
+ Live Speed + Total Vol +
+
+ +
+ ⬇ {{ formatBytes(liveSpeedRx) + }}/s + ⬆ {{ formatBytes(liveSpeedTx) + }}/s +
+ + +
+ ⬇ {{ (totalRx / 1048576) | number:'1.1-2' }} MB + ⬆ {{ (totalTx / 1048576) | number:'1.1-2' }} MB +
+
+
+
+
+
+
+ + + + + + + Live Server Traffic (Total Cumulative MB) + + +
+ +
+
+
+
+
+ + + + + + + + + VPN Peers + + +
+ + +
+
+
+
+ + + + + + + + + + + + + + + +
+ + + {{ item.name || item.assigned_ip || (item.public_key | slice:0:8) + '...' }} + + Unmanaged +
+
+ {{ item.assigned_ip || 'No IP' }} +
+
+
+ + + +
+ +
+ {{ item.description }} +
+ + +
+
+ {{ item.public_key | slice:0:16 }}... +
+
+ {{ item.created_at | date:'mediumDate' }} +
+
+
+
+
+ + + +
+ + {{ value | uppercase }} + +
+
+ Iface: {{ item.custom_interface || 'Auto' }} +
+
+
+ + + +
+ + View Device + +
+ Device + Not Linked + + +
+ Scanning... +
+
+ Scan Completed +
+ +
+ Scan Failed +
+ +
+ Keepalive: {{ item.persistent_keepalive }}s +
+ +
+ +
+ +
+
+ + + +
+ + {{ item.stats.last_handshake * 1000 | date:'shortTime' }}
+ {{ item.stats.last_handshake * 1000 | date:'shortDate' }} +
+ + None + +
+ - +
+
+ + + +
+ + {{ formatBytes(item.stats.rx_speed || 0) }}/s + + + {{ formatBytes(item.stats.tx_speed || 0) }}/s + +
+ - +
+
+ + + +
+ {{(item.stats.rx_bytes / + 1048576) | number:'1.2-2'}} MB + {{(item.stats.tx_bytes / + 1048576) | number:'1.2-2'}} MB +
+ - +
+
+ + + +
+ + + + +
+
+
+ + + + + + + + + +
+ + + + + + + +
+
+
+
+ +
+
+
+
+
+ + + + + +
VPN Server Configuration
+ +
+ + + API Endpoint + + + + API Token + + + + VPN Subnet + + + + Public Server IP/Host + + + + + + + +
+ + + + +
Warning: Delete Peer
+ +
+ +

+ Warning: This will permanently remove the VPN tunnel and NAT rules for this peer. Connected devices will + lose connection immediately. +

+

Are you sure you want to delete peer {{ peerToDelete?.assigned_ip }}?

+
+ + + + +
+ + + + +
Confirm Status Change
+ +
+ +

Are you sure you want to {{ + peerToToggle?.is_enabled ? 'Disable' : 'Enable' }} peer {{ + peerToToggle?.assigned_ip }}?

+
+ + + + +
+ + + + +
MikroTik Provisioning Script
+ +
+ +
+
+
{{ + configResult.script }}
+
+ +
+ +
+
+
+ + + +
+ + + + +
Mobile Quick Setup (QR Code)
+ +
+ +
+ WireGuard QR Code +
+
+ How to connect: Open the official WireGuard app on your + mobile device, tap the button, and select "Create from QR code". +
+
+ +
+ + +
+ +
+
+ + + + +
{{ editingPeer ? 'Edit VPN Peer' : 'Create New VPN Peer' }}
+ +
+ + +
+
Step 1: General Info
+ + + + + + + + +
+ + +
+
Step 2: Addressing
+ + Assigned IP + + + + Public Key + + + + Keepalive + + seconds + +
+ + +
+
Step 3: Routing
+ + NAT Mode + + + +
+
Split Tunnel Targets
+
+ + +
+ +
+
+ + +
+
Step 4: Identity & Integrations
+
+ + +
+ +
+ + + + + + + + + + + + + +
+ Once the peer connects and routes traffic (Online), the MikroWizard + native scanner will automatically use these credentials in the background to discover and safely add the + router into your Devices panel! +
+
+
+ + +
+
Step 5: Summary
+
    +
  • IP Assignment Preview: {{ peerForm.custom_ip || 'Auto-assigned' + }}
  • +
  • Routing NAT Mode: {{ peerForm.nat_mode | + uppercase }}
  • +
  • Split Targets: {{ + peerForm.split_targets.join(', ') || 'None' }}
  • +
  • MikroTik Integration: {{ peerForm.link_device ? 'Enabled' : + 'Disabled' }}
  • +
+
+
+ + + + + +
+ + + + + + +
Reset Server Counters
+ +
+ + Are you sure you want to reset all data transfer counters for the VPN Server? +

+ This will immediately set all Total Rx/Tx metrics to + zero. This does not disconnect any peers. +
+ + + + +
+ + + + +
Reset Peer Counters
+ +
+ + Are you sure you want to reset the traffic counters for peer: {{ peerToReset.name || + peerToReset.assigned_ip }}? +

+ This clears their individual Rx/Tx metrics to zero + and does not disconnect them. +
+ + + + +
+ + +
+ \ No newline at end of file diff --git a/src/app/views/vpn/vpn.component.scss b/src/app/views/vpn/vpn.component.scss new file mode 100644 index 0000000..df97831 --- /dev/null +++ b/src/app/views/vpn/vpn.component.scss @@ -0,0 +1,53 @@ +/* Scss styling for vpn component */ +::ng-deep pre { + display: block !important; + min-height: 60vh; +} + +.shadow-sm { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.border-0 { + border: 0 !important; +} + +.bg-success { + background: linear-gradient(135deg, #2eb85c 0%, #1d823e 100%) !important; +} + +.bg-danger { + background: linear-gradient(135deg, #e55353 0%, #ba2828 100%) !important; +} + +.bg-primary { + background: linear-gradient(135deg, #321fdb 0%, #1f1498 100%) !important; +} + +.bg-info { + background: linear-gradient(135deg, #39f 0%, #0076e6 100%) !important; +} + +.opacity-75 { + opacity: 0.75; +} + +.text-white { + color: #fff !important; +} + +.h-100 { + height: 100% !important; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-5px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/src/app/views/vpn/vpn.component.ts b/src/app/views/vpn/vpn.component.ts new file mode 100644 index 0000000..c5d7f22 --- /dev/null +++ b/src/app/views/vpn/vpn.component.ts @@ -0,0 +1,593 @@ +import { Component, OnInit, OnDestroy, ViewChild, ViewChildren, QueryList } from '@angular/core'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { loginChecker } from '../../providers/login_checker'; +import { VpnService, VpnStatusResponse, VpnPeer, VpnServerConfig } from '../../providers/mikrowizard/vpn.service'; +import { Subscription, delay, of, repeat, switchMap, timer, catchError } from 'rxjs'; +import { + GuiGridComponent, + GuiRowClass, + GuiSearching, + GuiColumn, + GuiColumnMenu, + GuiPaging, + GuiPagingDisplay, + GuiInfoPanel +} from "@generic-ui/ngx-grid"; +import { ToasterComponent, ToasterPlacement } from "@coreui/angular"; +import { AppToastComponent } from "../toast-simple/toast.component"; + +@Component({ + templateUrl: 'vpn.component.html', + styleUrls: ['vpn.component.scss'] +}) +export class VpnComponent implements OnInit, OnDestroy { + @ViewChild("grid", { static: true }) gridComponent!: GuiGridComponent; + @ViewChildren(ToasterComponent) viewChildren!: QueryList; + + toasterForm = { + autohide: true, + delay: 3000, + position: 'fixed' as ToasterPlacement, + fade: true, + closeButton: true, + }; + + public status: VpnStatusResponse | null = null; + private pollingSubscription?: Subscription; + private livePollingSubscription?: Subscription; + + // Aggregate stats + public totalRx: number = 0; + public totalTx: number = 0; + public liveSpeedRx: number = 0; + public liveSpeedTx: number = 0; + + formatBytes(bytes: number, decimals: number = 2): string { + if (!+bytes) return '0 B'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; + } + + public chartData: any = { + labels: [], + datasets: [ + { + label: 'Rx Speed', + backgroundColor: 'rgba(46, 184, 92, 0.1)', + borderColor: '#2eb85c', + pointBackgroundColor: '#2eb85c', + pointHoverBackgroundColor: '#fff', + borderWidth: 2, + fill: true, + data: [] + }, + { + label: 'Tx Speed', + backgroundColor: 'rgba(51, 153, 255, 0.1)', + borderColor: '#3399ff', + pointBackgroundColor: '#3399ff', + pointHoverBackgroundColor: '#fff', + borderWidth: 2, + fill: true, + data: [] + } + ] + }; + + public chartOptions: any = { + maintainAspectRatio: false, + plugins: { + legend: { display: true }, + tooltip: { + callbacks: { + label: (context: any) => { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + label += this.formatBytes(context.parsed.y) + '/s'; + } + return label; + } + } + } + }, + scales: { + x: { display: true }, + y: { + display: true, + beginAtZero: true, + ticks: { + callback: (value: any) => { + return this.formatBytes(value) + '/s'; + } + } + } + }, + elements: { + line: { tension: 0.4 }, + point: { radius: 0, hitRadius: 10, hoverRadius: 4 } + } + }; + + // Grid configs + public source: Array = []; + public loading: boolean = true; + + // Modals state + public addPeerModalVisible = false; + public addPeerStep = 1; + public peerForm: any = { + pubkey: '', + custom_ip: '', + name: '', + description: '', + nat_mode: 'full', + split_targets: [''], // Array of strings + link_device: false, + persistent_keepalive: 25, + custom_interface: '', + mt_user: '', + mt_pass: '', + mt_port: 8728 + }; + public editingPeer = false; + + public configResult: { script?: string, qrBlobUrl?: SafeUrl } = {}; + public activePeerConfig: VpnPeer | null = null; + + public serverConfigModalVisible = false; + public serverConfig: Partial = {}; + + public deleteModalVisible = false; + public peerToDelete: VpnPeer | null = null; + + public toggleModalVisible = false; + public peerToToggle: VpnPeer | null = null; + + public resetPeerModalVisible = false; + public peerToReset: VpnPeer | null = null; + + public resetServerModalVisible = false; + + rowClass: GuiRowClass = { class: "row-highlighted" }; + searching: GuiSearching = { enabled: true, placeholder: "Search Peers" }; + public paging: GuiPaging = { + enabled: true, + page: 1, + pageSize: 10, + pageSizes: [5, 10, 25, 50], + display: GuiPagingDisplay.ADVANCED, + }; + public columnMenu: GuiColumnMenu = { enabled: true, sort: true, columnsManager: true }; + public infoPanel: GuiInfoPanel = { enabled: true, infoDialog: false, columnsManager: true, schemaManager: true }; + public sorting = { enabled: true, multiSorting: true }; + + constructor( + private login_checker: loginChecker, + private router: Router, + private vpnService: VpnService, + private sanitizer: DomSanitizer + ) { + if (!this.login_checker.isLoggedIn()) { + setTimeout(() => this.router.navigate(["login"]), 100); + } + } + + ngOnInit(): void { + this.startPolling(); + } + + ngOnDestroy(): void { + if (this.pollingSubscription) { + this.pollingSubscription.unsubscribe(); + } + if (this.livePollingSubscription) { + this.livePollingSubscription.unsubscribe(); + } + } + + startPolling() { + // Slow poll for heavy metadata (10s) + this.pollingSubscription = timer(0, 10000).pipe( + switchMap(() => this.vpnService.getStatus().pipe( + catchError(err => { + console.error("VPN Polling Error:", err); + return of(null); + }) + )) + ).subscribe({ + next: (res) => { + if (res) { + this.status = res; + this.source = (res.peers || []).map(p => ({ + ...p, + _search_index: `${p.name || ''} ${p.assigned_ip || ''} ${p.public_key || ''} ${p.description || ''}` + })); + this.computeTotals(); + this.loading = false; + } else { + if (!this.status) { + this.loading = true; // Show loading if we never got a successful status + } + } + } + }); + + // Fast poll for live bandwidth (2s) + this.livePollingSubscription = timer(2000, 2000).pipe( + switchMap(() => this.vpnService.getLiveStatus().pipe( + catchError(err => of(null)) + )) + ).subscribe({ + next: (liveData) => { + if (liveData) { + // Update server top card speeds + this.liveSpeedRx = liveData.server.rx_speed || 0; + this.liveSpeedTx = liveData.server.tx_speed || 0; + this.totalRx = liveData.server.rx_bytes; + this.totalTx = liveData.server.tx_bytes; + + // Surgically update existing peers without re-creating array + if (this.source && this.source.length > 0 && Array.isArray(liveData.peers)) { + for (let i = 0; i < this.source.length; i++) { + const peer = this.source[i]; + const pubkey = peer.public_key; + + const livePeer = liveData.peers.find(p => p.public_key === pubkey); + + if (livePeer && livePeer.stats && peer.stats) { + // Instead of making a new object (which triggers grid redraw), mutate the stats deeply + peer.stats.rx_bytes = livePeer.stats.rx_bytes; + peer.stats.tx_bytes = livePeer.stats.tx_bytes; + peer.stats.rx_speed = livePeer.stats.rx_speed; + peer.stats.tx_speed = livePeer.stats.tx_speed; + } + } + } + } + } + }); + } + + refreshData() { + this.vpnService.getStatus().subscribe({ + next: (res) => { + if (res) { + this.status = res; + this.source = (res.peers || []).map(p => ({ + ...p, + _search_index: `${p.name || ''} ${p.assigned_ip || ''} ${p.public_key || ''} ${p.description || ''}` + })); + this.computeTotals(); + } + }, + error: (err) => console.error("Error refreshing data:", err) + }); + } + + computeTotals() { + this.totalRx = 0; + this.totalTx = 0; + for (const p of this.source) { + if (p.stats) { + this.totalRx += p.stats.rx_bytes / 1048576; // To MB + this.totalTx += p.stats.tx_bytes / 1048576; // To MB + } + } + + const now = new Date(); + const timeLabel = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0'); + + this.chartData.labels.push(timeLabel); + this.chartData.datasets[0].data.push(this.liveSpeedRx); + this.chartData.datasets[1].data.push(this.liveSpeedTx); + + // Keep last 30 intervals (2.5 minutes of history) + if (this.chartData.labels.length > 30) { + this.chartData.labels.shift(); + this.chartData.datasets[0].data.shift(); + this.chartData.datasets[1].data.shift(); + } + + // Trigger change detection for chart + this.chartData = { ...this.chartData }; + } + + show_toast(title: string, body: string, color: string) { + const props = { ...this.toasterForm, color, title, body }; + if (this.viewChildren && this.viewChildren.first) { + const componentRef = this.viewChildren.first.addToast( + AppToastComponent, + props, + {} + ); + if (componentRef) { + componentRef.instance["closeButton"] = props.closeButton; + } + } + } + + // --- Modals & Actions --- + + openAddPeerModal() { + this.addPeerModalVisible = true; + this.addPeerStep = 1; + this.editingPeer = false; + this.peerForm = { + pubkey: '', custom_ip: '', name: '', description: '', nat_mode: 'full', split_targets: [''], link_device: false, persistent_keepalive: 25, custom_interface: '', + mt_user: '', mt_pass: '', mt_port: 8728 + }; + } + + addSplitTarget() { + this.peerForm.split_targets.push(''); + } + + removeSplitTarget(index: number) { + this.peerForm.split_targets.splice(index, 1); + } + + trackByIndex(index: number, obj: any): any { + return index; + } + + submitPeer() { + // Filter empty splits + const payload = { ...this.peerForm }; + payload.split_targets = payload.split_targets.filter((s: string) => s.trim() !== ''); + + // Scrub credentials if the user unchecked "Link & Manage as MikroTik Device" + if (!payload.link_device) { + payload.mt_user = null; + payload.mt_pass = null; + payload.mt_port = null; + } + + const isLinkDevice = payload.link_device; + + // Remove the deprecated 'link_device' flag from the API payload entirely + delete payload.link_device; + + if (this.editingPeer) { + this.vpnService.editPeer(payload).subscribe({ + next: (res) => { + this.show_toast("Success", "Peer updated successfully", "success"); + this.addPeerModalVisible = false; + this.refreshData(); + }, + error: (err) => this.show_toast("Error", err.error?.message || "Failed to update peer", "danger") + }); + } else { + this.vpnService.addPeer(payload).subscribe({ + next: (res) => { + this.show_toast("Success", "Peer created successfully", "success"); + this.addPeerModalVisible = false; + this.refreshData(); + // Auto-open appropriate config wizard + if (isLinkDevice) { + this.openScriptModal(res.peer); + } else { + this.openQrModal(res.peer); + } + }, + error: (err) => this.show_toast("Error", err.error?.message || "Failed to add peer", "danger") + }); + } + } + + promptToggleEnabled(item: VpnPeer) { + this.peerToToggle = item; + this.toggleModalVisible = true; + } + + confirmToggle() { + if (!this.peerToToggle) return; + const item = this.peerToToggle; + const newState = !item.is_enabled; + + this.vpnService.togglePeer(item.public_key, newState).subscribe({ + next: () => { + this.show_toast("Success", `Peer ${newState ? 'enabled' : 'disabled'}`, "success"); + item.is_enabled = newState; + this.source = [...this.source]; // Force grid update + this.refreshData(); + this.toggleModalVisible = false; + this.peerToToggle = null; + }, + error: (err) => { + this.show_toast("Error", err.error?.message || "Failed to toggle peer", "danger"); + this.toggleModalVisible = false; + this.peerToToggle = null; + } + }); + } + + openEditModal(item: VpnPeer) { + this.editingPeer = true; + this.addPeerModalVisible = true; + this.addPeerStep = 1; + this.peerForm = { + pubkey: item.public_key, + custom_ip: item.assigned_ip, + name: item.name || '', + description: item.description || '', + nat_mode: item.nat_mode, + split_targets: [...item.split_targets], + link_device: !!item.linked_device_id || !!item.mt_user, + persistent_keepalive: item.persistent_keepalive, + custom_interface: item.custom_interface || '', + mt_user: item.mt_user || '', + mt_pass: item.mt_pass || '', + mt_port: item.mt_port || 8728 + }; + if (this.peerForm.split_targets.length === 0) this.peerForm.split_targets.push(''); + } + + scanDevice(item: VpnPeer) { + this.vpnService.scanLinkedDevice(item.public_key).subscribe({ + next: (res) => this.show_toast("Success", res.message || "Manual scan initiated asynchronously", "success"), + error: (err) => this.show_toast("Error", err.error?.message || "Scan failed", "danger") + }); + } + + public scriptModalVisible = false; + public qrModalVisible = false; + + // Fetch and show script + openScriptModal(item: VpnPeer) { + this.activePeerConfig = item; + this.configResult = {}; + this.scriptModalVisible = true; + this.qrModalVisible = false; // Auto-close QR if open + this.vpnService.getPeerMikrotikScript(item.public_key).subscribe({ + next: (res) => { + const rawScript = res.script || ''; + // Ensure double-escaped newlines and carriage returns are aggressively parsed to real newlines + this.configResult.script = rawScript.split('\\n').join('\n').replace(/\r\n/g, '\n'); + this.scriptModalVisible = true; + }, + error: (err) => this.show_toast("Error", "Failed to fetch Mikrotik script", "danger") + }); + } + + // Fetch and show QR + openQrModal(item: VpnPeer) { + this.activePeerConfig = item; + this.configResult = {}; + this.scriptModalVisible = false; // Auto-close Script if open + this.vpnService.getPeerQrCode(item.public_key).subscribe({ + next: (blob) => { + const url = URL.createObjectURL(blob); + this.configResult.qrBlobUrl = this.sanitizer.bypassSecurityTrustUrl(url); + this.qrModalVisible = true; + }, + error: (err) => this.show_toast("Error", "Failed to fetch QR code", "danger") + }); + } + + // Download config directly + downloadPeerConfigDirect(item: VpnPeer) { + this.vpnService.getPeerConfig(item.public_key).subscribe({ + next: (res) => { + const blob = new Blob([res.config], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${item.assigned_ip || 'peer'}.conf`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }, + error: (err) => this.show_toast("Error", "Failed to fetch config", "danger") + }); + } + + copyScript() { + if (this.configResult.script) { + navigator.clipboard.writeText(this.configResult.script).then(() => { + this.show_toast("Success", "Copied to clipboard", "success"); + }); + } + } + + confirmDelete(item: VpnPeer) { + this.peerToDelete = item; + this.deleteModalVisible = true; + } + + executeDelete() { + if (this.peerToDelete) { + this.vpnService.deletePeer(this.peerToDelete.public_key).subscribe({ + next: () => { + this.show_toast("Success", "Peer deleted permanently", "success"); + this.deleteModalVisible = false; + this.refreshData(); + }, + error: (err) => this.show_toast("Error", err.error?.message || "Delete failed", "danger") + }); + } + } + + openServerConfigModal() { + this.serverConfigModalVisible = true; + this.vpnService.getSystemConfig().subscribe({ + next: (res) => this.serverConfig = res.config || {}, + error: (err) => this.show_toast("Error", "Failed to fetch config", "danger") + }); + } + + saveServerConfig() { + this.vpnService.updateSystemConfig(this.serverConfig).subscribe({ + next: () => { + this.show_toast("Success", "Server config updated", "success"); + this.serverConfigModalVisible = false; + this.refreshData(); + }, + error: (err) => this.show_toast("Error", err.error?.message || "Update failed", "danger") + }); + } + + // === COUNTER RESETS === + promptResetServer() { + this.resetServerModalVisible = true; + } + + confirmResetServer() { + this.vpnService.resetServerCounters().subscribe({ + next: () => { + this.resetServerModalVisible = false; + this.show_toast('Success', 'Global server traffic counters have been reset.', 'success'); + this.totalRx = 0; + this.totalTx = 0; + // Force chart data flush + this.chartData = { + ...this.chartData, + labels: [], + datasets: [ + { ...this.chartData.datasets[0], data: [] }, + { ...this.chartData.datasets[1], data: [] } + ] + }; + }, + error: (err) => { + console.error(err); + this.resetServerModalVisible = false; + this.show_toast('Error', 'Failed to reset server counters.', 'danger'); + } + }); + } + + promptResetPeer(peer: VpnPeer) { + this.peerToReset = peer; + this.resetPeerModalVisible = true; + } + + confirmResetPeer() { + if (!this.peerToReset || !this.peerToReset.public_key) return; + this.vpnService.resetPeerCounters(this.peerToReset.public_key).subscribe({ + next: () => { + this.show_toast('Success', `Counters reset for ${this.peerToReset?.name || this.peerToReset?.assigned_ip}`, 'success'); + if (this.peerToReset && this.peerToReset.stats) { + this.peerToReset.stats.rx_bytes = 0; + this.peerToReset.stats.tx_bytes = 0; + this.peerToReset.stats.rx_speed = 0; + this.peerToReset.stats.tx_speed = 0; + } + this.resetPeerModalVisible = false; + this.peerToReset = null; + }, + error: (err) => { + console.error(err); + this.resetPeerModalVisible = false; + this.show_toast('Error', `Failed to reset counters for peer.`, 'danger'); + this.peerToReset = null; + } + }); + } +} diff --git a/src/app/views/vpn/vpn.module.ts b/src/app/views/vpn/vpn.module.ts new file mode 100644 index 0000000..5e72d27 --- /dev/null +++ b/src/app/views/vpn/vpn.module.ts @@ -0,0 +1,71 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { GuiGridModule } from '@generic-ui/ngx-grid'; +import { ChartjsModule } from '@coreui/angular-chartjs'; +import { HighlightJsModule } from 'ngx-highlight-js'; +import { ClipboardModule } from '@angular/cdk/clipboard'; + +import { + AvatarModule, + ButtonGroupModule, + ButtonModule, + CardModule, + FormModule, + GridModule as CoreUIGridModule, + NavModule, + ProgressModule, + TableModule, + TabsModule, + ModalModule, + DropdownModule, + SharedModule, + ListGroupModule, + BadgeModule, + TooltipModule, + ToastModule +} from '@coreui/angular'; +import { MatMenuModule } from '@angular/material/menu'; + +import { loginChecker } from '../../providers/login_checker'; +import { VpnService, VpnStatusResponse, VpnPeer, VpnServerConfig } from '../../providers/mikrowizard/vpn.service'; +import { IconModule } from '@coreui/icons-angular'; + +import { VpnRoutingModule } from './vpn-routing.module'; +import { VpnComponent } from './vpn.component'; + +@NgModule({ + imports: [ + VpnRoutingModule, + CardModule, + NavModule, + IconModule, + TabsModule, + CommonModule, + GuiGridModule, + ProgressModule, + ReactiveFormsModule, + ButtonModule, + FormModule, + ButtonModule, + ButtonGroupModule, + ChartjsModule, + AvatarModule, + TableModule, + ModalModule, + DropdownModule, + SharedModule, + ListGroupModule, + BadgeModule, + TooltipModule, + FormsModule, + ToastModule, + CoreUIGridModule, + MatMenuModule, + HighlightJsModule, + ClipboardModule + ], + declarations: [VpnComponent] +}) +export class VpnModule { +}