[WIP] A simple wake-on-lan service
1<script lang="ts">
2 import { ExternalLink, Power } from "@lucide/svelte";
3 import Api from "./api";
4
5 const {
6 name,
7 mac,
8 ip,
9 url,
10 }: {
11 name: string;
12 mac: string;
13 ip: string | null;
14 url: string | null;
15 } = $props();
16
17 let online: boolean | undefined = $state(undefined);
18 const checkStatus: () => Promise<void> = async () => {
19 if (!ip) return;
20 const active = await Api.ping({ ip }).catch(() => false);
21 online = active;
22 // check every 30s if online, or every 5 seconds if offline
23 setTimeout(checkStatus, (active ? 30 : 5) * 1000);
24 };
25 checkStatus();
26
27 // try clean up the url but dont lose sleep
28 function cleanUrl(url: string): string {
29 const REGEX =
30 /(?<=^(?:[a-z]*:\/\/)?)(?:[a-z]+(?:\.[a-z]+)+|localhost)(?::\d{1,5})?(?:\/+[^\/]+)*/gm;
31 const res = url.match(REGEX);
32 if (res && res[0]) return res[0];
33 return url;
34 }
35</script>
36
37<section>
38 <button
39 class="power"
40 onclick={() =>
41 Api.wake({ mac }).then((res) => alert(`Wake: ${mac} (${ip}) ${res}`))}
42 aria-label="power"
43 >
44 <Power />
45 </button>
46
47 <div class="name">
48 {name}
49 {#if ip}<span class="status" data-status={online}></span>{/if}
50 {#if url}<a href={url} class="url" title={url}>
51 <span class="url-text">{cleanUrl(url)}</span>
52 <ExternalLink />
53 </a>{/if}
54 </div>
55 <div class="mac">{mac}</div>
56 {#if ip}
57 <div class="at">@</div>
58 <div class="ip">{ip}</div>
59 {/if}
60</section>
61
62<style>
63 .status {
64 display: inline-block;
65 width: 0.5em;
66 height: 0.5em;
67 border-radius: 100%;
68 transition: background 500ms ease-in-out;
69
70 &[data-status="true"] {
71 background: var(--theme-accent-success);
72 }
73 &[data-status="false"] {
74 background: var(--theme-accent-fail);
75 }
76 }
77
78 section {
79 border-radius: 10px;
80 background-color: var(--theme-background-server);
81
82 box-sizing: border-box;
83 padding: 0.5rem;
84 display: grid;
85 grid-template:
86 "power name name name" 1lh
87 "power mac at ip" 1lh
88 / auto auto min-content auto;
89 justify-content: start;
90 align-items: center;
91 gap: 0 5px;
92
93 .name {
94 grid-area: name;
95 .url {
96 text-decoration: none;
97 .url-text {
98 text-decoration: underline;
99 &:hover {
100 text-decoration-style: dotted;
101 }
102
103 &:active {
104 text-decoration: none;
105 }
106 }
107 }
108 }
109
110 .mac {
111 grid-area: mac;
112 color: var(--theme-text-secondary);
113 }
114
115 .at {
116 grid-area: at;
117 color: var(--theme-text-secondary);
118 }
119
120 .ip {
121 grid-area: ip;
122 color: var(--theme-text-secondary);
123 }
124 }
125
126 @property --power-overlay {
127 syntax: "<color>";
128 inherits: false;
129 initial-value: transparent;
130 }
131
132 .power {
133 color: inherit;
134 grid-area: power;
135 aspect-ratio: 1;
136 height: 100%;
137 display: flex;
138 justify-content: center;
139 align-items: center;
140
141 border-radius: 100%;
142 border: 0px;
143 background: linear-gradient(var(--power-overlay), var(--power-overlay)),
144 linear-gradient(
145 var(--theme-background-button),
146 var(--theme-background-button)
147 );
148
149 transition:
150 scale 10ms,
151 --power-overlay 100ms;
152
153 &:hover {
154 --power-overlay: rgb(
155 from var(--theme-highlight) r g b / var(--theme-highlight-opacity)
156 );
157 }
158
159 &:active {
160 scale: 0.9;
161 }
162 }
163
164 :global {
165 .lucide-external-link {
166 width: 1em;
167 height: 1em;
168 }
169 }
170</style>