Compare commits

..
44 Commits
Author SHA1 Message Date
Lin, Hung-Shin 4a4ce11bcc Analytics revise (#91)
Analytics revise
2023-12-21 18:13:53 +08:00
Chen, Tai-Lin 9c650a4c26 fix: restore db name 2023-12-21 18:09:48 +08:00
Chen, Tai-Lin b96bcb27ad refactor: remove unused functions 2023-12-21 18:01:25 +08:00
Chen, Tai-Lin b0e12b2027 refactor: use questList to calculate total quests and completed 2023-12-21 17:58:52 +08:00
Chen, Tai-Lin 3b68693177 feat: add pagination buttons 2023-12-21 17:48:36 +08:00
Chen, Tai-Lin fa36b63fbe feat: add order to pagination 2023-12-21 17:28:48 +08:00
Chen, Tai-Lin 68db16419d refactor: add Pagination class 2023-12-21 17:09:20 +08:00
Chen, Tai-Lin 19cd10ab7b fix: session pagination 2023-12-21 16:37:37 +08:00
Chen, Tai-Lin bc0055781e feat: finish errorCommands pagination 2023-12-21 16:31:34 +08:00
Chen, Tai-Lin 8c634c8115 wip: models/commands analytics 2023-12-20 15:05:10 +08:00
Chen, Tai-Lin ceb9416fab update: mongoose version 2023-12-13 15:05:46 +08:00
Chen, Tai-Lin c6733814ba feat: add sorting order 2023-12-13 15:05:25 +08:00
Chen, Tai-Lin 1b94ae75ca feat: change to nextKey in users 2023-12-13 14:40:47 +08:00
Chen, Tai-Lin 3e7092dbd6 feat: modify analytics 2023-12-11 21:13:06 +08:00
Chen, Tai-Lin 61f627b125 feat: add quest page in analytics 2023-12-11 17:55:15 +08:00
Chen, Tai-Lin a578d4f910 fix: analytics completed rate 2023-12-11 01:09:06 +08:00
Lin, Hung-Shin 1bca61da40 feat: add mongo dump script, require ci checks before deployment (#89)
feat: add mongo dump script, require ci checks before deployment
2023-12-10 08:58:02 +08:00
Lin, Hung-Shin 660a7ce4e8 feat-automation-test (stabilise) (#90)
feat-automation-test (stabilise)
2023-12-10 08:57:18 +08:00
Chen, Tai-Lin ac31ca8659 feat: show analytics data page by page 2023-12-10 03:21:30 +08:00
Chen, Tai-Lin 9200336b49 fix: analytics modify 2023-12-10 02:21:02 +08:00
Chen, Tai-Lin c243e29002 fix: re-calculate completed rate in analytics 2023-12-10 01:25:27 +08:00
Annie Huang 175db7b8bc reduce timeout 2023-12-10 00:08:45 +08:00
Annie Huang 581e75ee57 Add configs retrying and longer timeout to load 2023-12-09 21:18:03 +08:00
Chen, Tai-Lin b625abae68 change docker compose file 2023-12-08 20:49:01 +08:00
dependabot[bot] b0bfbb789c build(deps-dev): bump vite from 4.4.4 to 4.4.12 (#88)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.4 to 4.4.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.4.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.4.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-06 15:27:17 +08:00
wancat 2ce277c7e5 fix: gpt-4-turbo not supported yet, rollback 2023-12-06 15:25:15 +08:00
wancat 7ef77267d3 fix: update to use main branch action 2023-12-06 15:21:32 +08:00
wancat d34ae3ef7a feat: change code review model to save budget 2023-12-06 15:17:13 +08:00
wancat bfa7ed1cf9 fix: require CI to be passed before deployment 2023-12-06 15:10:04 +08:00
wancat aa5088cc97 feat: add mongo dump and restore script 2023-12-06 15:07:46 +08:00
Annie Huang 0d0465f722 feat Automation Tests - Updates (#87)
- Change spec name
- Add spec and complete all quests
- Add Icon (totem version)
- Add some id for tests
- Add answer. sh for all quests

Not yet added verifying the buttons function and other pages
2023-12-05 00:46:18 +08:00
Annie Huang 360279f765 Add logo to nav bar 2023-12-05 00:21:15 +08:00
Annie Huang 6c73a8abc0 remove typing delay, add last (command) parameter
prevent pressing enter too quick
2023-12-04 17:21:13 +08:00
Annie Huang 55a0dcb443 edit typing delay 2023-12-04 16:51:01 +08:00
Annie Huang 5b3c52bd9b reduce typing delay and timeout 2023-12-04 16:45:25 +08:00
Annie Huang a911d953cd Add searching element timeout 2023-12-04 15:54:26 +08:00
Annie Huang c52b5a53cd extend typing delay 2023-12-04 15:41:53 +08:00
Annie Huang c9a43eeeb1 remove redundant class 2023-12-04 15:24:53 +08:00
Annie Huang 69624d7a5d Add logo to login/register page 2023-12-04 15:22:05 +08:00
Annie Huang 4681a743b4 update icon (previous one was boroken) 2023-12-04 14:43:59 +08:00
Annie Huang e0b3ec74de extend typing delay to prevent test fail 2023-12-04 14:05:21 +08:00
Annie Huang d0b179108a fix failing on finishing 2023-12-03 21:30:34 +08:00
Annie Huang 10ac3c720e Add check finished, typing delay 2023-12-03 19:16:02 +08:00
Annie Huang 7a3b6b1cfb - Add auto-complete quest command
- Change spec name
- Add spec complete all quests
- Add Icon (totem version)
- Add some id for tests
- Add answer.sh for all quests
2023-12-03 01:40:51 +08:00
48 changed files with 671 additions and 217 deletions

No files matched your search

+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
secrets: inherit
build:
# needs: ci-checks
needs: ci-checks
uses: ./.github/workflows/build.yml
secrets: inherit
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
uses: mattzcarey/code-review-gpt@v0.1.4-alpha
with:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MODEL: 'gpt-4'
MODEL: 'gpt-3.5-turbo'
GITHUB_TOKEN: ${{ github.token }}
build:
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
secrets: inherit
build:
# needs: ci-checks
needs: ci-checks
uses: ./.github/workflows/build.yml
secrets: inherit
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/icon_totem.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Linux Odyssey</title>
</head>
+1 -1
View File
@@ -38,6 +38,6 @@
"autoprefixer": "^10.4.14",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"vite": "^4.4.0"
"vite": "^4.4.12"
}
}
+9
View File
@@ -0,0 +1,9 @@
<svg width="638" height="732" viewBox="0 0 638 732" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H638V732H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_379_14" transform="matrix(0.00126582 0 0 0.00110327 0 -0.000333748)"/>
</pattern>
<image id="image0_379_14" width="790" height="907" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 630 KiB

+7 -1
View File
@@ -89,7 +89,12 @@ const handleChange = () => {
<div
class="bg-bg border-8 border-bg-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl font-black mb-2">{{ title }}</h1>
<div class="w-full flex items-center justify-center mb-2 gap-3">
<img src="../img/icon_totem.svg" class="h-10" />
<h1 class="text-text-primary text-3xl font-black" id="title">
{{ title }}
</h1>
</div>
<div class="text-error inline-flex justify-center items-center md:hidden">
<font-awesome-icon
:icon="['fas', 'circle-exclamation']"
@@ -163,6 +168,7 @@ const handleChange = () => {
/>
<label
class="text-sm font-normal text-text-secondary"
id="NameRules"
v-if="type === 'username' || type === 'register'"
>
<p>可用小寫字母、數字、"_"和"-"</p>
+3 -2
View File
@@ -78,15 +78,16 @@ onMounted(async () => {
class="text-border h-full w-full"
/>
</button> -->
<img src="../img/icon_totem.svg" class="h-5" />
<button
id="HeaderText"
id="GameTitle"
class="text-text-primary inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
Linux Odyssey
</button>
<p
id="HeaderText"
id="QuestTitle"
class="text-text inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
+9
View File
@@ -0,0 +1,9 @@
<svg width="638" height="732" viewBox="0 0 638 732" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H638V732H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_379_14" transform="matrix(0.00126582 0 0 0.00110327 0 -0.000333748)"/>
</pattern>
<image id="image0_379_14" width="790" height="907" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 630 KiB

+34
View File
@@ -29,4 +29,38 @@ down() {
docker compose -f docker-compose.testing.yml down
}
dump() {
# Define service name and dump directory name
SERVICE_NAME="db"
DUMP_NAME="mongodump-$(date --iso).archive"
CONTAINER_ID=$(docker compose ps -q $SERVICE_NAME)
# Fetch credentials from Docker environment variables
MONGO_URL=$(docker compose exec -T backend printenv MONGO_URL)
echo mongo uri: $MONGO_URL
# Dump MongoDB data using credentials
docker compose exec -T $SERVICE_NAME sh -c "mongodump --gzip --archive=/tmp/$DUMP_NAME -d odyssey-test" &&
# Copy the dump from the container to the host
# Note: You will need to know the exact container name for this step
docker cp ${CONTAINER_ID}:/tmp/$DUMP_NAME . &&
echo "MongoDB data dumped successfully."
}
restore() {
# Define service name and dump directory name
SERVICE_NAME="db"
DUMP_NAME=$1
CONTAINER_ID=$(docker compose ps -q $SERVICE_NAME)
# Copy the dump from the host to the container
docker cp $DUMP_NAME ${CONTAINER_ID}:/tmp &&
# Dump MongoDB data using credentials
docker compose exec -T $SERVICE_NAME sh -c "mongorestore --gzip --archive=/tmp/$DUMP_NAME" &&
echo "MongoDB data dumped successfully."
}
$@
+2 -1
View File
@@ -13,7 +13,8 @@
"@linux-odyssey/models": "*",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
"express-handlebars": "^7.1.2"
"express-handlebars": "^7.1.2",
"mongoose": "^8.0.3"
},
"devDependencies": {
"nodemon": "^3.0.1"
View File
Whitespace-only changes.
+18 -3
View File
@@ -1,9 +1,24 @@
/* eslint-disable object-shorthand */
import Pagination from '../models/pagination.js'
import { errorCommands } from '../models/commands.js'
// eslint-disable-next-line import/prefer-default-export
export async function commandListController(req, res) {
const payload = {
errorCommands: await errorCommands(),
const { nextKey, order } = req.query
const itemsPerPage = 100
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const payload = await errorCommands(pagination)
const newNextKey = payload[payload.length - 1]?._id
res.render('commands', {
errorCommands: payload,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.error(error)
res.status(500).send('Error fetching data')
}
res.render('commands', payload)
}
+13 -4
View File
@@ -1,15 +1,24 @@
import { userCount } from '../models/users.js'
import { sessionCount, sessionCompleted } from '../models/sessions.js'
import { questList } from '../models/quests.js'
// eslint-disable-next-line import/prefer-default-export
export async function homeController(req, res) {
const quests = await questList()
const [totalQuests, totalCompleted] = quests.reduce(
([total, completed], quest) => [
total + quest.started,
completed + quest.completed,
],
[0, 0]
)
const payload = {
title: 'Linux Odyssey Admin',
userCount: await userCount(),
sessionCount: await sessionCount(),
sessionCompleted: await sessionCompleted(),
totalQuests,
totalCompleted,
}
payload.completedRate =
Math.round((payload.sessionCompleted / payload.sessionCount) * 10000) / 100
Math.round((payload.totalCompleted / payload.totalQuests) * 10000) / 100
res.render('home', payload)
}
@@ -0,0 +1,10 @@
import { questList } from '../models/quests.js'
// eslint-disable-next-line import/prefer-default-export
export async function questListController(req, res) {
const quests = await questList()
quests.forEach((quest) => {
quest.failed = quest.started - quest.completed
})
res.render('quests', { quests })
}
+18 -2
View File
@@ -1,8 +1,24 @@
import Pagination from '../models/pagination.js'
import { sessionList, sessionDetail } from '../models/sessions.js'
export async function sessionListController(req, res) {
const sessions = await sessionList()
res.render('sessions', { sessions })
const { nextKey, order } = req.query
const itemsPerPage = 50
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const sessions = await sessionList(pagination)
const newNextKey = sessions[sessions.length - 1]?._id
res.render('sessions', {
sessions,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.log(error)
res.status(500).send('Error fetching session data')
}
}
export async function sessionDetailController(req, res) {
+18 -2
View File
@@ -1,6 +1,22 @@
import Pagination from '../models/pagination.js'
import { userList } from '../models/users.js'
// eslint-disable-next-line import/prefer-default-export
export async function userListController(req, res) {
res.render('users', { users: await userList() })
const { nextKey, order } = req.query
const itemsPerPage = 50
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const users = await userList(pagination)
const newNextKey = users[users.length - 1]?._id
res.render('users', {
users,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.error(error)
res.status(500).send('Error fetching user data')
}
}
+36 -14
View File
@@ -1,22 +1,44 @@
import { Command } from '@linux-odyssey/models'
// eslint-disable-next-line import/prefer-default-export
export async function errorCommands() {
const commands = await Command.find({
$and: [
{ error: { $exists: true } },
{ error: { $ne: null } },
{ error: { $ne: '' } },
],
})
.populate({
path: 'session',
populate: { path: 'user' },
})
.exec()
return commands.map(({ command, error, createdAt, session }) => {
export async function errorCommands(pagination) {
const commands = await Command.aggregate([
pagination.match('_id'),
{
$match: {
$and: [
{ error: { $exists: true } },
{ error: { $ne: null } },
{ error: { $ne: '' } },
],
},
},
{
$lookup: {
from: 'sessions', // 替換成 'session' 對應的集合名稱
localField: 'session', // 替換成 'session' 對應的字段名稱
foreignField: '_id',
as: 'session',
},
},
{ $unwind: '$session' },
{
$lookup: {
from: 'users', // 替換成 'user' 對應的集合名稱
localField: 'session.user', // 替換成 'user' 對應的字段名稱
foreignField: '_id',
as: 'session.user',
},
},
{ $unwind: '$session.user' },
pagination.sort('_id'),
pagination.limit(),
])
return commands.map(({ _id, command, error, createdAt, session }) => {
const { user, quest } = session
return {
_id,
command: command.slice(0, 20),
error: error.slice(0, 50),
createdAt: createdAt?.toLocaleString(),
@@ -0,0 +1,74 @@
/* eslint-disable lines-between-class-members */
/* eslint-disable class-methods-use-this */
// eslint-disable-next-line max-classes-per-file
import mongoose from 'mongoose'
export const DESC = 'desc'
export const ASC = 'asc'
class Asc {
getOperator() {
return '$gt'
}
getOrder() {
return 1
}
getName() {
return ASC
}
}
class Desc {
getOperator() {
return '$lt'
}
getOrder() {
return -1
}
getName() {
return DESC
}
}
export default class Pagination {
nextKey = null
itemsPerPage = 50
order = null
constructor(order, itemsPerPage = 50, nextKey = null) {
this.itemsPerPage = itemsPerPage
if (!order || order === DESC) {
this.order = new Desc()
} else if (order === ASC) {
this.order = new Asc()
} else {
throw new Error('order must be asc or desc')
}
if (nextKey) {
this.nextKey = new mongoose.Types.ObjectId(nextKey)
}
}
match(field) {
const stage = this.nextKey
? { [field]: { [this.order.getOperator()]: this.nextKey } }
: {}
return { $match: stage }
}
sort(field) {
return { $sort: { [field]: this.order.getOrder() } }
}
limit() {
return { $limit: this.itemsPerPage }
}
getOrder() {
return this.order.getName()
}
}
+59
View File
@@ -0,0 +1,59 @@
/* eslint-disable object-shorthand */
/* eslint-disable func-names */
import { UserProfile } from '@linux-odyssey/models'
export function questList() {
return UserProfile.aggregate([
{
$project: {
progress: {
$objectToArray: '$progress',
},
},
},
{
$unwind: '$progress',
},
{
$group: {
_id: '$progress.v.quest',
started: {
$sum: 1,
},
completed: {
$sum: {
$cond: {
if: { $eq: ['$progress.v.completed', true] },
then: 1,
else: 0,
},
},
},
},
},
{
$project: {
_id: 1,
started: 1,
completed: 1,
completedRate: {
$cond: {
if: { $eq: ['$started', 0] },
then: 0,
else: {
$round: [
{ $multiply: [{ $divide: ['$completed', '$started'] }, 100] },
0,
],
},
},
},
},
},
{
$sort: {
completed: -1,
},
},
])
}
+5 -24
View File
@@ -1,28 +1,5 @@
import { Command, Session } from '@linux-odyssey/models'
export function sessionCount() {
return Session.find().count()
}
export function sessionCompleted() {
return Session.find({ status: 'finished' }).count()
}
export function averageTimeUsage() {
return Session.aggregate([
{
$group: {
_id: '$quest',
average: {
$avg: {
$subtract: ['$finishedAt', '$createdAt'],
},
},
},
},
])
}
function formatTime(time) {
const seconds = Math.floor(time / 1000)
const minutes = Math.floor(seconds / 60)
@@ -30,8 +7,9 @@ function formatTime(time) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
}
export async function sessionList() {
export async function sessionList(pagination) {
const sessions = await Session.aggregate([
pagination.match('_id'),
{
$lookup: {
from: 'users',
@@ -49,7 +27,10 @@ export async function sessionList() {
as: 'commands',
},
},
pagination.sort('_id'),
pagination.limit(),
])
return sessions.map(
({
_id,
+7 -3
View File
@@ -11,8 +11,9 @@ export function userCount() {
return User.find().count()
}
export async function userList() {
export async function userList(pagination) {
const users = await Session.aggregate([
pagination.match('user'),
{
$group: {
_id: '$user',
@@ -29,11 +30,14 @@ export async function userList() {
},
},
{ $unwind: '$userData' },
{ $sort: { 'userData.createdAt': 1 } },
pagination.sort('userData._id'),
pagination.limit(),
])
return users.map(({ userData, count, lastActivityAt }) => {
const { username, email, createdAt } = userData
const { _id, username, email, createdAt } = userData
return {
_id,
username,
email,
createdAt: createdAt?.toLocaleString(),
+2
View File
@@ -2,6 +2,7 @@ import { Router } from 'express'
import { homeController } from './controllers/index.js'
import { userListController } from './controllers/users.js'
import { questListController } from './controllers/quests.js'
import { commandListController } from './controllers/commands.js'
import {
sessionDetailController,
@@ -12,6 +13,7 @@ const router = Router()
router.get('/', homeController)
router.get('/users', userListController)
router.get('/quests', questListController)
router.get('/commands', commandListController)
router.get('/sessions', sessionListController)
router.get('/sessions/:id', sessionDetailController)
@@ -0,0 +1,48 @@
import mongoose from 'mongoose'
import Pagination, { ASC, DESC } from '../src/models/pagination.js'
describe('Pagination', () => {
it('should failed when not provide order', () => {
expect(new Pagination('').getOrder()).toBe(DESC)
expect(() => new Pagination('Hahaha')).toThrow()
})
it('should return correct limit', () => {
const pagination = new Pagination(DESC, 10)
expect(pagination.limit()).toMatchObject({ $limit: 10 })
})
it('desc should return correct order', async () => {
const pagination = new Pagination(DESC, 10)
expect(pagination.sort('_id')).toMatchObject({ $sort: { _id: -1 } })
expect(pagination.sort('time')).toMatchObject({ $sort: { time: -1 } })
})
it('asc should return correct order', async () => {
const pagination = new Pagination(ASC, 10)
expect(pagination.sort('_id')).toMatchObject({ $sort: { _id: 1 } })
expect(pagination.sort('time')).toMatchObject({ $sort: { time: 1 } })
})
it('should return correct match stage for empty nextKey', async () => {
const pagination = new Pagination(DESC, 10)
expect(pagination.match('_id')).toMatchObject({ $match: {} })
})
it('should return correct match stage for empty nextKey', async () => {
const nextKeyStr = '656c7fea28a4c087bfcefe12'
const pagination = new Pagination(DESC, 10, nextKeyStr)
const nextKey = new mongoose.Types.ObjectId(nextKeyStr)
expect(pagination.match('_id')).toMatchObject({
$match: {
_id: {
$lt: nextKey,
},
},
})
})
})
@@ -1,6 +1,10 @@
<h1>Commands</h1>
<div>
<h2>Error commands</h2>
<div class='pagination'>
<a href='/commands?order=asc'>Ascending</a>
<a href='/commands?order=desc'>Descending</a>
</div>
<table>
<tr>
<th>Command</th>
@@ -21,4 +25,7 @@
</tr>
{{/each}}
</table>
<div class='pagination'>
<a href='/commands?nextKey={{nextKey}}&order={{order}}'>Next</a>
</div>
</div>
+5 -5
View File
@@ -1,18 +1,18 @@
<h1>Welcome to {{title}}</h1>
<div>
<h2>Users</h2>
<h2>Overview</h2>
<table>
<tr>
<th>Registered Users</th>
<td>{{userCount}}</td>
</tr>
<tr>
<th>Session Count</th>
<td>{{sessionCount}}</td>
<th>Total Started Quests</th>
<td>{{totalQuests}}</td>
</tr>
<tr>
<th>Completed Session</th>
<td>{{sessionCompleted}}</td>
<th>Completed Quests</th>
<td>{{totalCompleted}}</td>
</tr>
<tr>
<th>Completed Rate</th>
@@ -8,6 +8,7 @@
<ul>
<li><a href='/'>Home</a></li>
<li><a href='/users'>Users</a></li>
<li><a href='/quests'>Quests</a></li>
<li><a href='/commands'>Commands</a></li>
<li><a href='/sessions'>Sessions</a></li>
</ul>
@@ -0,0 +1,20 @@
<h1>Quests</h1>
<div>
<table>
<tr>
<th>Id</th>
<th>Started</th>
<th>Completed</th>
<th>Failed</th>
<th>Completed Rate</th>
</tr>
{{#each quests}}
<tr>
<td>{{_id}}</td>
<td>{{started}}</td>
<td>{{completed}}</td>
<td>{{failed}}</td>
<td>{{completedRate}} %</td>
</tr>
{{/each}}
</table>
@@ -36,20 +36,20 @@
<table>
<tr>
<th>Command</th>
<th>Stage</th>
<th>PWD</th>
<th>Input</th>
<th>Output</th>
<th>Error</th>
<th>Stage</th>
<th>Created At</th>
<th>Input Time</th>
</tr>
{{#each commands}}
<tr>
<td>{{command}}</td>
<td>{{stage}}</td>
<td>{{pwd}}</td>
<td>{{command}}</td>
<td>{{output}}</td>
<td>{{error}}</td>
<td>{{stage}}</td>
<td>{{createdAt}}</td>
</tr>
{{/each}}
+9 -2
View File
@@ -1,27 +1,34 @@
<h1>Sessions</h1>
<div class='pagination'>
<a href='/sessions?order=asc'>Ascending</a>
<a href='/sessions?order=desc'>Descending</a>
</div>
<div>
<table>
<tr>
<th>User</th>
<th>Quest</th>
<th></th>
<th>Status</th>
<th>Created At</th>
<th>Finished At</th>
<th>Used Time</th>
<th>Command Count</th>
<th>Session</th>
</tr>
{{#each sessions}}
<tr>
<td>{{user}}</td>
<td>{{quest}}</td>
<td><a href='/sessions/{{_id}}'>Detail</a></td>
<td>{{status}}</td>
<td>{{createdAt}}</td>
<td>{{finishedAt}}</td>
<td>{{usedTime}}</td>
<td>{{commandCount}}</td>
<td><a href='/sessions/{{_id}}'>Detail</a></td>
</tr>
{{/each}}
</table>
</div>
<div class='pagination'>
<a href='/sessions?nextKey={{nextKey}}&order={{order}}'>Next</a>
</div>
+16 -9
View File
@@ -1,23 +1,30 @@
<h1>Users List</h1>
<div class='pagination'>
<a href='/users?order=asc'>Ascending</a>
<a href='/users?order=desc'>Descending</a>
</div>
<div>
<table>
<tr>
<th>Username</th>
<th>Email</th>
<th>Registration Method</th>
<th>Registration Time</th>
<th>Sessions Count</th>
<th>Last Activity</th>
<th>Sessions Count</th>
<th>Email</th>
<th>Registration Time</th>
<th>Registration Method</th>
</tr>
{{#each users}}
<tr>
<td>{{username}}</td>
<td>{{email}}</td>
<td>{{method}}</td>
<td>{{createdAt}}</td>
<td>{{sessionCount}}</td>
<td href='/users/{{_id}}'>{{username}}</td>
<td>{{lastActivityAt}}</td>
<td>{{sessionCount}}</td>
<td>{{email}}</td>
<td>{{createdAt}}</td>
<td>{{method}}</td>
</tr>
{{/each}}
</table>
</div>
<div class='pagination'>
<a href='/users?nextKey={{nextKey}}&order={{order}}'>Next</a>
</div>
+1 -1
View File
@@ -6,6 +6,6 @@
"license": "GPL",
"exports": "./index.js",
"dependencies": {
"mongoose": "^7.6.0"
"mongoose": "^8.0.3"
}
}
+3
View File
@@ -0,0 +1,3 @@
echo start
echo example
echo How are you
+12
View File
@@ -0,0 +1,12 @@
echo start
cat hello.txt
cat world.txt
echo continue
cp hello.txt hello2.txt
cat hello2.txt
cp world.txt world2.txt
cat world2.txt
cp -r notes notes2
ls notes2
cp hello.txt world.txt
cat world.txt
+11
View File
@@ -0,0 +1,11 @@
echo start
touch hello.txt
ls
cat hello.txt
touch world.txt
ls
mkdir notes
ls
cd notes
touch note.txt
ls
+8
View File
@@ -0,0 +1,8 @@
echo start
ls
cd folder1
ls
cd ..
cd folder2
ls
cd ..
+3
View File
@@ -0,0 +1,3 @@
echo start
echo hello
echo finish
+4 -4
View File
@@ -1,4 +1,4 @@
echo help{enter}
ls {enter}
cat forgotten_scroll.txt{enter}
echo Hello World!{enter}
echo help
ls
cat forgotten_scroll.txt
echo Hello World
+12
View File
@@ -0,0 +1,12 @@
echo start
echo continue
mv hello.txt notes
ls
ls notes
echo continue
mv world.txt wow.txt
ls
mv notes docs
ls
echo hint
mv docs/hello.txt hi.txt
+8
View File
@@ -0,0 +1,8 @@
echo start
ls
cd folder1
cd ../folder2
ls ../folder1
pwd
ls ~
cd ~
+5
View File
@@ -0,0 +1,5 @@
echo start
ls
cat file1.txt
ls folder
cat folder/file.txt
+13
View File
@@ -0,0 +1,13 @@
echo start
rm hello.txt
ls
rm world.txt wow.txt
ls
rmdir empty
ls
rmdir notes
rm notes/note.txt
rmdir notes
ls trash
rm -r trash
ls
+1 -1
View File
@@ -26,7 +26,7 @@
"express-validator": "^7.0.1",
"jsonwebtoken": "^9.0.1",
"minimist": "^1.2.8",
"mongoose": "^7.4.0",
"mongoose": "^8.0.3",
"node-schedule": "^2.1.1",
"passport": "^0.6.0",
"passport-github2": "^0.1.12",
+3
View File
@@ -20,6 +20,9 @@ module.exports = defineConfig({
viewportWidth: 1920,
viewportHeight: 1080,
pageLoadTimeout: 120000,
redirectionLimit: 20,
defaultCommandTimeout: 10000,
retries: { runMode: 5, openMode: 5 },
},
env: {
defaultAccount: getOrFail('TESTING_USERNAME'),
@@ -1,8 +1,6 @@
/// <reference types="cypress" />
export default function checkLoginUI() {
cy.get('h1.text-text-primary')
.should('contain', 'Linux Odyssey')
.and('be.visible')
cy.get('#title').should('contain', 'Linux Odyssey').and('be.visible')
cy.CheckPlaceholder('#password', '密碼', 'Password')
}
describe('example helloworld app', () => {
@@ -13,7 +11,7 @@ describe('example helloworld app', () => {
it('Check Login Page Elements-Register', () => {
checkLoginUI()
cy.CheckPlaceholder('#username', '帳號名稱', 'Username')
cy.get('label.text-text-secondary').should('be.visible') // check the rules is displayed
cy.get('#NameRules').should('be.visible') // check the rules is displayed
cy.CheckPlaceholder('#email', '電子郵件', 'Email')
cy.CheckTextElement(
'#CheckRegistered',
@@ -79,15 +77,23 @@ describe('example helloworld app', () => {
cy.PrepareForGame()
})
it('Check Header', () => {
cy.get('#HeaderText').should('contain', 'Linux Odyssey').and('be.visible')
cy.get('#GameTitle').should('contain', 'Linux Odyssey').and('be.visible')
cy.get('#QuestTitle').should('contain', title).and('be.visible')
cy.get('#UsernameText')
.should('contain', Cypress.env('defaultAccount'))
.and('be.visible')
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
cy.findByRole('button', { name: 'Map' }).should('be.visible')
cy.findByRole('button', { name: 'LeaderBoard' }).should('be.visible')
cy.findByRole('link', { name: 'Survey' }).should('be.visible')
})
it('Check QuestInfo', () => {
cy.get('#topic').should('contain', title)
cy.get('#quest').find('p.text-text').should('have.length', 4)
cy.get('#topic').should('be.visible')
cy.get('#tasks').should('be.visible')
cy.get('#quest').find('p.text-text').should('have.length', 2)
cy.CheckTextElement('#tasks', '任務', 'Tasks:')
cy.getQuestInfo('輸入 `echo help` 來朗誦咒語').should('be.visible')
})
it('Check Terminal', () => {
cy.get('#terminal', { timeout: 20000 }).within(($terminal) => {
@@ -114,6 +120,7 @@ describe('example helloworld app', () => {
cy.CheckTextElement('#solution', '解答', 'Solution')
cy.CheckTextElement('#reset', '重來', 'Reset')
cy.CheckTextElement('#continue', '繼續', 'Continue')
cy.CheckTextElement('#survey', '問卷', 'Survey')
})
})
describe('Game Play', () => {
@@ -126,42 +133,31 @@ describe('example helloworld app', () => {
.should('contain', '12345')
.and('contain', 'zsh: command not found: 12345', { timeout: 50000 })
})
it('Complete the Game(relating UI)', () => {
// Stage1
cy.log('Stage1')
it('Complete the First Quest(relating UI)', () => {
cy.checkTaskInit()
cy.InitTerminal()
cy.typeInCommand('echo start{enter}')
cy.checkPending()
cy.waitUntilActive()
cy.get('#Lbutton').should('be.visible').and('be.disabled')
cy.get('#Rbutton').should('be.visible').and('be.disabled')
cy.checkHint(1, 1)
cy.getQuestInfo('✓ 輸入 `echo start` 來開始教程').should('be.visible')
cy.getQuestInfo('➤ 在終端機輸入 `echo hello`').should('be.visible')
// Stage2
cy.log('Stage2')
cy.getQuestInfo('✓ 輸入 echo start 來開始教程').should('be.visible')
cy.getQuestInfo('➤ 在終端機輸入 echo hello').should('be.visible')
cy.typeInCommand('echo hello{enter}')
cy.checkPending()
cy.waitUntilActive()
// cy.get('#tree')
// .get('a')
// .should('contain', 'forgotten_scroll.txt')
// .and('be.visible')
cy.get('#Lbutton').should('be.visible').and('be.enabled')
cy.get('#Rbutton').should('be.visible').and('be.disabled')
cy.checkHint(2, 2)
cy.get('#Lbutton').click()
cy.get('#Rbutton').should('be.enabled')
cy.getQuestInfo('✓ 在終端機輸入 `echo hello`').should('be.visible')
cy.getQuestInfo('✓ 在終端機輸入 echo hello').should('be.visible')
cy.getQuestInfo('➤ 結束關卡').should('be.visible')
// Stage3
cy.log('Stage3')
cy.typeInCommand('echo finish{enter}')
cy.checkPending()
cy.waitUntilActive()
cy.getQuestInfo('✓ 結束關卡').should('be.visible')
cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!')
cy.get('div[class="modal"]').find('p').should('be.visible')
cy.findByRole('link', { name: '填寫問卷' }).should('be.visible')
cy.get('#BacktoMap').should('be.visible').and('contain', '回到地圖')
})
})
})
+36
View File
@@ -0,0 +1,36 @@
/// <reference types="cypress" />
describe('Complete quests with only commands', () => {
beforeEach(() => {
cy.PrepareForGame()
})
it('Complete quest : get-started', () => {
cy.CompleteStageWithCommands('get-started')
})
it('Complete quest : cast', () => {
cy.CompleteStageWithCommands('cast')
})
it('Complete quest : explore', () => {
cy.CompleteStageWithCommands('explore')
})
it('Complete quest : path', () => {
cy.CompleteStageWithCommands('path')
})
it('Complete quest : read', () => {
cy.CompleteStageWithCommands('read')
})
it('Complete quest : create', () => {
cy.CompleteStageWithCommands('create')
})
it('Complete quest : copy', () => {
cy.CompleteStageWithCommands('copy')
})
it('Complete quest : move', () => {
cy.CompleteStageWithCommands('move')
})
it('Complete quest : remove', () => {
cy.CompleteStageWithCommands('remove')
})
it('Complete quest : helloworld', () => {
cy.CompleteStageWithCommands('helloworld')
})
})
+55 -13
View File
@@ -22,25 +22,31 @@ Cypress.Commands.add('PrepareForGame', () => {
cy.url().should('include', '/map')
cy.visit('/game/get-started')
cy.url().should('include', '/game/get-started')
// check task first so the terminal has better possibility loaded
cy.checkTaskInit()
cy.get('.xterm-screen', { timeout: 10000 })
.as('Terminaltextbox')
.should('be.visible')
cy.CheckTextElement('#reset', '重來', 'Reset').click()
cy.typeInCommand('clear{enter}')
cy.get('@Terminaltextbox', { timeout: 150000 }).should(
cy.checkTaskInit()
cy.InitTerminal()
})
Cypress.Commands.add('InitTerminal', () => {
cy.get('@Terminaltextbox', { timeout: 15000 }).should(
'contain',
'commander:~ $'
)
cy.log('Check terminal init done')
})
Cypress.Commands.add('typeInCommand', (command) => {
cy.get('.xterm-screen', { timeout: 150000 }).type(command)
cy.get('.xterm-screen', { timeout: 15000 }).type(command)
})
Cypress.Commands.add('getQuestInfo', (id) => {
return cy
.get('#quest')
.find('li')
.find('p')
.contains(`${id}`, { timeout: 100000 })
.contains(`${id}`, { timeout: 10000 })
})
Cypress.Commands.add('checkHint', (index, total) => {
cy.get('#hint')
@@ -48,29 +54,62 @@ Cypress.Commands.add('checkHint', (index, total) => {
.contains(`${index}/${total}`)
.should('be.visible')
})
Cypress.Commands.add('waitUntilActive', () => {
cy.get('input[id="currentStatus"]', { timeout: 1000000 })
Cypress.Commands.add('checkTaskInit', () => {
cy.get('#tasks').next().children().should('be.visible')
})
Cypress.Commands.add('waitUntilActive', (last = false) => {
cy.get('input[id="currentStatus"]', { timeout: 10000 })
.invoke('val')
.then((value) => {
if (value === 'active' || value === 'finished') {
if (value === 'active') {
cy.log('story ends')
return
}
cy.typeInCommand('{enter}')
cy.waitUntilActive()
if (value === 'finished' && last) {
cy.log('Quest Finished')
return
}
if (value === 'pending' && !last) {
cy.log(last)
cy.typeInCommand('{enter}')
cy.waitUntilActive()
}
})
})
Cypress.Commands.add('checkPending', () => {
cy.get('input[id="currentStatus"]', { timeout: 1000000 }).should(
cy.get('input[id="currentStatus"]', { timeout: 10000 }).should(
'have.value',
'pending'
)
})
Cypress.Commands.add('Complete the Stage (only command)', () => {
cy.log('')
Cypress.Commands.add('CompleteStageWithCommands', (stagename) => {
cy.visit(`/game/${stagename}`)
cy.url().should('include', `/game/${stagename}`, { timeout: 50000 })
cy.log(`Completing stage :${stagename}`)
cy.checkTaskInit()
cy.CheckTextElement('#reset', '重來', 'Reset').click()
cy.checkTaskInit()
cy.InitTerminal()
cy.readFile(`../quests/${stagename}/answer.sh`, 'utf-8').as('answers')
cy.get('@answers').then((answers) => {
const answerarr = answers.split('\n')
for (const element of answerarr) {
cy.typeInCommand(`${element}{enter}`)
if (answerarr.indexOf(element) + 1 !== answerarr.length) {
cy.checkPending()
cy.waitUntilActive()
}
if (answerarr.indexOf(element) + 1 === answerarr.length) {
cy.waitUntilActive(true)
cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!')
cy.get('div[class="modal"]').find('p').should('be.visible')
cy.get('#BacktoMap').should('be.visible').and('contain', '回到地圖')
}
}
})
})
Cypress.Commands.add('CheckTextElement', (id, chText, enText) => {
cy.get(id).within(($element) => {
cy.get(id, { timeout: 50000 }).within(($element) => {
if (Cypress.env('isCHVersion')) {
cy.get($element).should('contain', chText).and('be.visible')
} else {
@@ -89,3 +128,6 @@ Cypress.Commands.add('CheckPlaceholder', (id, chText, enText) => {
}
})
})
Cypress.Commands.add('CheckTreeElement', (element) => {
cy.get('#tree').get('a').should('contain', `${element}`).and('be.visible')
})
+35 -91
View File
@@ -1756,15 +1756,10 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
bson@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/bson/-/bson-5.4.0.tgz#0eea77276d490953ad8616b483298dbff07384c6"
integrity sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==
bson@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/bson/-/bson-5.5.0.tgz#a419cc69f368d2def3b8b22ea03ed1c9be40e53f"
integrity sha512-B+QB4YmDx9RStKv8LLSl/aVIEV3nYJc3cJNNTK2Cd1TL+7P+cNpw9mAPeCgc5K+j01Dv6sxUzcITXDx7ZU3F0w==
bson@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/bson/-/bson-6.2.0.tgz#4b6acafc266ba18eeee111373c2699304a9ba0a3"
integrity sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==
buffer-crc32@~0.2.3:
version "0.2.13"
@@ -3668,11 +3663,6 @@ internal-slot@^1.0.5:
has "^1.0.3"
side-channel "^1.0.4"
ip@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
@@ -4833,49 +4823,23 @@ mongodb-connection-string-url@^2.6.0:
"@types/whatwg-url" "^8.2.1"
whatwg-url "^11.0.0"
mongodb@5.7.0:
version "5.7.0"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.7.0.tgz#e16d2fcdfd9f8503ec2d88288392dc3235bb3ecc"
integrity sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==
mongodb@6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.2.0.tgz#2c9dcb3eeaf528ed850e94b3df392de6c6b0d7ab"
integrity sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==
dependencies:
bson "^5.4.0"
mongodb-connection-string-url "^2.6.0"
socks "^2.7.1"
optionalDependencies:
saslprep "^1.0.3"
mongodb@5.9.0:
version "5.9.0"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.9.0.tgz#5a22065fa8cfaf1d58bf2e3c451cd2c4bfa983a2"
integrity sha512-g+GCMHN1CoRUA+wb1Agv0TI4YTSiWr42B5ulkiAfLLHitGK1R+PkSAf3Lr5rPZwi/3F04LiaZEW0Kxro9Fi2TA==
dependencies:
bson "^5.5.0"
mongodb-connection-string-url "^2.6.0"
socks "^2.7.1"
optionalDependencies:
"@mongodb-js/saslprep" "^1.1.0"
bson "^6.2.0"
mongodb-connection-string-url "^2.6.0"
mongoose@^7.4.0:
version "7.4.0"
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.4.0.tgz#3726a8dea0c9e2d54651845cc84c813a6507906a"
integrity sha512-oHE1eqodfKzugXRlQxpo+msIea7jPcRoayDuEMr50+bYwM/juA5f+1stjkWlXcg6vo1PdJFVA6DGaKOPLuG5mA==
mongoose@^8.0.3:
version "8.0.3"
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-8.0.3.tgz#f3708ba0f40b6dec8f5f1a5d40abd69a80207114"
integrity sha512-LJRT0yP4TW14HT4r2RkxqyvoTylMSzWpl5QOeVHTnRggCLQSpkoBdgbUtORFq/mSL2o9cLCPJz+6uzFj25qbHw==
dependencies:
bson "^5.4.0"
bson "^6.2.0"
kareem "2.5.1"
mongodb "5.7.0"
mpath "0.9.0"
mquery "5.0.0"
ms "2.1.3"
sift "16.0.1"
mongoose@^7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.6.0.tgz#3037b754ccdf168385e52d4bcabeb7f5c3b17a5f"
integrity sha512-ztQ12rm0BQN5i7LB6xhWX4l9a9w2aa3jEwa/mM2vAutYJRyAwOzcusvKJBULMzFHyUDBOVW15grisexypgMIWA==
dependencies:
bson "^5.4.0"
kareem "2.5.1"
mongodb "5.9.0"
mongodb "6.2.0"
mpath "0.9.0"
mquery "5.0.0"
ms "2.1.3"
@@ -4922,10 +4886,10 @@ nan@^2.17.0:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
natural-compare@^1.4.0:
version "1.4.0"
@@ -5420,12 +5384,12 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.1.10, postcss@^8.4.23, postcss@^8.4.25, postcss@^8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
postcss@^8.1.10, postcss@^8.4.23, postcss@^8.4.27, postcss@^8.4.31:
version "8.4.32"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
dependencies:
nanoid "^3.3.6"
nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.0.2"
@@ -5701,10 +5665,10 @@ rimraf@^3.0.0, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rollup@^3.25.2:
version "3.26.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.26.2.tgz#2e76a37606cb523fc9fef43e6f59c93f86d95e7c"
integrity sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==
rollup@^3.27.1:
version "3.29.4"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981"
integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==
optionalDependencies:
fsevents "~2.3.2"
@@ -5758,13 +5722,6 @@ safe-stable-stringify@^2.3.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
saslprep@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226"
integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==
dependencies:
sparse-bitfield "^3.0.3"
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
@@ -5913,11 +5870,6 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
socket.io-adapter@~2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12"
@@ -5956,14 +5908,6 @@ socket.io@^4.7.1:
socket.io-adapter "~2.5.2"
socket.io-parser "~4.2.4"
socks@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55"
integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==
dependencies:
ip "^2.0.0"
smart-buffer "^4.2.0"
sorted-array-functions@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5"
@@ -6599,14 +6543,14 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
vite@^4.4.0:
version "4.4.4"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.4.tgz#b76e6049c0e080cb54e735ad2d18287753752118"
integrity sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==
vite@^4.4.12:
version "4.4.12"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.12.tgz#e9c355d5a0d8a47afa46cb4bad10820da333da5c"
integrity sha512-KtPlUbWfxzGVul8Nut8Gw2Qe8sBzWY+8QVc5SL8iRFnpnrcoCaNlzO40c1R6hPmcdTwIPEDkq0Y9+27a5tVbdQ==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.25"
rollup "^3.25.2"
postcss "^8.4.27"
rollup "^3.27.1"
optionalDependencies:
fsevents "~2.3.2"