Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b82975444a | ||
|
|
a3c31bf340 | ||
|
|
9447f9b0be | ||
|
|
1e9e111745 | ||
|
|
4fbd3ceabd | ||
|
|
85250d62e7 | ||
|
|
3d1328301a | ||
|
|
7cd062ee1f | ||
|
|
305a568899 | ||
|
|
01cbc4f759 | ||
|
|
9082596000 |
@@ -124,8 +124,22 @@ jobs:
|
||||
helm lint deploy/helm/ai-spel-frontend
|
||||
helm package deploy/helm/ai-spel-frontend --destination "$publish_dir" \
|
||||
--version "$version" --app-version "$version"
|
||||
registry_config="$RUNNER_TEMP/helm/registry.json"
|
||||
mkdir -p "$(dirname "$registry_config")"
|
||||
printf '{}' > "$registry_config"
|
||||
printf '%s' "$HARBOR_PASSWORD" | helm registry login harbor.k3s.k0.nu \
|
||||
--username "$HARBOR_USERNAME" --password-stdin \
|
||||
--registry-config "$registry_config"
|
||||
helm push "$publish_dir/ai-spel-frontend-$version.tgz" \
|
||||
oci://harbor.k3s.k0.nu/ai-spel
|
||||
oci://harbor.k3s.k0.nu/ai-spel --registry-config "$registry_config"
|
||||
chart_metadata="$(helm show chart oci://harbor.k3s.k0.nu/ai-spel/ai-spel-frontend \
|
||||
--version "$version" --registry-config "$registry_config")"
|
||||
chart_version="$(printf '%s\n' "$chart_metadata" | awk '$1 == "version:" { print $2 }')"
|
||||
chart_app_version="$(printf '%s\n' "$chart_metadata" | awk '$1 == "appVersion:" { gsub(/"/, "", $2); print $2 }')"
|
||||
[[ "$chart_version" == "$version" && "$chart_app_version" == "$version" ]] || {
|
||||
echo "Published frontend chart version mismatch: chart=$chart_version app=$chart_app_version expected=$version"
|
||||
exit 1
|
||||
}
|
||||
helm template ai-spel-frontend "$publish_dir/ai-spel-frontend-$version.tgz" \
|
||||
--namespace ai-spel > "$publish_dir/frontend-manifest.yaml"
|
||||
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
FROM nginxinc/nginx-unprivileged:1.27-alpine
|
||||
|
||||
ARG FRONTEND_VERSION
|
||||
USER root
|
||||
RUN test -n "$FRONTEND_VERSION" \
|
||||
&& sed -i "s/__FRONTEND_VERSION__/${FRONTEND_VERSION}/g" /usr/share/nginx/html/index.html
|
||||
|
||||
RUN test -n "$FRONTEND_VERSION"
|
||||
COPY index.html /usr/share/nginx/html/index.html
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
USER root
|
||||
RUN sed -i "s/__FRONTEND_VERSION__/${FRONTEND_VERSION}/g" /usr/share/nginx/html/index.html
|
||||
|
||||
USER 101
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -2,5 +2,5 @@ apiVersion: v2
|
||||
name: ai-spel-frontend
|
||||
description: Static web frontend for the Swedish life simulator
|
||||
type: application
|
||||
version: 0.1.14
|
||||
appVersion: "0.1.14"
|
||||
version: 0.1.19
|
||||
appVersion: "0.1.19"
|
||||
@@ -7,6 +7,13 @@ metadata:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
minReadySeconds: {{ .Values.deployment.minReadySeconds }}
|
||||
progressDeadlineSeconds: {{ .Values.deployment.progressDeadlineSeconds }}
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: {{ .Values.deployment.maxUnavailable }}
|
||||
maxSurge: {{ .Values.deployment.maxSurge }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
@@ -40,11 +47,15 @@ spec:
|
||||
port: http
|
||||
initialDelaySeconds: 2
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
replicaCount: 1
|
||||
deployment:
|
||||
# Keep the old single replica available until its replacement passes readiness.
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
minReadySeconds: 5
|
||||
progressDeadlineSeconds: 600
|
||||
image:
|
||||
repository: harbor.k3s.k0.nu/ai-spel/frontend
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
+55
-2
@@ -59,6 +59,11 @@
|
||||
<label>Beskrivning av platsen <textarea name="location_description" required maxlength="2000" rows="4"></textarea></label>
|
||||
<label>Person du kan möta <input name="npc_name" required maxlength="120"></label>
|
||||
<label>Om personen <textarea name="npc_biography" required maxlength="2000" rows="4"></textarea></label>
|
||||
<h3>Förslag på startläge</h3>
|
||||
<p>Startpengar och varan är bara förslag. De sparas först när du redigerat och godkänt dem.</p>
|
||||
<label>Startsaldo i kronor <input name="starting_balance_sek" type="number" min="0" step="1" required></label>
|
||||
<label>En vara som går att köpa här <input name="offer_name" required maxlength="120"></label>
|
||||
<label>Pris i hela kronor <input name="offer_price_sek" type="number" min="1" step="1" required></label>
|
||||
<div class="row"><button type="submit">Godkänn och börja spela</button><button id="discard-world-draft" type="button">Gör ett nytt förslag</button></div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -68,6 +73,15 @@
|
||||
<h3 id="place-heading"></h3><p id="place-description"></p>
|
||||
<h3 id="npc-heading"></h3><p id="npc-description"></p>
|
||||
<h3 id="player-heading"></h3><p id="player-biography"></p>
|
||||
<h3>Ekonomi och ägodelar</h3>
|
||||
<p>Saldo: <span id="balance"></span> kr</p>
|
||||
<ul id="inventory" aria-label="Dina ägodelar"></ul>
|
||||
</section>
|
||||
<section class="card" id="shop" aria-labelledby="shop-heading" hidden>
|
||||
<h3 id="shop-heading">Köp på platsen</h3>
|
||||
<p><span id="offer-description"></span></p>
|
||||
<p id="offer-affordability"></p>
|
||||
<button id="buy-offer" type="button">Köp</button>
|
||||
</section>
|
||||
<section class="card" aria-labelledby="conversation-heading">
|
||||
<h3 id="conversation-heading">Samtal</h3>
|
||||
@@ -125,6 +139,21 @@
|
||||
document.querySelector('#message-label').firstChild.textContent = `Skriv till ${world.npc.name} `;
|
||||
document.querySelector('#player-heading').textContent = `${gameState.character.name}, ${gameState.character.age} år`;
|
||||
document.querySelector('#player-biography').textContent = gameState.character.biography;
|
||||
document.querySelector('#balance').textContent = (world.balance_sek ?? 0).toLocaleString('sv-SE');
|
||||
const inventory = document.querySelector('#inventory');
|
||||
inventory.replaceChildren(...(world.inventory ?? []).map((item) => {
|
||||
const entry = document.createElement('li');
|
||||
entry.textContent = `${item.name}: ${item.quantity}`;
|
||||
return entry;
|
||||
}));
|
||||
const shop = document.querySelector('#shop');
|
||||
shop.hidden = !world.offer;
|
||||
if (world.offer) {
|
||||
document.querySelector('#offer-description').textContent = `${world.offer.name} — ${world.offer.price_sek.toLocaleString('sv-SE')} kr`;
|
||||
document.querySelector('#buy-offer').disabled = (world.balance_sek ?? 0) < world.offer.price_sek;
|
||||
document.querySelector('#offer-affordability').textContent = (world.balance_sek ?? 0) < world.offer.price_sek ? 'Du har inte tillräckligt saldo för det här köpet.' : '';
|
||||
document.querySelector('#buy-offer').setAttribute('aria-label', `Köp ${world.offer.name} för ${world.offer.price_sek} kronor`);
|
||||
}
|
||||
renderConversation(await request('/api/npc/conversation'), world.npc.name);
|
||||
}
|
||||
document.querySelector('#auth-form').addEventListener('submit', async (event) => {
|
||||
@@ -148,7 +177,7 @@
|
||||
try {
|
||||
const proposal = await request('/api/world/draft', {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({idea:data.get('idea')})});
|
||||
const form = document.querySelector('#world-draft-form');
|
||||
for (const key of ['name','location_name','location_description','npc_name','npc_biography']) form.elements[key].value = proposal[key];
|
||||
for (const key of ['name','location_name','location_description','npc_name','npc_biography','starting_balance_sek','offer_name','offer_price_sek']) form.elements[key].value = proposal[key];
|
||||
form.hidden = false; status.textContent='Förslaget är klart. Granska och redigera det innan du godkänner.';
|
||||
} catch(error) { status.textContent=`Kunde inte skapa ett förslag: ${error.message}`; }
|
||||
});
|
||||
@@ -158,7 +187,10 @@
|
||||
document.querySelector('#world-draft-form').addEventListener('submit', async (event) => {
|
||||
event.preventDefault(); const data = new FormData(event.currentTarget); status.textContent='Sparar din godkända värld…';
|
||||
try {
|
||||
await request('/api/world', {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(Object.fromEntries(data.entries()))});
|
||||
const approval = Object.fromEntries(data.entries());
|
||||
approval.starting_balance_sek = Number(approval.starting_balance_sek);
|
||||
approval.offer_price_sek = Number(approval.offer_price_sek);
|
||||
await request('/api/world', {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(approval)});
|
||||
await openGame(); status.textContent='Din värld har sparats.';
|
||||
} catch(error) { status.textContent=`Kunde inte spara världen: ${error.message}`; }
|
||||
});
|
||||
@@ -168,6 +200,27 @@
|
||||
try { const result=await request('/api/npc/messages',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({message,idempotency_key:crypto.randomUUID()})}); field.value=''; await openGame(); status.textContent='Samtalet har sparats.'; }
|
||||
catch(error){status.textContent=`Samtalet kunde inte skickas: ${error.message}`;}
|
||||
});
|
||||
document.querySelector('#buy-offer').addEventListener('click', async () => {
|
||||
if (!gameState?.world?.offer) return;
|
||||
const world = gameState.world;
|
||||
const storageKey = `pending-purchase:${world.id}:${world.offer.id}`;
|
||||
const idempotencyKey = sessionStorage.getItem(storageKey) || crypto.randomUUID();
|
||||
sessionStorage.setItem(storageKey, idempotencyKey);
|
||||
const button = document.querySelector('#buy-offer');
|
||||
button.disabled = true;
|
||||
status.textContent = 'Genomför köpet…';
|
||||
try {
|
||||
await request('/api/purchases', {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({offer_id:world.offer.id,expected_world_version:world.version,idempotency_key:idempotencyKey})});
|
||||
sessionStorage.removeItem(storageKey);
|
||||
} catch(error) {
|
||||
button.disabled = false;
|
||||
status.textContent = `Köpet kunde inte genomföras: ${error.message}`;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await openGame(); status.textContent = 'Köpet är sparat i din värld.';
|
||||
} catch(error) { status.textContent = 'Köpet är sparat. Världen kunde inte läsas in just nu; ladda om sidan för att visa det.'; }
|
||||
});
|
||||
document.querySelector('#logout').addEventListener('click', async () => { try { await request('/api/logout',{method:'POST'}); showAuth(); status.textContent='Du har loggat ut.'; } catch(error){status.textContent=`Kunde inte logga ut: ${error.message}`;} });
|
||||
request('/api/me').then(async () => { await openGame(); status.textContent='Din sparade spelvärld har lästs in.'; }).catch(() => { showAuth(); });
|
||||
</script>
|
||||
|
||||
@@ -8,6 +8,8 @@ grep -q 'id="frontend-version">__FRONTEND_VERSION__' index.html
|
||||
grep -q "request('/version')" index.html
|
||||
grep -q 'id="api-version"' index.html
|
||||
grep -Fq "credentials: 'same-origin'" index.html
|
||||
grep -q 'Köp på platsen' index.html
|
||||
grep -q "request('/api/purchases'" index.html
|
||||
grep -q 'location = /healthz' nginx/default.conf
|
||||
|
||||
echo 'Frontend source checks passed.'
|
||||
|
||||
Reference in New Issue
Block a user